npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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/socket

Basic 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. |

PulsoidRoomSocket does not have isOnline() — 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 online or offline events.


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 status

PulsoidTokenError

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

  1. The WebSocket closes unexpectedly.
  2. The library waits for Math.min(maxInterval, minInterval * 2^attempt) milliseconds.
  3. A 'reconnect' event is emitted with { attempt } (1-indexed).
  4. The token is re-validated via HTTP.
  5. If the token is valid, a new WebSocket connection opens.
  6. 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.
  7. If the attempt limit is reached, reconnection stops.
  8. 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))* close

isOnline() behavior (standard mode only)

  • Returns false initially.
  • Becomes true on the first 'heart-rate' message (emits 'online').
  • Stays true as long as messages keep arriving.
  • Reverts to false after 30 seconds of silence (emits 'offline').
  • Immediately reverts to false if the connection closes while online.

disconnect() behavior

Calling disconnect():

  1. Cancels any pending reconnection timers.
  2. Disables auto-reconnect for this connection.
  3. Closes the WebSocket (or finalizes if already closed during reconnection).
  4. 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

License

MIT