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.
- package/LICENSE +21 -0
- package/README.md +1086 -0
- package/dist/core/client.d.ts +41 -0
- package/dist/core/client.js +361 -0
- package/dist/core/emitter.d.ts +11 -0
- package/dist/core/emitter.js +46 -0
- package/dist/core/identity.d.ts +15 -0
- package/dist/core/identity.js +54 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/peer.d.ts +29 -0
- package/dist/core/peer.js +234 -0
- package/dist/core/transport.d.ts +35 -0
- package/dist/core/transport.js +174 -0
- package/dist/core/types.d.ts +173 -0
- package/dist/core/types.js +28 -0
- package/dist/crypto.d.ts +32 -0
- package/dist/crypto.js +168 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/media.d.ts +63 -0
- package/dist/media.js +275 -0
- package/dist/react/Audio.d.ts +6 -0
- package/dist/react/Audio.js +18 -0
- package/dist/react/PeerProvider.d.ts +17 -0
- package/dist/react/PeerProvider.js +46 -0
- package/dist/react/PeerStatus.d.ts +7 -0
- package/dist/react/PeerStatus.js +20 -0
- package/dist/react/TransferProgress.d.ts +10 -0
- package/dist/react/TransferProgress.js +17 -0
- package/dist/react/Video.d.ts +7 -0
- package/dist/react/Video.js +18 -0
- package/dist/react/index.d.ts +19 -0
- package/dist/react/index.js +18 -0
- package/dist/react/useBroadcast.d.ts +12 -0
- package/dist/react/useBroadcast.js +21 -0
- package/dist/react/useCRDT.d.ts +8 -0
- package/dist/react/useCRDT.js +37 -0
- package/dist/react/useE2E.d.ts +11 -0
- package/dist/react/useE2E.js +62 -0
- package/dist/react/useFileTransfer.d.ts +24 -0
- package/dist/react/useFileTransfer.js +133 -0
- package/dist/react/useIdentity.d.ts +9 -0
- package/dist/react/useIdentity.js +63 -0
- package/dist/react/useMatch.d.ts +11 -0
- package/dist/react/useMatch.js +33 -0
- package/dist/react/useMedia.d.ts +13 -0
- package/dist/react/useMedia.js +89 -0
- package/dist/react/useNamespace.d.ts +7 -0
- package/dist/react/useNamespace.js +38 -0
- package/dist/react/usePeer.d.ts +6 -0
- package/dist/react/usePeer.js +49 -0
- package/dist/react/usePeerClient.d.ts +7 -0
- package/dist/react/usePeerClient.js +5 -0
- package/dist/react/useRelay.d.ts +11 -0
- package/dist/react/useRelay.js +19 -0
- package/dist/react/useRoom.d.ts +17 -0
- package/dist/react/useRoom.js +67 -0
- package/dist/react/useSync.d.ts +8 -0
- package/dist/react/useSync.js +45 -0
- package/dist/room.d.ts +44 -0
- package/dist/room.js +246 -0
- package/dist/sync.d.ts +45 -0
- package/dist/sync.js +333 -0
- package/dist/transfer.d.ts +49 -0
- package/dist/transfer.js +454 -0
- 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
|