Skip to main content

Messaging

Sending​

send() takes a DID and any msgpack-serializable value: strings, numbers, booleans, null, arrays, plain objects and Uint8Array.

const { id } = await peer.send(bobDid, { text: "Hello" });

The promise resolves once the message is safely queued, not when it's delivered. Listen for delivered for that (see Message delivery).

send() throws only if the DID isn't a valid did:vaultys (INVALID_DID) or the peer is closed (CLOSED). An unreachable recipient isn't an error: the message waits.

Receiving​

peer.on("message", ({ id, from, body, sentAt }) => {
// from: the sender's proven DID
// body: what they sent (validate it: it's network input)
// sentAt: the sender's clock, in ms since epoch
});

on() returns a function that removes the listener, which is handy in React effects:

useEffect(() => peer.on("message", handleMessage), [peer]);

A message schema​

The SDK doesn't impose a message format. Define one and narrow incoming bodies before using them:

type ChatBody =
| { kind: "text"; text: string }
| { kind: "file"; name: string; type: string; data: Uint8Array };

function isChatBody(value: unknown): value is ChatBody {
return typeof value === "object" && value !== null && "kind" in value;
}

peer.on("message", ({ from, body }) => {
if (!isChatBody(body)) return; // ignore what you don't understand
if (body.kind === "text") showText(from, body.text);
});

Sending files​

Uint8Array values are sent as binary, with no base64 overhead:

async function sendFile(did: string, file: File) {
const data = new Uint8Array(await file.arrayBuffer());
return peer.send<ChatBody>(did, { kind: "file", name: file.name, type: file.type, data });
}

peer.on("message", ({ body }) => {
if (isChatBody(body) && body.kind === "file") {
// copy into a plain ArrayBuffer-backed array, which Blob accepts in every environment
const url = URL.createObjectURL(new Blob([new Uint8Array(body.data)], { type: body.type }));
showDownload(body.name, url);
}
});
Large files

A message is sent as a single frame, and queued messages are kept in the store until they're acknowledged. Keep messages to a few megabytes. For larger files, split them into chunks and send each chunk as its own message, or use a custom protocol. Built-in chunked transfers with progress are planned.

Delivery status in a UI​

A common pattern: show a single tick when queued and a double tick when delivered.

const status = new Map<string, "queued" | "delivered">();

async function sendText(did: string, text: string) {
const { id } = await peer.send(did, { kind: "text", text });
status.set(id, "queued");
}

peer.on("delivered", ({ id }) => status.set(id, "delivered"));

Keeping a history​

The SDK doesn't store conversations. Save what you send and receive in your own storage, keyed by message id:

peer.on("message", (message) => db.messages.put({ ...message, direction: "in" }));

async function sendAndRecord(did: string, body: ChatBody) {
const { id } = await peer.send(did, body);
await db.messages.put({ id, to: did, body, sentAt: Date.now(), direction: "out" });
}

Since ids are unique, writing the same message twice is harmless.

Broadcasting to several contacts​

There are no built-in groups: fan out to each member. Each copy is queued, retried and acknowledged on its own.

async function sendToGroup(members: string[], body: ChatBody) {
return Promise.all(members.map((did) => peer.send(did, body)));
}

For a shared conversation that everyone can see and edit, use a collaboration space instead.