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/dist/sync.js ADDED
@@ -0,0 +1,333 @@
1
+ import { Emitter } from './core/emitter';
2
+ import { LIMITS } from './core/types';
3
+ function createHLC(node, existing) {
4
+ const now = Date.now();
5
+ if (!existing)
6
+ return { ts: now, counter: 0, node };
7
+ const ts = Math.max(now, existing.ts);
8
+ const counter = ts === existing.ts ? existing.counter + 1 : 0;
9
+ return { ts, counter, node };
10
+ }
11
+ function mergeHLC(local, remote, node) {
12
+ const now = Date.now();
13
+ const maxTs = Math.max(now, local.ts, remote.ts);
14
+ let counter = 0;
15
+ if (maxTs === local.ts && maxTs === remote.ts) {
16
+ counter = Math.max(local.counter, remote.counter) + 1;
17
+ }
18
+ else if (maxTs === local.ts) {
19
+ counter = local.counter + 1;
20
+ }
21
+ else if (maxTs === remote.ts) {
22
+ counter = remote.counter + 1;
23
+ }
24
+ return { ts: maxTs, counter, node };
25
+ }
26
+ function compareHLC(a, b) {
27
+ if (a.ts !== b.ts)
28
+ return a.ts - b.ts;
29
+ if (a.counter !== b.counter)
30
+ return a.counter - b.counter;
31
+ return a.node < b.node ? -1 : a.node > b.node ? 1 : 0;
32
+ }
33
+ export class StateSync extends Emitter {
34
+ client;
35
+ state = new Map();
36
+ mode;
37
+ merge;
38
+ hlc;
39
+ roomId;
40
+ cleanups = [];
41
+ tombstoneTimer = null;
42
+ constructor(client, roomId, config) {
43
+ super();
44
+ this.client = client;
45
+ this.roomId = roomId;
46
+ this.mode = config.mode;
47
+ this.merge = config.merge;
48
+ this.hlc = { ts: Date.now(), counter: 0, node: client.fingerprint };
49
+ if (this.mode === 'operational' && !this.merge) {
50
+ throw new Error('Operational sync requires a merge function');
51
+ }
52
+ }
53
+ start() {
54
+ const offBroadcast = this.client.on('broadcast', (from, ns, payload) => {
55
+ if (ns === this.roomId && payload?._sync) {
56
+ this.handleRemoteUpdate(payload, from);
57
+ }
58
+ });
59
+ const offRelay = this.client.on('relay', (from, payload) => {
60
+ if (payload?._sync && payload?._room === this.roomId) {
61
+ this.handleRemoteUpdate(payload, from);
62
+ }
63
+ });
64
+ const offJoined = this.client.on('peer_joined', () => {
65
+ this.broadcastFullState();
66
+ });
67
+ this.tombstoneTimer = setInterval(() => this.purgeTombstones(), LIMITS.TOMBSTONE_TTL);
68
+ this.cleanups.push(offBroadcast, offRelay, offJoined);
69
+ }
70
+ tick() {
71
+ this.hlc = createHLC(this.client.fingerprint, this.hlc);
72
+ return this.hlc;
73
+ }
74
+ set(key, value) {
75
+ const hlc = this.tick();
76
+ const entry = {
77
+ key,
78
+ value,
79
+ hlc,
80
+ from: this.client.fingerprint,
81
+ version: hlc.counter,
82
+ };
83
+ this.state.set(key, entry);
84
+ this.emit('state_changed', key, value, this.client.fingerprint);
85
+ this.client.broadcast(this.roomId, {
86
+ _sync: true,
87
+ type: 'update',
88
+ entry,
89
+ });
90
+ }
91
+ get(key) {
92
+ const entry = this.state.get(key);
93
+ if (entry?.deleted)
94
+ return undefined;
95
+ return entry?.value;
96
+ }
97
+ getAll() {
98
+ const result = {};
99
+ this.state.forEach((entry, key) => {
100
+ if (!entry.deleted) {
101
+ result[key] = entry.value;
102
+ }
103
+ });
104
+ return result;
105
+ }
106
+ delete(key) {
107
+ const hlc = this.tick();
108
+ const entry = {
109
+ key,
110
+ value: undefined,
111
+ hlc,
112
+ from: this.client.fingerprint,
113
+ version: hlc.counter,
114
+ deleted: true,
115
+ };
116
+ this.state.set(key, entry);
117
+ this.client.broadcast(this.roomId, {
118
+ _sync: true,
119
+ type: 'delete',
120
+ entry,
121
+ });
122
+ this.emit('state_changed', key, undefined, this.client.fingerprint);
123
+ }
124
+ handleRemoteUpdate(payload, from) {
125
+ if (payload.type === 'full_state') {
126
+ this.handleFullState(payload.state, from);
127
+ return;
128
+ }
129
+ if (payload.type === 'request_state') {
130
+ this.sendStateTo(from);
131
+ return;
132
+ }
133
+ if (payload.type === 'delete') {
134
+ const remote = payload.entry;
135
+ if (!remote?.hlc)
136
+ return;
137
+ this.hlc = mergeHLC(this.hlc, remote.hlc, this.client.fingerprint);
138
+ const local = this.state.get(remote.key);
139
+ if (!local || compareHLC(remote.hlc, local.hlc) > 0) {
140
+ this.state.set(remote.key, remote);
141
+ this.emit('state_changed', remote.key, undefined, from);
142
+ }
143
+ this.emit('synced', from);
144
+ return;
145
+ }
146
+ if (payload.type !== 'update' || !payload.entry)
147
+ return;
148
+ const remote = payload.entry;
149
+ if (!remote?.hlc)
150
+ return;
151
+ this.hlc = mergeHLC(this.hlc, remote.hlc, this.client.fingerprint);
152
+ const local = this.state.get(remote.key);
153
+ switch (this.mode) {
154
+ case 'lww': {
155
+ if (!local || compareHLC(remote.hlc, local.hlc) > 0) {
156
+ this.state.set(remote.key, remote);
157
+ this.emit('state_changed', remote.key, remote.value, from);
158
+ }
159
+ break;
160
+ }
161
+ case 'operational': {
162
+ if (!local || local.deleted) {
163
+ this.state.set(remote.key, remote);
164
+ this.emit('state_changed', remote.key, remote.value, from);
165
+ }
166
+ else {
167
+ const merged = this.merge(local.value, remote.value);
168
+ const hlc = this.tick();
169
+ const entry = {
170
+ key: remote.key,
171
+ value: merged,
172
+ hlc,
173
+ from: this.client.fingerprint,
174
+ version: hlc.counter,
175
+ };
176
+ this.state.set(remote.key, entry);
177
+ this.emit('state_changed', remote.key, merged, from);
178
+ this.emit('conflict', remote.key, local.value, remote.value, merged);
179
+ }
180
+ break;
181
+ }
182
+ case 'crdt': {
183
+ this.emit('error', new Error('CRDT mode requires CRDTSync class'));
184
+ break;
185
+ }
186
+ }
187
+ this.emit('synced', from);
188
+ }
189
+ handleFullState(remoteState, from) {
190
+ for (const remote of remoteState) {
191
+ if (!remote?.hlc)
192
+ continue;
193
+ this.hlc = mergeHLC(this.hlc, remote.hlc, this.client.fingerprint);
194
+ const local = this.state.get(remote.key);
195
+ if (!local || compareHLC(remote.hlc, local.hlc) > 0) {
196
+ this.state.set(remote.key, remote);
197
+ if (!remote.deleted) {
198
+ this.emit('state_changed', remote.key, remote.value, from);
199
+ }
200
+ }
201
+ }
202
+ this.emit('synced', from);
203
+ }
204
+ broadcastFullState() {
205
+ const entries = Array.from(this.state.values());
206
+ if (entries.length === 0)
207
+ return;
208
+ this.client.broadcast(this.roomId, {
209
+ _sync: true,
210
+ type: 'full_state',
211
+ state: entries,
212
+ });
213
+ }
214
+ sendStateTo(fingerprint) {
215
+ const entries = Array.from(this.state.values());
216
+ if (entries.length === 0)
217
+ return;
218
+ this.client.relay(fingerprint, {
219
+ _sync: true,
220
+ _room: this.roomId,
221
+ type: 'full_state',
222
+ state: entries,
223
+ });
224
+ }
225
+ requestFullState(fingerprint) {
226
+ this.client.relay(fingerprint, {
227
+ _sync: true,
228
+ _room: this.roomId,
229
+ type: 'request_state',
230
+ });
231
+ }
232
+ purgeTombstones() {
233
+ const now = Date.now();
234
+ for (const [key, entry] of this.state) {
235
+ if (entry.deleted && now - entry.hlc.ts > LIMITS.TOMBSTONE_TTL) {
236
+ this.state.delete(key);
237
+ }
238
+ }
239
+ }
240
+ destroy() {
241
+ this.cleanups.forEach((fn) => fn());
242
+ this.cleanups = [];
243
+ if (this.tombstoneTimer) {
244
+ clearInterval(this.tombstoneTimer);
245
+ this.tombstoneTimer = null;
246
+ }
247
+ this.state.clear();
248
+ this.removeAllListeners();
249
+ }
250
+ }
251
+ export class CRDTSync extends Emitter {
252
+ client;
253
+ roomId;
254
+ doc = null;
255
+ cleanups = [];
256
+ Yjs = null;
257
+ constructor(client, roomId, yjsModule) {
258
+ super();
259
+ this.client = client;
260
+ this.roomId = roomId;
261
+ this.Yjs = yjsModule;
262
+ this.doc = new yjsModule.Doc();
263
+ }
264
+ getDoc() {
265
+ return this.doc;
266
+ }
267
+ getMap(name = 'shared') {
268
+ return this.doc.getMap(name);
269
+ }
270
+ getText(name = 'text') {
271
+ return this.doc.getText(name);
272
+ }
273
+ getArray(name = 'array') {
274
+ return this.doc.getArray(name);
275
+ }
276
+ start() {
277
+ this.doc.on('update', (update, origin) => {
278
+ if (origin !== 'remote') {
279
+ const encoded = btoa(String.fromCharCode(...update));
280
+ this.client.broadcast(this.roomId, {
281
+ _crdt_sync: true,
282
+ update: encoded,
283
+ });
284
+ }
285
+ });
286
+ const offBroadcast = this.client.on('broadcast', (from, ns, payload) => {
287
+ if (ns === this.roomId && payload?._crdt_sync && payload.update) {
288
+ const bytes = Uint8Array.from(atob(payload.update), (c) => c.charCodeAt(0));
289
+ this.Yjs.applyUpdate(this.doc, bytes, 'remote');
290
+ this.emit('synced', from);
291
+ }
292
+ });
293
+ const offRelay = this.client.on('relay', (from, payload) => {
294
+ if (payload?._crdt_sync && payload._room === this.roomId) {
295
+ if (payload.update) {
296
+ const bytes = Uint8Array.from(atob(payload.update), (c) => c.charCodeAt(0));
297
+ this.Yjs.applyUpdate(this.doc, bytes, 'remote');
298
+ this.emit('synced', from);
299
+ }
300
+ if (payload.type === 'request_state') {
301
+ this.sendStateTo(from);
302
+ }
303
+ }
304
+ });
305
+ const offJoined = this.client.on('peer_joined', (info) => {
306
+ this.sendStateTo(info.fingerprint);
307
+ });
308
+ this.cleanups.push(offBroadcast, offRelay, offJoined);
309
+ }
310
+ sendStateTo(fingerprint) {
311
+ const state = this.Yjs.encodeStateAsUpdate(this.doc);
312
+ const encoded = btoa(String.fromCharCode(...state));
313
+ this.client.relay(fingerprint, {
314
+ _crdt_sync: true,
315
+ _room: this.roomId,
316
+ update: encoded,
317
+ });
318
+ }
319
+ requestFullState(fingerprint) {
320
+ this.client.relay(fingerprint, {
321
+ _crdt_sync: true,
322
+ _room: this.roomId,
323
+ type: 'request_state',
324
+ });
325
+ }
326
+ destroy() {
327
+ this.cleanups.forEach((fn) => fn());
328
+ this.cleanups = [];
329
+ this.doc?.destroy();
330
+ this.doc = null;
331
+ this.removeAllListeners();
332
+ }
333
+ }
@@ -0,0 +1,49 @@
1
+ import { Emitter } from './core/emitter';
2
+ import type { PeerClient } from './core/client';
3
+ import type { Peer } from './core/peer';
4
+ import type { TransferProgress } from './core/types';
5
+ type TransferEvent = 'incoming' | 'progress' | 'complete' | 'error' | 'cancelled';
6
+ export declare class FileTransfer extends Emitter<TransferEvent> {
7
+ private client;
8
+ private sending;
9
+ private receiving;
10
+ private channelListeners;
11
+ constructor(client: PeerClient);
12
+ send(peer: Peer, file: File | Blob, filename?: string): Promise<string>;
13
+ accept(id: string): void;
14
+ reject(id: string): void;
15
+ cancel(id: string): void;
16
+ handleIncoming(peer: Peer): () => void;
17
+ requestResume(id: string, lastIndex: number): void;
18
+ getReceiveProgress(id: string): TransferProgress | null;
19
+ private handleControl;
20
+ private handleBinaryChunk;
21
+ private startSending;
22
+ private handleAck;
23
+ private assembleFile;
24
+ private cancelSend;
25
+ private cleanupSend;
26
+ destroy(): void;
27
+ }
28
+ export declare class JSONTransfer {
29
+ private client;
30
+ constructor(client: PeerClient);
31
+ sendToPeer(fingerprint: string, data: any, channel?: string): void;
32
+ sendToRoom(roomId: string, data: any): void;
33
+ onReceive(peer: Peer, callback: (data: any, from: string) => void): () => void;
34
+ onRelayReceive(callback: (data: any, from: string) => void): () => void;
35
+ onBroadcastReceive(roomId: string, callback: (data: any, from: string) => void): () => void;
36
+ }
37
+ export declare class ImageTransfer extends Emitter<TransferEvent> {
38
+ private ft;
39
+ private client;
40
+ constructor(client: PeerClient);
41
+ getFileTransfer(): FileTransfer;
42
+ send(peer: Peer, image: File | Blob, filename?: string): Promise<string>;
43
+ accept(id: string): void;
44
+ reject(id: string): void;
45
+ cancel(id: string): void;
46
+ handleIncoming(peer: Peer): () => void;
47
+ destroy(): void;
48
+ }
49
+ export {};