Migration
Pulse 3.0
Pulse 3.0 removes the Ripple runtime dependency and exposes state through framework-neutral external stores. Pulse 2.0 previously simplified the room/presence API and consolidated its schema generic.
Replace Readable access
status, rooms, and room presence now expose ExternalStore<T>. Replace .value and .peek() reads with getSnapshot().
// Before
console.log(pulse.status.value);
console.log([...room.presence.value]);
// After
console.log(pulse.status.getSnapshot());
console.log([...room.presence.getSnapshot()]);Subscribe to changes with subscribe() and read the current snapshot inside the listener.
const unsubscribe = pulse.status.subscribe(() => {
console.log(pulse.status.getSnapshot());
});Ripple and Flux integration
Pass Pulse stores directly to structural adapters.
import { fromStore } from '@vielzeug/flux';
import { fromSubscribable } from '@vielzeug/ripple';
const statusStream = fromStore(pulse.status);
const presence = fromSubscribable(room.presence, { signal: room.disposalSignal });Pulse 2.0
1. Unified schema generic
Before: Four generic parameters on createPulse().
const pulse = createPulse<TServer, TClient, TChannels, TPresence>('ws://...', { ... });After: One PulseSchema generic.
const pulse = createPulse<{
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 } };
};
}>('ws://...', { ... });2. Rooms and presence unified into room()
Before: Separate join()/leave() methods and a presence API on channels.
const channel = pulse.channel('lobby');
await pulse.join('lobby');
const presence = channel.presence;
presence.update({ name: 'Ada' });
presence.onJoin((id, state) => { ... });
pulse.leave('lobby');After: A single room() method returns a ref-counted room scope. When the room definition includes presence, the scope exposes reactive presence state.
const lobby = pulse.room('lobby');
await lobby.joined;
lobby.updatePresence({ name: 'Ada' });
lobby.onJoin((id, state) => { ... });
lobby.onLeave((id) => { ... });
lobby.dispose();3. pulse.join() and pulse.leave() removed
Use pulse.room(name) to join and scope.dispose() to leave. Room memberships are reference-counted across independent scopes.
4. channel.presence removed
Presence is now a property of room scopes, not channel scopes. Use pulse.room(name).presence instead.
5. PulsePresenceError removed
Room join failures now use standard error types:
PulseConnectionError— transport close before confirmation.PulseRoomTimeoutError— join timeout (new).PulseAbortError— join aborted via AbortSignal.PulseDisposedError— instance disposed before confirmation.
6. pulse.rooms tracks confirmed memberships
Before: pulse.rooms was a signal of room names with presence.
After: pulse.rooms is an ExternalStore<ReadonlySet<string>> tracking confirmed room memberships with or without presence.
const unsubscribe = pulse.rooms.subscribe(() => {
console.log('Joined rooms:', [...pulse.rooms.getSnapshot()]);
});7. PulseRoomTimeoutError added
Room scopes accept a timeout option. If the server does not confirm membership in time, joined rejects with PulseRoomTimeoutError.
const lobby = pulse.room('lobby', { timeout: 5_000 });
try {
await lobby.joined;
} catch (error) {
if (error instanceof PulseRoomTimeoutError) {
// handle timeout
}
}8. RoomScope type
Room scopes are typed as RoomScope<R> where R is the room definition. When R includes presence, the scope is a PresenceRoomScope<T>; otherwise it is a RoomScopeBase.
// lobby has presence → PresenceRoomScope<{ name: string }>
const lobby = pulse.room('lobby');
lobby.updatePresence({ name: 'Ada' }); // typed
// announcements has no presence → RoomScopeBase
const announcements = pulse.room('announcements');
// announcements.updatePresence — TypeScript errorMigration checklist
- Replace
createPulse<TServer, TClient, TChannels, TPresence>withcreatePulse<PulseSchema>. - Replace
pulse.join(name)/pulse.leave(name)withpulse.room(name)/scope.dispose(). - Replace
channel.presencewithpulse.room(name).presence. - Replace
PulsePresenceErrorhandling with the appropriate new error type. - Update
pulse.roomsconsumers to read from theExternalStore<ReadonlySet<string>>. - Add
timeoutandsignaloptions to room scopes where appropriate.