@leofcoin/chain 1.10.14 → 1.10.15

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 (39) hide show
  1. package/exports/browser/beacon-envelope.js +1 -1
  2. package/exports/browser/beacon-epoch.js +1 -1
  3. package/exports/browser/beacon-lifecycle.js +1 -1
  4. package/exports/browser/beacon-round.js +1 -1
  5. package/exports/browser/beacon-wire.js +2 -2
  6. package/exports/browser/beacon.js +1 -1
  7. package/exports/browser/{browser-DvU1xNFS-BJCQsckZ.js → browser-DvU1xNFS-DmmNUdE-.js} +3 -3
  8. package/exports/browser/{browser-crypto-Yk_e6OHw.js → browser-crypto-B9N9KlL4.js} +1 -1
  9. package/exports/browser/chain.js +3 -3
  10. package/exports/browser/{client-DDC8C510-CUi3_3uT.js → client-DDC8C510-3uaB70KL.js} +4 -4
  11. package/exports/browser/{connection-monitor-BjXI0nBP.js → connection-monitor-GPztgSSJ.js} +2 -1
  12. package/exports/browser/{index-Bxr5Iztg-AmkW0EHS.js → index-Bxr5Iztg-WbFIosyf.js} +3 -3
  13. package/exports/browser/{index-C_RakUAS.js → index-Ccn5sExe.js} +1 -1
  14. package/exports/browser/{messages-DH0tgNM9-CZwrcL37.js → messages-DH0tgNM9-C2CtyLsf.js} +3 -3
  15. package/exports/browser/{node-browser-CQugE1WZ.js → node-browser-BYi7D_oa.js} +5 -5
  16. package/exports/browser/node-browser.js +3 -3
  17. package/exports/browser/workers/machine-worker.js +16 -4
  18. package/exports/node.js +5 -0
  19. package/exports/workers/machine-worker.js +16 -4
  20. package/package.json +5 -1
  21. package/exports/browser/browser-DvU1xNFS-4-PGqr4c.js +0 -151
  22. package/exports/browser/browser-DvU1xNFS-D9eSzJGJ.js +0 -151
  23. package/exports/browser/browser-crypto-BHeOz9rj.js +0 -8893
  24. package/exports/browser/browser-crypto-CTP3Yk4M.js +0 -8893
  25. package/exports/browser/client-DDC8C510-DbIdQjhy.js +0 -2225
  26. package/exports/browser/client-DDC8C510-ZN_MLo1Y.js +0 -2225
  27. package/exports/browser/connection-monitor-Cf0vNDCA.js +0 -453
  28. package/exports/browser/connection-monitor-pdopeM1I.js +0 -453
  29. package/exports/browser/hkdf-B9AXp_7K.js +0 -222
  30. package/exports/browser/index-Bxr5Iztg-Bdpw_E83.js +0 -40
  31. package/exports/browser/index-Bxr5Iztg-CxXVYm3t.js +0 -40
  32. package/exports/browser/index-CPQ5yvZ2.js +0 -4995
  33. package/exports/browser/index-uFaocBqT.js +0 -4995
  34. package/exports/browser/messages-DH0tgNM9-BRp0gcJh.js +0 -208
  35. package/exports/browser/messages-DH0tgNM9-DkNnpePH.js +0 -208
  36. package/exports/browser/node-browser-1VSuCeew.js +0 -17880
  37. package/exports/browser/node-browser-wDmRaOEo.js +0 -17880
  38. package/exports/browser/sha2-Dkoic3vb.js +0 -602
  39. package/exports/browser/weierstrass-HGdA_prK.js +0 -2156
