@leofcoin/chain 1.10.21 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +95 -0
  2. package/exports/browser/beacon-wire.js +2 -2
  3. package/exports/browser/{browser-CCfR9KOM-BGMMnVsR.js → browser-CCfR9KOM-CNWKLusu.js} +3 -3
  4. package/exports/browser/{browser-crypto-CH0ua22L.js → browser-crypto-B_UJguhk.js} +453 -35
  5. package/exports/browser/chain.js +1275 -996
  6. package/exports/browser/client-DbZBfpG6-C1cXgvfr.js +5943 -0
  7. package/exports/browser/{connection-monitor-D-krNiRH.js → connection-monitor-BzbeYGEt.js} +153 -24
  8. package/exports/browser/{index-dTQVgkpf-DOL2rIjp.js → index-dTQVgkpf-CkoAiRNX.js} +3 -3
  9. package/exports/browser/{index-DCbLvMKL.js → index-phv5KvaE.js} +238 -234
  10. package/exports/browser/{messages-DCePyqKC-BSvJxDWZ.js → messages-DZYCxoZI-B472zldo.js} +3 -3
  11. package/exports/browser/{node-browser-Ha4Lvntj.js → node-browser-BmyPBkL5.js} +55 -11
  12. package/exports/browser/node-browser.js +3 -3
  13. package/exports/browser/workers/block-worker.js +8 -9
  14. package/exports/browser/workers/machine-worker.js +141 -1195
  15. package/exports/{workers/worker-BDTWOCzS-COrAszCt.js → browser/workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  16. package/exports/chain.js +296 -1078
  17. package/exports/connection-monitor-BdFLRh-J.js +465 -0
  18. package/exports/connection-monitor.js +2 -324
  19. package/exports/{constants-Dy9i9S_o.js → constants-BQ6f-_OT.js} +1 -1
  20. package/exports/hash-format-state.js +3 -3
  21. package/exports/machine.js +550 -0
  22. package/exports/node.js +35 -2
  23. package/exports/state-checkpoint-BiQ1EXzo.js +110 -0
  24. package/exports/state-checkpoint.js +3 -0
  25. package/exports/workers/block-worker.js +8 -9
  26. package/exports/workers/machine-worker.js +141 -1195
  27. package/exports/{browser/workers/worker-BDTWOCzS-COrAszCt.js → workers/proposal.proto-8zgMfutf-B3So_tnS.js} +910 -398
  28. package/package.json +9 -7
  29. package/exports/browser/client-DKKzPSlb-C_1ug6ux.js +0 -2401
@@ -1,2401 +0,0 @@
1
- import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-Ha4Lvntj.js';
2
- import './browser-crypto-CH0ua22L.js';
3
- import './connection-monitor-D-krNiRH.js';
4
- import './index-CvKt4UDE.js';
5
- import './sha2-BKcEJzgd.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-CCfR9KOM-BGMMnVsR.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
- // WebTransport streams carry bytes, not application message boundaries.
824
- const MAX_FRAME_BYTES = 64 * 1024;
825
- const encodeFrame = (payload) => {
826
- if (!(payload instanceof Uint8Array) || !payload.length || payload.length > MAX_FRAME_BYTES) {
827
- throw new Error('invalid transport frame length');
828
- }
829
- const frame = new Uint8Array(payload.length + 4);
830
- new DataView(frame.buffer).setUint32(0, payload.length);
831
- frame.set(payload, 4);
832
- return frame;
833
- };
834
- class FrameReader {
835
- constructor(reader) { this.reader = reader; this.chunk = new Uint8Array(); this.offset = 0; }
836
- async exact(length, allowEnd = false) {
837
- const result = new Uint8Array(length);
838
- let written = 0;
839
- while (written < length) {
840
- if (this.offset === this.chunk.length) {
841
- const { done, value } = await this.reader.read();
842
- if (done) {
843
- if (allowEnd && written === 0)
844
- return null;
845
- throw new Error('truncated transport frame');
846
- }
847
- this.chunk = value;
848
- this.offset = 0;
849
- if (!value?.length)
850
- continue;
851
- }
852
- const count = Math.min(length - written, this.chunk.length - this.offset);
853
- result.set(this.chunk.subarray(this.offset, this.offset + count), written);
854
- this.offset += count;
855
- written += count;
856
- }
857
- return result;
858
- }
859
- async read() {
860
- const header = await this.exact(4, true);
861
- if (!header)
862
- return null;
863
- const length = new DataView(header.buffer).getUint32(0);
864
- if (!length || length > MAX_FRAME_BYTES)
865
- throw new Error('invalid transport frame length');
866
- return this.exact(length);
867
- }
868
- }
869
- const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value));
870
- const parseJson = (bytes) => JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
871
-
872
- const getWebTransportCtor = async () => {
873
- const candidate = globalThis;
874
- if (!candidate.WebTransport && candidate.process?.versions?.node) {
875
- const { WebTransport } = await import('./index.browser-CFfJpgKV.js');
876
- return WebTransport;
877
- }
878
- if (!candidate.WebTransport) {
879
- throw new Error('WebTransport is not available in this runtime');
880
- }
881
- return candidate.WebTransport;
882
- };
883
- class WebTransportPeer {
884
- #session = null;
885
- #reader = null;
886
- #writer = null;
887
- #listeners = new Map();
888
- #destroyed = false;
889
- #connected = false;
890
- #core;
891
- initiator = true;
892
- peerId;
893
- channelName;
894
- version;
895
- compressionThreshold = 0.98;
896
- bw = { up: 0, down: 0 };
897
- get connected() {
898
- return this.#connected;
899
- }
900
- constructor(options) {
901
- this.peerId = options.to;
902
- this.channelName = `webtransport:${options.from}:${options.to}`;
903
- this.version = String(options.version);
904
- if (options.compressionThreshold !== undefined) {
905
- this.compressionThreshold = options.compressionThreshold;
906
- }
907
- this.#core = new PeerCore({
908
- kind: 'webtransport-bidi',
909
- isConnected: () => this.connected,
910
- sendRaw: (payload) => {
911
- void this.#sendRaw(payload).catch(error => {
912
- if (!this.#destroyed) {
913
- this.#emit('error', error);
914
- this.destroy();
915
- }
916
- });
917
- }
918
- }, {
919
- maxMessageSize: MAX_MESSAGE_SIZE,
920
- compressionThreshold: this.compressionThreshold,
921
- getPubSub: this.#getPubSub,
922
- onPayloadSend: (payloadBytes) => {
923
- this.bw.up += payloadBytes;
924
- }
925
- });
926
- queueMicrotask(() => {
927
- void this.#open(options);
928
- });
929
- }
930
- #createMessageId() {
931
- const randomUUID = globalThis.crypto?.randomUUID;
932
- if (typeof randomUUID === 'function') {
933
- return randomUUID.call(globalThis.crypto);
934
- }
935
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
936
- }
937
- #getPubSub() {
938
- const pubsubCandidate = globalThis
939
- .pubsub;
940
- if (pubsubCandidate &&
941
- typeof pubsubCandidate.subscribe === 'function' &&
942
- typeof pubsubCandidate.unsubscribe === 'function') {
943
- return pubsubCandidate;
944
- }
945
- return null;
946
- }
947
- #emit(event, ...args) {
948
- const listeners = this.#listeners.get(event);
949
- if (!listeners)
950
- return;
951
- for (const handler of listeners)
952
- handler(...args);
953
- }
954
- async #open(options) {
955
- try {
956
- const ticket = await options.getTicket();
957
- if (this.#destroyed)
958
- return;
959
- const WebTransportCtor = await getWebTransportCtor();
960
- const session = new WebTransportCtor(options.url);
961
- this.#session = session;
962
- void session.closed.then(() => this.destroy(), error => {
963
- if (!this.#destroyed) {
964
- this.#emit('error', error);
965
- this.destroy();
966
- }
967
- });
968
- await session.ready;
969
- if (this.#destroyed)
970
- return;
971
- const stream = (await session.createBidirectionalStream());
972
- if (this.#destroyed)
973
- return;
974
- this.#reader = stream.readable.getReader();
975
- this.#writer = stream.writable.getWriter();
976
- const frames = new FrameReader(this.#reader);
977
- await this.#writer.write(encodeFrame(jsonBytes({ protocol: 'swarm-relay/1', ticket })));
978
- const ready = parseJson(await frames.read());
979
- if (ready.protocol !== 'swarm-relay/1' || ready.peerId !== this.peerId || String(ready.version) !== this.version) {
980
- throw new Error('invalid WebTransport relay handshake');
981
- }
982
- if (this.#destroyed)
983
- return;
984
- this.#connected = true;
985
- this.#emit('connect');
986
- void this.#readLoop(frames);
987
- }
988
- catch (error) {
989
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
990
- this.destroy();
991
- }
992
- }
993
- async #readLoop(frames) {
994
- const reader = this.#reader;
995
- if (!reader)
996
- return;
997
- try {
998
- while (!this.#destroyed) {
999
- // eslint-disable-next-line no-await-in-loop
1000
- const value = await frames.read();
1001
- if (!value)
1002
- break;
1003
- this.bw.down += value.length;
1004
- this.#emit('data', value);
1005
- }
1006
- }
1007
- catch (error) {
1008
- if (!this.#destroyed) {
1009
- this.#emit('error', error instanceof Error ? error : new Error(String(error)));
1010
- }
1011
- }
1012
- if (!this.#destroyed)
1013
- this.destroy();
1014
- }
1015
- async #sendRaw(payload) {
1016
- if (!this.#writer)
1017
- throw new Error('WebTransport writer is not ready');
1018
- await this.#writer.write(encodeFrame(payload));
1019
- }
1020
- on(event, handler) {
1021
- const listeners = this.#listeners.get(event) ?? new Set();
1022
- listeners.add(handler);
1023
- this.#listeners.set(event, listeners);
1024
- return this;
1025
- }
1026
- off(event, handler) {
1027
- this.#listeners.get(event)?.delete(handler);
1028
- return this;
1029
- }
1030
- signal(_signalData) {
1031
- // WebTransport does not use SDP/ICE signaling in this peer implementation.
1032
- }
1033
- send(data, id = this.#createMessageId()) {
1034
- this.#core.send(data, id);
1035
- }
1036
- request(data, id = this.#createMessageId()) {
1037
- return this.#core.request(data, id);
1038
- }
1039
- async getNetworkStats() {
1040
- return null;
1041
- }
1042
- destroy() {
1043
- if (this.#destroyed)
1044
- return;
1045
- this.#destroyed = true;
1046
- this.#connected = false;
1047
- try {
1048
- void this.#reader?.cancel?.().catch(() => { });
1049
- }
1050
- catch (e) { }
1051
- this.#reader?.releaseLock?.();
1052
- this.#reader = null;
1053
- try {
1054
- void this.#writer?.close?.().catch(() => { });
1055
- }
1056
- catch (e) { }
1057
- this.#writer?.releaseLock?.();
1058
- this.#writer = null;
1059
- try {
1060
- this.#session?.close?.();
1061
- }
1062
- catch (e) { }
1063
- this.#session = null;
1064
- this.#emit('close');
1065
- }
1066
- }
1067
-
1068
- const getPubSub = () => {
1069
- const candidate = globalThis.pubsub;
1070
- if (candidate &&
1071
- typeof candidate.subscribe === 'function' &&
1072
- typeof candidate.unsubscribe === 'function') {
1073
- return candidate;
1074
- }
1075
- return null;
1076
- };
1077
- class CircuitPeer {
1078
- #listeners = new Map();
1079
- #destroyed = false;
1080
- #connected = false;
1081
- #sendViaCircuit;
1082
- #core;
1083
- initiator = true;
1084
- peerId;
1085
- channelName;
1086
- version;
1087
- bw = { up: 0, down: 0 };
1088
- get connected() {
1089
- return this.#connected;
1090
- }
1091
- constructor(options) {
1092
- this.peerId = options.to;
1093
- this.channelName = `circuit:${options.from}:${options.to}`;
1094
- this.version = String(options.version);
1095
- this.#sendViaCircuit = options.sendViaCircuit;
1096
- this.#core = new PeerCore({
1097
- kind: 'circuit',
1098
- isConnected: () => this.connected,
1099
- sendRaw: data => {
1100
- void this.#sendViaCircuit({ id: this.#createMessageId(), data: Array.from(data) }).catch(error => {
1101
- if (this.#destroyed)
1102
- return;
1103
- this.#emit('error', error);
1104
- this.destroy();
1105
- });
1106
- }
1107
- }, {
1108
- // JSON byte arrays expand up to four characters per byte. Keep the whole
1109
- // WebSocket frame below its default 64 KiB limit, including wrapper/ID.
1110
- maxMessageSize: 14_000,
1111
- compressionThreshold: 0.98,
1112
- getPubSub,
1113
- onPayloadSend: bytes => { this.bw.up += bytes; }
1114
- });
1115
- queueMicrotask(async () => {
1116
- try {
1117
- await options.handshake();
1118
- if (this.#destroyed)
1119
- return;
1120
- this.#connected = true;
1121
- this.#emit('connect');
1122
- }
1123
- catch (error) {
1124
- if (this.#destroyed)
1125
- return;
1126
- this.#emit('error', error);
1127
- this.destroy();
1128
- }
1129
- });
1130
- }
1131
- #createMessageId() {
1132
- const randomUUID = globalThis.crypto?.randomUUID;
1133
- if (typeof randomUUID === 'function') {
1134
- return randomUUID.call(globalThis.crypto);
1135
- }
1136
- return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1137
- }
1138
- #emit(event, ...args) {
1139
- const listeners = this.#listeners.get(event);
1140
- if (!listeners)
1141
- return;
1142
- for (const handler of listeners)
1143
- handler(...args);
1144
- }
1145
- on(event, handler) {
1146
- const listeners = this.#listeners.get(event) ?? new Set();
1147
- listeners.add(handler);
1148
- this.#listeners.set(event, listeners);
1149
- return this;
1150
- }
1151
- off(event, handler) {
1152
- this.#listeners.get(event)?.delete(handler);
1153
- return this;
1154
- }
1155
- signal(_signalData) {
1156
- // Circuit transport does not use SDP/ICE signaling.
1157
- }
1158
- send(data, id = this.#createMessageId()) {
1159
- if (new TextEncoder().encode(id).length > 1024) {
1160
- this.#emit('error', new Error('circuit message id exceeds 1024 bytes'));
1161
- return;
1162
- }
1163
- if (this.#destroyed) {
1164
- this.#emit('error', new Error('circuit peer is destroyed'));
1165
- return;
1166
- }
1167
- this.#core.send(data, id);
1168
- }
1169
- request(data, id = this.#createMessageId()) {
1170
- if (new TextEncoder().encode(id).length > 1024) {
1171
- return Promise.reject(new Error('circuit message id exceeds 1024 bytes'));
1172
- }
1173
- return this.#core.request(data, id);
1174
- }
1175
- async getNetworkStats() {
1176
- return null;
1177
- }
1178
- destroy() {
1179
- if (this.#destroyed)
1180
- return;
1181
- this.#destroyed = true;
1182
- this.#connected = false;
1183
- this.#emit('close');
1184
- }
1185
- }
1186
-
1187
- /** Normalize current star announcements and the legacy peer-id-only format. */
1188
- const normalizePeerAnnouncement = (value) => {
1189
- if (typeof value === 'string') {
1190
- const peerId = value.trim();
1191
- return peerId && peerId !== 'undefined' ? { peerId } : null;
1192
- }
1193
- if (!value || typeof value !== 'object')
1194
- return null;
1195
- const candidate = value;
1196
- if (typeof candidate.peerId !== 'string')
1197
- return null;
1198
- const peerId = candidate.peerId.trim();
1199
- if (!peerId || peerId === 'undefined')
1200
- return null;
1201
- return {
1202
- peerId,
1203
- version: typeof candidate.version === 'string' ? candidate.version : undefined,
1204
- transport: candidate.transport
1205
- };
1206
- };
1207
-
1208
- // Simple CRC32 implementation
1209
- const crc32 = (data) => {
1210
- let crc = 0xffffffff;
1211
- for (let i = 0; i < data.length; i++) {
1212
- crc ^= data[i];
1213
- for (let j = 0; j < 8; j++) {
1214
- crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
1215
- }
1216
- }
1217
- return (crc ^ 0xffffffff) >>> 0;
1218
- };
1219
- const debug = createDebugger('@netpeer/swarm/client');
1220
- const isWrtcImplementation = (candidate) => {
1221
- if (!candidate || typeof candidate !== 'object')
1222
- return false;
1223
- const wrtcCandidate = candidate;
1224
- return (typeof wrtcCandidate.RTCPeerConnection === 'function' &&
1225
- typeof wrtcCandidate.RTCSessionDescription === 'function' &&
1226
- typeof wrtcCandidate.RTCIceCandidate === 'function');
1227
- };
1228
- class Client {
1229
- #peerId;
1230
- #connections = {};
1231
- #stars = {};
1232
- #starListeners = {};
1233
- #reinitLock = null;
1234
- #connectEvent = 'peer:connected';
1235
- #supportedTransports = new Set(['webrtc']);
1236
- #preferredTransportKind = 'webrtc';
1237
- #wrtcBackend = 'vandeurenglenn';
1238
- #fallbackTransportOrder = [];
1239
- #enableCircuitFallback = true;
1240
- #transportConnectTimeoutMs = 10_000;
1241
- #peerTransportAttempts = new Map();
1242
- #peerTransportKinds = new Map();
1243
- #fallbackCircuitMethod = 'swarm:fallback:send';
1244
- #testHooks;
1245
- #webTransportUrlTemplate;
1246
- #discoverWebTransport = true;
1247
- #relayUrls = new WeakMap();
1248
- #circuitHandlers = new Map();
1249
- #pendingCircuitRequests = new Map();
1250
- #circuitRequestTimeoutMs = 30_000;
1251
- #retryOptions = { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 30000 };
1252
- id;
1253
- networkVersion;
1254
- starsConfig;
1255
- socketClient;
1256
- messageSize = 262144;
1257
- version;
1258
- #messagesToHandle = {};
1259
- get peerId() {
1260
- return this.#peerId;
1261
- }
1262
- get connections() {
1263
- return { ...this.#connections };
1264
- }
1265
- get peers() {
1266
- return Object.entries(this.#connections);
1267
- }
1268
- getPeer(peerId) {
1269
- return this.#connections[peerId];
1270
- }
1271
- constructor(options) {
1272
- const { peerId, networkVersion, version, connectEvent, stars } = {
1273
- ...defaultOptions,
1274
- ...options
1275
- };
1276
- this.#peerId = peerId;
1277
- this.networkVersion = networkVersion;
1278
- this.version = version;
1279
- this.#testHooks = options.testHooks;
1280
- this.#connectEvent = connectEvent;
1281
- this.starsConfig = stars;
1282
- const configuredKinds = options.transport?.kinds
1283
- ? options.transport.kinds
1284
- : options.transport?.kind
1285
- ? [options.transport.kind]
1286
- : (options.transport?.webtransport?.urlTemplate
1287
- ? ['webrtc', 'webtransport']
1288
- : ['webrtc']);
1289
- this.#supportedTransports = new Set(this.#normalizeTransportKinds(configuredKinds));
1290
- if (!this.#supportedTransports.size)
1291
- this.#supportedTransports.add('webrtc');
1292
- const configuredPreferred = options.transport?.preferredKind || options.transport?.kind || 'webrtc';
1293
- this.#preferredTransportKind = this.#supportedTransports.has(configuredPreferred)
1294
- ? configuredPreferred
1295
- : this.#supportedTransports.has('webrtc')
1296
- ? 'webrtc'
1297
- : 'webtransport';
1298
- this.#webTransportUrlTemplate = options.transport?.webtransport?.urlTemplate;
1299
- this.#discoverWebTransport = !options.transport?.kind && !options.transport?.kinds ||
1300
- !!options.transport?.kinds?.includes('webtransport') || options.transport?.kind === 'webtransport';
1301
- if (this.#discoverWebTransport && (typeof globalThis.WebTransport === 'function' || globalThis.process?.versions?.node)) {
1302
- this.#supportedTransports.add('webtransport');
1303
- }
1304
- const wrtcBackend = options.transport?.webrtc?.backend ||
1305
- globalThis.process?.env?.NETPEER_SWARM_WRTC_BACKEND ||
1306
- 'vandeurenglenn';
1307
- if (wrtcBackend !== 'vandeurenglenn' && wrtcBackend !== 'koush') {
1308
- throw new Error(`Unsupported WebRTC backend: ${wrtcBackend}`);
1309
- }
1310
- this.#wrtcBackend = wrtcBackend;
1311
- this.#enableCircuitFallback = options.transport?.fallback?.enabled ?? true;
1312
- if (typeof options.transport?.fallback?.connectTimeoutMs === 'number' &&
1313
- options.transport.fallback.connectTimeoutMs > 0) {
1314
- this.#transportConnectTimeoutMs =
1315
- options.transport.fallback.connectTimeoutMs;
1316
- }
1317
- this.#fallbackTransportOrder = this.#normalizeFallbackOrder(options.transport?.fallback?.order);
1318
- if (options?.retry)
1319
- this.#retryOptions = { ...this.#retryOptions, ...options.retry };
1320
- this.#circuitHandlers.set(this.#fallbackCircuitMethod, (payload, context) => this.#handleFallbackCircuitPayload(payload, context));
1321
- this.#circuitHandlers.set('swarm:fallback:hello', () => ({
1322
- peerId: this.peerId, version: this.version, encoding: 'peer-core/1'
1323
- }));
1324
- if (!this.#testHooks?.skipInit) {
1325
- this._init();
1326
- }
1327
- }
1328
- /**
1329
- * Safely reinitialize the client (used after system resume/sleep).
1330
- * It closes existing connections and reconnects to configured stars.
1331
- */
1332
- async reinit() {
1333
- // avoid concurrent reinit runs
1334
- if (this.#reinitLock)
1335
- return this.#reinitLock;
1336
- this.#reinitLock = (async () => {
1337
- debug('reinit: start');
1338
- try {
1339
- await this.close();
1340
- this.#stars = {};
1341
- this.#connections = {};
1342
- for (const star of this.starsConfig) {
1343
- try {
1344
- await this.setupStar(star);
1345
- }
1346
- catch (e) {
1347
- // If last star fails and none connected, surface error
1348
- if (Object.keys(this.#stars).length === 0)
1349
- throw new Error(`No star available to connect`);
1350
- }
1351
- }
1352
- }
1353
- finally {
1354
- debug('reinit: done');
1355
- this.#reinitLock = null;
1356
- }
1357
- })();
1358
- return this.#reinitLock;
1359
- }
1360
- async setupStar(star) {
1361
- const { retries, factor, minTimeout, maxTimeout } = this.#retryOptions;
1362
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1363
- let attempt = 0;
1364
- let lastErr;
1365
- while (attempt <= retries) {
1366
- try {
1367
- const client = new SocketRequestClient(star, this.networkVersion);
1368
- this.#stars[star] = await client.init();
1369
- this.setupStarListeners(this.#stars[star], star);
1370
- this.#stars[star].send({
1371
- url: 'join',
1372
- params: {
1373
- version: this.version,
1374
- peerId: this.peerId,
1375
- transport: {
1376
- kind: this.#preferredTransportKind,
1377
- kinds: [...this.#supportedTransports]
1378
- }
1379
- }
1380
- });
1381
- globalThis.pubsub.publishVerbose('star:connected', star);
1382
- debug(`setupStar ${star} succeeded`);
1383
- return this.#stars[star];
1384
- }
1385
- catch (e) {
1386
- lastErr = e;
1387
- attempt += 1;
1388
- if (attempt > retries)
1389
- break;
1390
- const delay = Math.min(maxTimeout, Math.round(minTimeout * Math.pow(factor, attempt - 1)));
1391
- debug(`setupStar ${star} failed, retrying in ${delay}ms (attempt ${attempt})`);
1392
- // eslint-disable-next-line no-await-in-loop
1393
- await sleep(delay);
1394
- }
1395
- }
1396
- throw lastErr;
1397
- }
1398
- async _init() {
1399
- if (this.#supportedTransports.has('webrtc') &&
1400
- !globalThis.RTCPeerConnection &&
1401
- !globalThis.wrtc) {
1402
- const { backend, implementation } = await this.#loadNodeWebrtcImplementation();
1403
- globalThis.wrtc = implementation;
1404
- globalThis.__swarmWrtcImpl = backend;
1405
- }
1406
- for (const star of this.starsConfig) {
1407
- try {
1408
- await this.setupStar(star);
1409
- }
1410
- catch (e) {
1411
- if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1 &&
1412
- !this.socketClient)
1413
- throw new Error(`No star available to connect`);
1414
- }
1415
- }
1416
- if (globalThis.process?.versions?.node) {
1417
- process.on('SIGINT', async () => {
1418
- process.stdin.resume();
1419
- await this.close();
1420
- process.exit();
1421
- });
1422
- }
1423
- else {
1424
- globalThis.addEventListener('beforeunload', this.close.bind(this));
1425
- }
1426
- }
1427
- async #loadNodeWebrtcImplementation() {
1428
- let wrtcModule;
1429
- if (this.#wrtcBackend === 'koush') {
1430
- try {
1431
- wrtcModule = await import('@koush/wrtc');
1432
- }
1433
- catch (error) {
1434
- throw new Error('The koush WebRTC backend is opt-in; install @koush/wrtc to use it', { cause: error });
1435
- }
1436
- }
1437
- else {
1438
- wrtcModule = await import('@vandeurenglenn/wrtc');
1439
- }
1440
- const moduleCandidate = wrtcModule;
1441
- const implementation = isWrtcImplementation(moduleCandidate.default)
1442
- ? moduleCandidate.default
1443
- : wrtcModule;
1444
- if (!isWrtcImplementation(implementation)) {
1445
- const packageName = this.#wrtcBackend === 'koush' ? '@koush/wrtc' : '@vandeurenglenn/wrtc';
1446
- throw new Error(`${packageName} does not match required wrtc contract`);
1447
- }
1448
- debug(`using ${this.#wrtcBackend} as wrtc implementation`);
1449
- return { backend: this.#wrtcBackend, implementation };
1450
- }
1451
- setupStarListeners(starConnection, starId) {
1452
- // create stable references to handlers so we can unsubscribe later
1453
- const onPeerJoined = (id) => this.#peerJoined(id, starConnection);
1454
- const onPeerLeft = (id) => this.#peerLeft(id, starConnection);
1455
- const onStarJoined = this.#starJoined;
1456
- const onStarLeft = this.#starLeft;
1457
- const onSignal = (message) => this.#inComingSignal(message, starConnection);
1458
- const onCircuit = (message) => this.#inComingCircuit(message, starConnection);
1459
- const onServerData = (message) => {
1460
- globalThis.pubsub.publish('server:data', message);
1461
- };
1462
- const onRelayReady = (message) => {
1463
- if (!this.#discoverWebTransport || message?.protocol !== 'swarm-relay/1' || typeof message.url !== 'string')
1464
- return;
1465
- let url;
1466
- try {
1467
- url = new URL(message.url);
1468
- }
1469
- catch {
1470
- return;
1471
- }
1472
- if (url.protocol !== 'https:' || url.username || url.password || url.hash)
1473
- return;
1474
- if (this.#relayUrls.get(starConnection) === url.href)
1475
- return;
1476
- this.#relayUrls.set(starConnection, url.href);
1477
- this.#supportedTransports.add('webtransport');
1478
- // Update capabilities on this same authenticated star connection. The
1479
- // server sends relay metadata before its peer snapshot, so fallback can
1480
- // resolve a real endpoint without users configuring transport URLs.
1481
- starConnection.send({ url: 'join', params: {
1482
- version: this.version, peerId: this.peerId,
1483
- transport: { kind: this.#preferredTransportKind, kinds: [...this.#supportedTransports] }
1484
- } });
1485
- };
1486
- starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
1487
- starConnection.pubsub.subscribe('peer:left', onPeerLeft);
1488
- starConnection.pubsub.subscribe('star:joined', onStarJoined);
1489
- starConnection.pubsub.subscribe('star:left', onStarLeft);
1490
- starConnection.pubsub.subscribe('signal', onSignal);
1491
- starConnection.pubsub.subscribe('circuit', onCircuit);
1492
- starConnection.pubsub.subscribe('server:data', onServerData);
1493
- starConnection.pubsub.subscribe('relay:ready', onRelayReady);
1494
- this.#starListeners[starId] = [
1495
- { topic: 'peer:joined', handler: onPeerJoined },
1496
- { topic: 'peer:left', handler: onPeerLeft },
1497
- { topic: 'star:joined', handler: onStarJoined },
1498
- { topic: 'star:left', handler: onStarLeft },
1499
- { topic: 'signal', handler: onSignal },
1500
- { topic: 'circuit', handler: onCircuit },
1501
- { topic: 'server:data', handler: onServerData },
1502
- { topic: 'relay:ready', handler: onRelayReady }
1503
- ];
1504
- }
1505
- #starJoined = (id) => {
1506
- if (this.#stars[id]) {
1507
- this.#stars[id].close(0);
1508
- delete this.#stars[id];
1509
- }
1510
- console.log(`star ${id} joined`);
1511
- };
1512
- #starLeft = async (id) => {
1513
- if (this.#stars[id]) {
1514
- this.#stars[id].close(0);
1515
- delete this.#stars[id];
1516
- }
1517
- // if we lost all stars, try to reconnect to configured stars with backoff
1518
- if (Object.keys(this.#stars).length === 0) {
1519
- for (const star of this.starsConfig) {
1520
- try {
1521
- await this.setupStar(star);
1522
- // stop at first success
1523
- return;
1524
- }
1525
- catch (e) {
1526
- debug(`reconnect star ${star} failed: ${e.message || e}`);
1527
- if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1)
1528
- throw new Error(`No star available to connect`);
1529
- }
1530
- }
1531
- }
1532
- debug(`star ${id} left`);
1533
- };
1534
- #peerLeft = (peer, star) => {
1535
- const id = peer.peerId || peer;
1536
- if (this.#connections[id]) {
1537
- this.#connections[id].destroy();
1538
- delete this.#connections[id];
1539
- }
1540
- debug(`peer ${id} left`);
1541
- };
1542
- #normalizeTransportKinds(kinds) {
1543
- if (!Array.isArray(kinds))
1544
- return [];
1545
- const normalized = new Set();
1546
- for (const kind of kinds) {
1547
- if (kind === 'webrtc' || kind === 'webtransport')
1548
- normalized.add(kind);
1549
- }
1550
- return [...normalized];
1551
- }
1552
- #normalizeFallbackOrder(kinds) {
1553
- if (!Array.isArray(kinds))
1554
- return [];
1555
- const normalized = new Set();
1556
- for (const kind of kinds) {
1557
- if (kind === 'webrtc' || kind === 'webtransport' || kind === 'circuit') {
1558
- normalized.add(kind);
1559
- }
1560
- }
1561
- return [...normalized];
1562
- }
1563
- #supportsTransport(kind) {
1564
- return this.#supportedTransports.has(kind);
1565
- }
1566
- #getRemoteTransportKinds(transport) {
1567
- const fromKinds = this.#normalizeTransportKinds(transport?.kinds);
1568
- if (fromKinds.length)
1569
- return fromKinds;
1570
- if (transport?.kind === 'webrtc' || transport?.kind === 'webtransport') {
1571
- return [transport.kind];
1572
- }
1573
- return ['webrtc'];
1574
- }
1575
- #selectTransportForPeer(peerId, transport) {
1576
- const remoteKinds = this.#getRemoteTransportKinds(transport);
1577
- const commonKinds = remoteKinds.filter((kind) => this.#supportsTransport(kind));
1578
- if (!commonKinds.length) {
1579
- debug(`peer ${peerId} has no compatible transport with local capabilities`);
1580
- return null;
1581
- }
1582
- if (transport?.kind && commonKinds.includes(transport.kind)) {
1583
- return transport.kind;
1584
- }
1585
- if (commonKinds.includes(this.#preferredTransportKind)) {
1586
- return this.#preferredTransportKind;
1587
- }
1588
- if (commonKinds.includes('webrtc'))
1589
- return 'webrtc';
1590
- return 'webtransport';
1591
- }
1592
- #buildTransportOrder(peerId, transport) {
1593
- const remoteKinds = this.#getRemoteTransportKinds(transport);
1594
- const commonKinds = remoteKinds.filter((kind) => this.#supportsTransport(kind));
1595
- if (!commonKinds.length) {
1596
- if (this.#enableCircuitFallback) {
1597
- debug(`peer ${peerId} has no direct transport overlap, using circuit`);
1598
- return ['circuit'];
1599
- }
1600
- return [];
1601
- }
1602
- const order = [];
1603
- if (transport?.kind && commonKinds.includes(transport.kind)) {
1604
- order.push(transport.kind);
1605
- }
1606
- if (commonKinds.includes(this.#preferredTransportKind) &&
1607
- !order.includes(this.#preferredTransportKind)) {
1608
- order.push(this.#preferredTransportKind);
1609
- }
1610
- for (const kind of this.#fallbackTransportOrder) {
1611
- if (kind !== 'circuit' &&
1612
- commonKinds.includes(kind) &&
1613
- !order.includes(kind)) {
1614
- order.push(kind);
1615
- }
1616
- }
1617
- if (commonKinds.includes('webrtc') && !order.includes('webrtc')) {
1618
- order.push('webrtc');
1619
- }
1620
- if (commonKinds.includes('webtransport') &&
1621
- !order.includes('webtransport')) {
1622
- order.push('webtransport');
1623
- }
1624
- if (this.#enableCircuitFallback && !order.includes('circuit')) {
1625
- order.push('circuit');
1626
- }
1627
- debug(`peer ${peerId} transport order: ${order.join(' -> ')}`);
1628
- return order;
1629
- }
1630
- #clearTransportAttempt(peerId) {
1631
- const attempt = this.#peerTransportAttempts.get(peerId);
1632
- if (attempt?.timeout)
1633
- clearTimeout(attempt.timeout);
1634
- this.#peerTransportAttempts.delete(peerId);
1635
- }
1636
- #emitTransportEvent(event) {
1637
- this.#testHooks?.onTransportEvent?.(event);
1638
- }
1639
- #advanceTransportAttempt(peerId) {
1640
- const attempt = this.#peerTransportAttempts.get(peerId);
1641
- if (!attempt)
1642
- return;
1643
- const previousIndex = attempt.index;
1644
- const previousTransport = attempt.order[previousIndex];
1645
- if (attempt.timeout)
1646
- clearTimeout(attempt.timeout);
1647
- this.#emitTransportEvent({
1648
- type: 'attempt-advanced',
1649
- peerId,
1650
- transport: previousTransport,
1651
- attemptIndex: previousIndex,
1652
- order: [...attempt.order]
1653
- });
1654
- attempt.index += 1;
1655
- this.#attemptPeerTransport(peerId);
1656
- }
1657
- #startPeerTransportAttempt(peerId, star, version, initiator, transport) {
1658
- if (this.#connections[peerId])
1659
- return;
1660
- if (this.#peerTransportAttempts.has(peerId))
1661
- return;
1662
- const order = this.#buildTransportOrder(peerId, transport);
1663
- if (!order.length) {
1664
- debug(`peer ${peerId} has no available transport to attempt`);
1665
- return;
1666
- }
1667
- this.#peerTransportAttempts.set(peerId, {
1668
- order,
1669
- index: 0,
1670
- star,
1671
- version,
1672
- initiator
1673
- });
1674
- this.#attemptPeerTransport(peerId);
1675
- }
1676
- #attemptPeerTransport(peerId) {
1677
- const attempt = this.#peerTransportAttempts.get(peerId);
1678
- if (!attempt)
1679
- return;
1680
- if (attempt.index >= attempt.order.length) {
1681
- const exhaustedTransport = attempt.order[attempt.order.length - 1] ?? 'circuit';
1682
- this.#emitTransportEvent({
1683
- type: 'attempts-exhausted',
1684
- peerId,
1685
- transport: exhaustedTransport,
1686
- attemptIndex: attempt.index,
1687
- order: [...attempt.order]
1688
- });
1689
- debug(`peer ${peerId} transport attempts exhausted`);
1690
- this.#clearTransportAttempt(peerId);
1691
- return;
1692
- }
1693
- if (this.#connections[peerId])
1694
- return;
1695
- const transportKind = attempt.order[attempt.index];
1696
- this.#emitTransportEvent({
1697
- type: 'attempt-started',
1698
- peerId,
1699
- transport: transportKind,
1700
- attemptIndex: attempt.index,
1701
- order: [...attempt.order]
1702
- });
1703
- let peer = null;
1704
- if (transportKind === 'webrtc') {
1705
- peer = this.#createRTCPeerConnection(peerId, attempt.star, attempt.version, attempt.initiator);
1706
- }
1707
- else if (transportKind === 'webtransport') {
1708
- const url = this.#resolveWebTransportUrl(peerId, attempt.star);
1709
- if (!url) {
1710
- this.#advanceTransportAttempt(peerId);
1711
- return;
1712
- }
1713
- peer = this.#createWebTransportConnection(peerId, url, attempt.version, attempt.star);
1714
- }
1715
- else {
1716
- peer = this.#createCircuitConnection(peerId, attempt.version);
1717
- }
1718
- if (!peer) {
1719
- this.#advanceTransportAttempt(peerId);
1720
- return;
1721
- }
1722
- attempt.timeout = setTimeout(() => {
1723
- const current = this.#connections[peerId];
1724
- if (!current || current.connected)
1725
- return;
1726
- this.#emitTransportEvent({
1727
- type: 'attempt-timeout',
1728
- peerId,
1729
- transport: transportKind,
1730
- attemptIndex: attempt.index,
1731
- order: [...attempt.order]
1732
- });
1733
- debug(`peer ${peerId} ${transportKind} connect timeout`);
1734
- current.destroy();
1735
- }, this.#transportConnectTimeoutMs);
1736
- }
1737
- #decodeCircuitFallbackPayload(payload) {
1738
- if (!payload || typeof payload !== 'object')
1739
- return null;
1740
- const candidate = payload;
1741
- if (!Array.isArray(candidate.data))
1742
- return null;
1743
- const id = typeof candidate.id === 'string' && candidate.id.length
1744
- ? candidate.id
1745
- : this.#createCircuitId();
1746
- return {
1747
- id,
1748
- data: new Uint8Array(candidate.data.map((byte) => Number(byte) & 255))
1749
- };
1750
- }
1751
- async #handleFallbackCircuitPayload(payload, context) {
1752
- const decoded = this.#decodeCircuitFallbackPayload(payload);
1753
- if (!decoded) {
1754
- throw new Error('invalid circuit fallback payload');
1755
- }
1756
- let peer = this.#connections[context.from];
1757
- if (!peer) {
1758
- peer = this.#createCircuitConnection(context.from, this.version);
1759
- }
1760
- if (!peer)
1761
- throw new Error('circuit peer unavailable');
1762
- if (!peer.connected) {
1763
- const waitingPeer = peer;
1764
- await new Promise((resolve, reject) => {
1765
- const finish = (error) => {
1766
- clearTimeout(timer);
1767
- waitingPeer.off('connect', onConnect);
1768
- waitingPeer.off('close', onClose);
1769
- error ? reject(error) : resolve();
1770
- };
1771
- const onConnect = () => finish();
1772
- const onClose = () => finish(new Error('circuit peer closed during handshake'));
1773
- const timer = setTimeout(() => finish(new Error('circuit handshake timed out')), this.#transportConnectTimeoutMs);
1774
- waitingPeer.on('connect', onConnect);
1775
- waitingPeer.on('close', onClose);
1776
- });
1777
- }
1778
- this.#peerData(peer, decoded.data);
1779
- return { ok: true };
1780
- }
1781
- #createCircuitId() {
1782
- const randomUUID = globalThis.crypto?.randomUUID;
1783
- if (typeof randomUUID === 'function') {
1784
- return randomUUID.call(globalThis.crypto);
1785
- }
1786
- return `circuit-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1787
- }
1788
- #encodeCircuitData(data) {
1789
- if (data instanceof Uint8Array) {
1790
- return { __swarmType: 'u8', data: Array.from(data) };
1791
- }
1792
- return data;
1793
- }
1794
- #decodeCircuitData(data) {
1795
- if (data &&
1796
- typeof data === 'object' &&
1797
- data.__swarmType === 'u8' &&
1798
- Array.isArray(data.data)) {
1799
- return new Uint8Array(data.data.map((item) => Number(item) & 255));
1800
- }
1801
- return data;
1802
- }
1803
- #pickStarConnection() {
1804
- const entries = Object.values(this.#stars);
1805
- return entries.find((client) => client?.connectionState?.() === 'open');
1806
- }
1807
- #sendCircuit(starConnection, payload) {
1808
- starConnection.send({
1809
- url: 'circuit',
1810
- params: payload
1811
- });
1812
- }
1813
- onCircuit(method, handler) {
1814
- this.#circuitHandlers.set(method, handler);
1815
- }
1816
- offCircuit(method) {
1817
- this.#circuitHandlers.delete(method);
1818
- }
1819
- onServerData(handler) {
1820
- globalThis.pubsub.subscribe('server:data', handler);
1821
- }
1822
- offServerData(handler) {
1823
- globalThis.pubsub.unsubscribe('server:data', handler);
1824
- }
1825
- circuitRequest(peerId, method, data, timeoutMs = this.#circuitRequestTimeoutMs) {
1826
- const starConnection = this.#pickStarConnection();
1827
- if (!starConnection) {
1828
- return Promise.reject(new Error('no connected star available for circuit request'));
1829
- }
1830
- const id = this.#createCircuitId();
1831
- return new Promise((resolve, reject) => {
1832
- const timeout = setTimeout(() => {
1833
- this.#pendingCircuitRequests.delete(id);
1834
- reject(new Error(`circuit request for ${id} timed out`));
1835
- }, timeoutMs);
1836
- this.#pendingCircuitRequests.set(id, { resolve, reject, timeout, peerId, starConnection });
1837
- try {
1838
- this.#sendCircuit(starConnection, {
1839
- to: peerId,
1840
- from: this.peerId,
1841
- id,
1842
- phase: 'request',
1843
- method,
1844
- data: this.#encodeCircuitData(data)
1845
- });
1846
- }
1847
- catch (error) {
1848
- clearTimeout(timeout);
1849
- this.#pendingCircuitRequests.delete(id);
1850
- reject(error);
1851
- }
1852
- });
1853
- }
1854
- #inComingCircuit = async ({ from, to, id, phase, method, data, error }, starConnection) => {
1855
- if (to !== this.peerId)
1856
- return;
1857
- if (phase === 'response') {
1858
- const pending = this.#pendingCircuitRequests.get(id);
1859
- if (!pending)
1860
- return;
1861
- if (pending.starConnection !== starConnection)
1862
- return;
1863
- if (from !== pending.peerId && !(from === 'server' && error))
1864
- return;
1865
- clearTimeout(pending.timeout);
1866
- this.#pendingCircuitRequests.delete(id);
1867
- if (error) {
1868
- pending.reject(new Error(String(error)));
1869
- return;
1870
- }
1871
- pending.resolve(this.#decodeCircuitData(data));
1872
- return;
1873
- }
1874
- if (phase !== 'request')
1875
- return;
1876
- if (typeof method !== 'string' || !method.length)
1877
- return;
1878
- const handler = this.#circuitHandlers.get(method);
1879
- if (!handler) {
1880
- this.#sendCircuit(starConnection, {
1881
- to: from,
1882
- from: this.peerId,
1883
- id,
1884
- phase: 'response',
1885
- error: `no circuit handler registered for method ${method}`
1886
- });
1887
- return;
1888
- }
1889
- try {
1890
- const response = await handler(this.#decodeCircuitData(data), {
1891
- from,
1892
- method,
1893
- id
1894
- });
1895
- this.#sendCircuit(starConnection, {
1896
- to: from,
1897
- from: this.peerId,
1898
- id,
1899
- phase: 'response',
1900
- data: this.#encodeCircuitData(response)
1901
- });
1902
- }
1903
- catch (handlerError) {
1904
- this.#sendCircuit(starConnection, {
1905
- to: from,
1906
- from: this.peerId,
1907
- id,
1908
- phase: 'response',
1909
- error: handlerError instanceof Error
1910
- ? handlerError.message
1911
- : String(handlerError)
1912
- });
1913
- }
1914
- };
1915
- connect(peerId, star, initiator = true) {
1916
- if (this.#connections[peerId]) {
1917
- debug(`peer ${peerId} already connected`);
1918
- return;
1919
- }
1920
- if (!this.#testHooks?.allowConnectWithoutStar &&
1921
- this.#stars[star]?.connectionState() !== 'open') {
1922
- console.warn(`Star ${star} is not connected, cannot reconnect to peer ${peerId}`);
1923
- return;
1924
- }
1925
- this.#startPeerTransportAttempt(peerId, star, this.version, initiator, {
1926
- kind: this.#preferredTransportKind,
1927
- kinds: [...this.#supportedTransports]
1928
- });
1929
- }
1930
- reconnect(peerId, star, initiator = false) {
1931
- delete this.#connections[peerId];
1932
- debug(`reconnecting to peer ${peerId}`);
1933
- return this.connect(peerId, star, initiator);
1934
- }
1935
- connectWebTransport(peerId, url) {
1936
- this.#createWebTransportConnection(peerId, url, this.version);
1937
- }
1938
- #resolveWebTransportUrl(peerId, starConnection) {
1939
- const urlTemplate = this.#webTransportUrlTemplate || (starConnection && this.#relayUrls.get(starConnection));
1940
- if (!urlTemplate) {
1941
- return null;
1942
- }
1943
- return urlTemplate
1944
- .replace('{peerId}', encodeURIComponent(peerId))
1945
- .replace('{selfId}', encodeURIComponent(this.peerId));
1946
- }
1947
- #createWebTransportConnection(peerId, url, version, starConnection) {
1948
- if (this.#connections[peerId]) {
1949
- debug(`peer ${peerId} already connected`);
1950
- return null;
1951
- }
1952
- const testPeer = this.#testHooks?.createPeer?.({
1953
- kind: 'webtransport',
1954
- peerId,
1955
- version,
1956
- initiator: true,
1957
- url
1958
- });
1959
- if (testPeer) {
1960
- testPeer.on('connect', () => this.#peerConnect(testPeer));
1961
- testPeer.on('close', () => this.#peerClose(testPeer));
1962
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
1963
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
1964
- this.#connections[peerId] = testPeer;
1965
- this.#peerTransportKinds.set(peerId, 'webtransport');
1966
- return testPeer;
1967
- }
1968
- const peer = new WebTransportPeer({
1969
- from: this.peerId,
1970
- to: peerId,
1971
- version,
1972
- url,
1973
- getTicket: async () => {
1974
- const star = starConnection || this.#pickStarConnection();
1975
- if (!star)
1976
- throw new Error('no connected star available for WebTransport ticket');
1977
- let timer;
1978
- try {
1979
- const response = await Promise.race([
1980
- star.request({ url: 'webtransport-ticket', params: { from: this.peerId, to: peerId } }),
1981
- new Promise((_, reject) => {
1982
- timer = setTimeout(() => reject(new Error('WebTransport ticket request timed out')), this.#transportConnectTimeoutMs);
1983
- })
1984
- ]);
1985
- if (typeof response?.ticket !== 'string')
1986
- throw new Error('star did not authorize WebTransport relay');
1987
- return response.ticket;
1988
- }
1989
- finally {
1990
- clearTimeout(timer);
1991
- }
1992
- }
1993
- });
1994
- peer.on('connect', () => this.#peerConnect(peer));
1995
- peer.on('close', () => this.#peerClose(peer));
1996
- peer.on('data', (data) => this.#peerData(peer, data));
1997
- peer.on('error', (error) => this.#peerError(peer, error));
1998
- this.#connections[peerId] = peer;
1999
- this.#peerTransportKinds.set(peerId, 'webtransport');
2000
- return peer;
2001
- }
2002
- #createCircuitConnection(peerId, version) {
2003
- if (this.#connections[peerId]) {
2004
- debug(`peer ${peerId} already connected`);
2005
- return null;
2006
- }
2007
- const testPeer = this.#testHooks?.createPeer?.({
2008
- kind: 'circuit',
2009
- peerId,
2010
- version,
2011
- initiator: true
2012
- });
2013
- if (testPeer) {
2014
- testPeer.on('connect', () => this.#peerConnect(testPeer));
2015
- testPeer.on('close', () => this.#peerClose(testPeer));
2016
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
2017
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
2018
- this.#connections[peerId] = testPeer;
2019
- this.#peerTransportKinds.set(peerId, 'circuit');
2020
- return testPeer;
2021
- }
2022
- const peer = new CircuitPeer({
2023
- from: this.peerId,
2024
- to: peerId,
2025
- version,
2026
- handshake: async () => {
2027
- const response = await this.circuitRequest(peerId, 'swarm:fallback:hello', undefined, this.#transportConnectTimeoutMs);
2028
- if (response?.peerId !== peerId || String(response?.version) !== String(version) || response.encoding !== 'peer-core/1') {
2029
- throw new Error('invalid circuit handshake response');
2030
- }
2031
- },
2032
- sendViaCircuit: (payload) => this.circuitRequest(peerId, this.#fallbackCircuitMethod, payload)
2033
- });
2034
- peer.on('connect', () => this.#peerConnect(peer));
2035
- peer.on('close', () => this.#peerClose(peer));
2036
- peer.on('error', (error) => this.#peerError(peer, error));
2037
- this.#connections[peerId] = peer;
2038
- this.#peerTransportKinds.set(peerId, 'circuit');
2039
- return peer;
2040
- }
2041
- #createRTCPeerConnection = (peerId, star, version, initiator = false) => {
2042
- const testPeer = this.#testHooks?.createPeer?.({
2043
- kind: 'webrtc',
2044
- peerId,
2045
- version,
2046
- initiator,
2047
- star
2048
- });
2049
- if (testPeer) {
2050
- testPeer.on('connect', () => this.#peerConnect(testPeer));
2051
- testPeer.on('close', () => this.#peerClose(testPeer));
2052
- testPeer.on('data', (data) => this.#peerData(testPeer, data));
2053
- testPeer.on('error', (error) => this.#peerError(testPeer, error));
2054
- this.#connections[peerId] = testPeer;
2055
- this.#peerTransportKinds.set(peerId, 'webrtc');
2056
- return testPeer;
2057
- }
2058
- const peer = new WebRTCPeer({
2059
- initiator: initiator,
2060
- from: this.peerId,
2061
- to: peerId,
2062
- version
2063
- });
2064
- peer.on('signal', (signal) => this.#peerSignal(peer, signal, star, this.version));
2065
- peer.on('connect', () => this.#peerConnect(peer));
2066
- peer.on('close', () => this.#peerClose(peer));
2067
- peer.on('data', (data) => this.#peerData(peer, data));
2068
- peer.on('error', (error) => this.#peerError(peer, error));
2069
- this.#connections[peerId] = peer;
2070
- this.#peerTransportKinds.set(peerId, 'webrtc');
2071
- return peer;
2072
- };
2073
- #peerJoined = async (announcement, star) => {
2074
- const normalized = normalizePeerAnnouncement(announcement);
2075
- if (!normalized) {
2076
- debug('ignored malformed peer announcement');
2077
- return;
2078
- }
2079
- const { peerId, transport } = normalized;
2080
- // Legacy stars announce only the peer id. This value is used for transport
2081
- // negotiation; applications still perform their own version handshake.
2082
- const version = normalized.version ?? this.version;
2083
- if (this.peerId === peerId)
2084
- return;
2085
- // Stars can announce the same peer more than once while its transport is
2086
- // still negotiating. Keep that connection/attempt alive; destroying it
2087
- // here makes simultaneous joins continually reset each other.
2088
- // Both sides receive the discovery event. Elect exactly one offerer so
2089
- // simultaneous joins do not create competing channels/SDP negotiations.
2090
- const initiator = String(this.peerId) < String(peerId);
2091
- this.#startPeerTransportAttempt(peerId, star, version, initiator, transport);
2092
- debug(`peer ${peerId} joined`);
2093
- };
2094
- #inComingSignal = async ({ from, signal, channelName, version }, star) => {
2095
- if (!this.#supportsTransport('webrtc')) {
2096
- debug(`ignoring webrtc signal for ${from} because local client does not support webrtc`);
2097
- return;
2098
- }
2099
- if (version !== this.version) {
2100
- console.warn(`${from} joined using the wrong version.\nexpected: ${this.version} but got:${version}`);
2101
- return;
2102
- }
2103
- if (from === this.peerId) {
2104
- console.warn(`${from} tried to connect to itself.`);
2105
- return;
2106
- }
2107
- let peer = this.#connections[from];
2108
- if (peer && String(peer.channelName).startsWith('webtransport:')) {
2109
- if (peer.connected) {
2110
- debug(`ignoring webrtc signal for ${from} because peer uses webtransport`);
2111
- return;
2112
- }
2113
- peer.destroy();
2114
- delete this.#connections[from];
2115
- this.#clearTransportAttempt(from);
2116
- peer = undefined;
2117
- }
2118
- if (!peer) {
2119
- this.#clearTransportAttempt(from);
2120
- this.#createRTCPeerConnection(from, star, version);
2121
- peer = this.#connections[from];
2122
- }
2123
- if (peer.connected) {
2124
- debug(`peer ${from} already connected`);
2125
- return;
2126
- }
2127
- // peer.channels[channelName]
2128
- if (String(peer.channelName) !== String(channelName)) {
2129
- console.warn(`channelNames don't match: got ${peer.channelName}, expected: ${channelName}.`);
2130
- peer.destroy();
2131
- delete this.#connections[from];
2132
- this.#clearTransportAttempt(from);
2133
- this.#createRTCPeerConnection(from, star, version, false);
2134
- peer = this.#connections[from];
2135
- if (!peer)
2136
- return;
2137
- }
2138
- peer.signal(signal);
2139
- };
2140
- #peerSignal = (peer, signal, star, version) => {
2141
- let client = this.#stars[star];
2142
- if (!client)
2143
- client = this.#stars[Object.keys(this.#stars)[0]];
2144
- client.send({
2145
- url: 'signal',
2146
- params: {
2147
- from: this.peerId,
2148
- to: peer.peerId,
2149
- channelName: peer.channelName,
2150
- version,
2151
- signal,
2152
- initiator: peer.initiator
2153
- }
2154
- });
2155
- };
2156
- #peerClose = (peer) => {
2157
- // A failed transport can finish closing after fallback has already placed a
2158
- // replacement in the connection map. Never let that stale close delete or
2159
- // advance the replacement's active attempt.
2160
- if (this.#connections[peer.peerId] !== peer) {
2161
- debug(`ignored stale close for ${peer.peerId}`);
2162
- return;
2163
- }
2164
- const wasConnected = peer.connected;
2165
- this.#peerTransportKinds.delete(peer.peerId);
2166
- delete this.#connections[peer.peerId];
2167
- if (!wasConnected && this.#peerTransportAttempts.has(peer.peerId)) {
2168
- this.#advanceTransportAttempt(peer.peerId);
2169
- }
2170
- debug(`closed ${peer.peerId}'s connection`);
2171
- };
2172
- #peerConnect = (peer) => {
2173
- const attempt = this.#peerTransportAttempts.get(peer.peerId);
2174
- const transport = this.#peerTransportKinds.get(peer.peerId) ||
2175
- attempt?.order[attempt.index] ||
2176
- 'webrtc';
2177
- this.#emitTransportEvent({
2178
- type: 'connected',
2179
- peerId: peer.peerId,
2180
- transport,
2181
- attemptIndex: attempt?.index ?? 0,
2182
- order: attempt ? [...attempt.order] : [transport]
2183
- });
2184
- this.#clearTransportAttempt(peer.peerId);
2185
- debug(`${peer.peerId} connected`);
2186
- globalThis.pubsub.publishVerbose(this.#connectEvent, peer.peerId);
2187
- };
2188
- #noticeMessage = (message, id, from, peer) => {
2189
- const dataOut = message instanceof Uint8Array
2190
- ? message
2191
- : new Uint8Array(Object.values(message));
2192
- if (globalThis.pubsub.hasSubscribers(id)) {
2193
- globalThis.pubsub.publish(id, {
2194
- data: dataOut,
2195
- id,
2196
- from,
2197
- peer
2198
- });
2199
- }
2200
- else {
2201
- globalThis.pubsub.publish('peer:data', {
2202
- data: dataOut,
2203
- id,
2204
- from,
2205
- peer
2206
- });
2207
- }
2208
- };
2209
- #peerData = (peer, data) => {
2210
- const tryJson = () => {
2211
- const parsed = JSON.parse(new TextDecoder().decode(data));
2212
- const { id, size, chunk, index, count } = parsed;
2213
- chunk ? Object.values(chunk).length : size;
2214
- return {
2215
- id,
2216
- size: Number(size),
2217
- index: Number(index ?? 0),
2218
- count: Number(count ?? 1),
2219
- chunk: new Uint8Array(Object.values(chunk)),
2220
- flags: 0,
2221
- crc: 0
2222
- };
2223
- };
2224
- const decodeBinary = () => {
2225
- let u8;
2226
- if (typeof data === 'string') {
2227
- // should not happen when sending binary, fallback to JSON
2228
- return tryJson();
2229
- }
2230
- else if (data instanceof ArrayBuffer) {
2231
- u8 = new Uint8Array(data);
2232
- }
2233
- else if (ArrayBuffer.isView(data)) {
2234
- const view = data;
2235
- const byteOffset = view.byteOffset || 0;
2236
- const byteLength = view.byteLength || data.length;
2237
- u8 = new Uint8Array(view.buffer, byteOffset, byteLength);
2238
- }
2239
- else if (data?.buffer) {
2240
- u8 = new Uint8Array(data.buffer);
2241
- }
2242
- else {
2243
- // last resort: attempt JSON
2244
- return tryJson();
2245
- }
2246
- const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
2247
- let offset = 0;
2248
- dv.getUint8(offset);
2249
- offset += 1;
2250
- const flags = dv.getUint8(offset);
2251
- offset += 1;
2252
- const size = dv.getUint32(offset, true);
2253
- offset += 4;
2254
- const index = dv.getUint32(offset, true);
2255
- offset += 4;
2256
- const count = dv.getUint32(offset, true);
2257
- offset += 4;
2258
- const expectedCrc = dv.getUint32(offset, true);
2259
- offset += 4;
2260
- const idLen = dv.getUint16(offset, true);
2261
- offset += 2;
2262
- const idBytes = u8.subarray(offset, offset + idLen);
2263
- offset += idLen;
2264
- const id = new TextDecoder().decode(idBytes);
2265
- const chunk = u8.subarray(offset);
2266
- return { id, size, index, count, chunk, flags, crc: expectedCrc };
2267
- };
2268
- const frame = decodeBinary();
2269
- peer.bw.down += frame.chunk.length;
2270
- // Single frame path: if compressed, inflate before publish
2271
- if (frame.count === 1) {
2272
- let payload = frame.chunk;
2273
- const compressed = Boolean(frame.flags & (1 << 1));
2274
- if (compressed) {
2275
- const actualCrc = crc32(payload);
2276
- if (actualCrc !== frame.crc) {
2277
- console.warn(`CRC mismatch: expected ${frame.crc}, got ${actualCrc}`);
2278
- }
2279
- try {
2280
- payload = inflate_1(payload);
2281
- }
2282
- catch (e) {
2283
- console.warn('inflate failed, passing compressed payload');
2284
- }
2285
- }
2286
- this.#noticeMessage(payload, frame.id, peer.peerId, peer);
2287
- return;
2288
- }
2289
- // Chunked message handling with indexed reassembly
2290
- if (!this.#messagesToHandle[frame.id] ||
2291
- Array.isArray(this.#messagesToHandle[frame.id])) {
2292
- this.#messagesToHandle[frame.id] = {
2293
- chunks: new Array(frame.count),
2294
- receivedBytes: 0,
2295
- expectedSize: Number(frame.size),
2296
- expectedCount: Number(frame.count)
2297
- };
2298
- }
2299
- const state = this.#messagesToHandle[frame.id];
2300
- // Verify CRC for this chunk
2301
- const actualCrc = crc32(frame.chunk);
2302
- if (actualCrc !== frame.crc) {
2303
- console.warn(`Chunk CRC mismatch for ${frame.id}[${frame.index}]: expected ${frame.crc}, got ${actualCrc}`);
2304
- }
2305
- state.chunks[frame.index] = frame.chunk;
2306
- state.receivedBytes += frame.chunk.length;
2307
- // If all chunks present and total size matches, reassemble
2308
- const allPresent = state.chunks.every((c) => c instanceof Uint8Array);
2309
- if (allPresent && state.receivedBytes === state.expectedSize) {
2310
- const result = new Uint8Array(state.expectedSize);
2311
- let offset2 = 0;
2312
- for (const c of state.chunks) {
2313
- result.set(c, offset2);
2314
- offset2 += c.length;
2315
- }
2316
- let payload = result;
2317
- const compressed = Boolean(frame.flags & (1 << 1));
2318
- if (compressed) {
2319
- try {
2320
- payload = inflate_1(result);
2321
- }
2322
- catch (e) {
2323
- console.warn('inflate failed, passing compressed payload');
2324
- }
2325
- }
2326
- this.#noticeMessage(payload, frame.id, peer.peerId, peer);
2327
- delete this.#messagesToHandle[frame.id];
2328
- }
2329
- };
2330
- #peerError = (peer, error) => {
2331
- const message = error instanceof Error ? error.message : `unknown error: ${String(error)}`;
2332
- const attempt = this.#peerTransportAttempts.get(peer.peerId);
2333
- const transport = this.#peerTransportKinds.get(peer.peerId) ||
2334
- attempt?.order[attempt.index] ||
2335
- 'webrtc';
2336
- this.#emitTransportEvent({
2337
- type: 'attempt-error',
2338
- peerId: peer.peerId,
2339
- transport,
2340
- attemptIndex: attempt?.index ?? 0,
2341
- order: attempt ? [...attempt.order] : [transport],
2342
- reason: message
2343
- });
2344
- console.warn(`Connection error: ${message}`);
2345
- peer.destroy();
2346
- };
2347
- async close() {
2348
- for (const pending of this.#pendingCircuitRequests.values()) {
2349
- clearTimeout(pending.timeout);
2350
- pending.reject(new Error('client closed'));
2351
- }
2352
- this.#pendingCircuitRequests.clear();
2353
- for (const peerId of this.#peerTransportAttempts.keys()) {
2354
- this.#clearTransportAttempt(peerId);
2355
- }
2356
- for (const peerId in this.#connections) {
2357
- const peer = this.#connections[peerId];
2358
- if (peer) {
2359
- peer.destroy();
2360
- delete this.#connections[peerId];
2361
- }
2362
- }
2363
- for (const star in this.#stars) {
2364
- // unsubscribe handlers we registered earlier
2365
- const listeners = this.#starListeners[star];
2366
- if (listeners && listeners.length) {
2367
- for (const { topic, handler } of listeners) {
2368
- try {
2369
- this.#stars[star].pubsub.unsubscribe(topic, handler);
2370
- }
2371
- catch (e) {
2372
- // ignore
2373
- }
2374
- }
2375
- }
2376
- if (this.#stars[star].connectionState() === 'open') {
2377
- await this.#stars[star].send({ url: 'leave', params: this.peerId });
2378
- }
2379
- }
2380
- const peerClosers = Object.values(this.#connections).map((connection) => {
2381
- try {
2382
- // destroy() may be sync or return a promise
2383
- return connection.destroy();
2384
- }
2385
- catch (e) {
2386
- return undefined;
2387
- }
2388
- });
2389
- const starClosers = Object.values(this.#stars).map((connection) => {
2390
- try {
2391
- return connection.close(0);
2392
- }
2393
- catch (e) {
2394
- return undefined;
2395
- }
2396
- });
2397
- return Promise.allSettled([...peerClosers, ...starClosers]);
2398
- }
2399
- }
2400
-
2401
- export { Client as default };