Custom protocols
Messages are stored and retried. Many things don't need that: a cursor position, "is typing", a game move that's stale after 100 ms. For those, open a protocol: a named, typed channel carried over the same encrypted sessions.
type Cursor = { x: number; y: number };
const cursors = peer.protocol<Cursor>("my-app/cursor/1");
// receive
const stop = cursors.on((from, { x, y }) => drawCursor(from, x, y));
// send (connects if needed)
await cursors.send(bobDid, { x: 120, y: 48 });
// later
stop();
How protocols behave
- Ephemeral. Nothing is stored or retried. If the peer can't be reached,
send()throws (UNREACHABLE,REJECTEDorTIMEOUT). - Ordered per peer. Values sent to one peer arrive in the order you sent them, as long as the session stays open.
- Verified and encrypted.
fromis the proven DID, exactly as for messages. - Typed. The type parameter types
send()and your handler. It's a compile-time convenience: validate incoming values anyway.
Naming
Name protocols like "<app>/<feature>/<version>", for example "acme-chat/typing/1". The version suffix lets you change a payload shape later and run old and new clients side by side. The vaultys/ prefix is reserved for the SDK, and using it throws.
Patterns
Presence and typing indicators
const typing = peer.protocol<{ conversation: string }>("acme-chat/typing/1");
input.addEventListener("input", () => {
typing.send(bobDid, { conversation: "general" }).catch(() => {}); // fine to drop
});
typing.on((from, { conversation }) => showTyping(from, conversation));
Request and response
Protocols are one-way. To get an answer, add an id and reply on the same protocol:
type Ping = { kind: "ping"; id: string } | { kind: "pong"; id: string };
const ping = peer.protocol<Ping>("acme/ping/1");
ping.on((from, value) => {
if (value.kind === "ping") ping.send(from, { kind: "pong", id: value.id });
});
function measureLatency(did: string): Promise<number> {
const id = crypto.randomUUID();
const started = performance.now();
return new Promise((resolve, reject) => {
const stop = ping.on((from, value) => {
if (from === did && value.kind === "pong" && value.id === id) {
stop();
resolve(performance.now() - started);
}
});
ping.send(did, { kind: "ping", id }).catch((error) => {
stop();
reject(error);
});
});
}
Throttling high-frequency updates
Sessions are fast, but there's no point sending 120 cursor updates a second. Throttle at the source:
let pending: Cursor | null = null;
canvas.addEventListener("pointermove", (event) => {
pending = { x: event.offsetX, y: event.offsetY };
});
setInterval(() => {
if (pending) cursors.send(bobDid, pending).catch(() => {});
pending = null;
}, 50);