Skip to content
pulse logoPulseWebsockets
Full-featured WebSocket client with typed messaging, channel multiplexing, room management, reactive presence, auto-reconnect, and heartbeat — built on ripple signals.
v0.0.14.5 KB gzip Browser · Node.js
createPulsePulsePulseChannelPresenceChannelPulseOptionsPulseErrorConnectionErrorTimeoutError +3 more →

Why Pulse?

Raw WebSocket gives you an untyped message stream — no event routing, no reconnection, no presence, no lifecycle management. Building those primitives for every project is repetitive and error-prone.

ts
// Before — raw WebSocket
const ws = new WebSocket('wss://api.example.com/ws');
ws.addEventListener('message', (ev) => {
  const { type, payload } = JSON.parse(ev.data); // untyped
  if (type === 'chat:message') renderMessage(payload); // manual routing
});
ws.addEventListener('close', () => setTimeout(reconnect, 3_000)); // manual reconnect
// No channels, no presence, no heartbeat, no disposal

// After — Pulse
const pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {
  reconnect: { maxAttempts: 5 },
  heartbeat: true,
});
pulse.on('chat:message', ({ user, text }) => renderMessage({ user, text })); // fully typed
pulse.send('chat:send', { text: 'Hello!' });
effect(() => console.log('status:', pulse.status.value)); // reactive via ripple
FeaturePulseNative WebSocketsocket.io-client
Bundle size4.5 KB0 B (native)~44 kB gzip
TypeScript inference Full None Basic
Auto-reconnect
Heartbeat (ping/pong)
Channel multiplexing
Reactive presence Manual
Reactive status
Server lock-in None None Required
Zero dependencies ripple

Use Pulse when you need typed, multiplexed real-time messaging with reactive state and a clean disposal lifecycle — without being locked to a specific server stack.

Consider native WebSocket when you need the absolute minimum footprint and are building a one-off, untyped connection with no reuse patterns.

Installation

sh
pnpm add @vielzeug/pulse @vielzeug/ripple
sh
npm install @vielzeug/pulse @vielzeug/ripple
sh
yarn add @vielzeug/pulse @vielzeug/ripple

Quick Start

ts
import { createPulse } from '@vielzeug/pulse';
import { effect } from '@vielzeug/ripple';

type ServerEvents = {
  'chat:message': { user: string; text: string };
  'user:joined': { userId: string };
};

type ClientEvents = {
  'chat:send': { text: string };
};

const pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {
  reconnect: { maxAttempts: 5, delay: (n) => Math.min(1000 * 2 ** n, 30_000) },
  heartbeat: true,
});

// Reactive status via ripple signal
effect(() => console.log('connection:', pulse.status.value));

// Typed server events
pulse.on('chat:message', ({ user, text }) => console.log(`${user}: ${text}`));

// Typed client messages
pulse.send('chat:send', { text: 'Hello!' });

// Isolated channel namespace
const notif = pulse.channel<{ alert: { level: string; msg: string } }>('notifications');
notif.on('alert', ({ level, msg }) => showNotification(level, msg));

// Reactive presence tracking
const lobby = pulse.presence<{ name: string; status: string }>('lobby');
effect(() => console.log('online:', [...lobby.state.value.keys()]));
lobby.update({ name: 'Alice', status: 'active' });

// Clean disposal
using _ = pulse;

Features

  • Typed event mapsTServer and TClient generics enforce payload types on both sides of the wire
  • on() / once() / wait() — persistent, one-shot, and async-await event subscriptions
  • channel() — isolated namespaces multiplexed over the shared connection; auto-resubscribed on reconnect; dispose() sends an unsubscribe frame
  • join() / leave() — room membership with server-confirmation promises
  • presence() — reactive Signal<Map<memberId, T>> state, with onJoin/onLeave callbacks and update() for broadcasting state
  • Middleware pipeline — intercept every outgoing send() call; omit next() to suppress
  • Auto-reconnect — exponential backoff (full-jitter by default), configurable maxAttempts, custom delay function, and onReconnect callback
  • Heartbeat — configurable ping/pong keep-alive with dead-connection detection and automatic reconnect trigger
  • Reactive status signal'connecting' | 'open' | 'reconnecting' | 'closed' exposed as a ripple ReadonlySignal
  • Reactive rooms signal — current room membership as a ReadonlySignal<ReadonlySet<string>>
  • disposalSignalAbortSignal that fires on dispose(); ties external cleanup to the connection lifetime
  • dispose() and [Symbol.dispose] — deterministic teardown; closes the socket, clears all listeners, aborts pending wait() calls
  • Protocol-agnostic — works with any WebSocket server that speaks the Pulse JSON frame format
  • Single dependency — only requires @vielzeug/ripple for reactive state

Documentation

See Also

  • Ripple — the reactive signal library powering pulse.status, pulse.rooms, and presence.state
  • Herald — typed in-process event bus; complement Pulse by bridging incoming WebSocket events to application-wide bus dispatches
  • Courier — typed HTTP client for the request/response traffic that runs alongside your WebSocket connection
  • Clockwork — finite state machine; model complex reconnection or auth-handshake logic as a proper state machine