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

peer-client

v2.2.0

Published

Universal WebRTC peer-to-peer library with signaling, rooms, file transfer, state sync, and E2E encryption

Downloads

819

Readme

peer-client

Universal WebRTC peer-to-peer library with signaling, rooms, media, file transfer, state sync, CRDT, offline sync, and end-to-end encryption. Framework-agnostic.

Installation

pnpm install peer-client

Quick Start

import { PeerClient } from 'peer-client';

const client = new PeerClient({ url: 'wss://your-signal-server.com' });
await client.connect();

const peers = await client.join('my-namespace');
const peer = client.connectToPeer(peers[0].fingerprint);
peer.on('connected', () => peer.send({ hello: true }));

Core API

PeerClient

Connection, signaling, namespace management, and matchmaking.

import { PeerClient } from 'peer-client';

Constructor

const client = new PeerClient({
  url: 'wss://signal.example.com',    // required
  alias: 'alice',                      // display name
  meta: { role: 'host' },             // arbitrary metadata
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
  autoReconnect: true,                 // default: true
  reconnectDelay: 1000,                // default: 1000ms
  reconnectMaxDelay: 30000,            // default: 30000ms
  maxReconnectAttempts: 10,            // default: Infinity
  pingInterval: 25000,                 // default: 25000ms
  identityKeys: exportedKeys,          // optional, for persistent identity
});

Methods

| Method | Returns | Description | |---|---|---| | connect() | Promise<void> | Connect to signaling server | | disconnect() | void | Disconnect from server | | join(namespace, appType?, version?) | Promise<PeerInfo[]> | Join namespace, returns existing peers | | leave(namespace) | void | Leave namespace | | discover(namespace, limit?) | Promise<PeerInfo[]> | Discover peers without joining | | match(namespace, criteria?, groupSize?) | Promise<MatchResult> | Matchmaking | | cancelMatch(namespace) | void | Cancel pending match | | connectToPeer(fingerprint, alias?, channelConfig?) | Peer | Establish direct P2P connection | | createPeer(fingerprint, alias) | Peer | Create peer without connecting | | getPeer(fingerprint) | Peer \| undefined | Get existing peer | | closePeer(fingerprint) | void | Close and remove a peer | | relay(to, payload) | void | Send data via server relay | | broadcast(namespace, payload) | void | Broadcast to all peers in namespace | | createRoom(roomId, config?) | Promise<RoomCreatedResult> | Create a managed room | | joinRoom(roomId) | Promise<PeerInfo[]> | Join existing room | | roomInfo(roomId) | Promise<RoomInfoResult> | Get room metadata | | kick(roomId, fingerprint) | void | Kick a peer from room | | updateMetadata(meta) | void | Update peer metadata on server | | getIdentity() | Identity | Get identity instance | | getTransport() | Transport | Get transport instance |

Properties

| Property | Type | Description | |---|---|---| | fingerprint | string | Unique identity fingerprint | | alias | string | Display name | | connected | boolean | Whether WebSocket is connected | | peerMap | ReadonlyMap<string, Peer> | All active peers |

Events

| Event | Callback | Description | |---|---|---| | connected | () | WebSocket connected | | disconnected | () | WebSocket disconnected | | registered | (fingerprint, alias) | Registered with server | | peer_joined | (PeerJoinedInfo) | Peer joined namespace | | peer_left | (fingerprint, namespace) | Peer left namespace | | peer_list | (namespace, PeerInfo[]) | Peer list received | | matched | (MatchResult) | Matchmaking result | | relay | (from, payload) | Relay message received | | broadcast | (from, namespace, payload) | Broadcast received | | error | (Error) | Error occurred | | reconnecting | (attempt, delay) | Reconnecting | | reconnected | () | Reconnected | | room_created | (RoomCreatedResult) | Room created | | room_closed | (result) | Room closed | | kicked | (payload) | Kicked from room |


Peer

Represents a direct P2P connection.

const peer = client.connectToPeer('fp-bob', 'bob');
peer.on('connected', () => {
  peer.send({ hello: true });
  peer.send('text', 'my-channel');
  peer.sendBinary(buffer);
});

Methods

| Method | Returns | Description | |---|---|---| | createOffer(channelConfig?) | Promise<void> | Initiate connection with offer | | handleSignal(payload) | Promise<void> | Handle incoming signal | | send(data, channel?) | void | Send JSON, string, or binary data | | sendBinary(data, channel?) | void | Send ArrayBuffer explicitly | | addStream(stream) | void | Add media stream | | removeStream(stream) | void | Remove media stream | | createDataChannel(config) | RTCDataChannel | Create named data channel | | getChannel(label) | RTCDataChannel \| undefined | Get channel by label | | getBufferedAmount(channel?) | number | Buffered bytes | | restartIce() | void | Restart ICE negotiation | | close() | void | Close connection |

