@leofcoin/peernet 1.2.19 → 1.2.21

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