@pulsoid/socket
v3.0.0
Published
Zero dependency WebSocket client for Pulsoid heart rate API
Readme
@pulsoid/socket
Zero-dependency WebSocket client for consuming real-time heart rate data from Pulsoid.
Supports two modes:
- Standard — stream your own heart rate
- Room — stream heart rate data from all members of a shared room
Live Demo · API Docs · Discord
Table of contents
- Getting started
- Basic usage
- Room mode
- Using CDN
- API
- Events
- Types
- Reconnection
- Connection lifecycle
- Error handling
- Examples
- Links
Getting started
Obtain a token
Just want to test or use it for yourself? Use Manual Token Issuing — no client credentials needed. Create a token in seconds and start receiving your heart rate data right away.
For production apps, choose the flow that fits your use case:
| Use case | Recommended flow | | --- | --- | | Personal use / testing | Manual Token Issuing (no client credentials needed) | | Websites | Implicit Grant | | Desktop apps with deep links | Implicit Grant | | Backend servers | Authorization Code Grant | | Plugins / desktop apps | Device Authorization Flow | | Enterprise | Contact [email protected] or Discord |
For flows other than manual issuing, create API client credentials at pulsoid.net/ui/api-clients.
Required scopes:
| Mode | Scope |
| --- | --- |
| Standard | data:heart_rate:read |
| Room | data:room:read |
Install
npm install @pulsoid/socketBasic usage
import PulsoidSocket from '@pulsoid/socket';
const pulsocket = PulsoidSocket.create('YOUR_ACCESS_TOKEN');
pulsocket.on('open', () => {
console.log('Connected');
});
pulsocket.on('heart-rate', (data) => {
console.log(`Heart rate: ${data.heartRate} BPM`);
});
pulsocket.on('online', () => {
console.log('Heart rate monitor is sending data');
});
pulsocket.on('offline', () => {
console.log('No data from monitor for 30 seconds');
});
pulsocket.on('close', () => {
console.log('Disconnected');
});
// connect() validates the token then opens the WebSocket
await pulsocket.connect();
// later…
pulsocket.disconnect();Room mode
Rooms allow multiple users to stream their heart rate data to a shared channel. All participants can see each other's heart rate in real time. Each event includes a profileId so you can identify individual members.
import { PulsoidSocket } from '@pulsoid/socket';
// or: import { PulsoidRoomSocket } from '@pulsoid/socket';
const room = PulsoidSocket.createRoom('YOUR_ACCESS_TOKEN', 'ROOM_ID', {
// optional: filter which event kinds to subscribe to (default: all)
kinds: ['heart_rate', 'room_member_updated'],
});
room.on('heart-rate', (data) => {
console.log(`${data.profileId}: ${data.bpm} BPM`);
});
room.on('room-member-updated', (data) => {
console.log(`Member ${data.profileId} config updated`, data.config);
});
room.on('room-member-removed', (data) => {
console.log(`Member ${data.profileId} left`);
});
room.on('room-updated', (data) => {
console.log(`Room ${data.roomId} config updated`, data.config);
});
await room.connect();Room event kinds
When creating a room socket, you can filter which event kinds to subscribe to using the kinds option. By default all kinds are subscribed.
| Kind | Description |
| --- | --- |
| 'heart_rate' | Heart rate updates from room members |
| 'room_member_updated' | A member's configuration was changed |
| 'room_member_removed' | A member was removed from the room |
| 'room_updated' | The room's configuration was changed |
Using CDN
<script crossorigin src="https://unpkg.com/@pulsoid/socket@2/dist/index.cjs.js"></script>const pulsocket = PulsoidSocket.create('YOUR_ACCESS_TOKEN');
pulsocket.on('heart-rate', (data) => {
console.log(`Heart rate: ${data.heartRate}`);
});
pulsocket.connect();API
PulsoidSocket
Static methods
| Method | Returns | Description |
| --- | --- | --- |
| PulsoidSocket.create(token, options?) | PulsoidSocket | Create a standard socket instance. Takes a token string and optional PulsoidSocketOptions. |
| PulsoidSocket.createRoom(token, roomId, options?) | PulsoidRoomSocket | Create a room socket instance. Takes a token string, room ID string, and optional PulsoidRoomSocketOptions. |
Instance methods
| Method | Returns | Description |
| --- | --- | --- |
| connect() | Promise<void> | Validate the token via HTTP, then open the WebSocket. Rejects with PulsoidTokenError if the token is invalid, expired, or missing the required scope. No-ops if already connected or connecting. |
| disconnect() | void | Close the WebSocket connection, cancel any pending reconnection timers, and stop auto-reconnect. |
| on(event, callback) | void | Add an event listener. See Events for typed signatures. |
| off(event, callback?) | void | Remove a specific listener, or all listeners for the event if no callback is given. |
| isConnected() | boolean | Returns true if the WebSocket is in the OPEN state. |
| isOnline() | boolean | Returns true if the heart rate monitor is actively sending data. Becomes false after 30 seconds of silence. Standard mode only. |
PulsoidRoomSocket
Created via PulsoidSocket.createRoom() or imported directly:
import { PulsoidRoomSocket } from '@pulsoid/socket';
const room = PulsoidRoomSocket.create(token, roomId, options?);Static methods
| Method | Returns | Description |
| --- | --- | --- |
| PulsoidRoomSocket.create(token, roomId, options?) | PulsoidRoomSocket | Create a room socket instance. Takes a token string, room ID string, and optional PulsoidRoomSocketOptions. |
Instance methods
| Method | Returns | Description |
| --- | --- | --- |
| connect() | Promise<void> | Validate the token via HTTP, then open the WebSocket. Rejects with PulsoidTokenError on failure. |
| disconnect() | void | Close the connection and stop auto-reconnect. |
| on(event, callback) | void | Add an event listener. See Room socket events. |
| off(event, callback?) | void | Remove a specific listener, or all listeners for the event. |
| isConnected() | boolean | Returns true if the WebSocket is in the OPEN state. |
PulsoidRoomSocketdoes not haveisOnline()— there is no single monitor to track since rooms have multiple members.
Events
Standard socket events
| Event | Callback signature | Description |
| --- | --- | --- |
| 'open' | (event: Event) => void | WebSocket connection established. |
| 'heart-rate' | (data: PulsoidHeartRateMessage) => void | Heart rate data received. data.heartRate is BPM, data.measuredAt is a Unix timestamp. |
| 'online' | () => void | Heart rate monitor started sending data. Fires on the first message received. |
| 'offline' | () => void | No data received from the monitor for 30 seconds (debounced). Also fires immediately when the connection closes if the monitor was online. |
| 'close' | (event: CloseEvent) => void | Connection fully closed. Only fires after all reconnection attempts have ended (or if reconnect is disabled). Never fires during reconnection. |
| 'error' | (event: Event) => void | WebSocket error occurred. |
| 'reconnect' | (e: { attempt: number }) => void | Auto-reconnect attempt starting. attempt is 1-indexed. |
| 'token-error' | (e: PulsoidTokenError) => void | Token validation failed during a reconnect attempt. Non-retriable errors (forbidden, insufficient_scope) stop reconnection; retriable errors (network_error, payment_required, unknown) keep retrying. See Error types. |
Room socket events
Shares open, close, error, reconnect, and token-error with the standard socket (same signatures). Room-specific events:
| Event | Callback signature | Description |
| --- | --- | --- |
| 'heart-rate' | (data: PulsoidRoomHeartRate) => void | Heart rate from a room member. data.profileId identifies the member, data.bpm is the heart rate, data.timestamp is an ISO 8601 string. |
| 'room-member-updated' | (data: PulsoidRoomMemberUpdated) => void | A member's configuration was updated. data.config is an arbitrary key-value object. |
| 'room-member-removed' | (data: PulsoidRoomMemberRemoved) => void | A member was removed from the room. |
| 'room-updated' | (data: PulsoidRoomUpdated) => void | The room configuration was updated. data.roomId identifies the room, data.config is an arbitrary key-value object. |
Room sockets do not emit
onlineorofflineevents.
Types
All types are exported from the package and can be imported for use with TypeScript:
import type {
PulsoidHeartRateMessage,
PulsoidSocketOptions,
PulsoidSocketEventType,
PulsoidTokenError,
PulsoidTokenErrorType,
ReconnectOptions,
PulsoidRoomEventKind,
PulsoidRoomHeartRate,
PulsoidRoomMemberUpdated,
PulsoidRoomMemberRemoved,
PulsoidRoomUpdated,
PulsoidRoomSocketOptions,
PulsoidRoomSocketEventType,
} from '@pulsoid/socket';Options
PulsoidSocketOptions
type PulsoidSocketOptions = {
reconnect?: ReconnectOptions;
};PulsoidRoomSocketOptions
type PulsoidRoomSocketOptions = {
kinds?: PulsoidRoomEventKind[]; // default: all kinds
reconnect?: ReconnectOptions;
};ReconnectOptions
type ReconnectOptions = {
enable?: boolean; // default: true
reconnectMinInterval?: number; // default: 2000 ms
reconnectMaxInterval?: number; // default: 10000 ms
reconnectAttempts?: number; // default: 100
};Reconnect interval uses exponential backoff: Math.min(maxInterval, minInterval * 2^attempt)
PulsoidRoomEventKind
type PulsoidRoomEventKind =
| 'heart_rate'
| 'room_member_updated'
| 'room_member_removed'
| 'room_updated';Data types
PulsoidHeartRateMessage
Emitted by the standard socket's 'heart-rate' event.
type PulsoidHeartRateMessage = {
heartRate: number; // beats per minute
measuredAt: number; // Unix timestamp (milliseconds)
};PulsoidRoomHeartRate
Emitted by the room socket's 'heart-rate' event.
type PulsoidRoomHeartRate = {
profileId: string; // unique member identifier
bpm: number; // beats per minute
timestamp: string; // ISO 8601 timestamp
};PulsoidRoomMemberUpdated
Emitted by the room socket's 'room-member-updated' event.
type PulsoidRoomMemberUpdated = {
profileId: string; // unique member identifier
config: Record<string, unknown>; // arbitrary configuration object
timestamp: string; // ISO 8601 timestamp
};PulsoidRoomMemberRemoved
Emitted by the room socket's 'room-member-removed' event.
type PulsoidRoomMemberRemoved = {
profileId: string; // unique member identifier
timestamp: string; // ISO 8601 timestamp
};PulsoidRoomUpdated
Emitted by the room socket's 'room-updated' event.
type PulsoidRoomUpdated = {
roomId: string; // room identifier
config: Record<string, unknown>; // arbitrary configuration object
timestamp: string; // ISO 8601 timestamp
};Event types
Union types representing all valid event names for each socket type. Useful for generic event handling code.
PulsoidSocketEventType
type PulsoidSocketEventType =
| 'open'
| 'heart-rate'
| 'error'
| 'close'
| 'online'
| 'offline'
| 'reconnect'
| 'token-error';PulsoidRoomSocketEventType
type PulsoidRoomSocketEventType =
| 'open'
| 'close'
| 'error'
| 'reconnect'
| 'token-error'
| 'heart-rate'
| 'room-member-updated'
| 'room-member-removed'
| 'room-updated';Error types
PulsoidTokenErrorType
type PulsoidTokenErrorType =
| 'unauthorized' // 401 — missing or malformed token
| 'forbidden' // 403 — invalid, expired, or revoked token
| 'payment_required' // 402 — subscription required
| 'network_error' // fetch failed (no internet, DNS, timeout)
| 'insufficient_scope' // token is missing the required scope
| 'unknown'; // unexpected HTTP statusPulsoidTokenError
Thrown by connect() and emitted by the 'token-error' event during reconnection.
type PulsoidTokenError = {
type: PulsoidTokenErrorType;
code: number;
message: string;
};| Type | Code | Message | Retriable | Description |
| --- | --- | --- | --- | --- |
| 'unauthorized' | varies | varies | No | 401 — missing or malformed token |
| 'forbidden' | 7005 | token_not_found | No | Token is invalid or does not exist |
| 'forbidden' | 7006 | token_expired | No | Token has expired |
| 'forbidden' | 7007 | premium_required | No | Token rejected by server |
| 'payment_required' | varies | varies | Yes | 402 — subscription/payment required |
| 'insufficient_scope' | 7008 | insufficient_scope | No | Token is missing the required scope (data:heart_rate:read or data:room:read) |
| 'network_error' | 0 | Network request failed... | Yes | No internet connection or network failure |
| 'unknown' | varies | varies | Yes | Unexpected HTTP error |
Retriable errors (network_error, payment_required, unknown) will keep reconnecting during auto-reconnect. Non-retriable errors (unauthorized, forbidden, insufficient_scope) stop reconnection immediately.
Reconnection
Auto-reconnection is enabled by default with exponential backoff.
Default behavior
| Setting | Default |
| --- | --- |
| Enabled | true |
| Min interval | 2000 ms |
| Max interval | 10000 ms |
| Max attempts | 100 |
Reconnection flow
- The WebSocket closes unexpectedly.
- The library waits for
Math.min(maxInterval, minInterval * 2^attempt)milliseconds. - A
'reconnect'event is emitted with{ attempt }(1-indexed). - The token is re-validated via HTTP.
- If the token is valid, a new WebSocket connection opens.
- If validation fails, a
'token-error'event is emitted.- Retriable errors (
network_error,payment_required,unknown) — reconnection continues. - Non-retriable errors (
forbidden,insufficient_scope) — reconnection stops.
- Retriable errors (
- If the attempt limit is reached, reconnection stops.
- On successful reconnection, the attempt counter resets.
Backoff timing examples
| Attempt | Interval (default settings) |
| --- | --- |
| 1 | min(10000, 2000 * 2^1) = 4000 ms |
| 2 | min(10000, 2000 * 2^2) = 8000 ms |
| 3 | min(10000, 2000 * 2^3) = 10000 ms (capped) |
| 4+ | 10000 ms (stays at max) |
Disabling reconnection
const socket = PulsoidSocket.create(token, {
reconnect: { enable: false },
});Custom reconnection settings
const socket = PulsoidSocket.create(token, {
reconnect: {
enable: true,
reconnectMinInterval: 1000, // start retrying after 1s
reconnectMaxInterval: 30000, // cap at 30s
reconnectAttempts: 10, // give up after 10 attempts
},
});Monitoring reconnection
socket.on('reconnect', (e) => {
console.log(`Reconnect attempt #${e.attempt}`);
});
socket.on('token-error', (e) => {
console.error(`Token error [${e.type}]: ${e.code} — ${e.message}`);
// Retriable errors (network_error, payment_required, unknown) will keep reconnecting.
// Non-retriable errors (forbidden, insufficient_scope) stop reconnection.
});Connection lifecycle
Standard socket
create() → connect() → [token validation] → [WebSocket open]
│
┌──────────────┤
▼ ▼
'open' 'token-error'
│ (connect rejects)
▼
'heart-rate' ──→ 'online'
│
(30s silence)
│
▼
'offline'
│
(connection lost)
│
┌──────────┴──────────┐
▼ ▼
(reconnect on) (reconnect off)
│ │
▼ ▼
'reconnect' 'close'
│
[token validation]
│
┌────┴────┐
▼ ▼
'open' 'token-error'
┌────┴────┐
▼ ▼
(retriable) (non-retriable)
continues 'close'
Event sequence as regexp:
open (reconnect (token-error | open))* closeisOnline() behavior (standard mode only)
- Returns
falseinitially. - Becomes
trueon the first'heart-rate'message (emits'online'). - Stays
trueas long as messages keep arriving. - Reverts to
falseafter 30 seconds of silence (emits'offline'). - Immediately reverts to
falseif the connection closes while online.
disconnect() behavior
Calling disconnect():
- Cancels any pending reconnection timers.
- Disables auto-reconnect for this connection.
- Closes the WebSocket (or finalizes if already closed during reconnection).
- The
'close'event fires.
Error handling
During initial connection
connect() returns a Promise that rejects with a PulsoidTokenError if the token is invalid:
try {
await socket.connect();
} catch (error) {
// error is PulsoidTokenError
console.error(`Connection failed [${error.type}]: ${error.code} — ${error.message}`);
switch (error.type) {
case 'unauthorized':
console.error('Missing or malformed token.');
break;
case 'forbidden':
console.error('Token is invalid, expired, or revoked.');
break;
case 'payment_required':
console.error('A Pulsoid subscription is required.');
break;
case 'insufficient_scope':
console.error('Token is missing the required scope.');
break;
case 'network_error':
console.error('No internet connection.');
break;
case 'unknown':
console.error('Unexpected server error.');
break;
}
}During reconnection
If the token becomes invalid while reconnecting, the error is emitted as a 'token-error' event instead of rejecting a promise:
socket.on('token-error', (error) => {
// Reconnection has stopped — handle the error
console.error(`Token error during reconnect: ${error.code} — ${error.message}`);
});WebSocket errors
WebSocket-level errors are emitted via the 'error' event. These typically trigger auto-reconnection (the 'close' event is deferred until reconnection ends):
socket.on('error', (event) => {
console.error('WebSocket error', event);
});Malformed messages
Malformed or missing data in incoming messages is handled internally — a warning is logged to the console and the message is silently skipped. No 'heart-rate' event is emitted for invalid messages.
Examples
Standard mode with full event handling
import PulsoidSocket, { type PulsoidHeartRateMessage } from '@pulsoid/socket';
const socket = PulsoidSocket.create('YOUR_TOKEN');
socket.on('open', () => console.log('Connected'));
socket.on('close', () => console.log('Disconnected'));
socket.on('error', (e) => console.error('WebSocket error', e));
socket.on('online', () => console.log('Monitor online'));
socket.on('offline', () => console.log('Monitor offline (30s silence)'));
socket.on('reconnect', (e) => console.log(`Reconnecting, attempt #${e.attempt}`));
socket.on('token-error', (e) => console.error(`Token error: ${e.code} ${e.message}`));
socket.on('heart-rate', (data: PulsoidHeartRateMessage) => {
console.log(`${data.heartRate} BPM at ${new Date(data.measuredAt).toISOString()}`);
});
try {
await socket.connect();
} catch (error) {
console.error('Failed to connect:', error);
}
// Check connection state
console.log('Connected:', socket.isConnected());
console.log('Online:', socket.isOnline());
// Clean up when done
socket.disconnect();Room mode with member tracking
import { PulsoidRoomSocket, type PulsoidRoomHeartRate } from '@pulsoid/socket';
const members = new Map<string, number>(); // profileId → latest BPM
const room = PulsoidRoomSocket.create('YOUR_TOKEN', 'ROOM_ID');
room.on('heart-rate', (data: PulsoidRoomHeartRate) => {
members.set(data.profileId, data.bpm);
console.log(`[${data.timestamp}] ${data.profileId}: ${data.bpm} BPM`);
});
room.on('room-member-removed', (data) => {
members.delete(data.profileId);
console.log(`${data.profileId} left the room`);
});
room.on('room-member-updated', (data) => {
console.log(`${data.profileId} config:`, data.config);
});
room.on('room-updated', (data) => {
console.log(`Room ${data.roomId} config:`, data.config);
});
await room.connect();Room mode with filtered event kinds
Subscribe only to heart rate updates to reduce traffic:
const room = PulsoidSocket.createRoom('YOUR_TOKEN', 'ROOM_ID', {
kinds: ['heart_rate'],
});
room.on('heart-rate', (data) => {
console.log(`${data.profileId}: ${data.bpm} BPM`);
});
// room-member-updated, room-member-removed, room-updated events
// will NOT be received since those kinds weren't subscribed to
await room.connect();Managing event listeners
const socket = PulsoidSocket.create(token);
const handler = (data) => console.log(data.heartRate);
// Add listener
socket.on('heart-rate', handler);
// Remove specific listener
socket.off('heart-rate', handler);
// Remove ALL listeners for an event
socket.off('heart-rate');Module formats
The library ships in three formats:
| Format | File | Usage |
| --- | --- | --- |
| ES Module | dist/index.es.js | import (bundlers, modern Node.js) |
| CommonJS | dist/index.cjs.js | require() (Node.js) |
| UMD | dist/index.umd.js | <script> tag (exposes global PulsoidSocket) |
TypeScript declarations are included at dist/index.d.ts.
Links
- Pulsoid — official website
- API Documentation — how to obtain tokens and full API reference
- Discord — community support
- GitHub — source code and issues
- Live Demo — try it in the browser
License
MIT