Properties

| Property | Type | Description | |---|---|---| | fingerprint | string | Remote peer fingerprint | | alias | string | Remote peer alias | | connectionState | string | Connection state | | channelLabels | string[] | Open data channel labels | | closed | boolean | Whether connection is closed | | pc | RTCPeerConnection | Underlying RTCPeerConnection |

Events

| Event | Callback | Description | |---|---|---| | connected | () | P2P connection established | | disconnected | (state) | Connection lost | | data | (data, channel) | Data received | | stream | (MediaStream) | Media stream received | | track | (track, streams) | Track received | | datachannel:create | (RTCDataChannel) | Channel created | | datachannel:open | (label, RTCDataChannel) | Channel opened | | datachannel:close | (label) | Channel closed | | error | (Error) | Error occurred |


Rooms

Managed P2P groups with automatic connection and relay fallback.

import { DirectRoom, GroupRoom } from 'peer-client';

DirectRoom (1:1)

const room = new DirectRoom(client, 'room-123');
await room.create(); // or room.join()
room.on('data', (data, from) => {});
room.on('peer_connected', (fingerprint) => {});
room.send({ msg: 'hi' });
room.close();

GroupRoom (N:N)

const room = new GroupRoom(client, 'team-room', 20);
await room.create(); // or room.join()
room.on('data', (data, from) => {});
room.on('peer_joined', (info) => {});
room.on('peer_left', (fingerprint) => {});
room.send({ msg: 'hello' });             // broadcast to all
room.send({ msg: 'dm' }, 'fp-bob');      // to specific peer
room.broadcastViaServer({ msg: 'announcement' });
room.kick('fp-bad-actor');
room.getPeers();       // Map<string, Peer>
room.getPeerCount();   // number
room.close();

Room Events

| Event | Callback | Description | |---|---|---| | data | (data, from) | Data received | | peer_joined | (PeerInfo) | Peer joined room | | peer_left | (fingerprint) | Peer left room | | peer_connected | (fingerprint) | P2P connection established with peer | | closed | () | Room closed | | error | (Error) | Error occurred |


E2EDirectRoom

End-to-end encrypted 1:1 room. Performs automatic ECDH key exchange and encrypts all messages with AES-GCM. Falls back to plaintext relay before keys are exchanged.

import { E2EDirectRoom } from 'peer-client';

const r1 = new E2EDirectRoom(client1, 'secure-room');
await r1.create();

const r2 = new E2EDirectRoom(client2, 'secure-room');
await r2.join();

r1.on('ready', (remoteFingerprint) => {
  r1.send({ secret: 'hello' }); // encrypted
});

r1.on('data', (data, from) => {});
r1.on('decrypt_error', (from, raw) => {});
r1.on('state_changed', (state) => {});

Constructor Options

new E2EDirectRoom(client, roomId, {
  e2e: existingE2EInstance,              // optional, reuse an E2E instance
  roomFactory: (client, id) => room,     // optional, custom DirectRoom factory
});

Methods

| Method | Returns | Description | |---|---|---| | create() | Promise<void> | Create and host the room | | join() | Promise<PeerInfo[]> | Join existing room | | send(data) | void | Send data (encrypted if ready, plain otherwise) | | close() | void | Close room | | hasEncryption() | boolean | Whether encryption is active | | getRoom() | DirectRoom \| null | Underlying DirectRoom | | getE2E() | E2E | Underlying E2E instance |

Properties

| Property | Type | Description | |---|---|---| | state | 'connecting' \| 'exchanging' \| 'ready' \| 'closed' | Current room state | | fingerprint | string | Remote peer's fingerprint |

Events

| Event | Callback | Description | |---|---|---| | ready | (remoteFingerprint) | Key exchange complete, encryption active | | data | (data, from) | Decrypted or plain data received | | decrypt_error | (from, raw) | Decryption failed; re-exchange triggered | | peer_joined | (PeerInfo) | Remote peer joined | | peer_left | (fingerprint) | Remote peer left | | state_changed | (state) | Room state changed | | closed | () | Room closed | | error | (Error) | Error occurred |


Media

Audio/video calls with mute/unmute controls.

import { DirectMedia, GroupMedia } from 'peer-client';

