Skip to content

API Overview ​

SymbolPurposeExecutionCommon gotcha
hostTavern()Host a subject: pairing, command validation, snapshot broadcastSync factoryDisposing ends hosting for every guest
joinTavern()Join a session: consume invitation, produce answerAsyncThe session lives only after the host accepts and the first snapshot mounts
TavernHostHost handle: invitation, answer acceptance, kick, notice relay, presence tapN/Adispose() is idempotent; control-plane methods throw TavernDisposedError after
TavernGuestGuest handle: command forwarding, rejection tap, disposalN/Adispose() fires onEnded exactly once
TavernHostEventHost observability union for tap(): presence and warningsN/ALifecycle transitions are callbacks, not events
TavernGuestEventGuest observability union for tap(): rejections and warningsN/AonJoined/onFailed fire before the handle exists, so they stay callbacks
TavernCommandsThe host's command tableN/AUnknown names and foreign subject ids reject without applying
TavernSubjectsSnapshot reading, change and removal subscriptionsN/Asnapshot returning null skips the broadcast
TavernNoticesWire serialization of local noticesN/AtoWire returning null skips that notice
TavernErrorBase error for every Tavern failureN/AN/A
TavernPairingErrorThe consumer pasted the wrong kind of pairing codeN/AN/A
TavernDisposedErrorA stopped host or ended guest was used againN/AN/A

Package Entry Point ​

ImportPurpose
@vielzeug/tavernComplete public Tavern API

hostTavern(options) ​

Creates a mesh host for one subject. Guest commands are validated against the wire shape (Tavern's own protocol), checked against commands.has and the subjectId, then applied through commands.apply: throwing rejects the guest with the error's message. Local changes (via subjects.onChanged) re-broadcast subjects.snapshot(): coalesced on a microtask; subjects.onRemoved ends hosting. Presence and transport warnings are reported through tap() as TavernHostEvents, and the live peer list is the peers getter; hosting ending: from removal or dispose(): fires onEnded exactly once. relayNotice serializes through notices.toWire and broadcasts to every guest.

Returns: TavernHost: the host handle.

OptionTypeDescription
commandsTavernCommandsThe host's command table: the same object the host's own UI calls
notices?TavernNoticesNotice relay; omit to disable notice broadcasting
onEnded?() => voidHosting ended: the subject was removed or dispose() ran. Fires exactly once
rtc?MeshRtcFactoryWebRTC factory: the injection point for tests and non-browser runtimes
subjectIdstringThe id of the hosted subject: guest commands targeting anything else reject
subjectsTavernSubjectsSnapshot reading, change and removal subscriptions

Example

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

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'),
  },
});

TavernHost members ​

MemberReturnsDescription
acceptAnswerText(text)Promise<MeshPeer>Consumes a guest's answer code; resolves with the peer once the channel opens. Throws TavernPairingError for any unusable code: wrong kind, malformed, expired: with the underlying error as cause. Throws TavernDisposedError after dispose()
createInvitationText()Promise<string>Produces a single-use invitation code, QR-compact when the environment allows. Throws TavernDisposedError after dispose()
disposalSignalAbortSignalAborted when hosting ends
dispose()voidStops hosting: closes every channel and detaches all subscriptions. Idempotent; fires onEnded exactly once
disposedbooleanWhether hosting has ended
kick(peerId)voidDisconnects a peer; the guest observes a disconnect. Throws TavernDisposedError after dispose()
peersreadonly MeshPeer[]The guests currently connected; current inside a peer-joined/peer-left tap event
relayNotice(notice)voidRelays one local notice to every guest; the serializer decides what crosses. A notice relayed after dispose() is dropped: ephemeral UI state with no audience left
tap(handler, options?)() => voidObserves TavernHostEvents; returns an unsubscribe function
[Symbol.dispose]()voidDelegates to dispose(). Enables using declarations

joinTavern(options) ​

Consumes an invitation and returns { answerText, guest }. The guest mounts snapshots through mount(snapshot): the first successful mount fires onJoined; wire notices are relayed through notices.fromWire; host rejections and transport warnings arrive through tap() as TavernGuestEvents. Channel failures before the first mount fire onFailed (once); afterwards, a disconnect or guest.dispose() fires onEnded with the mounted subject exactly once. guest.sendCommand forwards a command to the host with the subject id the consumer routes by. An unusable invitation: wrong kind, malformed, expired: rejects with TavernPairingError and leaves no node or subscriptions behind.

Returns: Promise<{ answerText: string; guest: TavernGuest }>: the answer code to show back and the guest handle.

OptionTypeDescription
invitationTextstringThe invitation code the host produced
mount(snapshot: unknown) => Mounted | nullMounts a received snapshot; the mounted value, or null to ignore
namestringThe guest's display name on the channel
notices?TavernNoticesNotice relay; wire notices re-emit locally through fromWire
onEnded?(subject: Mounted) => voidThe channel ended after joining; the mounted value is passed for unmounting
onFailed?(reason: string) => voidThe host never accepted the answer in time
onJoined?(subject: Mounted) => voidThe mounted value once the first snapshot arrives
rtc?MeshRtcFactoryWebRTC factory: the injection point for tests and non-browser runtimes

Example

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

