@leofcoin/chain 1.9.10 → 1.9.12

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,1047 @@
1
+ import { i as inflate_1, c as createDebugger, d as deflate_1, L as LittlePubSub } from './node-browser-Oq89xLSU.js';
2
+ import './identity-B8_RBemH-Dq17MYzU.js';
3
+ import './constants-V2VjIc2r.js';
4
+
5
+ class Api {
6
+ _pubsub;
7
+ constructor(_pubsub) {
8
+ this._pubsub = _pubsub;
9
+ }
10
+ subscribe(topic, cb) {
11
+ this._pubsub.subscribe(topic, cb);
12
+ }
13
+ unsubscribe(topic, cb) {
14
+ this._pubsub.unsubscribe(topic, cb);
15
+ }
16
+ publish(topic, value) {
17
+ this._pubsub.publish(topic, value);
18
+ }
19
+ subscribers() {
20
+ this._pubsub.subscribers;
21
+ }
22
+ connectionState(state) {
23
+ switch (state) {
24
+ case 0:
25
+ return 'connecting';
26
+ case 1:
27
+ return 'open';
28
+ case 2:
29
+ return 'closing';
30
+ case 3:
31
+ return 'closed';
32
+ }
33
+ }
34
+ /**
35
+ * @param {string} type
36
+ * @param {string} name
37
+ * @param {object} params
38
+ */
39
+ request(client, request) {
40
+ return new Promise((resolve, reject) => {
41
+ const state = this.connectionState(client.readyState);
42
+ if (state !== 'open')
43
+ return reject(`coudn't send request to ${client.id}, no open connection found.`);
44
+ request.id = Math.random().toString(36).slice(-12);
45
+ const handler = (result) => {
46
+ if (result && result.error)
47
+ return reject(result.error);
48
+ resolve({ result, id: request.id, handler });
49
+ this.unsubscribe(request.id, handler);
50
+ };
51
+ this.subscribe(request.id, handler);
52
+ this.send(client, request);
53
+ });
54
+ }
55
+ async send(client, request) {
56
+ return client.send(JSON.stringify(request));
57
+ }
58
+ pubsub(client) {
59
+ return {
60
+ publish: (topic = 'pubsub', value) => {
61
+ return this.send(client, { url: 'pubsub', params: { topic, value } });
62
+ },
63
+ subscribe: (topic = 'pubsub', cb) => {
64
+ this.subscribe(topic, cb);
65
+ return this.send(client, {
66
+ url: 'pubsub',
67
+ params: { topic, subscribe: true }
68
+ });
69
+ },
70
+ unsubscribe: (topic = 'pubsub', cb) => {
71
+ this.unsubscribe(topic, cb);
72
+ return this.send(client, {
73
+ url: 'pubsub',
74
+ params: { topic, unsubscribe: true }
75
+ });
76
+ },
77
+ subscribers: this._pubsub.subscribers
78
+ };
79
+ }
80
+ server(client) {
81
+ return {
82
+ uptime: async () => {
83
+ try {
84
+ const { result, id, handler } = await this.request(client, {
85
+ url: 'uptime'
86
+ });
87
+ this.unsubscribe(id, handler);
88
+ return result;
89
+ }
90
+ catch (e) {
91
+ throw e;
92
+ }
93
+ },
94
+ ping: async () => {
95
+ try {
96
+ const now = new Date().getTime();
97
+ const { result, id, handler } = await this.request(client, {
98
+ url: 'ping'
99
+ });
100
+ this.unsubscribe(id, handler);
101
+ return Number(result) - now;
102
+ }
103
+ catch (e) {
104
+ throw e;
105
+ }
106
+ }
107
+ };
108
+ }
109
+ peernet(client) {
110
+ return {
111
+ join: async (params) => {
112
+ try {
113
+ params.join = true;
114
+ const requested = { url: 'peernet', params };
115
+ const { result, id, handler } = await this.request(client, requested);
116
+ this.unsubscribe(id, handler);
117
+ return result;
118
+ }
119
+ catch (e) {
120
+ throw e;
121
+ }
122
+ },
123
+ leave: async (params) => {
124
+ try {
125
+ params.join = false;
126
+ const requested = { url: 'peernet', params };
127
+ const { result, id, handler } = await this.request(client, requested);
128
+ this.unsubscribe(id, handler);
129
+ return result;
130
+ }
131
+ catch (e) {
132
+ throw e;
133
+ }
134
+ },
135
+ peers: async () => {
136
+ try {
137
+ const requested = { url: 'peernet', params: { peers: true } };
138
+ const { result, id, handler } = await this.request(client, requested);
139
+ this.unsubscribe(id, handler);
140
+ return result;
141
+ }
142
+ catch (e) {
143
+ throw e;
144
+ }
145
+ }
146
+ };
147
+ }
148
+ }
149
+
150
+ class ClientConnection {
151
+ client;
152
+ api;
153
+ #startTime;
154
+ constructor(client, api) {
155
+ this.#startTime = new Date().getTime();
156
+ this.client = client;
157
+ this.api = api;
158
+ }
159
+ request = async (req) => {
160
+ const { result, id, handler } = await this.api.request(this.client, req);
161
+ globalThis.pubsub.unsubscribe(id, handler);
162
+ return result;
163
+ };
164
+ send = (req) => this.api.send(this.client, req);
165
+ get subscribe() {
166
+ return this.api.subscribe;
167
+ }
168
+ get unsubscribe() {
169
+ return this.api.unsubscribe;
170
+ }
171
+ get subscribers() {
172
+ return this.api.subscribers;
173
+ }
174
+ get publish() {
175
+ return this.api.publish;
176
+ }
177
+ get pubsub() {
178
+ return this.api.pubsub(this.client);
179
+ }
180
+ uptime = () => {
181
+ const now = new Date().getTime();
182
+ return (now - this.#startTime);
183
+ };
184
+ get peernet() {
185
+ return this.api.peernet(this.client);
186
+ }
187
+ get server() {
188
+ return this.api.server(this.client);
189
+ }
190
+ connectionState = () => this.api.connectionState(this.client.readyState);
191
+ close = exit => {
192
+ // client.onclose = message => {
193
+ // if (exit) process.exit()
194
+ // }
195
+ this.client.close();
196
+ };
197
+ }
198
+
199
+ if (!globalThis.PubSub)
200
+ globalThis.PubSub = LittlePubSub;
201
+ if (!globalThis.pubsub)
202
+ globalThis.pubsub = new LittlePubSub(false);
203
+ class SocketRequestClient {
204
+ api;
205
+ clientConnection;
206
+ #tries = 0;
207
+ #retry = false;
208
+ #timeout = 10_000;
209
+ #times = 10;
210
+ #options;
211
+ #protocol;
212
+ #url;
213
+ #experimentalWebsocket = false;
214
+ constructor(url, protocol, options) {
215
+ let { retry, timeout, times, experimentalWebsocket } = options || {};
216
+ if (retry !== undefined)
217
+ this.#retry = retry;
218
+ if (timeout !== undefined)
219
+ this.#timeout = timeout;
220
+ if (times !== undefined)
221
+ this.#times = times;
222
+ if (experimentalWebsocket !== undefined)
223
+ this.#experimentalWebsocket = experimentalWebsocket;
224
+ this.#url = url;
225
+ this.#protocol = protocol;
226
+ this.#options = options;
227
+ this.api = new Api(globalThis.pubsub);
228
+ }
229
+ init() {
230
+ return new Promise(async (resolve, reject) => {
231
+ const init = async () => {
232
+ // @ts-ignore
233
+ if (!globalThis.WebSocket && !this.#experimentalWebsocket)
234
+ globalThis.WebSocket = (await import('./browser-DTwbEd2v-BvEtTVTB.js').then(function (n) { return n.b; })).default.w3cwebsocket;
235
+ const client = new WebSocket(this.#url, this.#protocol);
236
+ if (this.#experimentalWebsocket) {
237
+ client.addEventListener('error', this.onerror);
238
+ client.addEventListener('message', this.onmessage);
239
+ client.addEventListener('open', () => {
240
+ this.#tries = 0;
241
+ resolve(new ClientConnection(client, this.api));
242
+ });
243
+ client.addEventListener('close', (client.onclose = (message) => {
244
+ this.#tries++;
245
+ if (!this.#retry)
246
+ return reject(this.#options);
247
+ if (this.#tries > this.#times) {
248
+ console.log(`${this.#options.protocol} Client Closed`);
249
+ console.error(`could not connect to - ${this.#url}/`);
250
+ return resolve(new ClientConnection(client, this.api));
251
+ }
252
+ if (message.code === 1006) {
253
+ console.log(`Retrying in ${this.#timeout} ms`);
254
+ setTimeout(() => {
255
+ return init();
256
+ }, this.#timeout);
257
+ }
258
+ }));
259
+ }
260
+ else {
261
+ client.onmessage = this.onmessage;
262
+ client.onerror = this.onerror;
263
+ client.onopen = () => {
264
+ this.#tries = 0;
265
+ resolve(new ClientConnection(client, this.api));
266
+ };
267
+ client.onclose = (message) => {
268
+ this.#tries++;
269
+ if (!this.#retry)
270
+ return reject(this.#options);
271
+ if (this.#tries > this.#times) {
272
+ console.log(`${this.#options.protocol} Client Closed`);
273
+ console.error(`could not connect to - ${this.#url}/`);
274
+ return resolve(new ClientConnection(client, this.api));
275
+ }
276
+ if (message.code === 1006) {
277
+ console.log(`Retrying in ${this.#timeout} ms`);
278
+ setTimeout(() => {
279
+ return init();
280
+ }, this.#timeout);
281
+ }
282
+ };
283
+ }
284
+ };
285
+ return init();
286
+ });
287
+ }
288
+ onerror = (error) => {
289
+ if (globalThis.pubsub.hasSubscribers('error')) {
290
+ globalThis.pubsub.publish('error', error);
291
+ }
292
+ else {
293
+ console.error(error);
294
+ }
295
+ };
296
+ onmessage(message) {
297
+ if (!message.data) {
298
+ console.warn(`message ignored because it contained no data`);
299
+ return;
300
+ }
301
+ const { value, url, status, id } = JSON.parse(message.data.toString());
302
+ const publisher = id ? id : url;
303
+ if (status === 200) {
304
+ globalThis.pubsub.publish(publisher, value);
305
+ }
306
+ else {
307
+ globalThis.pubsub.publish(publisher, { error: value });
308
+ }
309
+ }
310
+ }
311
+
312
+ const MAX_MESSAGE_SIZE = 16000;
313
+ const defaultOptions = {
314
+ networkVersion: 'peach',
315
+ version: 'v1',
316
+ stars: ['wss://star.leofcoin.org'],
317
+ connectEvent: 'peer:connected'
318
+ };
319
+
320
+ // Simple CRC32 implementation
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
+ const iceServers = [
332
+ {
333
+ urls: 'stun:stun.l.google.com:19302' // Google's public STUN server
334
+ },
335
+ {
336
+ urls: 'stun:openrelay.metered.ca:80'
337
+ },
338
+ {
339
+ urls: 'turn:openrelay.metered.ca:443',
340
+ username: 'openrelayproject',
341
+ credential: 'openrelayproject'
342
+ },
343
+ {
344
+ urls: 'turn:openrelay.metered.ca:443?transport=tcp',
345
+ username: 'openrelayproject',
346
+ credential: 'openrelayproject'
347
+ }
348
+ ];
349
+ const SimplePeer = (await import('./index-avzvc6cK-CDbEoIOc.js').then(function (n) { return n.i; })).default;
350
+ class Peer extends SimplePeer {
351
+ peerId;
352
+ channelName;
353
+ version;
354
+ compressionThreshold = 0.98;
355
+ bw = { up: 0, down: 0 };
356
+ get connected() {
357
+ return super.connected;
358
+ }
359
+ constructor(options) {
360
+ const { from, to, initiator, trickle, config, version, wrtc, compressionThreshold } = options;
361
+ const channelName = initiator ? `${from}:${to}` : `${to}:${from}`;
362
+ super({
363
+ channelName,
364
+ initiator,
365
+ trickle: trickle ?? true,
366
+ config: { iceServers, ...config },
367
+ wrtc: wrtc ?? globalThis.wrtc
368
+ });
369
+ this.version = String(version);
370
+ this.peerId = to;
371
+ this.channelName = channelName;
372
+ if (compressionThreshold !== undefined)
373
+ this.compressionThreshold = compressionThreshold;
374
+ }
375
+ async #chunkit(data, id) {
376
+ this.bw.up = data.length;
377
+ // attempt compression; use compressed only if beneficial
378
+ let sendData = data;
379
+ try {
380
+ const c = deflate_1(data);
381
+ if (c?.length && c.length < data.length * this.compressionThreshold)
382
+ sendData = c;
383
+ }
384
+ catch (e) {
385
+ // ignore
386
+ }
387
+ const size = sendData.length;
388
+ const encodeFrame = (idStr, totalSize, index, count, payload, flags) => {
389
+ const te = new TextEncoder();
390
+ const idBytes = te.encode(idStr);
391
+ const crc = crc32$1(payload);
392
+ const headerLen = 1 + 1 + 4 + 4 + 4 + 4 + 2 + idBytes.length;
393
+ const buffer = new ArrayBuffer(headerLen + payload.length);
394
+ const view = new DataView(buffer);
395
+ const out = new Uint8Array(buffer);
396
+ let offset = 0;
397
+ view.setUint8(offset, 1); // version
398
+ offset += 1;
399
+ view.setUint8(offset, flags); // flags: bit0 chunked, bit1 compressed
400
+ offset += 1;
401
+ view.setUint32(offset, totalSize, true);
402
+ offset += 4;
403
+ view.setUint32(offset, index, true);
404
+ offset += 4;
405
+ view.setUint32(offset, count, true);
406
+ offset += 4;
407
+ view.setUint32(offset, crc, true); // CRC32
408
+ offset += 4;
409
+ view.setUint16(offset, idBytes.length, true);
410
+ offset += 2;
411
+ out.set(idBytes, offset);
412
+ offset += idBytes.length;
413
+ out.set(payload, offset);
414
+ return out;
415
+ };
416
+ // no needles chunking, keep it simple, if data is smaller then max size just send it
417
+ if (size <= MAX_MESSAGE_SIZE) {
418
+ const flags = ((size > MAX_MESSAGE_SIZE ? 1 : 0) << 0) |
419
+ ((sendData !== data ? 1 : 0) << 1);
420
+ super.send(encodeFrame(id, size, 0, 1, sendData, flags));
421
+ return;
422
+ }
423
+ function* chunks(data) {
424
+ while (data.length !== 0) {
425
+ const amountToSlice = data.length >= MAX_MESSAGE_SIZE ? MAX_MESSAGE_SIZE : data.length;
426
+ const subArray = data.subarray(0, amountToSlice);
427
+ data = data.subarray(amountToSlice, data.length);
428
+ yield subArray;
429
+ // super.send(JSON.stringify({ chunk: subArray, id, size }))
430
+ }
431
+ }
432
+ // while (data.length !== 0) {
433
+ // const amountToSlice =
434
+ // data.length >= MAX_MESSAGE_SIZE ? MAX_MESSAGE_SIZE : data.length
435
+ // const subArray = data.subarray(0, amountToSlice)
436
+ // data = data.subarray(amountToSlice, data.length)
437
+ // super.send(JSON.stringify({ chunk: subArray, id, size }))
438
+ // }
439
+ // backpressure-aware send loop with indexed chunks
440
+ const count = Math.ceil(size / MAX_MESSAGE_SIZE);
441
+ let index = 0;
442
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
443
+ const threshold = 4 * 1024 * 1024; // 4MB bufferedAmount threshold
444
+ const flags = (1 << 0) | ((sendData !== data ? 1 : 0) << 1);
445
+ for (const chunk of chunks(sendData)) {
446
+ // wait while channel is congested
447
+ // eslint-disable-next-line no-await-in-loop
448
+ while (
449
+ // @ts-ignore underlying channel is not part of public types
450
+ this._channel?.bufferedAmount > threshold) {
451
+ // if connection closed, abort
452
+ // eslint-disable-next-line no-await-in-loop
453
+ if (!this.connected)
454
+ return;
455
+ // eslint-disable-next-line no-await-in-loop
456
+ await sleep(10);
457
+ }
458
+ super.send(encodeFrame(id, size, index, count, chunk, flags));
459
+ index += 1;
460
+ }
461
+ }
462
+ /**
463
+ * send to peer
464
+ * @param data ArrayLike
465
+ * @param id custom id to listen to
466
+ */
467
+ send(data, id = crypto.randomUUID()) {
468
+ // send chuncks till ndata support for SCTP is added
469
+ // wraps data
470
+ this.#chunkit(data, id);
471
+ }
472
+ /**
473
+ * send to peer & wait for response
474
+ * @param data ArrayLike
475
+ * @param id custom id to listen to
476
+ */
477
+ request(data, id = crypto.randomUUID()) {
478
+ return new Promise((resolve, reject) => {
479
+ let timeout;
480
+ const onrequest = ({ data }) => {
481
+ clearTimeout(timeout);
482
+ resolve(data);
483
+ globalThis.pubsub.unsubscribe(id, onrequest);
484
+ };
485
+ timeout = setTimeout(() => {
486
+ globalThis.pubsub.unsubscribe(id, onrequest);
487
+ reject(`request for ${id} timed out`);
488
+ }, 30_000);
489
+ globalThis.pubsub.subscribe(id, onrequest);
490
+ this.send(data, id);
491
+ });
492
+ }
493
+ /**
494
+ * Get comprehensive network statistics from WebRTC
495
+ * @returns NetworkStats object with detailed metrics
496
+ */
497
+ async getNetworkStats() {
498
+ try {
499
+ const pc = this._pc;
500
+ if (!pc)
501
+ return null;
502
+ const stats = await pc.getStats();
503
+ const result = {
504
+ latency: null,
505
+ jitter: null,
506
+ bytesReceived: 0,
507
+ bytesSent: 0,
508
+ packetsLost: 0,
509
+ fractionLost: null,
510
+ inboundBitrate: null,
511
+ outboundBitrate: null,
512
+ availableOutgoingBitrate: null,
513
+ timestamp: Date.now()
514
+ };
515
+ let prevBytesReceived = 0;
516
+ let prevBytesSent = 0;
517
+ let prevTimestamp = 0;
518
+ stats.forEach((report) => {
519
+ // Latency from candidate pair
520
+ if (report.type === 'candidate-pair' && report.currentRoundTripTime) {
521
+ result.latency = Math.round(report.currentRoundTripTime * 1000);
522
+ }
523
+ // Inbound RTP stats
524
+ if (report.type === 'inbound-rtp') {
525
+ result.bytesReceived += report.bytesReceived || 0;
526
+ result.packetsLost += report.packetsLost || 0;
527
+ if (report.jitter) {
528
+ result.jitter = Math.round(report.jitter * 1000);
529
+ }
530
+ if (report.fractionLost) {
531
+ result.fractionLost = report.fractionLost;
532
+ }
533
+ }
534
+ // Outbound RTP stats
535
+ if (report.type === 'outbound-rtp') {
536
+ result.bytesSent += report.bytesSent || 0;
537
+ }
538
+ // Available bandwidth
539
+ if (report.type === 'remote-candidate' &&
540
+ report.availableOutgoingBitrate) {
541
+ result.availableOutgoingBitrate = Math.round(report.availableOutgoingBitrate);
542
+ }
543
+ });
544
+ return result;
545
+ }
546
+ catch (e) {
547
+ return null;
548
+ }
549
+ }
550
+ toJSON() {
551
+ return {
552
+ peerId: this.peerId,
553
+ channelName: this.channelName,
554
+ version: this.version,
555
+ bw: this.bw
556
+ };
557
+ }
558
+ }
559
+
560
+ // Simple CRC32 implementation
561
+ const crc32 = (data) => {
562
+ let crc = 0xffffffff;
563
+ for (let i = 0; i < data.length; i++) {
564
+ crc ^= data[i];
565
+ for (let j = 0; j < 8; j++) {
566
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
567
+ }
568
+ }
569
+ return (crc ^ 0xffffffff) >>> 0;
570
+ };
571
+ const debug = createDebugger('@netpeer/swarm/client');
572
+ class Client {
573
+ #peerId;
574
+ #connections = {};
575
+ #stars = {};
576
+ #starListeners = {};
577
+ #reinitLock = null;
578
+ #connectEvent = 'peer:connected';
579
+ #retryOptions = { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 30000 };
580
+ id;
581
+ networkVersion;
582
+ starsConfig;
583
+ socketClient;
584
+ messageSize = 262144;
585
+ version;
586
+ #messagesToHandle = {};
587
+ get peerId() {
588
+ return this.#peerId;
589
+ }
590
+ get connections() {
591
+ return { ...this.#connections };
592
+ }
593
+ get peers() {
594
+ return Object.entries(this.#connections);
595
+ }
596
+ getPeer(peerId) {
597
+ return this.#connections[peerId];
598
+ }
599
+ constructor(options) {
600
+ const { peerId, networkVersion, version, connectEvent, stars } = {
601
+ ...defaultOptions,
602
+ ...options
603
+ };
604
+ this.#peerId = peerId;
605
+ this.networkVersion = networkVersion;
606
+ this.version = version;
607
+ this.#connectEvent = connectEvent;
608
+ this.starsConfig = stars;
609
+ if (options?.retry)
610
+ this.#retryOptions = { ...this.#retryOptions, ...options.retry };
611
+ this._init();
612
+ }
613
+ /**
614
+ * Safely reinitialize the client (used after system resume/sleep).
615
+ * It closes existing connections and reconnects to configured stars.
616
+ */
617
+ async reinit() {
618
+ // avoid concurrent reinit runs
619
+ if (this.#reinitLock)
620
+ return this.#reinitLock;
621
+ this.#reinitLock = (async () => {
622
+ debug('reinit: start');
623
+ try {
624
+ await this.close();
625
+ this.#stars = {};
626
+ this.#connections = {};
627
+ for (const star of this.starsConfig) {
628
+ try {
629
+ await this.setupStar(star);
630
+ }
631
+ catch (e) {
632
+ // If last star fails and none connected, surface error
633
+ if (Object.keys(this.#stars).length === 0)
634
+ throw new Error(`No star available to connect`);
635
+ }
636
+ }
637
+ }
638
+ finally {
639
+ debug('reinit: done');
640
+ this.#reinitLock = null;
641
+ }
642
+ })();
643
+ return this.#reinitLock;
644
+ }
645
+ async setupStar(star) {
646
+ const { retries, factor, minTimeout, maxTimeout } = this.#retryOptions;
647
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
648
+ let attempt = 0;
649
+ let lastErr;
650
+ while (attempt <= retries) {
651
+ try {
652
+ const client = new SocketRequestClient(star, this.networkVersion);
653
+ this.#stars[star] = await client.init();
654
+ this.setupStarListeners(this.#stars[star], star);
655
+ this.#stars[star].send({
656
+ url: 'join',
657
+ params: { version: this.version, peerId: this.peerId }
658
+ });
659
+ globalThis.pubsub.publishVerbose('star:connected', star);
660
+ debug(`setupStar ${star} succeeded`);
661
+ return this.#stars[star];
662
+ }
663
+ catch (e) {
664
+ lastErr = e;
665
+ attempt += 1;
666
+ if (attempt > retries)
667
+ break;
668
+ const delay = Math.min(maxTimeout, Math.round(minTimeout * Math.pow(factor, attempt - 1)));
669
+ debug(`setupStar ${star} failed, retrying in ${delay}ms (attempt ${attempt})`);
670
+ // eslint-disable-next-line no-await-in-loop
671
+ await sleep(delay);
672
+ }
673
+ }
674
+ throw lastErr;
675
+ }
676
+ async _init() {
677
+ if (!globalThis.RTCPeerConnection)
678
+ globalThis.wrtc = (await import('./browser-AkMNdace-11JEjCcV.js').then(function (n) { return n.b; })).default;
679
+ for (const star of this.starsConfig) {
680
+ try {
681
+ await this.setupStar(star);
682
+ }
683
+ catch (e) {
684
+ if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1 &&
685
+ !this.socketClient)
686
+ throw new Error(`No star available to connect`);
687
+ }
688
+ }
689
+ if (globalThis.process?.versions?.node) {
690
+ process.on('SIGINT', async () => {
691
+ process.stdin.resume();
692
+ await this.close();
693
+ process.exit();
694
+ });
695
+ }
696
+ else {
697
+ globalThis.addEventListener('beforeunload', this.close.bind(this));
698
+ }
699
+ }
700
+ setupStarListeners(starConnection, starId) {
701
+ // create stable references to handlers so we can unsubscribe later
702
+ const onPeerJoined = (id) => this.#peerJoined(id, starConnection);
703
+ const onPeerLeft = (id) => this.#peerLeft(id, starConnection);
704
+ const onStarJoined = this.#starJoined;
705
+ const onStarLeft = this.#starLeft;
706
+ const onSignal = (message) => this.#inComingSignal(message, starConnection);
707
+ starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
708
+ starConnection.pubsub.subscribe('peer:left', onPeerLeft);
709
+ starConnection.pubsub.subscribe('star:joined', onStarJoined);
710
+ starConnection.pubsub.subscribe('star:left', onStarLeft);
711
+ starConnection.pubsub.subscribe('signal', onSignal);
712
+ this.#starListeners[starId] = [
713
+ { topic: 'peer:joined', handler: onPeerJoined },
714
+ { topic: 'peer:left', handler: onPeerLeft },
715
+ { topic: 'star:joined', handler: onStarJoined },
716
+ { topic: 'star:left', handler: onStarLeft },
717
+ { topic: 'signal', handler: onSignal }
718
+ ];
719
+ }
720
+ #starJoined = (id) => {
721
+ if (this.#stars[id]) {
722
+ this.#stars[id].close(0);
723
+ delete this.#stars[id];
724
+ }
725
+ console.log(`star ${id} joined`);
726
+ };
727
+ #starLeft = async (id) => {
728
+ if (this.#stars[id]) {
729
+ this.#stars[id].close(0);
730
+ delete this.#stars[id];
731
+ }
732
+ // if we lost all stars, try to reconnect to configured stars with backoff
733
+ if (Object.keys(this.#stars).length === 0) {
734
+ for (const star of this.starsConfig) {
735
+ try {
736
+ await this.setupStar(star);
737
+ // stop at first success
738
+ return;
739
+ }
740
+ catch (e) {
741
+ debug(`reconnect star ${star} failed: ${e.message || e}`);
742
+ if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1)
743
+ throw new Error(`No star available to connect`);
744
+ }
745
+ }
746
+ }
747
+ debug(`star ${id} left`);
748
+ };
749
+ #peerLeft = (peer, star) => {
750
+ const id = peer.peerId || peer;
751
+ if (this.#connections[id]) {
752
+ this.#connections[id].destroy();
753
+ delete this.#connections[id];
754
+ }
755
+ debug(`peer ${id} left`);
756
+ };
757
+ connect(peerId, star, initiator = true) {
758
+ if (this.#connections[peerId]) {
759
+ debug(`peer ${peerId} already connected`);
760
+ return;
761
+ }
762
+ if (this.#stars[star]?.connectionState() !== 'open') {
763
+ console.warn(`Star ${star} is not connected, cannot reconnect to peer ${peerId}`);
764
+ return;
765
+ }
766
+ this.#createRTCPeerConnection(peerId, star, this.version, initiator);
767
+ }
768
+ reconnect(peerId, star, initiator = false) {
769
+ delete this.#connections[peerId];
770
+ debug(`reconnecting to peer ${peerId}`);
771
+ return this.connect(peerId, star, initiator);
772
+ }
773
+ #createRTCPeerConnection = (peerId, star, version, initiator = false) => {
774
+ const peer = new Peer({
775
+ initiator: initiator,
776
+ from: this.peerId,
777
+ to: peerId,
778
+ version
779
+ });
780
+ peer.on('signal', (signal) => this.#peerSignal(peer, signal, star, this.version));
781
+ peer.on('connect', () => this.#peerConnect(peer));
782
+ peer.on('close', () => this.#peerClose(peer));
783
+ peer.on('data', (data) => this.#peerData(peer, data));
784
+ peer.on('error', (error) => this.#peerError(peer, error));
785
+ this.#connections[peerId] = peer;
786
+ };
787
+ #peerJoined = async ({ peerId, version }, star) => {
788
+ // check if peer rejoined before the previous connection closed
789
+ if (this.#connections[peerId]) {
790
+ this.#connections[peerId].destroy();
791
+ delete this.#connections[peerId];
792
+ }
793
+ if (this.peerId !== peerId)
794
+ this.#createRTCPeerConnection(peerId, star, version, true);
795
+ debug(`peer ${peerId} joined`);
796
+ };
797
+ #inComingSignal = async ({ from, signal, channelName, version }, star) => {
798
+ if (version !== this.version) {
799
+ console.warn(`${from} joined using the wrong version.\nexpected: ${this.version} but got:${version}`);
800
+ return;
801
+ }
802
+ if (from === this.peerId) {
803
+ console.warn(`${from} tried to connect to itself.`);
804
+ return;
805
+ }
806
+ let peer = this.#connections[from];
807
+ if (!peer) {
808
+ this.#createRTCPeerConnection(from, star, version);
809
+ peer = this.#connections[from];
810
+ }
811
+ if (peer.connected) {
812
+ debug(`peer ${from} already connected`);
813
+ return;
814
+ }
815
+ // peer.channels[channelName]
816
+ if (String(peer.channelName) !== String(channelName)) {
817
+ console.warn(`channelNames don't match: got ${peer.channelName}, expected: ${channelName}.`);
818
+ // Destroy the existing peer connection
819
+ // peer.destroy()
820
+ // delete this.#connections[from]
821
+ // // // Create a new peer connection with the correct configuration
822
+ // this.#createRTCPeerConnection(from, star, version, false)
823
+ // peer = this.#connections[from]
824
+ return;
825
+ }
826
+ peer.signal(signal);
827
+ };
828
+ #peerSignal = (peer, signal, star, version) => {
829
+ let client = this.#stars[star];
830
+ if (!client)
831
+ client = this.#stars[Object.keys(this.#stars)[0]];
832
+ client.send({
833
+ url: 'signal',
834
+ params: {
835
+ from: this.peerId,
836
+ to: peer.peerId,
837
+ channelName: peer.channelName,
838
+ version,
839
+ signal,
840
+ initiator: peer.initiator
841
+ }
842
+ });
843
+ };
844
+ #peerClose = (peer) => {
845
+ if (this.#connections[peer.peerId]) {
846
+ peer.destroy();
847
+ delete this.#connections[peer.peerId];
848
+ }
849
+ debug(`closed ${peer.peerId}'s connection`);
850
+ };
851
+ #peerConnect = (peer) => {
852
+ debug(`${peer.peerId} connected`);
853
+ globalThis.pubsub.publishVerbose(this.#connectEvent, peer.peerId);
854
+ };
855
+ #noticeMessage = (message, id, from, peer) => {
856
+ const dataOut = message instanceof Uint8Array
857
+ ? message
858
+ : new Uint8Array(Object.values(message));
859
+ if (globalThis.pubsub.hasSubscribers(id)) {
860
+ globalThis.pubsub.publish(id, {
861
+ data: dataOut,
862
+ id,
863
+ from,
864
+ peer
865
+ });
866
+ }
867
+ else {
868
+ globalThis.pubsub.publish('peer:data', {
869
+ data: dataOut,
870
+ id,
871
+ from,
872
+ peer
873
+ });
874
+ }
875
+ };
876
+ #peerData = (peer, data) => {
877
+ const tryJson = () => {
878
+ const parsed = JSON.parse(new TextDecoder().decode(data));
879
+ const { id, size, chunk, index, count } = parsed;
880
+ chunk ? Object.values(chunk).length : size;
881
+ return {
882
+ id,
883
+ size: Number(size),
884
+ index: Number(index ?? 0),
885
+ count: Number(count ?? 1),
886
+ chunk: new Uint8Array(Object.values(chunk)),
887
+ flags: 0,
888
+ crc: 0
889
+ };
890
+ };
891
+ const decodeBinary = () => {
892
+ let u8;
893
+ if (typeof data === 'string') {
894
+ // should not happen when sending binary, fallback to JSON
895
+ return tryJson();
896
+ }
897
+ else if (data instanceof ArrayBuffer) {
898
+ u8 = new Uint8Array(data);
899
+ }
900
+ else if (ArrayBuffer.isView(data)) {
901
+ const view = data;
902
+ const byteOffset = view.byteOffset || 0;
903
+ const byteLength = view.byteLength || data.length;
904
+ u8 = new Uint8Array(view.buffer, byteOffset, byteLength);
905
+ }
906
+ else if (data?.buffer) {
907
+ u8 = new Uint8Array(data.buffer);
908
+ }
909
+ else {
910
+ // last resort: attempt JSON
911
+ return tryJson();
912
+ }
913
+ const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
914
+ let offset = 0;
915
+ dv.getUint8(offset);
916
+ offset += 1;
917
+ const flags = dv.getUint8(offset);
918
+ offset += 1;
919
+ const size = dv.getUint32(offset, true);
920
+ offset += 4;
921
+ const index = dv.getUint32(offset, true);
922
+ offset += 4;
923
+ const count = dv.getUint32(offset, true);
924
+ offset += 4;
925
+ const expectedCrc = dv.getUint32(offset, true);
926
+ offset += 4;
927
+ const idLen = dv.getUint16(offset, true);
928
+ offset += 2;
929
+ const idBytes = u8.subarray(offset, offset + idLen);
930
+ offset += idLen;
931
+ const id = new TextDecoder().decode(idBytes);
932
+ const chunk = u8.subarray(offset);
933
+ return { id, size, index, count, chunk, flags, crc: expectedCrc };
934
+ };
935
+ const frame = decodeBinary();
936
+ peer.bw.down += frame.chunk.length;
937
+ // Single frame path: if compressed, inflate before publish
938
+ if (frame.count === 1) {
939
+ let payload = frame.chunk;
940
+ const compressed = Boolean(frame.flags & (1 << 1));
941
+ if (compressed) {
942
+ const actualCrc = crc32(payload);
943
+ if (actualCrc !== frame.crc) {
944
+ console.warn(`CRC mismatch: expected ${frame.crc}, got ${actualCrc}`);
945
+ }
946
+ try {
947
+ payload = inflate_1(payload);
948
+ }
949
+ catch (e) {
950
+ console.warn('inflate failed, passing compressed payload');
951
+ }
952
+ }
953
+ this.#noticeMessage(payload, frame.id, peer.peerId, peer);
954
+ return;
955
+ }
956
+ // Chunked message handling with indexed reassembly
957
+ if (!this.#messagesToHandle[frame.id] ||
958
+ Array.isArray(this.#messagesToHandle[frame.id])) {
959
+ this.#messagesToHandle[frame.id] = {
960
+ chunks: new Array(frame.count),
961
+ receivedBytes: 0,
962
+ expectedSize: Number(frame.size),
963
+ expectedCount: Number(frame.count)
964
+ };
965
+ }
966
+ const state = this.#messagesToHandle[frame.id];
967
+ // Verify CRC for this chunk
968
+ const actualCrc = crc32(frame.chunk);
969
+ if (actualCrc !== frame.crc) {
970
+ console.warn(`Chunk CRC mismatch for ${frame.id}[${frame.index}]: expected ${frame.crc}, got ${actualCrc}`);
971
+ }
972
+ state.chunks[frame.index] = frame.chunk;
973
+ state.receivedBytes += frame.chunk.length;
974
+ // If all chunks present and total size matches, reassemble
975
+ const allPresent = state.chunks.every((c) => c instanceof Uint8Array);
976
+ if (allPresent && state.receivedBytes === state.expectedSize) {
977
+ const result = new Uint8Array(state.expectedSize);
978
+ let offset2 = 0;
979
+ for (const c of state.chunks) {
980
+ result.set(c, offset2);
981
+ offset2 += c.length;
982
+ }
983
+ let payload = result;
984
+ const compressed = Boolean(frame.flags & (1 << 1));
985
+ if (compressed) {
986
+ try {
987
+ payload = inflate_1(result);
988
+ }
989
+ catch (e) {
990
+ console.warn('inflate failed, passing compressed payload');
991
+ }
992
+ }
993
+ this.#noticeMessage(payload, frame.id, peer.peerId, peer);
994
+ delete this.#messagesToHandle[frame.id];
995
+ }
996
+ };
997
+ #peerError = (peer, error) => {
998
+ console.warn(`Connection error: ${error.message}`);
999
+ peer.destroy();
1000
+ };
1001
+ async close() {
1002
+ for (const peerId in this.#connections) {
1003
+ const peer = this.#connections[peerId];
1004
+ if (peer) {
1005
+ peer.destroy();
1006
+ delete this.#connections[peerId];
1007
+ }
1008
+ }
1009
+ for (const star in this.#stars) {
1010
+ // unsubscribe handlers we registered earlier
1011
+ const listeners = this.#starListeners[star];
1012
+ if (listeners && listeners.length) {
1013
+ for (const { topic, handler } of listeners) {
1014
+ try {
1015
+ this.#stars[star].pubsub.unsubscribe(topic, handler);
1016
+ }
1017
+ catch (e) {
1018
+ // ignore
1019
+ }
1020
+ }
1021
+ }
1022
+ if (this.#stars[star].connectionState() === 'open') {
1023
+ await this.#stars[star].send({ url: 'leave', params: this.peerId });
1024
+ }
1025
+ }
1026
+ const peerClosers = Object.values(this.#connections).map((connection) => {
1027
+ try {
1028
+ // destroy() may be sync or return a promise
1029
+ return connection.destroy();
1030
+ }
1031
+ catch (e) {
1032
+ return undefined;
1033
+ }
1034
+ });
1035
+ const starClosers = Object.values(this.#stars).map((connection) => {
1036
+ try {
1037
+ return connection.close(0);
1038
+ }
1039
+ catch (e) {
1040
+ return undefined;
1041
+ }
1042
+ });
1043
+ return Promise.allSettled([...peerClosers, ...starClosers]);
1044
+ }
1045
+ }
1046
+
1047
+ export { Client as default };