DirectMedia (1:1 Call)

const call = new DirectMedia(client, 'call-room');
const localStream = await call.createAndJoin({ audio: true, video: true });
call.on('remote_stream', (stream, from) => { videoEl.srcObject = stream; });
call.muteAudio();
call.unmuteAudio();
call.muteVideo();
call.unmuteVideo();
call.isAudioMuted();  // boolean
call.isVideoMuted();  // boolean
call.getLocalStream();
call.getRemoteStream();
call.close();

GroupMedia (Conference)

const conf = new GroupMedia(client, 'conf-room');
const { stream, peers } = await conf.joinAndStart({ audio: true, video: true });
conf.on('remote_stream', (stream, fingerprint) => {});
conf.on('remote_stream_removed', (fingerprint) => {});
conf.getRemoteStreams();              // Map<string, MediaStream>
conf.getRemoteStream('fp-alice');    // MediaStream | undefined
conf.getPeerCount();                 // number
conf.kick('fp-bad-actor');
conf.close();

Media Events

| Event | Callback | Description | |---|---|---| | local_stream | (MediaStream) | Local stream acquired | | remote_stream | (MediaStream, fingerprint) | Remote stream received | | remote_stream_removed | (fingerprint) | Remote stream removed | | muted | ('audio' \| 'video') | Track muted | | unmuted | ('audio' \| 'video') | Track unmuted | | error | (Error) | Error occurred | | closed | () | Media session closed |


FileTransfer

Stream files over P2P data channels with backpressure control.

import { FileTransfer } from 'peer-client';

const ft = new FileTransfer(client);

Sending

const peer = client.connectToPeer('fp-receiver');
peer.on('connected', async () => {
  await ft.send(peer, file, 'report.pdf');
});

ft.on('progress', ({ id, percentage, bytesPerSecond }) => {});

Receiving

ft.handleIncoming(peer);

ft.on('incoming', (meta, from) => {
  ft.accept(meta.id);  // or ft.reject(meta.id)
});

ft.on('complete', (id, blob, meta, from) => {
  const url = URL.createObjectURL(blob);
});

Methods

| Method | Returns | Description | |---|---|---| | send(peer, file, filename?) | Promise<string> | Send file, resolves with transfer id | | handleIncoming(peer) | () => void | Listen for incoming transfers, returns cleanup | | accept(id) | void | Accept incoming transfer | | reject(id) | void | Reject incoming transfer | | cancel(id) | void | Cancel active transfer | | requestResume(id, lastIndex) | void | Request resume from chunk index | | getReceiveProgress(id) | TransferProgress \| null | Get receive progress | | destroy() | void | Clean up all listeners |

Events

| Event | Callback | Description | |---|---|---| | incoming | (FileMetadata, from) | Incoming transfer offer | | progress | (TransferProgress) | Transfer progress update | | complete | (id, Blob, FileMetadata, from) | Transfer completed | | cancelled | (id) | Transfer cancelled | | error | (error) | Transfer error |


JSONTransfer

Lightweight JSON messaging over relay or P2P.

import { JSONTransfer } from 'peer-client';

const jt = new JSONTransfer(client);

jt.sendToPeer('fp-bob', { hello: 'world' });    // P2P or relay fallback
jt.sendToRoom('room-id', { broadcast: true });   // broadcast to room

jt.onReceive(peer, (data, from) => {});           // direct P2P
jt.onRelayReceive((data, from) => {});            // relay messages
jt.onBroadcastReceive('room-id', (data, from) => {});  // broadcast messages

ImageTransfer

Convenience wrapper around FileTransfer for images.

import { ImageTransfer } from 'peer-client';

const it = new ImageTransfer(client);
it.handleIncoming(peer);

it.on('incoming', (meta, from) => it.accept(meta.id));
it.on('complete', (id, blob, meta, from) => {});

await it.send(peer, imageBlob, 'photo.jpg');
it.destroy();

StateSync

Distributed key-value state with Hybrid Logical Clocks (HLC).

import { StateSync } from 'peer-client';

Last-Writer-Wins

const sync = new StateSync(client, 'room-1', { mode: 'lww' });
sync.start();
sync.set('score', 100);
sync.get('score');       // 100
sync.getAll();           // { score: 100 }
sync.delete('score');
sync.destroy();

Operational (Custom Merge)

const sync = new StateSync(client, 'room-1', {
  mode: 'operational',
  merge: (local, remote) => [...new Set([...local, ...remote])],
});
sync.start();
sync.set('tags', ['a', 'b']);
sync.on('conflict', (key, local, remote, merged) => {});