const { answerText, guest } = await joinTavern({
  invitationText: invitation,
  mount: (snapshot) => sanitizeAndMount(snapshot),
  name: 'Alex',
  onEnded: (subject) => unmountMirror(subject.id),
  onJoined: (subject) => mountMirror(subject),
});
guest.tap((event) => {
  if (event.type === 'rejected') console.warn(event.commandId, event.message);
});

guest.sendCommand('doc-1', 'rename', ['New name']);
guest.dispose();

TavernGuest members ​

MemberReturnsDescription
disposalSignalAbortSignalAborted when the session ends
dispose()voidLeaves the session and drops the channel. Fires onEnded exactly once
disposedbooleanWhether the session has ended: the channel dropped or dispose() ran
sendCommand(subjectId, name, args)voidForwards a command to the host. Throws TavernDisposedError if the session has ended
tap(handler, options?)() => voidObserves TavernGuestEvents; returns an unsubscribe function
[Symbol.dispose]()voidDelegates to dispose(). Enables using declarations

Types ​

ts
/** What the host can do on behalf of a guest. */
interface TavernCommands {
  /** Whether a command name is known; unknown names reject without applying. */
  has(name: string): boolean;
  /** Applies the command; throwing rejects the guest with the error's message. */
  apply(name: string, args: readonly unknown[]): unknown;
}

/**
 * How the host reads and watches the subject it is sharing. All three close over
 * whatever subject state the consumer owns: the host routes by `subjectId`,
 * not by a subject object.
 */
interface TavernSubjects {
  /** Reads the snapshot to broadcast; null while the subject is missing. */
  snapshot(): unknown;
  /** Subscribes to local changes of the hosted subject: the re-broadcast trigger. */
  onChanged(listener: () => void): () => void;
  /** Subscribes to the hosted subject's removal: hosting ends when it fires. */
  onRemoved(listener: () => void): () => void;
}

/**
 * Notice relay between host and guests. Notices cross the wire as opaque
 * values: each client translates locally.
 */
interface TavernNotices {
  /** Serializes a local notice for the wire; null skips this one. */
  toWire(notice: unknown): unknown | null;
  /** Re-emits a received wire notice locally. */
  fromWire(wire: unknown): void;
}

/** Notable host moments reported through `TavernHost.tap`. */
type TavernHostEvent =
  | { readonly peer: MeshPeer; readonly type: 'peer-joined' }
  | { readonly peer: MeshPeer; readonly reason?: string; readonly type: 'peer-left' }
  | { readonly error: unknown; readonly message: string; readonly type: 'warning' };

/** Notable guest moments reported through `TavernGuest.tap`. */
type TavernGuestEvent =
  | { readonly commandId: string; readonly message: string; readonly type: 'rejected' }
  | { readonly error: unknown; readonly message: string; readonly type: 'warning' };

/** Options for `hostTavern`. */
interface TavernHostOptions {
  commands: TavernCommands;
  notices?: TavernNotices;
  /** Hosting ended: the subject was removed or `dispose()` ran. Fires exactly once. */
  onEnded?(): void;
  rtc?: MeshRtcFactory;
  subjectId: string;
  subjects: TavernSubjects;
}

/** Options for `joinTavern`. */
interface TavernGuestOptions<Mounted> {
  invitationText: string;
  /** Mounts a received snapshot; the mounted value, or null to ignore. */
  mount(snapshot: unknown): Mounted | null;
  name: string;
  notices?: TavernNotices;
  onEnded?(subject: Mounted): void;
  onFailed?(reason: string): void;
  onJoined?(subject: Mounted): void;
  rtc?: MeshRtcFactory;
}

/** Host handle returned by `hostTavern`. */
interface TavernHost {
  acceptAnswerText(text: string): Promise<MeshPeer>;
  createInvitationText(): Promise<string>;
  readonly disposalSignal: AbortSignal;
  dispose(): void;
  readonly disposed: boolean;
  kick(peerId: string): void;
  readonly peers: readonly MeshPeer[];
  relayNotice(notice: unknown): void;
  tap(handler: (event: TavernHostEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
  [Symbol.dispose](): void;
}

/** Guest handle returned by `joinTavern`. */
interface TavernGuest {
  readonly disposalSignal: AbortSignal;
  dispose(): void;
  readonly disposed: boolean;
  sendCommand(subjectId: string, name: string, args: readonly unknown[]): void;
  tap(handler: (event: TavernGuestEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
  [Symbol.dispose](): void;
}

Errors ​

ErrorTriggered byNotable properties
TavernErrorBase class for every Tavern failure: catch this to handle all Tavern errors in one branchN/A
TavernPairingErrorA pairing code the consumer pasted could not be used: wrong kind, malformed, expired, or refused by the host. The mesh-level failure is chained as causecause
TavernDisposedErrorA stopped host (kick, createInvitationText, acceptAnswerText) or an ended guest (sendCommand) was used againN/A

TavernPairingError and TavernDisposedError extend TavernError. Every user-input pairing mistake: a garbage code, the wrong code kind, an expired invitation, a refused answer: surfaces as TavernPairingError from acceptAnswerText and joinTavern; transport-level failures (timeouts, connection errors) propagate unchanged. Post-dispose use fails loudly with TavernDisposedError, mirroring mesh inside the Tavern error hierarchy.