Version v3.0.0 Size 4.6 KB gzip Dependencies Zero dependencies
Why Pulse?
Native WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.
ts
// Before
const socket = new WebSocket('wss://api.example.com/ws');
socket.addEventListener('message', (event) => route(JSON.parse(event.data)));
socket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));
// After
const pulse = createPulse<{ server: { 'chat:message': { text: string } }; client: { 'chat:send': { text: string } } }>(
'wss://api.example.com/ws',
{ reconnect: true },
);
try {
await pulse.connect();
pulse.on('chat:message', (message) => console.log(message.text));
pulse.send('chat:send', { text: 'Hello!' });
} catch (error) {
console.error('Pulse connection failed:', error);
}| Feature | Pulse | Native WebSocket | socket.io-client |
|---|---|---|---|
| Bundle size | 4.6 KB | 0 B | ~44 kB gzip |
| Explicit readiness | Manual | ||
| Session restoration | Protocol-specific | ||
| Typed scoped channels | Basic | ||
| Typed rooms with presence | |||
| Zero runtime dependencies |
Use Pulse when you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.
Consider native WebSocket when a single untyped connection does not need retry, routing, or session restoration.
Installation
sh
pnpm add @vielzeug/pulsesh
npm install @vielzeug/pulsesh
yarn add @vielzeug/pulseQuick Start
Define the protocol schema at construction time, create scopes, then connect before sending.
ts
import { createPulse } from '@vielzeug/pulse';
type Schema = {
server: { 'chat:message': { text: string } };
client: { 'chat:send': { text: string } };
channels: {
chat: {
client: { send: { text: string } };
server: { message: { text: string } };
};
};
rooms: {
lobby: { presence: { name: string } };
};
};
const pulse = createPulse<Schema>('wss://api.example.com/ws', {
reconnect: true,
});
pulse.tap((event) => {
if (event.type === 'error') console.error(event.error);
if (event.type === 'status-change') console.log('status:', event.status);
});
const chat = pulse.channel('chat');
const lobby = pulse.room('lobby');
try {
await pulse.connect();
chat.send('send', { text: 'Hello!' });
await lobby.joined;
lobby.updatePresence({ name: 'Ada' });
} catch (error) {
console.error('Pulse connection failed:', error);
}
pulse.dispose();Features
connect()— explicit readiness; application messages throw while disconnected.channel()— named, schema-bound scopes with independent disposal and reference-counted server subscriptions.room()— named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sendsjoin; the last disposal sendsleave.reconnect— ordered restoration of channel subscriptions, room memberships, and local presence state.transform— one synchronous transform or filter for application messages.tap()— subscribe to lifecycle events (status changes, errors, disposal) via a typedPulseEventstream.heartbeat— ping/pong liveness detection that uses the same reconnect controller.statusandrooms— framework-neutral external stores for transport and confirmed membership state.