peer-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1086 -0
  3. package/dist/core/client.d.ts +41 -0
  4. package/dist/core/client.js +361 -0
  5. package/dist/core/emitter.d.ts +11 -0
  6. package/dist/core/emitter.js +46 -0
  7. package/dist/core/identity.d.ts +15 -0
  8. package/dist/core/identity.js +54 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/peer.d.ts +29 -0
  12. package/dist/core/peer.js +234 -0
  13. package/dist/core/transport.d.ts +35 -0
  14. package/dist/core/transport.js +174 -0
  15. package/dist/core/types.d.ts +173 -0
  16. package/dist/core/types.js +28 -0
  17. package/dist/crypto.d.ts +32 -0
  18. package/dist/crypto.js +168 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +11 -0
  21. package/dist/media.d.ts +63 -0
  22. package/dist/media.js +275 -0
  23. package/dist/react/Audio.d.ts +6 -0
  24. package/dist/react/Audio.js +18 -0
  25. package/dist/react/PeerProvider.d.ts +17 -0
  26. package/dist/react/PeerProvider.js +46 -0
  27. package/dist/react/PeerStatus.d.ts +7 -0
  28. package/dist/react/PeerStatus.js +20 -0
  29. package/dist/react/TransferProgress.d.ts +10 -0
  30. package/dist/react/TransferProgress.js +17 -0
  31. package/dist/react/Video.d.ts +7 -0
  32. package/dist/react/Video.js +18 -0
  33. package/dist/react/index.d.ts +19 -0
  34. package/dist/react/index.js +18 -0
  35. package/dist/react/useBroadcast.d.ts +12 -0
  36. package/dist/react/useBroadcast.js +21 -0
  37. package/dist/react/useCRDT.d.ts +8 -0
  38. package/dist/react/useCRDT.js +37 -0
  39. package/dist/react/useE2E.d.ts +11 -0
  40. package/dist/react/useE2E.js +62 -0
  41. package/dist/react/useFileTransfer.d.ts +24 -0
  42. package/dist/react/useFileTransfer.js +133 -0
  43. package/dist/react/useIdentity.d.ts +9 -0
  44. package/dist/react/useIdentity.js +63 -0
  45. package/dist/react/useMatch.d.ts +11 -0
  46. package/dist/react/useMatch.js +33 -0
  47. package/dist/react/useMedia.d.ts +13 -0
  48. package/dist/react/useMedia.js +89 -0
  49. package/dist/react/useNamespace.d.ts +7 -0
  50. package/dist/react/useNamespace.js +38 -0
  51. package/dist/react/usePeer.d.ts +6 -0
  52. package/dist/react/usePeer.js +49 -0
  53. package/dist/react/usePeerClient.d.ts +7 -0
  54. package/dist/react/usePeerClient.js +5 -0
  55. package/dist/react/useRelay.d.ts +11 -0
  56. package/dist/react/useRelay.js +19 -0
  57. package/dist/react/useRoom.d.ts +17 -0
  58. package/dist/react/useRoom.js +67 -0
  59. package/dist/react/useSync.d.ts +8 -0
  60. package/dist/react/useSync.js +45 -0
  61. package/dist/room.d.ts +44 -0
  62. package/dist/room.js +246 -0
  63. package/dist/sync.d.ts +45 -0
  64. package/dist/sync.js +333 -0
  65. package/dist/transfer.d.ts +49 -0
  66. package/dist/transfer.js +454 -0
  67. package/package.json +76 -0