Methods

| Method | Returns | Description | |---|---|---| | start() | void | Start sync listeners | | set(key, value) | void | Set a key-value pair | | get(key) | any | Get value by key | | getAll() | Record<string, any> | Get all non-deleted entries | | getState() | Map<string, SyncState> | Get full internal state map | | getHLC() | HLC | Get current HLC | | delete(key) | void | Tombstone delete | | loadState(entries) | void | Bulk load state entries | | handleFullState(state, from) | void | Process a full state snapshot | | requestFullState(fingerprint) | void | Request state from a peer | | destroy() | void | Clean up |

Events

| Event | Callback | Description | |---|---|---| | state_changed | (key, value, from) | State changed | | conflict | (key, local, remote, merged) | Merge conflict (operational mode) | | synced | (from) | Synced with a peer | | error | (Error) | Error occurred |


CRDTSync

Yjs CRDT integration for real-time collaborative editing.

import { CRDTSync } from 'peer-client';
import * as Y from 'yjs';

const crdt = new CRDTSync(client, 'collab-room', Y);
crdt.start();

const map = crdt.getMap('shared');
map.set('title', 'Hello');

const text = crdt.getText('doc');
text.insert(0, 'Hello world');

const arr = crdt.getArray('items');
arr.push(['item1']);

crdt.requestFullState('fp-peer');
crdt.destroy();

Methods

| Method | Returns | Description | |---|---|---| | start() | void | Start CRDT sync | | getDoc() | Y.Doc | Get Yjs document | | getMap(name?) | Y.Map | Get shared map (default: 'shared') | | getText(name?) | Y.Text | Get shared text (default: 'text') | | getArray(name?) | Y.Array | Get shared array (default: 'array') | | requestFullState(fingerprint) | void | Request full state from peer | | destroy() | void | Clean up |

Events

| Event | Callback | Description | |---|---|---| | synced | (from) | Synced with a peer | | error | (Error) | Error occurred |


OfflineSyncRoom

Offline-first key-value sync with IndexedDB persistence, HLC ordering, and optional E2E encryption. Automatically flushes pending operations when connectivity is restored.

import { OfflineSyncRoom } from 'peer-client';

const room = new OfflineSyncRoom(client, 'my-room', {
  dbName: 'my-db',               // default: 'osr-{roomId}'
  encryptionEnabled: true,       // default: true
  maxPendingOps: 10000,          // default: 10000
  syncBatchSize: 50,             // default: 50
  conflictResolution: 'lww',     // 'lww' | 'merge', default: 'lww'
  merge: (local, remote) => ..., // required if conflictResolution is 'merge'
});

await room.init();
await room.createAndJoin();  // or room.joinExisting()

await room.set('key', 'value');
room.get('key');       // 'value'
room.has('key');       // true
room.keys();           // string[]
room.getAll();         // Record<string, any>
room.size;             // number
await room.delete('key');
await room.pendingCount();

room.online;   // boolean
room.ready;    // boolean
room.syncing;  // boolean

room.getE2E();  // E2E instance

await room.close();
await room.destroy();  // also deletes IndexedDB

Events

| Event | Callback | Description | |---|---|---| | ready | () | Store initialized | | state_changed | (key, value, from) | Key changed | | synced | (from) | Synced with a peer | | sync_started | () | Flush started | | sync_complete | () | Flush completed | | online | () | Went online | | offline | () | Went offline | | peer_joined | (PeerJoinedInfo) | Peer joined | | peer_left | (fingerprint) | Peer left | | conflict | (key, local, remote, merged) | Merge conflict | | error | (Error) | Error occurred | | closed | () | Room closed |


E2E Encryption

ECDH key exchange with AES-GCM encryption.

import { GroupKeyManager, E2E } from 'peer-client';

GroupKeyManager

const km = new GroupKeyManager(client);
await km.init();

await km.exchangeWith(peer);

const encrypted = await km.encryptForPeer('fp-bob', { secret: true });
peer.send({ _encrypted: true, data: encrypted });

peer.on('data', async (msg) => {
  if (msg._encrypted) {
    const decrypted = await km.decryptFromPeer(peer.fingerprint, msg.data);
  }
});

km.destroy();

Methods