@@ -1,2225 +0,0 @@
1
- import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-1VSuCeew.js';
2
- import './browser-crypto-BHeOz9rj.js';
3
- import './connection-monitor-BjXI0nBP.js';
4
- import './index-CvKt4UDE.js';
5
- import './sha2-Dkoic3vb.js';
6
-
7
- class Api {
8
- _pubsub;
9
- constructor(_pubsub) {
10
- this._pubsub = _pubsub;
11
- }
12
- subscribe(topic, cb) {
13
- this._pubsub.subscribe(topic, cb);
14
- }
15
- unsubscribe(topic, cb) {
16
- this._pubsub.unsubscribe(topic, cb);
17
- }
18
- publish(topic, value) {
19
- this._pubsub.publish(topic, value);
20
- }
21
- subscribers() {
22
- this._pubsub.subscribers;
23
- }
24
- connectionState(state) {
25
- switch (state) {
26
- case 0:
27
- return 'connecting';
28
- case 1:
29
- return 'open';
30
- case 2:
31
- return 'closing';
32
- case 3:
33
- return 'closed';
34
- }
35
- }
36
- /**
37
- * @param {string} type
38
- * @param {string} name
39
- * @param {object} params
40
- */
41
- request(client, request) {
42
- return new Promise((resolve, reject) => {
43
- const state = this.connectionState(client.readyState);
44
- if (state !== 'open')
45
- return reject(`coudn't send request to ${client.id}, no open connection found.`);
46
- request.id = Math.random().toString(36).slice(-12);
47
- const handler = (result) => {
48
- if (result && result.error)
49
- return reject(result.error);
50
- resolve({ result, id: request.id, handler });
51
- this.unsubscribe(request.id, handler);
52
- };
53
- this.subscribe(request.id, handler);
54
- this.send(client, request);
55
- });
56
- }
57
- async send(client, request) {
58
- return client.send(JSON.stringify(request));
59
- }
60
- pubsub(client) {
61
- return {
62
- publish: (topic = 'pubsub', value) => {
63
- return this.send(client, { url: 'pubsub', params: { topic, value } });
64
- },
65
- subscribe: (topic = 'pubsub', cb) => {
66
- this.subscribe(topic, cb);
67
- return this.send(client, {
68
- url: 'pubsub',
69
- params: { topic, subscribe: true }
70
- });
71
- },
72
- unsubscribe: (topic = 'pubsub', cb) => {
73
- this.unsubscribe(topic, cb);
74
- return this.send(client, {
75
- url: 'pubsub',
76
- params: { topic, unsubscribe: true }
77
- });
78
- },
79
- subscribers: this._pubsub.subscribers
80
- };
81
- }
82
- server(client) {
83
- return {
84
- uptime: async () => {
85
- try {
86
- const { result, id, handler } = await this.request(client, {
87
- url: 'uptime'
88
- });
89
- this.unsubscribe(id, handler);
90
- return result;
91
- }
92
- catch (e) {
93
- throw e;
94
- }
95
- },
96
- ping: async () => {
97
- try {
98
- const now = new Date().getTime();
99
- const { result, id, handler } = await this.request(client, {
100
- url: 'ping'
101
- });
102
- this.unsubscribe(id, handler);
103
- return Number(result) - now;
104
- }
105
- catch (e) {
106
- throw e;
107
- }
108
- }
109
- };
110
- }
111
- peernet(client) {
112
- return {
113
- join: async (params) => {
114
- try {
115
- params.join = true;
116
- const requested = { url: 'peernet', params };
117
- const { result, id, handler } = await this.request(client, requested);
118
- this.unsubscribe(id, handler);
119
- return result;
120
- }
121
- catch (e) {
122
- throw e;
123
- }
124
- },
125
- leave: async (params) => {
126
- try {
127
- params.join = false;
128
- const requested = { url: 'peernet', params };
129
- const { result, id, handler } = await this.request(client, requested);
130
- this.unsubscribe(id, handler);
131
- return result;
132
- }
133
- catch (e) {
134
- throw e;
135
- }
136
- },
137
- peers: async () => {
138
- try {
139
- const requested = { url: 'peernet', params: { peers: true } };
140
- const { result, id, handler } = await this.request(client, requested);
141
- this.unsubscribe(id, handler);
142
- return result;
143
- }
144
- catch (e) {
145
- throw e;
146
- }
147
- }
148
- };
149
- }
150
- }
151
-
152
- class ClientConnection {
153
- client;
154
- api;
155
- #startTime;
156
- constructor(client, api) {
157
- this.#startTime = new Date().getTime();
158
- this.client = client;
159
- this.api = api;
160
- }
161
- request = async (req) => {
162
- const { result, id, handler } = await this.api.request(this.client, req);
163
- globalThis.pubsub.unsubscribe(id, handler);
164
- return result;
165
- };
166
- send = (req) => this.api.send(this.client, req);
167
- get subscribe() {
168
- return this.api.subscribe;
169
- }
170
- get unsubscribe() {
171
- return this.api.unsubscribe;
172
- }
173
- get subscribers() {
174
- return this.api.subscribers;
175
- }
176
- get publish() {
177
- return this.api.publish;
178
- }
179
- get pubsub() {
180
- return this.api.pubsub(this.client);
181
- }
182
- uptime = () => {
183
- const now = new Date().getTime();
184
- return (now - this.#startTime);
185
- };
186
- get peernet() {
187
- return this.api.peernet(this.client);
188
- }
189
- get server() {
190
- return this.api.server(this.client);
191
- }
192
- connectionState = () => this.api.connectionState(this.client.readyState);
193
- close = exit => {
194
- // client.onclose = message => {
195
- // if (exit) process.exit()
196
- // }
197
- this.client.close();
198
- };
199
- }
200
-
201
- if (!globalThis.PubSub)
202
- globalThis.PubSub = LittlePubSub;
203
- if (!globalThis.pubsub)
204
- globalThis.pubsub = new LittlePubSub(false);
205
- class SocketRequestClient {
206
- api;
207
- clientConnection;
208
- #tries = 0;
209
- #retry = false;
210
- #timeout = 10_000;
211
- #times = 10;
212
- #options;
213
- #protocol;
214
- #url;
215
- #experimentalWebsocket = false;
216
- constructor(url, protocol, options) {
217
- let { retry, timeout, times, experimentalWebsocket } = options || {};
218
- if (retry !== undefined)
219
- this.#retry = retry;
220
- if (timeout !== undefined)
221
- this.#timeout = timeout;
222
- if (times !== undefined)
223
- this.#times = times;
224
- if (experimentalWebsocket !== undefined)
225
- this.#experimentalWebsocket = experimentalWebsocket;
226
- this.#url = url;
227
- this.#protocol = protocol;
228
- this.#options = options;
229
- this.api = new Api(globalThis.pubsub);
230
- }
231
- init() {
232
- return new Promise(async (resolve, reject) => {
233
- const init = async () => {
234
- // @ts-ignore
235
- if (!globalThis.WebSocket && !this.#experimentalWebsocket)
236
- globalThis.WebSocket = (await import('./browser-DvU1xNFS-4-PGqr4c.js').then(function (n) { return n.b; })).default.w3cwebsocket;
237
- const client = new WebSocket(this.#url, this.#protocol);
238
- if (this.#experimentalWebsocket) {
239
- client.addEventListener('error', this.onerror);
240
- client.addEventListener('message', this.onmessage);
241
- client.addEventListener('open', () => {
242
- this.#tries = 0;
243
- resolve(new ClientConnection(client, this.api));
244
- });
245
- client.addEventListener('close', (client.onclose = (message) => {
246
- this.#tries++;
247
- if (!this.#retry)
248
- return reject(this.#options);
249
- if (this.#tries > this.#times) {
250
- console.log(`${this.#options.protocol} Client Closed`);
251
- console.error(`could not connect to - ${this.#url}/`);
252
- return resolve(new ClientConnection(client, this.api));
253
- }
254
- if (message.code === 1006) {
255
- console.log(`Retrying in ${this.#timeout} ms`);
256
- setTimeout(() => {
257
- return init();
258
- }, this.#timeout);
259
- }
260
- }));
261
- }
262
- else {
263
- client.onmessage = this.onmessage;
264
- client.onerror = this.onerror;
265
- client.onopen = () => {
266
- this.#tries = 0;
267
- resolve(new ClientConnection(client, this.api));
268
- };
269
- client.onclose = (message) => {
270
- this.#tries++;
271
- if (!this.#retry)
272
- return reject(this.#options);
273
- if (this.#tries > this.#times) {
274
- console.log(`${this.#options.protocol} Client Closed`);
275
- console.error(`could not connect to - ${this.#url}/`);
276
- return resolve(new ClientConnection(client, this.api));
277
- }
278
- if (message.code === 1006) {
279
- console.log(`Retrying in ${this.#timeout} ms`);
280
- setTimeout(() => {
281
- return init();
282
- }, this.#timeout);
283
- }
284
- };
285
- }
286
- };
287
- return init();
288
- });
289
- }
290
- onerror = (error) => {
291
- if (globalThis.pubsub.hasSubscribers('error')) {
292
- globalThis.pubsub.publish('error', error);
293
- }
294
- else {
295
- console.error(error);
296
- }
297
- };
298
- onmessage(message) {
299
- if (!message.data) {
300
- console.warn(`message ignored because it contained no data`);
301
- return;
302
- }
303
- const { value, url, status, id } = JSON.parse(message.data.toString());
304
- const publisher = id ? id : url;
305
- if (status === 200) {
306
- globalThis.pubsub.publish(publisher, value);
307
- }
308
- else {
309
- globalThis.pubsub.publish(publisher, { error: value });
310
- }
311
- }
312
- }
313
-
314
- const MAX_MESSAGE_SIZE = 16000;
315
- const defaultOptions = {
316
- networkVersion: 'peach',
317
- version: 'v1',
318
- stars: ['wss://star.leofcoin.org'],
319
- connectEvent: 'peer:connected'
320
- };
321
-
322
- const crc32$1 = (data) => {
323
- let crc = 0xffffffff;
324
- for (let i = 0; i < data.length; i++) {
325
- crc ^= data[i];
326
- for (let j = 0; j < 8; j++) {
327
- crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
328
- }
329
- }
330
- return (crc ^ 0xffffffff) >>> 0;
331
- };
332
- class PeerCore {
333
- transport;
334
- options;
335
- constructor(transport, options) {
336
- this.transport = transport;
337
- this.options = options;
338
- }
339
- #createMessageId() {
340
- const randomUUID = globalThis.crypto?.randomUUID;
341
- if (typeof randomUUID === 'function')
342
- return randomUUID.call(globalThis.crypto);
343
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
344
- }
345
- #encodeFrame(id, totalSize, index, count, payload, flags) {
346
- const te = new TextEncoder();
347
- const idBytes = te.encode(id);
348
- const crc = crc32$1(payload);
349
- const headerLen = 1 + 1 + 4 + 4 + 4 + 4 + 2 + idBytes.length;
350
- const buffer = new ArrayBuffer(headerLen + payload.length);
351
- const view = new DataView(buffer);
352
- const out = new Uint8Array(buffer);
353
- let offset = 0;
354
- view.setUint8(offset, 1);
355
- offset += 1;
356
- view.setUint8(offset, flags);
357
- offset += 1;
358
- view.setUint32(offset, totalSize, true);
359
- offset += 4;
360
- view.setUint32(offset, index, true);
361
- offset += 4;
362
- view.setUint32(offset, count, true);
363
- offset += 4;
364
- view.setUint32(offset, crc, true);
365
- offset += 4;
366
- view.setUint16(offset, idBytes.length, true);
367
- offset += 2;
368
- out.set(idBytes, offset);
369
- offset += idBytes.length;
370
- out.set(payload, offset);
371
- return out;
372
- }
373
- async #chunkAndSend(data, id) {
374
- if (!this.transport.isConnected())
375
- return;
376
- this.options.onPayloadSend?.(data.length);
377
- let sendData = data;
378
- try {
379
- const compressed = deflate_1(data);
380
- if (compressed?.length &&
381
- compressed.length < data.length * this.options.compressionThreshold) {
382
- sendData = compressed;
383
- }
384
- }
385
- catch (e) { }
386
- const size = sendData.length;
387
- if (size <= this.options.maxMessageSize) {
388
- if (!this.transport.isConnected())
389
- return;
390
- const flags = (sendData !== data ? 1 : 0) << 1;
391
- this.transport.sendRaw(this.#encodeFrame(id, size, 0, 1, sendData, flags));
392
- return;
393
- }
394
- const threshold = 4 * 1024 * 1024;
395
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
396
- const count = Math.ceil(size / this.options.maxMessageSize);
397
- const flags = (1 << 0) | ((sendData !== data ? 1 : 0) << 1);
398
- let index = 0;
399
- let remaining = sendData;
400
- while (remaining.length !== 0) {
401
- const amountToSlice = remaining.length >= this.options.maxMessageSize
402
- ? this.options.maxMessageSize
403
- : remaining.length;
404
- const chunk = remaining.subarray(0, amountToSlice);
405
- remaining = remaining.subarray(amountToSlice);
406
- while ((this.options.getBufferedAmount?.() || 0) > threshold) {
407
- if (!this.transport.isConnected())
408
- return;
409
- // eslint-disable-next-line no-await-in-loop
410
- await sleep(10);
411
- }
412
- if (!this.transport.isConnected())
413
- return;
414
- this.transport.sendRaw(this.#encodeFrame(id, size, index, count, chunk, flags));
415
- index += 1;
416
- }
417
- }
418
- send(data, id = this.#createMessageId()) {
419
- void this.#chunkAndSend(data, id).catch(() => { });
420
- }
421
- request(data, id = this.#createMessageId()) {
422
- return new Promise((resolve, reject) => {
423
- if (!this.transport.isConnected()) {
424
- reject(new Error('peer is not connected'));
425
- return;
426
- }
427
- const pubsub = this.options.getPubSub();
428
- if (!pubsub) {
429
- reject(new Error('globalThis.pubsub is not available'));
430
- return;
431
- }
432
- let timeout;
433
- let settled = false;
434
- const finish = (fn) => {
435
- if (settled)
436
- return;
437
- settled = true;
438
- clearTimeout(timeout);
439
- try {
440
- pubsub.unsubscribe(id, onrequest);
441
- }
442
- catch (e) { }
443
- fn();
444
- };
445
- const onrequest = ({ data }) => {
446
- finish(() => resolve(data));
447
- };
448
- timeout = setTimeout(() => {
449
- finish(() => reject(new Error(`request for ${id} timed out`)));
450
- }, 30_000);
451
- try {
452
- pubsub.subscribe(id, onrequest);
453
- }
454
- catch (error) {
455
- finish(() => {
456
- reject(error instanceof Error ? error : new Error(String(error)));
457
- });
458
- return;
459
- }
460
- if (!this.transport.isConnected()) {
461
- finish(() => reject(new Error('peer disconnected before request send')));
462
- return;
463
- }
464
- void this.#chunkAndSend(data, id).catch((error) => {
465
- finish(() => {
466
- reject(error instanceof Error ? error : new Error(String(error)));
467
- });
468
- });
469
- });
470
- }
471
- }
472
-
473
- const iceServers = [
474
- {
475
- urls: 'stun:stun.l.google.com:19302' // Google's public STUN server
476
- },
477
- {
478
- urls: 'stun:openrelay.metered.ca:80'
479
- },
480
- {
481
- urls: 'turn:openrelay.metered.ca:443',
482
- username: 'openrelayproject',
483
- credential: 'openrelayproject'
484
- },
485
- {
486
- urls: 'turn:openrelay.metered.ca:443?transport=tcp',
487
- username: 'openrelayproject',
488
- credential: 'openrelayproject'
489
- }
490
- ];
491
- class WebRTCPeer {
492
- #pc;
493
- #listeners = new Map();
494
- #pendingCandidates = [];
495
- #connected = false;
496
- #destroyed = false;
497
- #trickle = true;
498
- #core;
499
- _pc;
500
- _channel = null;
501
- initiator;
502
- peerId;
503
- channelName;
504
- version;
505
- compressionThreshold = 0.98;
506
- bw = { up: 0, down: 0 };
507
- get connected() {
508
- return this.#connected;
509
- }
510
- constructor(options) {
511
- const { from, to, initiator, trickle, config, version, wrtc, compressionThreshold } = options;
512
- const rtc = this.#resolveRtc(wrtc);
513
- const channelName = initiator ? `${from}:${to}` : `${to}:${from}`;
514
- const pc = new rtc.RTCPeerConnection({ iceServers, ...config });
515
- this.#pc = pc;
516
- this._pc = pc;
517
- this.initiator = Boolean(initiator);
518
- this.#trickle = trickle ?? true;
519
- this.version = String(version);
520
- this.peerId = to;
521
- this.channelName = channelName;
522
- if (compressionThreshold !== undefined)
523
- this.compressionThreshold = compressionThreshold;
524
- this.#setupPeerConnectionHandlers(rtc);
525
- if (this.initiator) {
526
- this.#attachDataChannel(this.#pc.createDataChannel(channelName));
527
- void this.#createOffer();
528
- }
529
- this.#core = new PeerCore({
530
- kind: 'webrtc-datachannel',
531
- isConnected: () => this.connected,
532
- sendRaw: (payload) => this.#sendRaw(payload)
533
- }, {
534
- maxMessageSize: MAX_MESSAGE_SIZE,
535
- compressionThreshold: this.compressionThreshold,
536
- getBufferedAmount: () => this._channel?.bufferedAmount || 0,
537
- getPubSub: this.#getPubSub,
538
- onPayloadSend: (payloadBytes) => {
539
- this.bw.up += payloadBytes;
540
- }
541
- });
542
- }
543
- #createMessageId() {
544
- const randomUUID = globalThis.crypto?.randomUUID;
545
- if (typeof randomUUID === 'function')
546
- return randomUUID.call(globalThis.crypto);
547
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
548
- }
549
- #resolveRtc(wrtc) {
550
- if (wrtc)
551
- return wrtc;
552
- const globalCandidate = globalThis;
553
- if (globalCandidate.RTCPeerConnection) {
554
- return {
555
- RTCPeerConnection: globalCandidate.RTCPeerConnection,
556
- RTCSessionDescription: globalCandidate.RTCSessionDescription,
557
- RTCIceCandidate: globalCandidate.RTCIceCandidate
558
- };
559
- }
560
- if (globalCandidate.wrtc?.RTCPeerConnection)
561
- return globalCandidate.wrtc;
562
- throw new Error('No WebRTC implementation available');
563
- }
564
- #setupPeerConnectionHandlers(rtc) {
565
- this.#pc.onicecandidate = (event) => {
566
- if (this.#destroyed)
567
- return;
568
- if (event.candidate && this.#trickle) {
569
- const payload = event.candidate.toJSON
570
- ? event.candidate.toJSON()
571
- : {
572
- candidate: event.candidate.candidate,
573
- sdpMLineIndex: event.candidate.sdpMLineIndex,
574
- sdpMid: event.candidate.sdpMid,
575
- usernameFragment: event.candidate.usernameFragment
576
- };
577
- this.#emit('signal', payload);
578
- }
579
- if (!event.candidate && !this.#trickle && this.#pc.localDescription) {
580
- this.#emit('signal', this.#serializeDescription(this.#pc.localDescription));
581
- }
582
- };
583
- this.#pc.onconnectionstatechange = () => {
584
- const state = this.#pc.connectionState;
585
- if (state === 'failed' || state === 'closed')
586
- this.destroy();
587
- };
588
- this.#pc.ondatachannel = (event) => {
589
- if (!this._channel)
590
- this.#attachDataChannel(event.channel);
591
- };
592
- }
593
- #attachDataChannel(channel) {
594
- this._channel = channel;
595
- channel.binaryType = 'arraybuffer';
596
- channel.onopen = () => {
597
- if (this.#destroyed || this.#connected)
598
- return;
599
- this.#connected = true;
600
- this.#emit('connect');
601
- };
602
- channel.onclose = () => {
603
- if (this.#destroyed)
604
- return;
605
- this.destroy();
606
- };
607
- channel.onerror = () => {
608
- this.#emit('error', new Error('DataChannel error'));
609
- };
610
- channel.onmessage = (event) => {
611
- this.#emit('data', event.data);
612
- };
613
- }
614
- async #createOffer() {
615
- try {
616
- const offer = await this.#pc.createOffer();
617
- await this.#pc.setLocalDescription(offer);
618
- if (this.#trickle && this.#pc.localDescription) {
619
- this.#emit('signal', this.#serializeDescription(this.#pc.localDescription));
620
- }
621
- }
622
- catch (error) {
623
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
624
- }
625
- }
626
- async #applyRemoteDescription(rtc, description) {
627
- const sessionDescription = rtc.RTCSessionDescription
628
- ? new rtc.RTCSessionDescription(description)
629
- : description;
630
- await this.#pc.setRemoteDescription(sessionDescription);
631
- if (description.type === 'offer') {
632
- const answer = await this.#pc.createAnswer();
633
- await this.#pc.setLocalDescription(answer);
634
- if (this.#pc.localDescription) {
635
- this.#emit('signal', this.#serializeDescription(this.#pc.localDescription));
636
- }
637
- }
638
- const pending = [...this.#pendingCandidates];
639
- this.#pendingCandidates = [];
640
- for (const candidate of pending) {
641
- // eslint-disable-next-line no-await-in-loop
642
- await this.#addIceCandidate(rtc, candidate);
643
- }
644
- }
645
- async #addIceCandidate(rtc, candidate) {
646
- if (!this.#pc.remoteDescription) {
647
- this.#pendingCandidates.push(candidate);
648
- return;
649
- }
650
- const iceCandidate = rtc.RTCIceCandidate
651
- ? new rtc.RTCIceCandidate(candidate)
652
- : candidate;
653
- await this.#pc.addIceCandidate(iceCandidate);
654
- }
655
- #serializeDescription(description) {
656
- return {
657
- type: description.type,
658
- sdp: description.sdp || ''
659
- };
660
- }
661
- #sendRaw(payload) {
662
- if (!this._channel || this._channel.readyState !== 'open') {
663
- throw new Error('datachannel is not open');
664
- }
665
- this._channel.send(payload);
666
- }
667
- on(event, handler) {
668
- const listeners = this.#listeners.get(event) ?? new Set();
669
- listeners.add(handler);
670
- this.#listeners.set(event, listeners);
671
- return this;
672
- }
673
- off(event, handler) {
674
- this.#listeners.get(event)?.delete(handler);
675
- return this;
676
- }
677
- #emit(event, ...args) {
678
- const listeners = this.#listeners.get(event);
679
- if (!listeners)
680
- return;
681
- for (const handler of listeners)
682
- handler(...args);
683
- }
684
- signal(signalData) {
685
- const rtc = this.#resolveRtc();
686
- void (async () => {
687
- try {
688
- const signalCandidate = signalData;
689
- if (typeof signalCandidate.candidate === 'string') {
690
- await this.#addIceCandidate(rtc, signalCandidate);
691
- return;
692
- }
693
- const signalDescription = signalData;
694
- if (typeof signalDescription.type === 'string' &&
695
- typeof signalDescription.sdp === 'string') {
696
- await this.#applyRemoteDescription(rtc, signalDescription);
697
- return;
698
- }
699
- throw new Error('invalid signal payload');
700
- }
701
- catch (error) {
702
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
703
- }
704
- })();
705
- }
706
- destroy() {
707
- if (this.#destroyed)
708
- return;
709
- this.#destroyed = true;
710
- this.#connected = false;
711
- if (this._channel) {
712
- this._channel.onopen = null;
713
- this._channel.onclose = null;
714
- this._channel.onmessage = null;
715
- this._channel.onerror = null;
716
- try {
717
- this._channel.close();
718
- }
719
- catch (e) { }
720
- this._channel = null;
721
- }
722
- this.#pc.onicecandidate = null;
723
- this.#pc.onconnectionstatechange = null;
724
- this.#pc.ondatachannel = null;
725
- try {
726
- this.#pc.close();
727
- }
728
- catch (e) { }
729
- this.#emit('close');
730
- }
731
- #getPubSub() {
732
- const pubsubCandidate = globalThis
733
- .pubsub;
734
- if (pubsubCandidate &&
735
- typeof pubsubCandidate.subscribe === 'function' &&
736
- typeof pubsubCandidate.unsubscribe === 'function') {
737
- return pubsubCandidate;
738
- }
739
- return null;
740
- }
741
- /**
742
- * send to peer
743
- * @param data Uint8Array
744
- * @param id custom id to listen to
745
- */
746
- send(data, id = this.#createMessageId()) {
747
- this.#core.send(data, id);
748
- }
749
- /**
750
- * send to peer & wait for response
751
- * @param data Uint8Array
752
- * @param id custom id to listen to
753
- */
754
- request(data, id = this.#createMessageId()) {
755
- return this.#core.request(data, id);
756
- }
757
- /**
758
- * Get comprehensive network statistics from WebRTC
759
- * @returns NetworkStats object with detailed metrics
760
- */
761
- async getNetworkStats() {
762
- try {
763
- const pc = this._pc;
764
- if (!pc)
765
- return null;
766
- const stats = await pc.getStats();
767
- const result = {
768
- latency: null,
769
- jitter: null,
770
- bytesReceived: 0,
771
- bytesSent: 0,
772
- packetsLost: 0,
773
- fractionLost: null,
774
- inboundBitrate: null,
775
- outboundBitrate: null,
776
- availableOutgoingBitrate: null,
777
- timestamp: Date.now()
778
- };
779
- stats.forEach((report) => {
780
- const typedReport = report;
781
- // Latency from candidate pair
782
- if (typedReport.type === 'candidate-pair' &&
783
- typeof typedReport.currentRoundTripTime === 'number') {
784
- result.latency = Math.round(typedReport.currentRoundTripTime * 1000);
785
- }
786
- // Inbound RTP stats
787
- if (typedReport.type === 'inbound-rtp') {
788
- result.bytesReceived += Number(typedReport.bytesReceived || 0);
789
- result.packetsLost += Number(typedReport.packetsLost || 0);
790
- if (typeof typedReport.jitter === 'number') {
791
- result.jitter = Math.round(typedReport.jitter * 1000);
792
- }
793
- if (typeof typedReport.fractionLost === 'number') {
794
- result.fractionLost = typedReport.fractionLost;
795
- }
796
- }
797
- // Outbound RTP stats
798
- if (typedReport.type === 'outbound-rtp') {
799
- result.bytesSent += Number(typedReport.bytesSent || 0);
800
- }
801
- // Available bandwidth
802
- if (typedReport.type === 'remote-candidate' &&
803
- typeof typedReport.availableOutgoingBitrate === 'number') {
804
- result.availableOutgoingBitrate = Math.round(typedReport.availableOutgoingBitrate);
805
- }
806
- });
807
- return result;
808
- }
809
- catch (e) {
810
- return null;
811
- }
812
- }
813
- toJSON() {
814
- return {
815
- peerId: this.peerId,
816
- channelName: this.channelName,
817
- version: this.version,
818
- bw: this.bw
819
- };
820
- }
821
- }
822
-
823
- const getWebTransportCtor = () => {
824
- const candidate = globalThis;
825
- if (!candidate.WebTransport) {
826
- throw new Error('WebTransport is not available in this runtime');
827
- }
828
- return candidate.WebTransport;
829
- };
830
- class WebTransportPeer {
831
- #session = null;
832
- #reader = null;
833
- #writer = null;
834
- #listeners = new Map();
835
- #destroyed = false;
836
- #connected = false;
837
- #core;
838
- initiator = true;
839
- peerId;
840
- channelName;
841
- version;
842
- compressionThreshold = 0.98;
843
- bw = { up: 0, down: 0 };
844
- get connected() {
845
- return this.#connected;
846
- }
847
- constructor(options) {
848
- this.peerId = options.to;
849
- this.channelName = `webtransport:${options.from}:${options.to}`;
850
- this.version = String(options.version);
851
- if (options.compressionThreshold !== undefined) {
852
- this.compressionThreshold = options.compressionThreshold;
853
- }
854
- this.#core = new PeerCore({
855
- kind: 'webtransport-bidi',
856
- isConnected: () => this.connected,
857
- sendRaw: (payload) => {
858
- void this.#sendRaw(payload);
859
- }
860
- }, {
861
- maxMessageSize: MAX_MESSAGE_SIZE,
862
- compressionThreshold: this.compressionThreshold,
863
- getPubSub: this.#getPubSub,
864
- onPayloadSend: (payloadBytes) => {
865
- this.bw.up += payloadBytes;
866
- }
867
- });
868
- queueMicrotask(() => {
869
- void this.#open(options.url);
870
- });
871
- }
872
- #createMessageId() {
873
- const randomUUID = globalThis.crypto?.randomUUID;
874
- if (typeof randomUUID === 'function') {
875
- return randomUUID.call(globalThis.crypto);
876
- }
877
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
878
- }
879
- #getPubSub() {
880
- const pubsubCandidate = globalThis
881
- .pubsub;
882
- if (pubsubCandidate &&
883
- typeof pubsubCandidate.subscribe === 'function' &&
884
- typeof pubsubCandidate.unsubscribe === 'function') {
885
- return pubsubCandidate;
886
- }
887
- return null;
888
- }
889
- #emit(event, ...args) {
890
- const listeners = this.#listeners.get(event);
891
- if (!listeners)
892
- return;
893
- for (const handler of listeners)
894
- handler(...args);
895
- }
896
- async #open(url) {
897
- try {
898
- const WebTransportCtor = getWebTransportCtor();
899
- const session = new WebTransportCtor(url);
900
- this.#session = session;
901
- await session.ready;
902
- const stream = (await session.createBidirectionalStream());
903
- this.#reader = stream.readable.getReader();
904
- this.#writer = stream.writable.getWriter();
905
- this.#connected = true;
906
- this.#emit('connect');
907
- void this.#readLoop();
908
- void session.closed.catch((error) => {
909
- if (this.#destroyed)
910
- return;
911
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
912
- });
913
- }
914
- catch (error) {
915
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
916
- this.destroy();
917
- }
918
- }
919
- async #readLoop() {
920
- const reader = this.#reader;
921
- if (!reader)
922
- return;
923
- try {
924
- while (!this.#destroyed) {
925
- // eslint-disable-next-line no-await-in-loop
926
- const { done, value } = await reader.read();
927
- if (done)
928
- break;
929
- if (!value)
930
- continue;
931
- this.bw.down += value.length;
932
- this.#emit('data', value);
933
- }
934
- }
935
- catch (error) {
936
- if (!this.#destroyed) {
937
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
938
- }
939
- }
940
- if (!this.#destroyed)
941
- this.destroy();
942
- }
943
- async #sendRaw(payload) {
944
- if (!this.#writer)
945
- throw new Error('WebTransport writer is not ready');
946
- await this.#writer.write(payload);
947
- }
948
- on(event, handler) {
949
- const listeners = this.#listeners.get(event) ?? new Set();
950
- listeners.add(handler);
951
- this.#listeners.set(event, listeners);
952
- return this;
953
- }
954
- off(event, handler) {
955
- this.#listeners.get(event)?.delete(handler);
956
- return this;
957
- }
958
- signal(_signalData) {
959
- // WebTransport does not use SDP/ICE signaling in this peer implementation.
960
- }
961
- send(data, id = this.#createMessageId()) {
962
- this.#core.send(data, id);
963
- }
964
- request(data, id = this.#createMessageId()) {
965
- return this.#core.request(data, id);
966
- }
967
- async getNetworkStats() {
968
- return null;
969
- }
970
- destroy() {
971
- if (this.#destroyed)
972
- return;
973
- this.#destroyed = true;
974
- this.#connected = false;
975
- try {
976
- void this.#reader?.cancel?.();
977
- }
978
- catch (e) { }
979
- this.#reader?.releaseLock?.();
980
- this.#reader = null;
981
- try {
982
- void this.#writer?.close?.();
983
- }
984
- catch (e) { }
985
- this.#writer?.releaseLock?.();
986
- this.#writer = null;
987
- try {
988
- this.#session?.close?.();
989
- }
990
- catch (e) { }
991
- this.#session = null;
992
- this.#emit('close');
993
- }
994
- }
995
-
996
- const getPubSub = () => {
997
- const candidate = globalThis.pubsub;
998
- if (candidate &&
999
- typeof candidate.subscribe === 'function' &&
1000
- typeof candidate.unsubscribe === 'function') {
1001
- return candidate;
1002
- }
1003
- return null;
1004
- };
1005
- class CircuitPeer {
1006
- #listeners = new Map();
1007
- #destroyed = false;
1008
- #connected = false;
1009
- #sendViaCircuit;
1010
- initiator = true;
1011
- peerId;
1012
- channelName;
1013
- version;
1014
- bw = { up: 0, down: 0 };
1015
- get connected() {
1016
- return this.#connected;
1017
- }
1018
- constructor(options) {
1019
- this.peerId = options.to;
1020
- this.channelName = `circuit:${options.from}:${options.to}`;
1021
- this.version = String(options.version);
1022
- this.#sendViaCircuit = options.sendViaCircuit;
1023
- setTimeout(() => {
1024
- if (this.#destroyed)
1025
- return;
1026
- this.#connected = true;
1027
- this.#emit('connect');
1028
- }, 0);
1029
- }
1030
- #createMessageId() {
1031
- const randomUUID = globalThis.crypto?.randomUUID;
1032
- if (typeof randomUUID === 'function') {
1033
- return randomUUID.call(globalThis.crypto);
1034
- }
1035
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1036
- }
1037
- #emit(event, ...args) {
1038
- const listeners = this.#listeners.get(event);
1039
- if (!listeners)
1040
- return;
1041
- for (const handler of listeners)
1042
- handler(...args);
1043
- }
1044
- on(event, handler) {
1045
- const listeners = this.#listeners.get(event) ?? new Set();
1046
- listeners.add(handler);
1047
- this.#listeners.set(event, listeners);
1048
- return this;
1049
- }
1050
- off(event, handler) {
1051
- this.#listeners.get(event)?.delete(handler);
1052
- return this;
1053
- }
1054
- signal(_signalData) {
1055
- // Circuit transport does not use SDP/ICE signaling.
1056
- }
1057
- send(data, id = this.#createMessageId()) {
1058
- if (this.#destroyed) {
1059
- this.#emit('error', new Error('circuit peer is destroyed'));
1060
- return;
1061
- }
1062
- this.bw.up += data.length;
1063
- void this.#sendViaCircuit({ id, data: Array.from(data) }).catch((error) => {
1064
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
1065
- });
1066
- }
1067
- request(data, id = this.#createMessageId()) {
1068
- const pubsub = getPubSub();
1069
- if (!pubsub) {
1070
- return Promise.reject(new Error('globalThis.pubsub is not available'));
1071
- }
1072
- return new Promise((resolve, reject) => {
1073
- let timeout;
1074
- let settled = false;
1075
- const finish = (fn) => {
1076
- if (settled)
1077
- return;
1078
- settled = true;
1079
- clearTimeout(timeout);
1080
- try {
1081
- pubsub.unsubscribe(id, onResponse);
1082
- }
1083
- catch (e) { }
1084
- fn();
1085
- };
1086
- const onResponse = ({ data: response }) => {
1087
- finish(() => resolve(response));
1088
- };
1089
- timeout = setTimeout(() => {
1090
- finish(() => reject(new Error(`request for ${id} timed out`)));
1091
- }, 30_000);
1092
- pubsub.subscribe(id, onResponse);
1093
- this.send(data, id);
1094
- });
1095
- }
1096
- async getNetworkStats() {
1097
- return null;
1098
- }
1099
- destroy() {
1100
- if (this.#destroyed)
1101
- return;
1102
- this.#destroyed = true;
1103
- this.#connected = false;
1104
- this.#emit('close');
1105
- }
1106
- }
1107
-
1108
- /** Normalize current star announcements and the legacy peer-id-only format. */
1109
- const normalizePeerAnnouncement = (value) => {
1110
- if (typeof value === 'string') {
1111
- const peerId = value.trim();
1112
- return peerId && peerId !== 'undefined' ? { peerId } : null;
1113
- }
1114
- if (!value || typeof value !== 'object')
1115
- return null;
1116
- const candidate = value;
1117
- if (typeof candidate.peerId !== 'string')
1118
- return null;
1119
- const peerId = candidate.peerId.trim();
1120
- if (!peerId || peerId === 'undefined')
1121
- return null;
1122
- return {
1123
- peerId,
1124
- version: typeof candidate.version === 'string' ? candidate.version : undefined,
1125
- transport: candidate.transport
1126
- };
1127
- };
1128
-
1129
- // Simple CRC32 implementation
1130
- const crc32 = (data) => {
1131
- let crc = 0xffffffff;
1132
- for (let i = 0; i < data.length; i++) {
1133
- crc ^= data[i];
1134
- for (let j = 0; j < 8; j++) {
1135
- crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
1136
- }
1137
- }
1138
- return (crc ^ 0xffffffff) >>> 0;
1139
- };
1140
- const debug = createDebugger('@netpeer/swarm/client');
1141
- const isWrtcImplementation = (candidate) => {
1142
- if (!candidate || typeof candidate !== 'object')
1143
- return false;
1144
- const wrtcCandidate = candidate;
1145
- return (typeof wrtcCandidate.RTCPeerConnection === 'function' &&
1146
- typeof wrtcCandidate.RTCSessionDescription === 'function' &&
1147
- typeof wrtcCandidate.RTCIceCandidate === 'function');
1148
- };
1149
- class Client {
1150
- #peerId;
1151
- #connections = {};
1152
- #stars = {};
1153
- #starListeners = {};
1154
- #reinitLock = null;
1155
- #connectEvent = 'peer:connected';
1156
- #supportedTransports = new Set(['webrtc']);
1157
- #preferredTransportKind = 'webrtc';
1158
- #wrtcBackend = 'vandeurenglenn';
1159
- #fallbackTransportOrder = [];
1160
- #enableCircuitFallback = true;
1161
- #transportConnectTimeoutMs = 10_000;
1162
- #peerTransportAttempts = new Map();
1163
- #peerTransportKinds = new Map();
1164
- #fallbackCircuitMethod = 'swarm:fallback:send';
1165
- #testHooks;
1166
- #webTransportUrlTemplate;
1167
- #circuitHandlers = new Map();
1168
- #pendingCircuitRequests = new Map();
1169
- #circuitRequestTimeoutMs = 30_000;
1170
- #retryOptions = { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 30000 };
1171
- id;
1172
- networkVersion;
1173
- starsConfig;
1174
- socketClient;
1175
- messageSize = 262144;
1176
- version;
1177
- #messagesToHandle = {};
1178
- get peerId() {
1179
- return this.#peerId;
1180
- }
1181
- get connections() {
1182
- return { ...this.#connections };
1183
- }
1184
- get peers() {
1185
- return Object.entries(this.#connections);
1186
- }
1187
- getPeer(peerId) {
1188
- return this.#connections[peerId];
1189
- }
1190
- constructor(options) {
1191
- const { peerId, networkVersion, version, connectEvent, stars } = {
1192
- ...defaultOptions,
1193
- ...options
1194
- };
1195
- this.#peerId = peerId;
1196
- this.networkVersion = networkVersion;
1197
- this.version = version;
1198
- this.#testHooks = options.testHooks;
1199
- this.#connectEvent = connectEvent;
1200
- this.starsConfig = stars;
1201
- const configuredKinds = options.transport?.kinds
1202
- ? options.transport.kinds
1203
- : options.transport?.kind
1204
- ? [options.transport.kind]
1205
- : ['webrtc', 'webtransport'];
1206
- this.#supportedTransports = new Set(this.#normalizeTransportKinds(configuredKinds));
1207
- if (!this.#supportedTransports.size)
1208
- this.#supportedTransports.add('webrtc');
1209
- const configuredPreferred = options.transport?.preferredKind || options.transport?.kind || 'webrtc';
1210
- this.#preferredTransportKind = this.#supportedTransports.has(configuredPreferred)
1211
- ? configuredPreferred
1212
- : this.#supportedTransports.has('webrtc')
1213
- ? 'webrtc'
1214
- : 'webtransport';
1215
- this.#webTransportUrlTemplate = options.transport?.webtransport?.urlTemplate;
1216
- const wrtcBackend = options.transport?.webrtc?.backend ||
1217
- globalThis.process?.env?.NETPEER_SWARM_WRTC_BACKEND ||
1218
- 'vandeurenglenn';
1219
- if (wrtcBackend !== 'vandeurenglenn' && wrtcBackend !== 'koush') {
1220
- throw new Error(`Unsupported WebRTC backend: ${wrtcBackend}`);
1221
- }
1222
- this.#wrtcBackend = wrtcBackend;
1223
- this.#enableCircuitFallback = options.transport?.fallback?.enabled ?? true;
1224
- if (typeof options.transport?.fallback?.connectTimeoutMs === 'number' &&
1225
- options.transport.fallback.connectTimeoutMs > 0) {
1226
- this.#transportConnectTimeoutMs =
1227
- options.transport.fallback.connectTimeoutMs;
1228
- }
1229
- this.#fallbackTransportOrder = this.#normalizeFallbackOrder(options.transport?.fallback?.order);
1230
- if (options?.retry)
1231
- this.#retryOptions = { ...this.#retryOptions, ...options.retry };
1232
- this.#circuitHandlers.set(this.#fallbackCircuitMethod, (payload, context) => this.#handleFallbackCircuitPayload(payload, context));
1233
- if (!this.#testHooks?.skipInit) {
1234
- this._init();
1235
- }
1236
- }
1237
- /**
1238
- * Safely reinitialize the client (used after system resume/sleep).
1239
- * It closes existing connections and reconnects to configured stars.
1240
- */
1241
- async reinit() {
1242
- // avoid concurrent reinit runs
1243
- if (this.#reinitLock)
1244
- return this.#reinitLock;
1245
- this.#reinitLock = (async () => {
1246
- debug('reinit: start');
1247
- try {
1248
- await this.close();
1249
- this.#stars = {};
1250
- this.#connections = {};
1251
- for (const star of this.starsConfig) {
1252
- try {
1253
- await this.setupStar(star);
1254
- }
1255
- catch (e) {
1256
- // If last star fails and none connected, surface error
1257
- if (Object.keys(this.#stars).length === 0)
1258
- throw new Error(`No star available to connect`);
1259
- }
1260
- }
1261
- }
1262
- finally {
1263
- debug('reinit: done');
1264
- this.#reinitLock = null;
1265
- }
1266
- })();
1267
- return this.#reinitLock;
1268
- }
1269
- async setupStar(star) {
1270
- const { retries, factor, minTimeout, maxTimeout } = this.#retryOptions;
1271
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1272
- let attempt = 0;
1273
- let lastErr;
1274
- while (attempt <= retries) {
1275
- try {
1276
- const client = new SocketRequestClient(star, this.networkVersion);
1277
- this.#stars[star] = await client.init();
1278
- this.setupStarListeners(this.#stars[star], star);
1279
- this.#stars[star].send({
1280
- url: 'join',
1281
- params: {
1282
- version: this.version,
1283
- peerId: this.peerId,
1284
- transport: {
1285
- kind: this.#preferredTransportKind,
1286
- kinds: [...this.#supportedTransports]
1287
- }
1288
- }
1289
- });
1290
- globalThis.pubsub.publishVerbose('star:connected', star);
1291
- debug(`setupStar ${star} succeeded`);
1292
- return this.#stars[star];
1293
- }
1294
- catch (e) {
1295
- lastErr = e;
1296
- attempt += 1;
1297
- if (attempt > retries)
1298
- break;
1299
- const delay = Math.min(maxTimeout, Math.round(minTimeout * Math.pow(factor, attempt - 1)));
1300
- debug(`setupStar ${star} failed, retrying in ${delay}ms (attempt ${attempt})`);
1301
- // eslint-disable-next-line no-await-in-loop
1302
- await sleep(delay);
1303
- }
1304
- }
1305
- throw lastErr;
1306
- }
1307
- async _init() {
1308
- if (this.#supportedTransports.has('webrtc') &&
1309
- !globalThis.RTCPeerConnection &&
1310
- !globalThis.wrtc) {
1311
- const { backend, implementation } = await this.#loadNodeWebrtcImplementation();
1312
- globalThis.wrtc = implementation;
1313
- globalThis.__swarmWrtcImpl = backend;
1314
- }
1315
- for (const star of this.starsConfig) {
1316
- try {
1317
- await this.setupStar(star);
1318
- }
1319
- catch (e) {
1320
- if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1 &&
1321
- !this.socketClient)
1322
- throw new Error(`No star available to connect`);
1323
- }
1324
- }
1325
- if (globalThis.process?.versions?.node) {
1326
- process.on('SIGINT', async () => {
1327
- process.stdin.resume();
1328
- await this.close();
1329
- process.exit();
1330
- });
1331
- }
1332
- else {
1333
- globalThis.addEventListener('beforeunload', this.close.bind(this));
1334
- }
1335
- }
1336
- async #loadNodeWebrtcImplementation() {
1337
- let wrtcModule;
1338
- if (this.#wrtcBackend === 'koush') {
1339
- try {
1340
- wrtcModule = await import('@koush/wrtc');
1341
- }
1342
- catch (error) {
1343
- throw new Error('The koush WebRTC backend is opt-in; install @koush/wrtc to use it', { cause: error });
1344
- }
1345
- }
1346
- else {
1347
- wrtcModule = await import('@vandeurenglenn/wrtc');
1348
- }
1349
- const moduleCandidate = wrtcModule;
1350
- const implementation = isWrtcImplementation(moduleCandidate.default)
1351
- ? moduleCandidate.default
1352
- : wrtcModule;
1353
- if (!isWrtcImplementation(implementation)) {
1354
- const packageName = this.#wrtcBackend === 'koush' ? '@koush/wrtc' : '@vandeurenglenn/wrtc';
1355
- throw new Error(`${packageName} does not match required wrtc contract`);
1356
- }
1357
- debug(`using ${this.#wrtcBackend} as wrtc implementation`);
1358
- return { backend: this.#wrtcBackend, implementation };
1359
- }
1360
- setupStarListeners(starConnection, starId) {
1361
- // create stable references to handlers so we can unsubscribe later
1362
- const onPeerJoined = (id) => this.#peerJoined(id, starConnection);
1363
- const onPeerLeft = (id) => this.#peerLeft(id, starConnection);
1364
- const onStarJoined = this.#starJoined;
1365
- const onStarLeft = this.#starLeft;
1366
- const onSignal = (message) => this.#inComingSignal(message, starConnection);
1367
- const onCircuit = (message) => this.#inComingCircuit(message, starConnection);
1368
- const onServerData = (message) => {
1369
- globalThis.pubsub.publish('server:data', message);
1370
- };
1371
- starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
1372
- starConnection.pubsub.subscribe('peer:left', onPeerLeft);
1373
- starConnection.pubsub.subscribe('star:joined', onStarJoined);
1374
- starConnection.pubsub.subscribe('star:left', onStarLeft);
1375
- starConnection.pubsub.subscribe('signal', onSignal);
1376
- starConnection.pubsub.subscribe('circuit', onCircuit);
1377
- starConnection.pubsub.subscribe('server:data', onServerData);
1378
- this.#starListeners[starId] = [
1379
- { topic: 'peer:joined', handler: onPeerJoined },
1380
- { topic: 'peer:left', handler: onPeerLeft },
1381
- { topic: 'star:joined', handler: onStarJoined },
1382
- { topic: 'star:left', handler: onStarLeft },
1383
- { topic: 'signal', handler: onSignal },
1384
- { topic: 'circuit', handler: onCircuit },
1385
- { topic: 'server:data', handler: onServerData }
1386
- ];
1387
- }
1388
- #starJoined = (id) => {
1389
- if (this.#stars[id]) {
1390
- this.#stars[id].close(0);
1391
- delete this.#stars[id];
1392
- }
1393
- console.log(`star ${id} joined`);
1394
- };
1395
- #starLeft = async (id) => {
1396
- if (this.#stars[id]) {
1397
- this.#stars[id].close(0);
1398
- delete this.#stars[id];
1399
- }
1400
- // if we lost all stars, try to reconnect to configured stars with backoff
1401
- if (Object.keys(this.#stars).length === 0) {
1402
- for (const star of this.starsConfig) {
1403
- try {
1404
- await this.setupStar(star);
1405
- // stop at first success
1406
- return;
1407
- }
1408
- catch (e) {
1409
- debug(`reconnect star ${star} failed: ${e.message || e}`);
1410
- if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1)
1411
- throw new Error(`No star available to connect`);
1412
- }
1413
- }
1414
- }
1415
- debug(`star ${id} left`);
1416
- };
1417
- #peerLeft = (peer, star) => {
1418
- const id = peer.peerId || peer;
1419
- if (this.#connections[id]) {
1420
- this.#connections[id].destroy();
1421
- delete this.#connections[id];
1422
- }
1423
- debug(`peer ${id} left`);
1424
- };
1425
- #normalizeTransportKinds(kinds) {
1426
- if (!Array.isArray(kinds))
1427
- return [];
1428
- const normalized = new Set();
1429
- for (const kind of kinds) {
1430
- if (kind === 'webrtc' || kind === 'webtransport')
1431
- normalized.add(kind);
1432
- }
1433
- return [...normalized];
1434
- }
1435
- #normalizeFallbackOrder(kinds) {
1436
- if (!Array.isArray(kinds))
1437
- return [];
1438
- const normalized = new Set();
1439
- for (const kind of kinds) {
1440
- if (kind === 'webrtc' || kind === 'webtransport' || kind === 'circuit') {
1441
- normalized.add(kind);
1442
- }
1443
- }
1444
- return [...normalized];
1445
- }
1446
- #supportsTransport(kind) {
1447
- return this.#supportedTransports.has(kind);
1448
- }
1449
- #getRemoteTransportKinds(transport) {
1450
- const fromKinds = this.#normalizeTransportKinds(transport?.kinds);
1451
- if (fromKinds.length)
1452
- return fromKinds;
1453
- if (transport?.kind === 'webrtc' || transport?.kind === 'webtransport') {
1454
- return [transport.kind];
1455
- }
1456
- return ['webrtc'];
1457
- }
1458
- #selectTransportForPeer(peerId, transport) {
1459
- const remoteKinds = this.#getRemoteTransportKinds(transport);
1460
- const commonKinds = remoteKinds.filter((kind) => this.#supportsTransport(kind));
1461
- if (!commonKinds.length) {
1462
- debug(`peer ${peerId} has no compatible transport with local capabilities`);
1463
- return null;
1464
- }
1465
- if (transport?.kind && commonKinds.includes(transport.kind)) {
1466
- return transport.kind;
1467
- }
1468
- if (commonKinds.includes(this.#preferredTransportKind)) {
1469
- return this.#preferredTransportKind;
1470
- }
1471
- if (commonKinds.includes('webrtc'))
1472
- return 'webrtc';
1473
- return 'webtransport';
1474
- }
1475
- #buildTransportOrder(peerId, transport) {
1476
- const remoteKinds = this.#getRemoteTransportKinds(transport);
1477
- const commonKinds = remoteKinds.filter((kind) => this.#supportsTransport(kind));
1478
- if (!commonKinds.length) {
1479
- if (this.#enableCircuitFallback) {
1480
- debug(`peer ${peerId} has no direct transport overlap, using circuit`);
1481
- return ['circuit'];
1482
- }
1483
- return [];
1484
- }
1485
- const order = [];
1486
- if (transport?.kind && commonKinds.includes(transport.kind)) {
1487
- order.push(transport.kind);
1488
- }
1489
- if (commonKinds.includes(this.#preferredTransportKind) &&
1490
- !order.includes(this.#preferredTransportKind)) {
1491
- order.push(this.#preferredTransportKind);
1492
- }
1493
- for (const kind of this.#fallbackTransportOrder) {
1494
- if (kind !== 'circuit' &&
1495
- commonKinds.includes(kind) &&
1496
- !order.includes(kind)) {
1497
- order.push(kind);
1498
- }
1499
- }
1500
- if (commonKinds.includes('webrtc') && !order.includes('webrtc')) {
1501
- order.push('webrtc');
1502
- }
1503
- if (commonKinds.includes('webtransport') &&
1504
- !order.includes('webtransport')) {
1505
- order.push('webtransport');
1506
- }
1507
- if (this.#enableCircuitFallback && !order.includes('circuit')) {
1508
- order.push('circuit');
1509
- }
1510
- debug(`peer ${peerId} transport order: ${order.join(' -> ')}`);
1511
- return order;
1512
- }
1513
- #clearTransportAttempt(peerId) {
1514
- const attempt = this.#peerTransportAttempts.get(peerId);
1515
- if (attempt?.timeout)
1516
- clearTimeout(attempt.timeout);
1517
- this.#peerTransportAttempts.delete(peerId);
1518
- }
1519
- #emitTransportEvent(event) {
1520
- this.#testHooks?.onTransportEvent?.(event);
1521
- }
1522
- #advanceTransportAttempt(peerId) {
1523
- const attempt = this.#peerTransportAttempts.get(peerId);
1524
- if (!attempt)
1525
- return;
1526
- const previousIndex = attempt.index;
1527
- const previousTransport = attempt.order[previousIndex];
1528
- if (attempt.timeout)
1529
- clearTimeout(attempt.timeout);
1530
- this.#emitTransportEvent({
1531
- type: 'attempt-advanced',
1532
- peerId,
1533
- transport: previousTransport,
1534
- attemptIndex: previousIndex,
1535
- order: [...attempt.order]
1536
- });
1537
- attempt.index += 1;
1538
- this.#attemptPeerTransport(peerId);
1539
- }
1540
- #startPeerTransportAttempt(peerId, star, version, initiator, transport) {
1541
- if (this.#connections[peerId])
1542
- return;
1543
- if (this.#peerTransportAttempts.has(peerId))
1544
- return;
1545
- const order = this.#buildTransportOrder(peerId, transport);
1546
- if (!order.length) {
1547
- debug(`peer ${peerId} has no available transport to attempt`);
1548
- return;
1549
- }
1550
- this.#peerTransportAttempts.set(peerId, {
1551
- order,
1552
- index: 0,
1553
- star,
1554
- version,
1555
- initiator
1556
- });
1557
- this.#attemptPeerTransport(peerId);
1558
- }
1559
- #attemptPeerTransport(peerId) {
1560
- const attempt = this.#peerTransportAttempts.get(peerId);
1561
- if (!attempt)
1562
- return;
1563
- if (attempt.index >= attempt.order.length) {
1564
- const exhaustedTransport = attempt.order[attempt.order.length - 1] ?? 'circuit';
1565
- this.#emitTransportEvent({
1566
- type: 'attempts-exhausted',
1567
- peerId,
1568
- transport: exhaustedTransport,
1569
- attemptIndex: attempt.index,
1570
- order: [...attempt.order]
1571
- });
1572
- debug(`peer ${peerId} transport attempts exhausted`);
1573
- this.#clearTransportAttempt(peerId);
1574
- return;
1575
- }
1576
- if (this.#connections[peerId])
1577
- return;
1578
- const transportKind = attempt.order[attempt.index];
1579
- this.#emitTransportEvent({
1580
- type: 'attempt-started',
1581
- peerId,
1582
- transport: transportKind,
1583
- attemptIndex: attempt.index,
1584
- order: [...attempt.order]
1585
- });
1586
- let peer = null;
1587
- if (transportKind === 'webrtc') {
1588
- peer = this.#createRTCPeerConnection(peerId, attempt.star, attempt.version, attempt.initiator);
1589
- }
1590
- else if (transportKind === 'webtransport') {
1591
- const url = this.#resolveWebTransportUrl(peerId);
1592
- if (!url) {
1593
- this.#advanceTransportAttempt(peerId);
1594
- return;
1595
- }
1596
- peer = this.#createWebTransportConnection(peerId, url, attempt.version);
1597
- }
1598
- else {
1599
- peer = this.#createCircuitConnection(peerId, attempt.version);
1600
- }
1601
- if (!peer) {
1602
- this.#advanceTransportAttempt(peerId);
1603
- return;
1604
- }
1605
- attempt.timeout = setTimeout(() => {
1606
- const current = this.#connections[peerId];
1607
- if (!current || current.connected)
1608
- return;
1609
- this.#emitTransportEvent({
1610
- type: 'attempt-timeout',
1611
- peerId,
1612
- transport: transportKind,
1613
- attemptIndex: attempt.index,
1614
- order: [...attempt.order]
1615
- });
1616
- debug(`peer ${peerId} ${transportKind} connect timeout`);
1617
- current.destroy();
1618
- }, this.#transportConnectTimeoutMs);
1619
- }
1620
- #decodeCircuitFallbackPayload(payload) {
1621
- if (!payload || typeof payload !== 'object')
1622
- return null;
1623
- const candidate = payload;
1624
- if (!Array.isArray(candidate.data))
1625
- return null;
1626
- const id = typeof candidate.id === 'string' && candidate.id.length
1627
- ? candidate.id
1628
- : this.#createCircuitId();
1629
- return {
1630
- id,
1631
- data: new Uint8Array(candidate.data.map((byte) => Number(byte) & 255))
1632
- };
1633
- }
1634
- async #handleFallbackCircuitPayload(payload, context) {
1635
- const decoded = this.#decodeCircuitFallbackPayload(payload);
1636
- if (!decoded) {
1637
- throw new Error('invalid circuit fallback payload');
1638
- }
1639
- let peer = this.#connections[context.from];
1640
- if (!peer) {
1641
- peer = this.#createCircuitConnection(context.from, this.version);
1642
- }
1643
- this.#noticeMessage(decoded.data, decoded.id, context.from, peer);
1644
- return { ok: true };
1645
- }
1646
- #createCircuitId() {
1647
- const randomUUID = globalThis.crypto?.randomUUID;
1648
- if (typeof randomUUID === 'function') {
1649
- return randomUUID.call(globalThis.crypto);
1650
- }
1651
- return `circuit-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1652
- }
1653
- #encodeCircuitData(data) {
1654
- if (data instanceof Uint8Array) {
1655
- return { __swarmType: 'u8', data: Array.from(data) };
1656
- }
1657
- return data;
1658
- }
1659
- #decodeCircuitData(data) {
1660
- if (data &&
1661
- typeof data === 'object' &&
1662
- data.__swarmType === 'u8' &&
1663
- Array.isArray(data.data)) {
1664
- return new Uint8Array(data.data.map((item) => Number(item) & 255));
1665
- }
1666
- return data;
1667
- }
1668
- #pickStarConnection() {
1669
- const entries = Object.values(this.#stars);
1670
- return entries.find((client) => client?.connectionState?.() === 'open');
1671
- }
1672
- #sendCircuit(starConnection, payload) {
1673
- starConnection.send({
1674
- url: 'circuit',
1675
- params: payload
1676
- });
1677
- }
1678
- onCircuit(method, handler) {
1679
- this.#circuitHandlers.set(method, handler);
1680
- }
1681
- offCircuit(method) {
1682
- this.#circuitHandlers.delete(method);
1683
- }
1684
- onServerData(handler) {
1685
- globalThis.pubsub.subscribe('server:data', handler);
1686
- }
1687
- offServerData(handler) {
1688
- globalThis.pubsub.unsubscribe('server:data', handler);
1689
- }
1690
- circuitRequest(peerId, method, data, timeoutMs = this.#circuitRequestTimeoutMs) {
1691
- const starConnection = this.#pickStarConnection();
1692
- if (!starConnection) {
1693
- return Promise.reject(new Error('no connected star available for circuit request'));
1694
- }
1695
- const id = this.#createCircuitId();
1696
- return new Promise((resolve, reject) => {
1697
- const timeout = setTimeout(() => {
1698
- this.#pendingCircuitRequests.delete(id);
1699
- reject(new Error(`circuit request for ${id} timed out`));
1700
- }, timeoutMs);
1701
- this.#pendingCircuitRequests.set(id, { resolve, reject, timeout });
1702
- this.#sendCircuit(starConnection, {
1703
- to: peerId,
1704
- from: this.peerId,
1705
- id,
1706
- phase: 'request',
1707
- method,
1708
- data: this.#encodeCircuitData(data)
1709
- });
1710
- });
1711
- }
1712
- #inComingCircuit = async ({ from, to, id, phase, method, data, error }, starConnection) => {
1713
- if (to !== this.peerId)
1714
- return;
1715
- if (phase === 'response') {
1716
- const pending = this.#pendingCircuitRequests.get(id);
1717
- if (!pending)
1718
- return;
1719
- clearTimeout(pending.timeout);
1720
- this.#pendingCircuitRequests.delete(id);
1721
- if (error) {
1722
- pending.reject(new Error(String(error)));
1723
- return;
1724
- }
1725
- pending.resolve(this.#decodeCircuitData(data));
1726
- return;
1727
- }
1728
- if (phase !== 'request')
1729
- return;
1730
- if (typeof method !== 'string' || !method.length)
1731
- return;
1732
- const handler = this.#circuitHandlers.get(method);
1733
- if (!handler) {
1734
- this.#sendCircuit(starConnection, {
1735
- to: from,
1736
- from: this.peerId,
1737
- id,
1738
- phase: 'response',
1739
- error: `no circuit handler registered for method ${method}`
1740
- });
1741
- return;
1742
- }
1743
- try {
1744
- const response = await handler(this.#decodeCircuitData(data), {
1745
- from,
1746
- method,
1747
- id
1748
- });
1749
- this.#sendCircuit(starConnection, {
1750
- to: from,
1751
- from: this.peerId,
1752
- id,
1753
- phase: 'response',
1754
- data: this.#encodeCircuitData(response)
1755
- });
1756
- }
1757
- catch (handlerError) {
1758
- this.#sendCircuit(starConnection, {
1759
- to: from,
1760
- from: this.peerId,
1761
- id,
1762
- phase: 'response',
1763
- error: handlerError instanceof Error
1764
- ? handlerError.message
1765
- : String(handlerError)
1766
- });
1767
- }
1768
- };
1769
- connect(peerId, star, initiator = true) {
1770
- if (this.#connections[peerId]) {
1771
- debug(`peer ${peerId} already connected`);
1772
- return;
1773
- }
1774
- if (!this.#testHooks?.allowConnectWithoutStar &&
1775
- this.#stars[star]?.connectionState() !== 'open') {
1776
- console.warn(`Star ${star} is not connected, cannot reconnect to peer ${peerId}`);
1777
- return;
1778
- }
1779
- this.#startPeerTransportAttempt(peerId, star, this.version, initiator, {
1780
- kind: this.#preferredTransportKind,
1781
- kinds: [...this.#supportedTransports]
1782
- });
1783
- }
1784
- reconnect(peerId, star, initiator = false) {
1785
- delete this.#connections[peerId];
1786
- debug(`reconnecting to peer ${peerId}`);
1787
- return this.connect(peerId, star, initiator);
1788
- }
1789
- connectWebTransport(peerId, url) {
1790
- this.#createWebTransportConnection(peerId, url, this.version);
1791
- }
1792
- #resolveWebTransportUrl(peerId) {
1793
- const urlTemplate = this.#webTransportUrlTemplate;
1794
- if (!urlTemplate) {
1795
- console.warn('WebTransport requires transport.webtransport.urlTemplate in client options');
1796
- return null;
1797
- }
1798
- return urlTemplate
1799
- .replace('{peerId}', encodeURIComponent(peerId))
1800
- .replace('{selfId}', encodeURIComponent(this.peerId));
1801
- }
1802
- #createWebTransportConnection(peerId, url, version) {
1803
- if (this.#connections[peerId]) {
1804
- debug(`peer ${peerId} already connected`);
1805
- return null;
1806
- }
1807
- const testPeer = this.#testHooks?.createPeer?.({
1808
- kind: 'webtransport',
1809
- peerId,
1810
- version,
1811
- initiator: true,
1812
- url
1813
- });
1814
- if (testPeer) {
1815
- testPeer.on('connect', () => this.#peerConnect(testPeer));
1816
- testPeer.on('close', () => this.#peerClose(testPeer));
1817
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
1818
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
1819
- this.#connections[peerId] = testPeer;
1820
- this.#peerTransportKinds.set(peerId, 'webtransport');
1821
- return testPeer;
1822
- }
1823
- const peer = new WebTransportPeer({
1824
- from: this.peerId,
1825
- to: peerId,
1826
- version,
1827
- url
1828
- });
1829
- peer.on('connect', () => this.#peerConnect(peer));
1830
- peer.on('close', () => this.#peerClose(peer));
1831
- peer.on('data', (data) => this.#peerData(peer, data));
1832
- peer.on('error', (error) => this.#peerError(peer, error));
1833
- this.#connections[peerId] = peer;
1834
- this.#peerTransportKinds.set(peerId, 'webtransport');
1835
- return peer;
1836
- }
1837
- #createCircuitConnection(peerId, version) {
1838
- if (this.#connections[peerId]) {
1839
- debug(`peer ${peerId} already connected`);
1840
- return null;
1841
- }
1842
- const testPeer = this.#testHooks?.createPeer?.({
1843
- kind: 'circuit',
1844
- peerId,
1845
- version,
1846
- initiator: true
1847
- });
1848
- if (testPeer) {
1849
- testPeer.on('connect', () => this.#peerConnect(testPeer));
1850
- testPeer.on('close', () => this.#peerClose(testPeer));
1851
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
1852
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
1853
- this.#connections[peerId] = testPeer;
1854
- this.#peerTransportKinds.set(peerId, 'circuit');
1855
- return testPeer;
1856
- }
1857
- const peer = new CircuitPeer({
1858
- from: this.peerId,
1859
- to: peerId,
1860
- version,
1861
- sendViaCircuit: (payload) => this.circuitRequest(peerId, this.#fallbackCircuitMethod, payload)
1862
- });
1863
- peer.on('connect', () => this.#peerConnect(peer));
1864
- peer.on('close', () => this.#peerClose(peer));
1865
- peer.on('error', (error) => this.#peerError(peer, error));
1866
- this.#connections[peerId] = peer;
1867
- this.#peerTransportKinds.set(peerId, 'circuit');
1868
- return peer;
1869
- }
1870
- #createRTCPeerConnection = (peerId, star, version, initiator = false) => {
1871
- const testPeer = this.#testHooks?.createPeer?.({
1872
- kind: 'webrtc',
1873
- peerId,
1874
- version,
1875
- initiator,
1876
- star
1877
- });
1878
- if (testPeer) {
1879
- testPeer.on('connect', () => this.#peerConnect(testPeer));
1880
- testPeer.on('close', () => this.#peerClose(testPeer));
1881
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
1882
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
1883
- this.#connections[peerId] = testPeer;
1884
- this.#peerTransportKinds.set(peerId, 'webrtc');
1885
- return testPeer;
1886
- }
1887
- const peer = new WebRTCPeer({
1888
- initiator: initiator,
1889
- from: this.peerId,
1890
- to: peerId,
1891
- version
1892
- });
1893
- peer.on('signal', (signal) => this.#peerSignal(peer, signal, star, this.version));
1894
- peer.on('connect', () => this.#peerConnect(peer));
1895
- peer.on('close', () => this.#peerClose(peer));
1896
- peer.on('data', (data) => this.#peerData(peer, data));
1897
- peer.on('error', (error) => this.#peerError(peer, error));
1898
- this.#connections[peerId] = peer;
1899
- this.#peerTransportKinds.set(peerId, 'webrtc');
1900
- return peer;
1901
- };
1902
- #peerJoined = async (announcement, star) => {
1903
- const normalized = normalizePeerAnnouncement(announcement);
1904
- if (!normalized) {
1905
- debug('ignored malformed peer announcement');
1906
- return;
1907
- }
1908
- const { peerId, transport } = normalized;
1909
- // Legacy stars announce only the peer id. This value is used for transport
1910
- // negotiation; applications still perform their own version handshake.
1911
- const version = normalized.version ?? this.version;
1912
- if (this.peerId === peerId)
1913
- return;
1914
- // Stars can announce the same peer more than once while its transport is
1915
- // still negotiating. Keep that connection/attempt alive; destroying it
1916
- // here makes simultaneous joins continually reset each other.
1917
- // Both sides receive the discovery event. Elect exactly one offerer so
1918
- // simultaneous joins do not create competing channels/SDP negotiations.
1919
- const initiator = String(this.peerId) < String(peerId);
1920
- this.#startPeerTransportAttempt(peerId, star, version, initiator, transport);
1921
- debug(`peer ${peerId} joined`);
1922
- };
1923
- #inComingSignal = async ({ from, signal, channelName, version }, star) => {
1924
- if (!this.#supportsTransport('webrtc')) {
1925
- debug(`ignoring webrtc signal for ${from} because local client does not support webrtc`);
1926
- return;
1927
- }
1928
- if (version !== this.version) {
1929
- console.warn(`${from} joined using the wrong version.\nexpected: ${this.version} but got:${version}`);
1930
- return;
1931
- }
1932
- if (from === this.peerId) {
1933
- console.warn(`${from} tried to connect to itself.`);
1934
- return;
1935
- }
1936
- let peer = this.#connections[from];
1937
- if (peer && String(peer.channelName).startsWith('webtransport:')) {
1938
- if (peer.connected) {
1939
- debug(`ignoring webrtc signal for ${from} because peer uses webtransport`);
1940
- return;
1941
- }
1942
- peer.destroy();
1943
- delete this.#connections[from];
1944
- this.#clearTransportAttempt(from);
1945
- peer = undefined;
1946
- }
1947
- if (!peer) {
1948
- this.#clearTransportAttempt(from);
1949
- this.#createRTCPeerConnection(from, star, version);
1950
- peer = this.#connections[from];
1951
- }
1952
- if (peer.connected) {
1953
- debug(`peer ${from} already connected`);
1954
- return;
1955
- }
1956
- // peer.channels[channelName]
1957
- if (String(peer.channelName) !== String(channelName)) {
1958
- console.warn(`channelNames don't match: got ${peer.channelName}, expected: ${channelName}.`);
1959
- peer.destroy();
1960
- delete this.#connections[from];
1961
- this.#clearTransportAttempt(from);
1962
- this.#createRTCPeerConnection(from, star, version, false);
1963
- peer = this.#connections[from];
1964
- if (!peer)
1965
- return;
1966
- }
1967
- peer.signal(signal);
1968
- };
1969
- #peerSignal = (peer, signal, star, version) => {
1970
- let client = this.#stars[star];
1971
- if (!client)
1972
- client = this.#stars[Object.keys(this.#stars)[0]];
1973
- client.send({
1974
- url: 'signal',
1975
- params: {
1976
- from: this.peerId,
1977
- to: peer.peerId,
1978
- channelName: peer.channelName,
1979
- version,
1980
- signal,
1981
- initiator: peer.initiator
1982
- }
1983
- });
1984
- };
1985
- #peerClose = (peer) => {
1986
- // A failed transport can finish closing after fallback has already placed a
1987
- // replacement in the connection map. Never let that stale close delete or
1988
- // advance the replacement's active attempt.
1989
- if (this.#connections[peer.peerId] !== peer) {
1990
- debug(`ignored stale close for ${peer.peerId}`);
1991
- return;
1992
- }
1993
- const wasConnected = peer.connected;
1994
- this.#peerTransportKinds.delete(peer.peerId);
1995
- delete this.#connections[peer.peerId];
1996
- if (!wasConnected && this.#peerTransportAttempts.has(peer.peerId)) {
1997
- this.#advanceTransportAttempt(peer.peerId);
1998
- }
1999
- debug(`closed ${peer.peerId}'s connection`);
2000
- };
2001
- #peerConnect = (peer) => {
2002
- const attempt = this.#peerTransportAttempts.get(peer.peerId);
2003
- const transport = this.#peerTransportKinds.get(peer.peerId) ||
2004
- attempt?.order[attempt.index] ||
2005
- 'webrtc';
2006
- this.#emitTransportEvent({
2007
- type: 'connected',
2008
- peerId: peer.peerId,
2009
- transport,
2010
- attemptIndex: attempt?.index ?? 0,
2011
- order: attempt ? [...attempt.order] : [transport]
2012
- });
2013
- this.#clearTransportAttempt(peer.peerId);
2014
- debug(`${peer.peerId} connected`);
2015
- globalThis.pubsub.publishVerbose(this.#connectEvent, peer.peerId);
2016
- };
2017
- #noticeMessage = (message, id, from, peer) => {
2018
- const dataOut = message instanceof Uint8Array
2019
- ? message
2020
- : new Uint8Array(Object.values(message));
2021
- if (globalThis.pubsub.hasSubscribers(id)) {
2022
- globalThis.pubsub.publish(id, {
2023
- data: dataOut,
2024
- id,
2025
- from,
2026
- peer
2027
- });
2028
- }
2029
- else {
2030
- globalThis.pubsub.publish('peer:data', {
2031
- data: dataOut,
2032
- id,
2033
- from,
2034
- peer
2035
- });
2036
- }
2037
- };
2038
- #peerData = (peer, data) => {
2039
- const tryJson = () => {
2040
- const parsed = JSON.parse(new TextDecoder().decode(data));
2041
- const { id, size, chunk, index, count } = parsed;
2042
- chunk ? Object.values(chunk).length : size;
2043
- return {
2044
- id,
2045
- size: Number(size),
2046
- index: Number(index ?? 0),
2047
- count: Number(count ?? 1),
2048
- chunk: new Uint8Array(Object.values(chunk)),
2049
- flags: 0,
2050
- crc: 0
2051
- };
2052
- };
2053
- const decodeBinary = () => {
2054
- let u8;
2055
- if (typeof data === 'string') {
2056
- // should not happen when sending binary, fallback to JSON
2057
- return tryJson();
2058
- }
2059
- else if (data instanceof ArrayBuffer) {
2060
- u8 = new Uint8Array(data);
2061
- }
2062
- else if (ArrayBuffer.isView(data)) {
2063
- const view = data;
2064
- const byteOffset = view.byteOffset || 0;
2065
- const byteLength = view.byteLength || data.length;
2066
- u8 = new Uint8Array(view.buffer, byteOffset, byteLength);
2067
- }
2068
- else if (data?.buffer) {
2069
- u8 = new Uint8Array(data.buffer);
2070
- }
2071
- else {
2072
- // last resort: attempt JSON
2073
- return tryJson();
2074
- }
2075
- const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
2076
- let offset = 0;
2077
- dv.getUint8(offset);
2078
- offset += 1;
2079
- const flags = dv.getUint8(offset);
2080
- offset += 1;
2081
- const size = dv.getUint32(offset, true);
2082
- offset += 4;
2083
- const index = dv.getUint32(offset, true);
2084
- offset += 4;
2085
- const count = dv.getUint32(offset, true);
2086
- offset += 4;
2087
- const expectedCrc = dv.getUint32(offset, true);
2088
- offset += 4;
2089
- const idLen = dv.getUint16(offset, true);
2090
- offset += 2;
2091
- const idBytes = u8.subarray(offset, offset + idLen);
2092
- offset += idLen;
2093
- const id = new TextDecoder().decode(idBytes);
2094
- const chunk = u8.subarray(offset);
2095
- return { id, size, index, count, chunk, flags, crc: expectedCrc };
2096
- };
2097
- const frame = decodeBinary();
2098
- peer.bw.down += frame.chunk.length;
2099
- // Single frame path: if compressed, inflate before publish
2100
- if (frame.count === 1) {
2101
- let payload = frame.chunk;
2102
- const compressed = Boolean(frame.flags & (1 << 1));
2103
- if (compressed) {
2104
- const actualCrc = crc32(payload);
2105
- if (actualCrc !== frame.crc) {
2106
- console.warn(`CRC mismatch: expected ${frame.crc}, got ${actualCrc}`);
2107
- }
2108
- try {
2109
- payload = inflate_1(payload);
2110
- }
2111
- catch (e) {
2112
- console.warn('inflate failed, passing compressed payload');
2113
- }
2114
- }
2115
- this.#noticeMessage(payload, frame.id, peer.peerId, peer);
2116
- return;
2117
- }
2118
- // Chunked message handling with indexed reassembly
2119
- if (!this.#messagesToHandle[frame.id] ||
2120
- Array.isArray(this.#messagesToHandle[frame.id])) {
2121
- this.#messagesToHandle[frame.id] = {
2122
- chunks: new Array(frame.count),
2123
- receivedBytes: 0,
2124
- expectedSize: Number(frame.size),
2125
- expectedCount: Number(frame.count)
2126
- };
2127
- }
2128
- const state = this.#messagesToHandle[frame.id];
2129
- // Verify CRC for this chunk
2130
- const actualCrc = crc32(frame.chunk);
2131
- if (actualCrc !== frame.crc) {
2132
- console.warn(`Chunk CRC mismatch for ${frame.id}[${frame.index}]: expected ${frame.crc}, got ${actualCrc}`);
2133
- }
2134
- state.chunks[frame.index] = frame.chunk;
2135
- state.receivedBytes += frame.chunk.length;
2136
- // If all chunks present and total size matches, reassemble
2137
- const allPresent = state.chunks.every((c) => c instanceof Uint8Array);
2138
- if (allPresent && state.receivedBytes === state.expectedSize) {
2139
- const result = new Uint8Array(state.expectedSize);
2140
- let offset2 = 0;
2141
- for (const c of state.chunks) {
2142
- result.set(c, offset2);
2143
- offset2 += c.length;
2144
- }
2145
- let payload = result;
2146
- const compressed = Boolean(frame.flags & (1 << 1));
2147
- if (compressed) {
2148
- try {
2149
- payload = inflate_1(result);
2150
- }
2151
- catch (e) {
2152
- console.warn('inflate failed, passing compressed payload');
2153
- }
2154
- }
2155
- this.#noticeMessage(payload, frame.id, peer.peerId, peer);
2156
- delete this.#messagesToHandle[frame.id];
2157
- }
2158
- };
2159
- #peerError = (peer, error) => {
2160
- const message = error instanceof Error ? error.message : `unknown error: ${String(error)}`;
2161
- const attempt = this.#peerTransportAttempts.get(peer.peerId);
2162
- const transport = this.#peerTransportKinds.get(peer.peerId) ||
2163
- attempt?.order[attempt.index] ||
2164
- 'webrtc';
2165
- this.#emitTransportEvent({
2166
- type: 'attempt-error',
2167
- peerId: peer.peerId,
2168
- transport,
2169
- attemptIndex: attempt?.index ?? 0,
2170
- order: attempt ? [...attempt.order] : [transport],
2171
- reason: message
2172
- });
2173
- console.warn(`Connection error: ${message}`);
2174
- peer.destroy();
2175
- };
2176
- async close() {
2177
- for (const peerId of this.#peerTransportAttempts.keys()) {
2178
- this.#clearTransportAttempt(peerId);
2179
- }
2180
- for (const peerId in this.#connections) {
2181
- const peer = this.#connections[peerId];
2182
- if (peer) {
2183
- peer.destroy();
2184
- delete this.#connections[peerId];
2185
- }
2186
- }
2187
- for (const star in this.#stars) {
2188
- // unsubscribe handlers we registered earlier
2189
- const listeners = this.#starListeners[star];
2190
- if (listeners && listeners.length) {
2191
- for (const { topic, handler } of listeners) {
2192
- try {
2193
- this.#stars[star].pubsub.unsubscribe(topic, handler);
2194
- }
2195
- catch (e) {
2196
- // ignore
2197
- }
2198
- }
2199
- }
2200
- if (this.#stars[star].connectionState() === 'open') {
2201
- await this.#stars[star].send({ url: 'leave', params: this.peerId });
2202
- }
2203
- }
2204
- const peerClosers = Object.values(this.#connections).map((connection) => {
2205
- try {
2206
- // destroy() may be sync or return a promise
2207
- return connection.destroy();
2208
- }
2209
- catch (e) {
2210
- return undefined;
2211
- }
2212
- });
2213
- const starClosers = Object.values(this.#stars).map((connection) => {
2214
- try {
2215
- return connection.close(0);
2216
- }
2217
- catch (e) {
2218
- return undefined;
2219
- }
2220
- });
2221
- return Promise.allSettled([...peerClosers, ...starClosers]);
2222
- }
2223
- }
2224
-
2225
- export { Client as default };