Skip to content
tavern logoTavernUtilities
Table sessions over mesh — host-owned state replication with guest command forwarding
Version
v3.0.0
Size
1.7 KB gzip
BrowserNode ≥22
hostTavernjoinTavernTavernHostTavernGuestTavernCommands View all 8 exports

Why Tavern? ​

Peer-to-peer apps share one arrangement problem: the canonical state of something — a document, a dashboard, a record — lives on exactly one device, while every other participant should see it and be able to change it from their own. Tavern turns that arrangement into a session: one host owns the canonical state, guests mirror it, and every guest command is forwarded through the host's own command table — so remote actions follow exactly the same code path as local ones.

ts
// Before: hand-rolled session orchestration — pairing, protocol validation,
// command routing, snapshot broadcasting, notice relaying, presence tracking.
const host = createMeshHost({ /* ... */ });
host.on('command', ({ peerId, payload }) => {
  const command = mySchema.safeParse(payload);
  if (!command.success) { host.send(peerId, 'rejected', { /* ... */ }); return; }
  try { applySubjectCommand(command.data.name, subject, command.data.args); }
  catch (error) { host.send(peerId, 'rejected', { /* ... */ }); }
});
bus.on('subject:updated', () => { host.broadcast('snapshot', snapshotFor(subject)); });
// ... 40+ more lines of plumbing

// After: tavern owns the protocol, the validation, the broadcasting.
const host = hostTavern({
  commands: { apply: (name, args) => store.apply(name, args), has: (name) => name in store },
  subjectId: 'doc-1',
  subjects: {
    onChanged: (listener) => store.onUpdated('doc-1', listener),
    onRemoved: (listener) => store.onRemoved('doc-1', listener),
    snapshot: () => store.read('doc-1'),
  },
});
FeatureTavernRaw meshSocket.io rooms
Bundle size1.7 KB6.0 KB—
Zero dependencies
Host-authoritative commands — same table as local UI — wire it yourself — wire it yourself
Snapshot replication — microtask-coalesced
Out-of-band pairing (QR / copy-paste) — needs a server
Notice relaying — consumer-defined wire shape — server-mediated

Use Tavern when one device should own the canonical state of something — a shared document, a live dashboard, a record under review — and every other device should mirror it and send commands back through the host's own command table, which stays the single validation path.

Consider raw @vielzeug/mesh when you need P2P messaging without the host-authoritative replication model — chat, whiteboards, file transfer, or custom topologies.

Installation ​

sh
pnpm add @vielzeug/tavern
sh
npm install @vielzeug/tavern
sh
yarn add @vielzeug/tavern

Quick Start ​

ts
import { hostTavern, joinTavern } from '@vielzeug/tavern';

// Host: pick a subject, wire the command table and snapshot access.
const host = hostTavern({
  commands: {
    apply: (name, args) => myStore.apply(name, args),
    has: (name) => name in myStore,
  },
  subjectId: 'doc-1',
  subjects: {
    onChanged: (listener) => myStore.onUpdated('doc-1', listener),
    onRemoved: (listener) => myStore.onRemoved('doc-1', listener),
    snapshot: () => myStore.read('doc-1'),
  },
});

// Produce a single-use invitation; deliver it out-of-band (QR, copy/paste).
const invitation = await host.createInvitationText();

// Guest: consume the invitation, produce an answer for the host.
const { answerText, guest } = await joinTavern({
  invitationText: invitation,
  mount: (snapshot) => parseAndMount(snapshot),
  name: 'Alex',
});
await host.acceptAnswerText(answerText);

// The guest's commands run through the host's own table.
guest.sendCommand('doc-1', 'rename', ['Quarterly report']);

// Cleanup — hosting ends for every guest; onEnded fires exactly once on both sides.
host.dispose();
guest.dispose();

Features ​

  • hostTavern — host one subject: pairing, protocol validation, command application, coalesced snapshot broadcasts
  • joinTavern — join as a guest: consume the invitation, produce the answer, mirror the host's snapshots
  • TavernCommands — the host's command table: the same object the host's own UI calls, so remote actions cannot bypass validation
  • TavernNotices — consumer-defined notice serialization: catalog keys cross the wire, each client translates locally
  • Coalesced snapshots — bursts of local changes ship one snapshot per microtask, not one per change
  • onEnded fires exactly once — on both handles: channel drops, subject removal, and explicit disposal share a single idempotent cleanup path
  • Typed errors — TavernPairingError covers every unusable pasted code with the mesh failure chained as cause

Documentation ​

See Also ​

  • Mesh — the P2P transport underneath: WebRTC pairing, data channels, QR-optimized codecs.
  • Ledger — undo/redo history for the commands the host applies.
  • Herald — typed event bus for the local notice stream tavern relays.