Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createPulse()Create a typed WebSocket session instance.Sync (returns Pulse)Does not open the connection — call connect().
PulseMain instance: channels, rooms, messaging, lifecycle.Sync methods, async connect()/wait()send() throws while disconnected.
PulseChannelScoped channel namespace with independent disposal.Sync methods, async wait()Each call returns a new scope; ref-counted subscription.
RoomScopeRef-counted room membership with optional presence.Sync methods, async joinedjoined rejects on transport close or timeout.
PulseSchemaDeclares server/client events, channels, and rooms.Type-onlyInfer all named scope types from this schema.
PulseOptionsConfiguration: heartbeat, reconnect, transform.Type-onlyreconnect and heartbeat default to false.
PulseErrorBase class for all Pulse errors.RuntimeCheck instanceof against subclasses.

Package Entry Point

ImportPurpose
@vielzeug/pulseAll public exports: createPulse, types, and error classes.

createPulse()

ts
function createPulse<S extends PulseSchema = PulseSchema>(url: string, options?: PulseOptions): Pulse<S>

Creates a Pulse instance. The WebSocket is not opened until connect() is called.

Type parameters

ParameterConstraintDescription
SPulseSchemaSchema declaring server events, client events, channels, and rooms.

Parameters

ParameterTypeDescription
urlstringWebSocket URL.
optionsPulseOptionsOptional configuration.

Returns

Pulse<S> — the Pulse instance.


PulseSchema

ts
type PulseSchema = {
  server?: MessageMap;
  client?: MessageMap;
  channels?: ChannelDefinitions;
  rooms?: RoomDefinitions;
};

Declare all protocol surfaces once at construction. Named scopes infer their types from this schema.

FieldTypeDescription
serverMessageMapRoot events the server sends.
clientMessageMapRoot events the client sends.
channelsChannelDefinitionsNamed channel schemas.
roomsRoomDefinitionsNamed room schemas with optional presence.

PulseOptions

ts
type PulseOptions = {
  heartbeat?: boolean | HeartbeatOptions;
  protocols?: string | string[];
  reconnect?: boolean | ReconnectOptions;
  transform?: OutgoingTransform;
};
OptionTypeDefaultDescription
heartbeatboolean | HeartbeatOptionsfalsePing/pong keep-alive.
protocolsstring | string[]Sub-protocols passed to the WebSocket constructor.
reconnectboolean | ReconnectOptionsfalseAuto-reconnect on unexpected close.
transformOutgoingTransformTransform or filter outgoing application messages.

HeartbeatOptions

ts
type HeartbeatOptions = {
  interval?: number;
  timeout?: number;
};
OptionTypeDefaultDescription
intervalnumber30_000Interval between pings in ms.
timeoutnumber5_000How long to wait for a pong before treating the connection as dead.

ReconnectOptions

ts
type ReconnectOptions = {
  delay?: number | ((attempt: number) => number);
  maxAttempts?: number;
};
OptionTypeDefaultDescription
delaynumber | ((attempt: number) => number)Full-jitter exponential backoff capped at 30 sDelay between reconnect attempts in ms. attempt is zero-based.
maxAttemptsnumber5Maximum number of reconnect attempts after initial failure.

OutgoingMessage

ts
type OutgoingMessage = { channel?: string; event: string; payload: unknown };

An outgoing application message before it is serialized.


OutgoingTransform

ts
type OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;

Transform or filter outgoing application messages. Internal protocol frames (subscribe, join, leave, presence, ping) bypass this hook. Return null to drop the message.


Pulse

ts
type Pulse<S extends PulseSchema = PulseSchema> = {
  // Channels
  channel<K extends keyof ChannelMap<S> & string>(
    name: K,
  ): PulseChannel<ChannelMap<S>[K]['server'], ChannelMap<S>[K]['client']>;

  // Connection
  connect(): Promise<void>;
  disconnect(code?: number, reason?: string): void;

  // Lifecycle
  readonly disposalSignal: AbortSignal;
  dispose(): void;
  readonly disposed: boolean;

  // Messaging
  on<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;
  once<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;
  send<K extends EventKey<ClientEvents<S>>>(event: K, payload: ClientEvents<S>[K]): void;
  wait<K extends EventKey<ServerEvents<S>>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<ServerEvents<S>[K]>;

  // Rooms
  room<K extends keyof RoomMap<S> & string>(name: K, opts?: RoomOptions): RoomScope<RoomMap<S>[K]>;
  readonly rooms: Readable<ReadonlySet<string>>;

  // Status
  readonly status: Readable<PulseStatus>;

  // Tap
  tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;

  [Symbol.dispose](): void;
};

channel(name)

Creates an isolated message namespace over the shared connection. Each call returns an independently disposable scope. The server subscription is reference-counted.

connect()

Explicitly opens the connection. Resolves after session restoration completes. Rejects if the connection closes before opening.

disconnect(code?, reason?)

Closes the connection without triggering reconnection. Default code is 1000.

dispose()

Permanently closes the connection and releases all resources. Idempotent.

on(event, handler)

Subscribes to a typed server event. Returns an unsubscribe function.

once(event, handler)