package/README.md ADDED
@@ -0,0 +1,1086 @@
1
+ # peer-client
2
+
3
+ Universal WebRTC peer-to-peer library with signaling, rooms, media, file transfer, state sync, CRDT, and end-to-end encryption. Framework-agnostic core with first-class React bindings.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm install peer-client
9
+ ```
10
+
11
+ For React hooks and components:
12
+
13
+ ```bash
14
+ pnpm install peer-client react
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### Vanilla / Any Framework
20
+
21
+ ```ts
22
+ import { PeerClient } from 'peer-client';
23
+
24
+ const client = new PeerClient({ url: 'wss://your-signal-server.com' });
25
+ await client.connect();
26
+
27
+ const peers = await client.join('my-namespace');
28
+ const peer = client.connectToPeer(peers[0].fingerprint);
29
+ peer.on('connected', () => peer.send({ hello: true }));
30
+ ```
31
+
32
+ ### React
33
+
34
+ ```tsx
35
+ import { PeerProvider, useRoom } from 'peer-client/react';
36
+
37
+ function App() {
38
+ return (
39
+ <PeerProvider config={{ url: 'wss://your-signal-server.com' }}>
40
+ <Chat />
41
+ </PeerProvider>
42
+ );
43
+ }
44
+
45
+ function Chat() {
46
+ const { joined, messages, send } = useRoom('chat-room', 'group');
47
+ return (
48
+ <div>
49
+ {messages.map((m, i) => <p key={i}>{m.data}</p>)}
50
+ <button onClick={() => send('hello')}>Send</button>
51
+ </div>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Core API (Framework-Agnostic)
59
+
60
+ ### PeerClient
61
+
62
+ Connection, signaling, namespace management, and matchmaking.
63
+
64
+ ```ts
65
+ import { PeerClient } from 'peer-client';
66
+ ```
67
+
68
+ #### Constructor
69
+
70
+ ```ts
71
+ const client = new PeerClient({
72
+ url: 'wss://signal.example.com', // required
73
+ alias: 'alice', // display name
74
+ meta: { role: 'host' }, // arbitrary metadata
75
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
76
+ autoReconnect: true, // default: true
77
+ reconnectDelay: 1000, // default: 1000ms
78
+ reconnectMaxDelay: 30000, // default: 30000ms
79
+ maxReconnectAttempts: 10, // default: Infinity
80
+ pingInterval: 25000, // default: 25000ms
81
+ identityKeys: exportedKeys, // optional, for persistent identity
82
+ });
83
+ ```
84
+
85
+ #### Methods
86
+
87
+ | Method | Returns | Description |
88
+ |---|---|---|
89
+ | `connect()` | `Promise<void>` | Connect to signaling server |
90
+ | `disconnect()` | `void` | Disconnect from server |
91
+ | `join(namespace, appType?, version?)` | `Promise<PeerInfo[]>` | Join namespace, returns existing peers |
92
+ | `leave(namespace)` | `void` | Leave namespace |
93
+ | `discover(namespace, limit?)` | `Promise<PeerInfo[]>` | Discover peers without joining |
94
+ | `match(namespace, criteria?, groupSize?)` | `Promise<MatchResult>` | Matchmaking |
95
+ | `connectToPeer(fingerprint, alias?, channelConfig?)` | `Peer` | Establish direct P2P connection |
96
+ | `getPeer(fingerprint)` | `Peer \| undefined` | Get existing peer |
97
+ | `relay(to, payload)` | `void` | Send data via server relay |
98
+ | `broadcast(namespace, payload)` | `void` | Broadcast to all peers in namespace |
99
+ | `createRoom(roomId, config?)` | `Promise<RoomCreatedResult>` | Create a managed room |
100
+ | `joinRoom(roomId)` | `Promise<PeerInfo[]>` | Join existing room |
101
+ | `roomInfo(roomId)` | `Promise<RoomInfoResult>` | Get room metadata |
102
+ | `kick(roomId, fingerprint)` | `void` | Kick a peer from room |
103
+ | `getIdentity()` | `Identity` | Get identity instance |
104
+ | `getTransport()` | `Transport` | Get transport instance |
105
+
106
+ #### Properties
107
+
108
+ | Property | Type | Description |
109
+ |---|---|---|
110
+ | `fingerprint` | `string` | Unique identity fingerprint |
111
+ | `alias` | `string` | Display name |
112
+
113
+ #### Events
114
+
115
+ | Event | Callback | Description |
116
+ |---|---|---|
117
+ | `connected` | `()` | WebSocket connected |
118
+ | `disconnected` | `()` | WebSocket disconnected |
119
+ | `registered` | `(fingerprint, alias)` | Registered with server |
120
+ | `peer_joined` | `(PeerInfo)` | Peer joined namespace |
121
+ | `peer_left` | `(fingerprint)` | Peer left namespace |
122
+ | `peer_list` | `(PeerInfo[])` | Peer list received |
123
+ | `matched` | `(MatchResult)` | Matchmaking result |
124
+ | `relay` | `(from, payload)` | Relay message received |
125
+ | `broadcast` | `(from, namespace, payload)` | Broadcast received |
126
+ | `error` | `(Error)` | Error occurred |
127
+ | `reconnecting` | `(attempt)` | Reconnecting |
128
+ | `reconnected` | `()` | Reconnected |
129
+ | `room_created` | `(RoomCreatedResult)` | Room created |
130
+ | `room_closed` | `(roomId)` | Room closed |
131
+ | `kicked` | `(roomId)` | Kicked from room |
132
+
133
+ ---
134
+
135
+ ### Peer
136
+
137
+ Represents a direct P2P connection with another client.
138
+
139
+ ```ts
140
+ const peer = client.connectToPeer('fp-bob', 'bob');
141
+ peer.on('connected', () => {
142
+ peer.send({ hello: true });
143
+ peer.send('binary-channel-data', 'my-channel');
144
+ peer.sendBinary(buffer);
145
+ });
146
+ ```
147
+
148
+ #### Methods
149
+
150
+ | Method | Returns | Description |
151
+ |---|---|---|
152
+ | `send(data, channel?)` | `void` | Send JSON or string data |
153
+ | `sendBinary(data, channel?)` | `void` | Send ArrayBuffer |
154
+ | `addStream(stream)` | `void` | Add media stream |
155
+ | `removeStream(stream)` | `void` | Remove media stream |
156
+ | `createDataChannel(config)` | `RTCDataChannel` | Create named data channel |
157
+ | `getChannel(label)` | `RTCDataChannel \| undefined` | Get channel by label |
158
+ | `getBufferedAmount(channel?)` | `number` | Buffered bytes |
159
+ | `restartIce()` | `void` | Restart ICE negotiation |
160
+ | `close()` | `void` | Close connection |
161
+
162
+ #### Properties
163
+
164
+ | Property | Type | Description |
165
+ |---|---|---|
166
+ | `fingerprint` | `string` | Remote peer fingerprint |
167
+ | `alias` | `string` | Remote peer alias |
168
+ | `connectionState` | `string` | ICE connection state |
169
+ | `channelLabels` | `string[]` | Open data channel labels |
170
+ | `closed` | `boolean` | Whether connection is closed |
171
+
172
+ #### Events
173
+
174
+ | Event | Callback | Description |
175
+ |---|---|---|
176
+ | `connected` | `()` | P2P connection established |
177
+ | `disconnected` | `(state)` | Connection lost |
178
+ | `data` | `(data, channel)` | Data received |
179
+ | `stream` | `(MediaStream)` | Media stream received |
180
+ | `track` | `(RTCTrackEvent)` | Track received |
181
+ | `datachannel:create` | `(RTCDataChannel)` | Channel created |
182
+ | `datachannel:open` | `(label)` | Channel opened |
183
+ | `datachannel:close` | `(label)` | Channel closed |
184
+ | `error` | `(Error)` | Error occurred |
185
+
186
+ ---
187
+
188
+ ### Rooms
189
+
190
+ Managed P2P groups with automatic connection and relay fallback.
191
+
192
+ ```ts
193
+ import { DirectRoom, GroupRoom } from 'peer-client';
194
+ ```
195
+
196
+ #### DirectRoom (1:1)
197
+
198
+ ```ts
199
+ const room = new DirectRoom(client, 'room-123');
200
+ await room.create(); // or room.join()
201
+ room.on('data', (data, from) => {});
202
+ room.send({ msg: 'hi' });
203
+ room.close();
204
+ ```
205
+
206
+ #### GroupRoom (N:N)
207
+
208
+ ```ts
209
+ const room = new GroupRoom(client, 'team-room', 20);
210
+ await room.create(); // or room.join()
211
+ room.on('data', (data, from) => {});
212
+ room.on('peer_joined', (info) => {});
213
+ room.on('peer_left', (fingerprint) => {});
214
+ room.send({ msg: 'hello' }); // broadcast to all
215
+ room.send({ msg: 'dm' }, 'fp-bob'); // to specific peer
216
+ room.broadcastViaServer({ msg: 'announcement' });
217
+ room.kick('fp-bad-actor');
218
+ room.close();
219
+ ```
220
+
221
+ #### Room Events
222
+
223
+ | Event | Callback | Description |
224
+ |---|---|---|
225
+ | `data` | `(data, from)` | Data received |
226
+ | `peer_joined` | `(PeerInfo)` | Peer joined room |
227
+ | `peer_left` | `(fingerprint)` | Peer left room |
228
+ | `closed` | `()` | Room closed |
229
+ | `error` | `(Error)` | Error occurred |
230
+
231
+ ---
232
+
233
+ ### Media
234
+
235
+ Audio/video calls with mute/unmute controls.
236
+
237
+ ```ts
238
+ import { DirectMedia, GroupMedia } from 'peer-client';
239
+ ```
240
+
241
+ #### DirectMedia (1:1 Call)
242
+
243
+ ```ts
244
+ const call = new DirectMedia(client, 'call-room');
245
+ const localStream = await call.createAndJoin({ audio: true, video: true });
246
+ call.on('remote_stream', (stream, from) => { videoEl.srcObject = stream; });
247
+ call.muteAudio();
248
+ call.unmuteAudio();
249
+ call.muteVideo();
250
+ call.unmuteVideo();
251
+ call.close();
252
+ ```
253
+
254
+ #### GroupMedia (Conference)
255
+
256
+ ```ts
257
+ const conf = new GroupMedia(client, 'conf-room');
258
+ const { stream, peers } = await conf.joinAndStart({ audio: true, video: true });
259
+ conf.on('remote_stream', (stream, fingerprint) => {});
260
+ conf.on('remote_stream_removed', (fingerprint) => {});
261
+ conf.close();
262
+ ```
263
+
264
+ #### Media Events
265
+
266
+ | Event | Callback | Description |
267
+ |---|---|---|
268
+ | `local_stream` | `(MediaStream)` | Local stream acquired |
269
+ | `remote_stream` | `(MediaStream, fingerprint)` | Remote stream received |
270
+ | `remote_stream_removed` | `(fingerprint)` | Remote stream removed |
271
+ | `error` | `(Error)` | Error occurred |
272
+
273
+ ---
274
+
275
+ ### FileTransfer
276
+
277
+ Stream files up to 4GB over P2P data channels with backpressure control. Never loads the full file into memory.
278
+
279
+ ```ts
280
+ import { FileTransfer } from 'peer-client';
281
+
282
+ const ft = new FileTransfer(client);
283
+ ```
284
+
285
+ #### Sending
286
+
287
+ ```ts
288
+ const peer = client.connectToPeer('fp-receiver');
289
+ peer.on('connected', async () => {
290
+ await ft.send(peer, file, 'report.pdf');
291
+ });
292
+
293
+ ft.on('progress', ({ id, percentage, bytesPerSecond }) => {});
294
+ ```
295
+
296
+ #### Receiving
297
+
298
+ ```ts
299
+ ft.handleIncoming(peer);
300
+
301
+ ft.on('incoming', (meta, from) => {
302
+ ft.accept(meta.id); // or ft.reject(meta.id)
303
+ });
304
+
305
+ ft.on('complete', (id, blob, meta, from) => {
306
+ const url = URL.createObjectURL(blob);
307
+ });
308
+ ```
309
+
310
+ #### Methods
311
+
312
+ | Method | Returns | Description |
313
+ |---|---|---|
314
+ | `send(peer, file, filename?)` | `Promise<void>` | Send file to peer |
315
+ | `handleIncoming(peer)` | `() => void` | Listen for incoming transfers, returns cleanup |
316
+ | `accept(id)` | `void` | Accept incoming transfer |
317
+ | `reject(id)` | `void` | Reject incoming transfer |
318
+ | `cancel(id)` | `void` | Cancel active transfer |
319
+ | `destroy()` | `void` | Clean up all listeners |
320
+
321
+ #### Events
322
+
323
+ | Event | Callback | Description |
324
+ |---|---|---|
325
+ | `incoming` | `(FileMetadata, from)` | Incoming transfer offer |
326
+ | `progress` | `(TransferProgress)` | Transfer progress update |
327
+ | `complete` | `(id, Blob, FileMetadata, from)` | Transfer completed |
328
+ | `cancelled` | `(id)` | Transfer cancelled |
329
+ | `error` | `(Error)` | Transfer error |
330
+
331
+ #### JSONTransfer & ImageTransfer
332
+
333
+ ```ts
334
+ import { JSONTransfer, ImageTransfer } from 'peer-client';
335
+
336
+ const jt = new JSONTransfer(client);
337
+ jt.send(peer, { large: 'object' });
338
+ jt.on('data', (data, from) => {});
339
+
340
+ const it = new ImageTransfer(client);
341
+ await it.send(peer, imageBlob, 'photo.jpg');
342
+ it.on('complete', (id, blob) => {});
343
+ ```
344
+
345
+ ---
346
+
347
+ ### StateSync
348
+
349
+ Distributed key-value state with Hybrid Logical Clocks (HLC) for consistent ordering.
350
+
351
+ ```ts
352
+ import { StateSync } from 'peer-client';
353
+ ```
354
+
355
+ #### Last-Writer-Wins
356
+
357
+ ```ts
358
+ const sync = new StateSync(client, 'room-1', { mode: 'lww' });
359
+ sync.start();
360
+ sync.set('score', 100);
361
+ sync.get('score'); // 100
362
+ sync.getAll(); // { score: 100 }
363
+ sync.delete('score');
364
+ sync.destroy();
365
+ ```
366
+
367
+ #### Operational (Custom Merge)
368
+
369
+ ```ts
370
+ const sync = new StateSync(client, 'room-1', {
371
+ mode: 'operational',
372
+ merge: (local, remote) => [...new Set([...local, ...remote])],
373
+ });
374
+ sync.start();
375
+ sync.set('tags', ['a', 'b']);
376
+ sync.on('conflict', (key, local, remote, merged) => {});
377
+ ```
378
+
379
+ #### Methods
380
+
381
+ | Method | Returns | Description |
382
+ |---|---|---|
383
+ | `start()` | `void` | Start sync listeners |
384
+ | `set(key, value)` | `void` | Set a key-value pair |
385
+ | `get(key)` | `any` | Get value by key |
386
+ | `getAll()` | `Record<string, any>` | Get all non-deleted entries |
387
+ | `delete(key)` | `void` | Tombstone delete |
388
+ | `destroy()` | `void` | Clean up |
389
+
390
+ #### Events
391
+
392
+ | Event | Callback | Description |
393
+ |---|---|---|
394
+ | `state_changed` | `(key, value, from)` | State changed locally or remotely |
395
+ | `conflict` | `(key, local, remote, merged)` | Merge conflict (operational mode) |
396
+ | `error` | `(Error)` | Error occurred |
397
+
398
+ ---
399
+
400
+ ### CRDTSync
401
+
402
+ Yjs CRDT integration for real-time collaborative editing.
403
+
404
+ ```ts
405
+ import { CRDTSync } from 'peer-client';
406
+ import * as Y from 'yjs';
407
+
408
+ const crdt = new CRDTSync(client, 'collab-room', Y);
409
+ crdt.start();
410
+
411
+ const map = crdt.getMap('shared');
412
+ map.set('title', 'Hello');
413
+
414
+ const text = crdt.getText('doc');
415
+ text.insert(0, 'Hello world');
416
+
417
+ const arr = crdt.getArray('items');
418
+ arr.push(['item1']);
419
+
420
+ crdt.destroy();
421
+ ```
422
+
423
+ #### Methods
424
+
425
+ | Method | Returns | Description |
426
+ |---|---|---|
427
+ | `start()` | `void` | Start CRDT sync |
428
+ | `getDoc()` | `Y.Doc` | Get Yjs document |
429
+ | `getMap(name?)` | `Y.Map` | Get shared map (default: `'shared'`) |
430
+ | `getText(name?)` | `Y.Text` | Get shared text (default: `'text'`) |
431
+ | `getArray(name?)` | `Y.Array` | Get shared array (default: `'array'`) |
432
+ | `destroy()` | `void` | Clean up |
433
+
434
+ ---
435
+
436
+ ### E2E Encryption
437
+
438
+ ECDH key exchange with identity-signed ephemeral keys.
439
+
440
+ ```ts
441
+ import { GroupKeyManager, E2E } from 'peer-client';
442
+ ```
443
+
444
+ #### GroupKeyManager (Recommended)
445
+
446
+ ```ts
447
+ const km = new GroupKeyManager(client);
448
+ await km.init();
449
+
450
+ await km.exchangeWith(peer);
451
+
452
+ const encrypted = await km.encryptForPeer('fp-bob', { secret: true });
453
+ peer.send({ _encrypted: true, data: encrypted });
454
+
455
+ peer.on('data', async (msg) => {
456
+ if (msg._encrypted) {
457
+ const decrypted = await km.decryptFromPeer(peer.fingerprint, msg.data);
458
+ }
459
+ });
460
+
461
+ km.destroy();
462
+ ```
463
+
464
+ #### Methods
465
+
466
+ | Method | Returns | Description |
467
+ |---|---|---|
468
+ | `init()` | `Promise<void>` | Generate ephemeral key pair |
469
+ | `exchangeWith(peer)` | `Promise<void>` | Exchange keys with peer |
470
+ | `handleIncomingKeyExchange(peer, data)` | `Promise<void>` | Handle incoming exchange |
471
+ | `encryptForPeer(fingerprint, data)` | `Promise<string>` | Encrypt data for peer |
472
+ | `decryptFromPeer(fingerprint, data)` | `Promise<any>` | Decrypt data from peer |
473
+ | `getE2E()` | `E2E` | Get underlying E2E instance |
474
+ | `destroy()` | `void` | Clean up keys |
475
+
476
+ #### E2E (Low-Level)
477
+
478
+ ```ts
479
+ const e2e = new E2E();
480
+ await e2e.init();
481
+ const pubKey = e2e.getPublicKeyB64();
482
+ await e2e.deriveKey('fp-bob', remotePubKeyB64);
483
+ const encrypted = await e2e.encrypt('fp-bob', 'secret');
484
+ const decrypted = await e2e.decrypt('fp-bob', encrypted);
485
+ e2e.hasKey('fp-bob'); // true
486
+ e2e.removeKey('fp-bob');
487
+ e2e.destroy();
488
+ ```
489
+
490
+ ---
491
+
492
+ ### Identity
493
+
494
+ Persistent ECDSA identity for signing and fingerprinting.
495
+
496
+ ```ts
497
+ import { Identity } from 'peer-client';
498
+
499
+ const id = new Identity();
500
+ await id.generate();
501
+ console.log(id.fingerprint);
502
+
503
+ const keys = await id.export();
504
+ localStorage.setItem('keys', JSON.stringify(keys));
505
+
506
+ const restored = new Identity();
507
+ await restored.restore(JSON.parse(localStorage.getItem('keys')!));
508
+
509
+ const client = new PeerClient({
510
+ url: 'wss://signal.example.com',
511
+ identityKeys: keys,
512
+ });
513
+ ```
514
+
515
+ ---
516
+
517
+ ### Emitter
518
+
519
+ All peer-client classes extend `Emitter`:
520
+
521
+ ```ts
522
+ const off = emitter.on('event', (...args) => {}); // returns cleanup function
523
+ emitter.once('event', (...args) => {});
524
+ emitter.off('event', handler);
525
+ emitter.emit('event', ...args);
526
+
527
+ import { setEmitterErrorHandler } from 'peer-client';
528
+ setEmitterErrorHandler((error, event) => {
529
+ console.error(`Error in ${event}:`, error);
530
+ });
531
+ ```
532
+
533
+ ---
534
+
535
+ ## React API
536
+
537
+ All hooks and components are exported from `peer-client/react`. They require a `<PeerProvider>` ancestor.
538
+
539
+ ```ts
540
+ import { PeerProvider, useRoom, useMedia, Video } from 'peer-client/react';
541
+ ```
542
+
543
+ ---
544
+
545
+ ### `<PeerProvider>`
546
+
547
+ Wraps your app with a PeerClient context. Connects on mount, disconnects on unmount.
548
+
549
+ ```tsx
550
+ <PeerProvider config={{
551
+ url: 'wss://signal.example.com',
552
+ alias: 'alice',
553
+ meta: { role: 'player' },
554
+ }}>
555
+ <App />
556
+ </PeerProvider>
557
+ ```
558
+
559
+ | Prop | Type | Description |
560
+ |---|---|---|
561
+ | `config` | `ClientConfig` | PeerClient configuration |
562
+ | `children` | `ReactNode` | Child components |
563
+
564
+ ---
565
+
566
+ ### `usePeerClient()`
567
+
568
+ Access the raw PeerClient and connection state.
569
+
570
+ ```ts
571
+ const { client, connected, fingerprint, alias, error } = usePeerClient();
572
+ ```
573
+
574
+ | Return | Type | Description |
575
+ |---|---|---|
576
+ | `client` | `PeerClient \| null` | Client instance |
577
+ | `connected` | `boolean` | WebSocket connected |
578
+ | `fingerprint` | `string` | Identity fingerprint |
579
+ | `alias` | `string` | Display name |
580
+ | `error` | `Error \| null` | Connection error |
581
+
582
+ ---
583
+
584
+ ### `usePeer(fingerprint)`
585
+
586
+ Track a specific peer's connection lifecycle.
587
+
588
+ ```ts
589
+ const { peer, connectionState, send } = usePeer('fp-bob');
590
+ ```
591
+
592
+ | Param | Type | Description |
593
+ |---|---|---|
594
+ | `fingerprint` | `string` | Remote peer fingerprint |
595
+
596
+ | Return | Type | Description |
597
+ |---|---|---|
598
+ | `peer` | `Peer \| null` | Peer instance |
599
+ | `connectionState` | `string` | `new` · `connected` · `disconnected` · `failed` · `closed` |
600
+ | `send` | `(data, channel?) => void` | Send data to peer |
601
+
602
+ ---
603
+
604
+ ### `useNamespace(namespace)`
605
+
606
+ Join a namespace on mount, leave on unmount. Tracks peer list reactively.
607
+
608
+ ```ts
609
+ const { peers, joined, discover, error } = useNamespace('lobby');
610
+ ```
611
+
612
+ | Param | Type | Description |
613
+ |---|---|---|
614
+ | `namespace` | `string` | Namespace to join |
615
+
616
+ | Return | Type | Description |
617
+ |---|---|---|
618
+ | `peers` | `PeerInfo[]` | Current peers in namespace |
619
+ | `joined` | `boolean` | Whether successfully joined |
620
+ | `discover` | `(limit?) => Promise<PeerInfo[]>` | Discover peers |
621
+ | `error` | `Error \| null` | Join error |
622
+
623
+ ---
624
+
625
+ ### `useRelay()`
626
+
627
+ Send and receive server-relayed messages.
628
+
629
+ ```ts
630
+ const { messages, send, clear } = useRelay();
631
+ send('fp-bob', { msg: 'hi' });
632
+ ```
633
+
634
+ | Return | Type | Description |
635
+ |---|---|---|
636
+ | `messages` | `RelayMessage[]` | Received messages `{ from, payload, ts }` |
637
+ | `send` | `(to, payload) => void` | Send relay message |
638
+ | `clear` | `() => void` | Clear message history |
639
+
640
+ ---
641
+
642
+ ### `useBroadcast(namespace)`
643
+
644
+ Send and receive namespace broadcasts.
645
+
646
+ ```ts
647
+ const { messages, send, clear } = useBroadcast('lobby');
648
+ send({ announcement: 'hello all' });
649
+ ```
650
+
651
+ | Param | Type | Description |
652
+ |---|---|---|
653
+ | `namespace` | `string` | Namespace to listen on |
654
+
655
+ | Return | Type | Description |
656
+ |---|---|---|
657
+ | `messages` | `BroadcastMessage[]` | Received messages `{ from, namespace, payload, ts }` |
658
+ | `send` | `(payload) => void` | Broadcast to namespace |
659
+ | `clear` | `() => void` | Clear message history |
660
+
661
+ ---
662
+
663
+ ### `useMatch()`
664
+
665
+ Matchmaking with reactive status tracking.
666
+
667
+ ```ts
668
+ const { match, status, peers, sessionId, reset, error } = useMatch();
669
+ await match('game', { skill: 'beginner' }, 2);
670
+ ```
671
+
672
+ | Return | Type | Description |
673
+ |---|---|---|
674
+ | `match` | `(namespace, meta?, count?) => Promise` | Start matchmaking |
675
+ | `status` | `'idle' \| 'matching' \| 'matched' \| 'error'` | Current status |
676
+ | `peers` | `PeerInfo[]` | Matched peers |
677
+ | `sessionId` | `string` | Match session ID |
678
+ | `reset` | `() => void` | Reset to idle |
679
+ | `error` | `Error \| null` | Match error |
680
+
681
+ ---
682
+
683
+ ### `useRoom(roomId, type?, create?, maxSize?)`
684
+
685
+ Join or create a room on mount, leave on unmount. Tracks peers and messages.
686
+
687
+ ```ts
688
+ const { joined, peers, messages, send, clearMessages, error, room } = useRoom('room-1', 'group');
689
+ ```
690
+
691
+ | Param | Type | Default | Description |
692
+ |---|---|---|---|
693
+ | `roomId` | `string` | — | Room identifier |
694
+ | `type` | `'direct' \| 'group'` | `'direct'` | Room type |
695
+ | `create` | `boolean` | `false` | Create vs join |
696
+ | `maxSize` | `number` | `20` | Max peers (group only) |
697
+
698
+ | Return | Type | Description |
699
+ |---|---|---|
700
+ | `joined` | `boolean` | Successfully joined |
701
+ | `peers` | `PeerInfo[]` | Current room peers |
702
+ | `messages` | `RoomMessage[]` | Received messages `{ data, from, ts }` |
703
+ | `send` | `(data, to?) => void` | Send to all or specific peer |
704
+ | `clearMessages` | `() => void` | Clear message history |
705
+ | `error` | `Error \| null` | Room error |
706
+ | `room` | `DirectRoom \| GroupRoom \| null` | Room instance |
707
+
708
+ ---
709
+
710
+ ### `useMedia(roomId, type?, create?, audio?, video?)`
711
+
712
+ Audio/video calls with mute controls.
713
+
714
+ ```ts
715
+ const {
716
+ localStream, remoteStreams,
717
+ audioMuted, videoMuted,
718
+ muteAudio, unmuteAudio, muteVideo, unmuteVideo,
719
+ toggleAudio, toggleVideo,
720
+ error,
721
+ } = useMedia('call-room', 'group', false, true, true);
722
+ ```
723
+
724
+ | Param | Type | Default | Description |
725
+ |---|---|---|---|
726
+ | `roomId` | `string` | — | Room identifier |
727
+ | `type` | `'direct' \| 'group'` | `'direct'` | Call type |
728
+ | `create` | `boolean` | `false` | Create vs join |
729
+ | `audio` | `boolean` | `true` | Enable audio |
730
+ | `video` | `boolean` | `true` | Enable video |
731
+
732
+ | Return | Type | Description |
733
+ |---|---|---|
734
+ | `localStream` | `MediaStream \| null` | Local media stream |
735
+ | `remoteStreams` | `Map<string, MediaStream>` | Remote streams by fingerprint |
736
+ | `audioMuted` | `boolean` | Audio muted |
737
+ | `videoMuted` | `boolean` | Video muted |
738
+ | `muteAudio` | `() => void` | Mute audio |
739
+ | `unmuteAudio` | `() => void` | Unmute audio |
740
+ | `muteVideo` | `() => void` | Mute video |
741
+ | `unmuteVideo` | `() => void` | Unmute video |
742
+ | `toggleAudio` | `() => void` | Toggle audio |
743
+ | `toggleVideo` | `() => void` | Toggle video |
744
+ | `error` | `Error \| null` | Media error |
745
+
746
+ ---
747
+
748
+ ### `useFileTransfer()`
749
+
750
+ File transfer with reactive transfer state.
751
+
752
+ ```ts
753
+ const { transfers, send, accept, reject, cancel, listenToPeer, clearCompleted } = useFileTransfer();
754
+
755
+ const id = await send(peer, file, 'report.pdf');
756
+
757
+ listenToPeer(peer);
758
+ accept(transferId);
759
+ reject(transferId);
760
+ cancel(transferId);
761
+ ```
762
+
763
+ | Return | Type | Description |
764
+ |---|---|---|
765
+ | `transfers` | `Map<string, Transfer>` | All active/completed transfers |
766
+ | `send` | `(peer, file, filename?) => Promise<string>` | Send file, returns transfer ID |
767
+ | `accept` | `(id) => void` | Accept incoming transfer |
768
+ | `reject` | `(id) => void` | Reject incoming transfer |
769
+ | `cancel` | `(id) => void` | Cancel active transfer |
770
+ | `listenToPeer` | `(peer) => () => void` | Listen for incoming, returns cleanup |
771
+ | `clearCompleted` | `() => void` | Remove completed/cancelled/errored transfers |
772
+
773
+ #### Transfer Object
774
+
775
+ ```ts
776
+ interface Transfer {
777
+ id: string;
778
+ filename: string;
779
+ size: number;
780
+ direction: 'send' | 'receive';
781
+ from?: string;
782
+ progress: number; // 0-100
783
+ bytesPerSecond: number;
784
+ status: 'pending' | 'active' | 'complete' | 'cancelled' | 'error';
785
+ blob?: Blob; // available on complete
786
+ meta?: FileMetadata;
787
+ }
788
+ ```
789
+
790
+ ---
791
+
792
+ ### `useSync(roomId, mode?, merge?)`
793
+
794
+ Distributed state sync with reactive state object.
795
+
796
+ ```ts
797
+ const { state, set, delete: del, get, error } = useSync('room-1', 'lww');
798
+
799
+ set('score', 100);
800
+ del('oldKey');
801
+ get('score'); // 100
802
+ state; // { score: 100 }
803
+ ```
804
+
805
+ | Param | Type | Default | Description |
806
+ |---|---|---|---|
807
+ | `roomId` | `string` | — | Room to sync in |
808
+ | `mode` | `'lww' \| 'operational' \| 'crdt'` | `'lww'` | Sync mode |
809
+ | `merge` | `(local, remote) => any` | — | Custom merge (operational mode) |
810
+
811
+ | Return | Type | Description |
812
+ |---|---|---|
813
+ | `state` | `Record<string, any>` | Current state object |
814
+ | `set` | `(key, value) => void` | Set key-value |
815
+ | `delete` | `(key) => void` | Delete key |
816
+ | `get` | `(key) => any` | Get value |
817
+ | `error` | `Error \| null` | Sync error |
818
+
819
+ ---
820
+
821
+ ### `useE2E()`
822
+
823
+ End-to-end encryption with key exchange tracking.
824
+
825
+ ```ts
826
+ const { ready, exchangedPeers, exchange, handleIncoming, encrypt, decrypt, hasKey, error } = useE2E();
827
+
828
+ await exchange(peer);
829
+ const encrypted = await encrypt('fp-bob', { secret: true });
830
+ const decrypted = await decrypt('fp-bob', encrypted);
831
+ ```
832
+
833
+ | Return | Type | Description |
834
+ |---|---|---|
835
+ | `ready` | `boolean` | Keys generated |
836
+ | `exchangedPeers` | `Set<string>` | Fingerprints with established keys |
837
+ | `exchange` | `(peer) => Promise<void>` | Exchange keys with peer |
838
+ | `handleIncoming` | `(peer, data) => Promise<void>` | Handle incoming exchange |
839
+ | `encrypt` | `(fingerprint, data) => Promise<string>` | Encrypt for peer |
840
+ | `decrypt` | `(fingerprint, data) => Promise<any>` | Decrypt from peer |
841
+ | `hasKey` | `(fingerprint) => boolean` | Check if key exists |
842
+ | `error` | `Error \| null` | E2E error |
843
+
844
+ ---
845
+
846
+ ### `useIdentity(persistKey?)`
847
+
848
+ Persistent identity with localStorage.
849
+
850
+ ```ts
851
+ const { ready, fingerprint, exportKeys, regenerate, clear, error } = useIdentity();
852
+ ```
853
+
854
+ | Param | Type | Default | Description |
855
+ |---|---|---|---|
856
+ | `persistKey` | `string` | `'peer-client_identity'` | localStorage key |
857
+
858
+ | Return | Type | Description |
859
+ |---|---|---|
860
+ | `ready` | `boolean` | Keys loaded |
861
+ | `fingerprint` | `string` | Identity fingerprint |
862
+ | `exportKeys` | `() => Promise<IdentityKeys>` | Export key material |
863
+ | `regenerate` | `() => Promise<void>` | Generate new identity |
864
+ | `clear` | `() => void` | Remove from localStorage |
865
+ | `error` | `Error \| null` | Identity error |
866
+
867
+ ---
868
+
869
+ ### `useCRDT(roomId, Y)`
870
+
871
+ Yjs CRDT integration.
872
+
873
+ ```ts
874
+ import * as Y from 'yjs';
875
+
876
+ const { ready, getDoc, getMap, getText, getArray, error } = useCRDT('collab', Y);
877
+
878
+ const map = getMap('shared');
879
+ map.set('title', 'Hello');
880
+ ```
881
+
882
+ | Param | Type | Description |
883
+ |---|---|---|
884
+ | `roomId` | `string` | Room to sync in |
885
+ | `Y` | `typeof import('yjs')` | Yjs module reference |
886
+
887
+ | Return | Type | Description |
888
+ |---|---|---|
889
+ | `ready` | `boolean` | CRDT initialized |
890
+ | `getDoc` | `() => Y.Doc` | Get Yjs document |
891
+ | `getMap` | `(name) => Y.Map` | Get shared map |
892
+ | `getText` | `(name) => Y.Text` | Get shared text |
893
+ | `getArray` | `(name) => Y.Array` | Get shared array |
894
+ | `error` | `Error \| null` | CRDT error |
895
+
896
+ ---
897
+
898
+ ## React Components
899
+
900
+ ### `<Video>`
901
+
902
+ Attaches a `MediaStream` to a `<video>` element with autoplay handling.
903
+
904
+ ```tsx
905
+ import { Video } from 'peer-client/react';
906
+
907
+ <Video stream={localStream} muted />
908
+ <Video stream={remoteStream} />
909
+ ```
910
+
911
+ | Prop | Type | Default | Description |
912
+ |---|---|---|---|
913
+ | `stream` | `MediaStream \| null` | — | Media stream to display |
914
+ | `muted` | `boolean` | `false` | Mute audio |
915
+ | `...props` | `VideoHTMLAttributes` | — | Passed to `<video>` |
916
+
917
+ ### `<Audio>`
918
+
919
+ Attaches a `MediaStream` to an `<audio>` element.
920
+
921
+ ```tsx
922
+ import { Audio } from 'peer-client/react';
923
+
924
+ <Audio stream={remoteStream} />
925
+ ```
926
+
927
+ | Prop | Type | Description |
928
+ |---|---|---|
929
+ | `stream` | `MediaStream \| null` | Media stream |
930
+ | `...props` | `AudioHTMLAttributes` | Passed to `<audio>` |
931
+
932
+ ### `<TransferProgress>`
933
+
934
+ Renders file transfer state with progress, speed, and action buttons.
935
+
936
+ ```tsx
937
+ import { TransferProgress } from 'peer-client/react';
938
+
939
+ <TransferProgress
940
+ transfer={transfer}
941
+ onAccept={() => accept(transfer.id)}
942
+ onReject={() => reject(transfer.id)}
943
+ onCancel={() => cancel(transfer.id)}
944
+ className="my-transfer"
945
+ />
946
+ ```
947
+
948
+ Uses `data-*` attributes for styling: `data-status`, `data-direction`, `data-part`, `data-action`.
949
+
950
+ | Prop | Type | Description |
951
+ |---|---|---|
952
+ | `transfer` | `Transfer` | Transfer object from `useFileTransfer` |
953
+ | `onAccept` | `() => void` | Accept handler (shown when `status === 'pending'`) |
954
+ | `onReject` | `() => void` | Reject handler |
955
+ | `onCancel` | `() => void` | Cancel handler (shown when `status === 'active'`) |
956
+ | `className` | `string` | CSS class |
957
+
958
+ ### `<PeerStatus>`
959
+
960
+ Connection status indicator dot.
961
+
962
+ ```tsx
963
+ import { PeerStatus } from 'peer-client/react';
964
+
965
+ <PeerStatus state={connectionState} />
966
+ <PeerStatus state="connected" label="Online" />
967
+ ```
968
+
969
+ | Prop | Type | Description |
970
+ |---|---|---|
971
+ | `state` | `string` | `connected` · `connecting` · `disconnected` · `failed` · `closed` · `new` |
972
+ | `label` | `string` | Override label (defaults to state name) |
973
+ | `className` | `string` | CSS class |
974
+
975
+ Colors: `connected` green · `connecting` yellow · `disconnected`/`failed` red · `closed`/`new` gray
976
+
977
+ ---
978
+
979
+ ## Types
980
+
981
+ All types are exported from the main package:
982
+
983
+ ```ts
984
+ import type {
985
+ ClientConfig,
986
+ PeerInfo,
987
+ MatchResult,
988
+ RoomConfig,
989
+ RoomCreatedResult,
990
+ RoomInfoResult,
991
+ MediaConfig,
992
+ DataChannelConfig,
993
+ FileMetadata,
994
+ TransferProgress,
995
+ TransferState,
996
+ SyncConfig,
997
+ SyncMode,
998
+ IdentityKeys,
999
+ HLC,
1000
+ } from 'peer-client';
1001
+ ```
1002
+
1003
+ ### Key Types
1004
+
1005
+ ```ts
1006
+ interface PeerInfo {
1007
+ fingerprint: string;
1008
+ alias: string;
1009
+ meta?: Record<string, any>;
1010
+ app_type?: string;
1011
+ }
1012
+
1013
+ interface MatchResult {
1014
+ namespace: string;
1015
+ session_id: string;
1016
+ peers: PeerInfo[];
1017
+ }
1018
+
1019
+ interface FileMetadata {
1020
+ id: string;
1021
+ filename: string;
1022
+ size: number;
1023
+ mimeType: string;
1024
+ chunkSize: number;
1025
+ totalChunks: number;
1026
+ hash?: string;
1027
+ }
1028
+
1029
+ interface IdentityKeys {
1030
+ publicKey: string;
1031
+ privateKey: string;
1032
+ fingerprint: string;
1033
+ }
1034
+
1035
+ type SyncMode = 'lww' | 'operational' | 'crdt';
1036
+ ```
1037
+
1038
+ ---
1039
+
1040
+ ## Configuration Reference
1041
+
1042
+ | Option | Type | Default | Description |
1043
+ |---|---|---|---|
1044
+ | `url` | `string` | **required** | WebSocket signaling server URL |
1045
+ | `iceServers` | `RTCIceServer[]` | Google STUN | ICE/TURN servers |
1046
+ | `alias` | `string` | `''` | Display name |
1047
+ | `meta` | `Record<string, any>` | `{}` | Arbitrary metadata |
1048
+ | `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
1049
+ | `reconnectDelay` | `number` | `1000` | Initial reconnect delay (ms) |
1050
+ | `reconnectMaxDelay` | `number` | `30000` | Max reconnect delay (ms) |
1051
+ | `maxReconnectAttempts` | `number` | `Infinity` | Max reconnect attempts |
1052
+ | `pingInterval` | `number` | `25000` | WebSocket keepalive interval (ms) |
1053
+ | `identityKeys` | `IdentityKeys` | auto-generated | Pre-existing identity keys |
1054
+
1055
+ ---
1056
+
1057
+ ## Scripts
1058
+
1059
+ ```bash
1060
+ npm run build # Compile TypeScript to dist/
1061
+ npm test # Run unit tests
1062
+ npm run test:watch # Run tests in watch mode
1063
+ npm run test:coverage # Run tests with coverage
1064
+ npm run test:react # Run React hook/component tests (72 tests)
1065
+ npm run test:integration # Run integration tests against live server
1066
+ ```
1067
+
1068
+ ## Publishing
1069
+
1070
+ ```bash
1071
+ npm login
1072
+ npm pack --dry-run # Verify package contents
1073
+ npm publish # Publish to npm
1074
+ ```
1075
+
1076
+ The `prepublishOnly` script automatically runs `tsc` before publish.
1077
+
1078
+ ## Requirements
1079
+
1080
+ - Node.js >= 18
1081
+ - React >= 18 (optional, for `peer-client/react`)
1082
+ - A WebRTC signaling server compatible with the peer-client protocol
1083
+
1084
+ ## License
1085
+
1086
+ MIT