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);
}
});
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.