Subscribes once — auto-removes after first invocation.

send(event, payload)

Sends a typed event to the server. Throws PulseConnectionError unless the connection is open.

wait(event, opts?)

Resolves on the next emission of the given server event. Rejects when opts.signal aborts, the timeout elapses, or the instance is disposed.

room(name, opts?)

Creates a ref-counted room scope. The first scope sends join; the last disposal sends leave. When the room definition includes presence, the scope exposes reactive presence state.

rooms

Reactive set of rooms the client is currently a confirmed member of.

status

Reactive connection status: 'connecting' | 'open' | 'reconnecting' | 'closed'.

tap(handler, options?)

Subscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union PulseEvent. Returns an unsubscribe function.

ParameterTypeDescription
handler(event: PulseEvent) => voidCalled for each lifecycle event.
options.signalAbortSignalOptional signal to stop the subscription.
ts
const pulse = createPulse(url, { reconnect: true });
pulse.tap((event) => {
  if (event.type === 'error') console.error(event.error);
  if (event.type === 'status-change') console.log('status:', event.status);
});

PulseEvent

ts
type PulseEvent =
  | { type: 'status-change'; status: PulseStatus }
  | { type: 'error'; error: PulseError }
  | { type: 'dispose' };

A discriminated union of lifecycle events emitted by a Pulse instance. Inspect event.type to narrow the payload.

typePayloadWhen
status-changestatus: PulseStatusThe connection status transitions.
errorerror: PulseErrorA typed transport or protocol error occurs.
disposeThe instance is disposed.

PulseChannel

ts
type PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {
  readonly disposalSignal: AbortSignal;
  readonly disposed: boolean;
  readonly name: string;
  dispose(): void;
  on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;
  once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;
  send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;
  wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;
  [Symbol.dispose](): void;
};

RoomScope

ts
type RoomScope<R extends RoomDefinition = RoomDefinition> = R extends { presence: infer P }
  ? P extends undefined
    ? RoomScopeBase
    : PresenceRoomScope<P>
  : RoomScopeBase;

A room scope. When the room definition includes presence, the scope is a PresenceRoomScope; otherwise it is a RoomScopeBase.

RoomScopeBase

ts
type RoomScopeBase = {
  readonly disposalSignal: AbortSignal;
  readonly disposed: boolean;
  readonly name: string;
  readonly joined: Promise<void>;
  dispose(): void;
  [Symbol.dispose](): void;
};

PresenceRoomScope

ts
type PresenceRoomScope<T = unknown> = RoomScopeBase & {
  readonly presence: Readable<ReadonlyMap<string, T>>;
  updatePresence(state: T): void;
  onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;
  onLeave(handler: (memberId: string) => void): Unsubscribe;
};
MemberTypeDescription
presenceReadable<ReadonlyMap<string, T>>Reactive map of memberId → state.
updatePresence(state)(state: T) => voidBroadcast this client's presence state. Throws PulseConnectionError unless open.
onJoin(handler)(handler) => UnsubscribeCalled whenever a new member joins with their initial state.
onLeave(handler)(handler) => UnsubscribeCalled whenever a member leaves.

RoomOptions

ts
type RoomOptions = {
  signal?: AbortSignal;
  timeout?: number;
};
OptionTypeDescription
signalAbortSignalAborts the join, rejecting joined with PulseAbortError.
timeoutnumberJoin timeout in ms. Rejects joined with PulseRoomTimeoutError.

Errors

All errors extend PulseError.

PulseError

Base class for all Pulse errors.

PulseConnectionError

Transport failure, send while disconnected, or room join rejected on close.

PulseProtocolError

Malformed frame or server error frame.

PulseTimeoutError

wait() timed out before the server event arrived.

PulseRoomTimeoutError

Room scope joined timed out before the server confirmed membership.

PulseAbortError

wait() or room joined aborted via AbortSignal.

PulseDisposedError

Operation attempted after disposal.


Channel and room definitions

ChannelDefinition

ts
type ChannelDefinition = { client: MessageMap; server: MessageMap };

ChannelDefinitions

ts
type ChannelDefinitions = Record<string, ChannelDefinition>;

RoomDefinition

ts
type RoomDefinition = { presence?: unknown };

RoomDefinitions

ts
type RoomDefinitions = Record<string, RoomDefinition>;

Utility types

MessageMap

ts
type MessageMap = Record<string, unknown>;

EventKey

ts
type EventKey<T extends MessageMap> = keyof T & string;

ServerEvents

ts
type ServerEvents<S extends PulseSchema> = S extends { server: infer M extends MessageMap } ? M : MessageMap;

Extract server events from a schema, defaulting to an empty map.

ClientEvents

ts
type ClientEvents<S extends PulseSchema> = S extends { client: infer M extends MessageMap } ? M : MessageMap;

Extract client events from a schema, defaulting to an empty map.

RoomMap

ts
type RoomMap<S extends PulseSchema> = S extends { rooms: infer R extends RoomDefinitions } ? R : RoomDefinitions;

Extract room definitions from a schema, defaulting to an empty map.

Unsubscribe

ts
type Unsubscribe = () => void;

PulseStatus

ts
type PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';