Skip to main content

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, REJECTED or TIMEOUT).
  • 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. from is 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);