| Method | Returns | Description | |---|---|---| | init() | Promise<void> | Generate ephemeral key pair | | exchangeWith(peer) | Promise<void> | Initiate key exchange with peer | | handleIncomingKeyExchange(peer, data) | Promise<void> | Handle incoming key exchange | | encryptForPeer(fingerprint, data) | Promise<string> | Encrypt data for peer | | decryptFromPeer(fingerprint, data) | Promise<any> | Decrypt data from peer | | getE2E() | E2E | Get underlying E2E instance | | destroy() | void | Clean up keys |

E2E (Low-Level)

const e2e = new E2E();
await e2e.init();
const pubKey = e2e.getPublicKeyB64();
const pubKeyRaw = e2e.getPublicKeyRaw();
await e2e.deriveKey('fp-bob', remotePubKeyB64);
const encrypted = await e2e.encrypt('fp-bob', 'secret');
const decrypted = await e2e.decrypt('fp-bob', encrypted);
e2e.hasKey('fp-bob');     // true
e2e.removeKey('fp-bob');
e2e.isInitialized();      // boolean
e2e.destroy();

Identity

Persistent ECDSA identity for signing and fingerprinting.

import { Identity } from 'peer-client';

const id = new Identity();
await id.generate();
console.log(id.fingerprint);

const keys = await id.export();
localStorage.setItem('keys', JSON.stringify(keys));

const restored = new Identity();
await restored.restore(JSON.parse(localStorage.getItem('keys')!));

const client = new PeerClient({
  url: 'wss://signal.example.com',
  identityKeys: keys,
});

Methods

| Method | Returns | Description | |---|---|---| | generate() | Promise<string> | Generate new key pair, returns public key B64 | | restore(keys) | Promise<void> | Restore from exported keys | | export() | Promise<IdentityKeys> | Export keys for persistence | | setRegistered(fingerprint, alias) | void | Set server-assigned fingerprint | | sign(data) | Promise<ArrayBuffer> | Sign data with private key | | verify(publicKey, signature, data) | Promise<boolean> | Verify a signature | | getPublicKey() | CryptoKey \| null | Get public CryptoKey | | getPrivateKey() | CryptoKey \| null | Get private CryptoKey |


Emitter

All classes extend Emitter:

const off = emitter.on('event', (...args) => {});   // returns cleanup function
emitter.once('event', (...args) => {});
emitter.off('event', handler);
emitter.emit('event', ...args);
emitter.listenerCount('event');
emitter.removeAllListeners('event');
emitter.removeAllListeners();

import { setEmitterErrorHandler } from 'peer-client';
setEmitterErrorHandler((error, event) => {
  console.error(`Error in ${event}:`, error);
});

Types

import type {
  ClientConfig,
  PeerInfo,
  PeerJoinedInfo,
  MatchResult,
  RoomConfig,
  RoomCreatedResult,
  RoomInfoResult,
  MediaConfig,
  DataChannelConfig,
  FileMetadata,
  TransferProgress,
  TransferControl,
  SyncConfig,
  SyncMode,
  SyncState,
  HLC,
  IdentityKeys,
  OfflineSyncRoomConfig,
  OfflineOperation,
} from 'peer-client';

Key Types

interface PeerInfo {
  fingerprint: string;
  alias: string;
  meta?: Record<string, any>;
  app_type?: string;
}

interface PeerJoinedInfo extends PeerInfo {
  namespace?: string;
}

interface MatchResult {
  namespace: string;
  session_id: string;
  peers: PeerInfo[];
}

interface FileMetadata {
  id: string;
  filename: string;
  size: number;
  mime: string;
  totalChunks: number;
  chunkSize: number;
}

interface IdentityKeys {
  fingerprint: string;
  alias: string;
  publicKeyB64: string;
  privateKeyJwk: JsonWebKey;
  publicKeyJwk: JsonWebKey;
}

interface HLC {
  ts: number;
  counter: number;
  node: string;
}

type SyncMode = 'lww' | 'operational' | 'crdt';

Configuration Reference

| Option | Type | Default | Description | |---|---|---|---| | url | string | required | WebSocket signaling server URL | | iceServers | RTCIceServer[] | Google STUN | ICE/TURN servers | | alias | string | '' | Display name | | meta | Record<string, any> | {} | Arbitrary metadata | | autoReconnect | boolean | true | Auto-reconnect on disconnect | | reconnectDelay | number | 1000 | Initial reconnect delay (ms) | | reconnectMaxDelay | number | 30000 | Max reconnect delay (ms) | | maxReconnectAttempts | number | Infinity | Max reconnect attempts | | pingInterval | number | 25000 | WebSocket keepalive interval (ms) | | identityKeys | IdentityKeys | auto-generated | Pre-existing identity keys |


License

MIT