@rljson/server 0.0.9 → 0.0.11

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.
package/dist/server.js CHANGED
@@ -1,949 +1,11 @@
1
- import { hshBuffer } from "@rljson/hash";
2
- import { Readable } from "node:stream";
1
+ import { BsPeerBridge, BsMulti, BsPeer, BsServer, BsMem } from "@rljson/bs";
3
2
  import { Db, Connector } from "@rljson/db";
4
3
  import { IoPeerBridge, IoMulti, IoPeer, IoServer, IoMem, SocketMock } from "@rljson/io";
5
4
  import { existsSync, mkdirSync, appendFileSync } from "node:fs";
6
5
  import { dirname } from "node:path";
6
+ import { NetworkManager } from "@rljson/network";
7
7
  import { syncEvents, Route } from "@rljson/rljson";
8
8
  import { syncEvents as syncEvents2 } from "@rljson/rljson";
9
- class BsMem {
10
- blobs = /* @__PURE__ */ new Map();
11
- /**
12
- * Convert content to Buffer
13
- * @param content - Content to convert (Buffer, string, or ReadableStream)
14
- */
15
- async toBuffer(content) {
16
- if (Buffer.isBuffer(content)) {
17
- return content;
18
- }
19
- if (typeof content === "string") {
20
- return Buffer.from(content, "utf8");
21
- }
22
- const reader = content.getReader();
23
- const chunks = [];
24
- while (true) {
25
- const { done, value } = await reader.read();
26
- if (done) break;
27
- chunks.push(value);
28
- }
29
- return Buffer.concat(chunks);
30
- }
31
- async setBlob(content) {
32
- const buffer = await this.toBuffer(content);
33
- const blobId = hshBuffer(buffer);
34
- const existing = this.blobs.get(blobId);
35
- if (existing) {
36
- return existing.properties;
37
- }
38
- const properties = {
39
- blobId,
40
- size: buffer.length,
41
- createdAt: /* @__PURE__ */ new Date()
42
- };
43
- this.blobs.set(blobId, {
44
- content: buffer,
45
- properties
46
- });
47
- return properties;
48
- }
49
- async getBlob(blobId, options) {
50
- const stored = this.blobs.get(blobId);
51
- if (!stored) {
52
- throw new Error(`Blob not found: ${blobId}`);
53
- }
54
- let content = stored.content;
55
- if (options?.range) {
56
- const { start, end } = options.range;
57
- content = stored.content.subarray(start, end);
58
- }
59
- return {
60
- content,
61
- properties: stored.properties
62
- };
63
- }
64
- async getBlobStream(blobId) {
65
- const stored = this.blobs.get(blobId);
66
- if (!stored) {
67
- throw new Error(`Blob not found: ${blobId}`);
68
- }
69
- const nodeStream = Readable.from(stored.content);
70
- return Readable.toWeb(nodeStream);
71
- }
72
- async deleteBlob(blobId) {
73
- const deleted = this.blobs.delete(blobId);
74
- if (!deleted) {
75
- throw new Error(`Blob not found: ${blobId}`);
76
- }
77
- }
78
- async blobExists(blobId) {
79
- return this.blobs.has(blobId);
80
- }
81
- async getBlobProperties(blobId) {
82
- const stored = this.blobs.get(blobId);
83
- if (!stored) {
84
- throw new Error(`Blob not found: ${blobId}`);
85
- }
86
- return stored.properties;
87
- }
88
- async listBlobs(options) {
89
- let blobs = Array.from(this.blobs.values()).map(
90
- (stored) => stored.properties
91
- );
92
- if (options?.prefix) {
93
- blobs = blobs.filter((blob) => blob.blobId.startsWith(options.prefix));
94
- }
95
- blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
96
- const maxResults = options?.maxResults ?? blobs.length;
97
- let startIndex = 0;
98
- if (options?.continuationToken) {
99
- const tokenIndex = blobs.findIndex(
100
- (blob) => blob.blobId === options.continuationToken
101
- );
102
- startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
103
- }
104
- const endIndex = Math.min(startIndex + maxResults, blobs.length);
105
- const pageBlobs = blobs.slice(startIndex, endIndex);
106
- const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
107
- return {
108
- blobs: pageBlobs,
109
- continuationToken
110
- };
111
- }
112
- async generateSignedUrl(blobId, expiresIn, permissions) {
113
- if (!this.blobs.has(blobId)) {
114
- throw new Error(`Blob not found: ${blobId}`);
115
- }
116
- const expires = Date.now() + expiresIn * 1e3;
117
- const perm = permissions ?? "read";
118
- return `mem://${blobId}?expires=${expires}&permissions=${perm}`;
119
- }
120
- /**
121
- * Clear all blobs from storage (useful for testing)
122
- */
123
- clear() {
124
- this.blobs.clear();
125
- }
126
- /**
127
- * Get the number of blobs in storage
128
- */
129
- get size() {
130
- return this.blobs.size;
131
- }
132
- }
133
- class BsPeer {
134
- constructor(_socket) {
135
- this._socket = _socket;
136
- }
137
- isOpen = false;
138
- // ...........................................................................
139
- /**
140
- * Initializes the Peer connection.
141
- */
142
- async init() {
143
- this._socket.on("connect", () => {
144
- this.isOpen = true;
145
- });
146
- this._socket.on("disconnect", () => {
147
- this.isOpen = false;
148
- });
149
- this._socket.connect();
150
- return new Promise((resolve) => {
151
- if (this._socket.connected) {
152
- this.isOpen = true;
153
- resolve();
154
- } else {
155
- this._socket.on("connect", () => {
156
- resolve();
157
- });
158
- }
159
- });
160
- }
161
- // ...........................................................................
162
- /**
163
- * Closes the Peer connection.
164
- */
165
- async close() {
166
- if (!this._socket.connected) return;
167
- return new Promise((resolve) => {
168
- this._socket.on("disconnect", () => {
169
- resolve();
170
- });
171
- this._socket.disconnect();
172
- });
173
- }
174
- // ...........................................................................
175
- /**
176
- * Returns a promise that resolves once the Peer connection is ready.
177
- */
178
- async isReady() {
179
- if (!!this._socket && this._socket.connected === true) this.isOpen = true;
180
- else this.isOpen = false;
181
- return !!this.isOpen ? Promise.resolve() : Promise.reject();
182
- }
183
- // ...........................................................................
184
- /**
185
- * Stores a blob from Buffer, string, or ReadableStream and returns properties.
186
- * @param content - The blob content to store
187
- * @returns Promise resolving to blob properties
188
- */
189
- setBlob(content) {
190
- return new Promise((resolve, reject) => {
191
- if (content instanceof ReadableStream) {
192
- const reader = content.getReader();
193
- const chunks = [];
194
- const readStream = async () => {
195
- try {
196
- while (true) {
197
- const { done, value } = await reader.read();
198
- if (done) break;
199
- chunks.push(value);
200
- }
201
- const totalLength = chunks.reduce(
202
- (sum, chunk) => sum + chunk.length,
203
- 0
204
- );
205
- const buffer = Buffer.concat(
206
- chunks.map((chunk) => Buffer.from(chunk)),
207
- totalLength
208
- );
209
- this._socket.emit(
210
- "setBlob",
211
- buffer,
212
- (error, result) => {
213
- if (error) reject(error);
214
- else resolve(result);
215
- }
216
- );
217
- } catch (err) {
218
- reject(err);
219
- }
220
- };
221
- readStream();
222
- } else {
223
- this._socket.emit(
224
- "setBlob",
225
- content,
226
- (error, result) => {
227
- if (error) reject(error);
228
- else resolve(result);
229
- }
230
- );
231
- }
232
- });
233
- }
234
- // ...........................................................................
235
- /**
236
- * Retrieves a blob by its ID as a Buffer.
237
- * @param blobId - The unique identifier of the blob
238
- * @param options - Download options
239
- * @returns Promise resolving to blob content and properties
240
- */
241
- getBlob(blobId, options) {
242
- return new Promise((resolve, reject) => {
243
- this._socket.emit(
244
- "getBlob",
245
- blobId,
246
- options,
247
- (error, result) => {
248
- if (error) reject(error);
249
- else resolve(result);
250
- }
251
- );
252
- });
253
- }
254
- // ...........................................................................
255
- /**
256
- * Retrieves a blob by its ID as a ReadableStream.
257
- * @param blobId - The unique identifier of the blob
258
- * @returns Promise resolving to readable stream
259
- */
260
- getBlobStream(blobId) {
261
- return new Promise((resolve, reject) => {
262
- this._socket.emit(
263
- "getBlobStream",
264
- blobId,
265
- (error, result) => {
266
- if (error) reject(error);
267
- else resolve(result);
268
- }
269
- );
270
- });
271
- }
272
- // ...........................................................................
273
- /**
274
- * Deletes a blob by its ID.
275
- * @param blobId - The unique identifier of the blob
276
- * @returns Promise that resolves when deletion is complete
277
- */
278
- deleteBlob(blobId) {
279
- return new Promise((resolve, reject) => {
280
- this._socket.emit("deleteBlob", blobId, (error) => {
281
- if (error) reject(error);
282
- else resolve();
283
- });
284
- });
285
- }
286
- // ...........................................................................
287
- /**
288
- * Checks if a blob exists.
289
- * @param blobId - The unique identifier of the blob
290
- * @returns Promise resolving to true if blob exists
291
- */
292
- blobExists(blobId) {
293
- return new Promise((resolve, reject) => {
294
- this._socket.emit(
295
- "blobExists",
296
- blobId,
297
- (error, exists) => {
298
- if (error) reject(error);
299
- else resolve(exists);
300
- }
301
- );
302
- });
303
- }
304
- // ...........................................................................
305
- /**
306
- * Gets blob properties (size, createdAt) without retrieving content.
307
- * @param blobId - The unique identifier of the blob
308
- * @returns Promise resolving to blob properties
309
- */
310
- getBlobProperties(blobId) {
311
- return new Promise((resolve, reject) => {
312
- this._socket.emit(
313
- "getBlobProperties",
314
- blobId,
315
- (error, result) => {
316
- if (error) reject(error);
317
- else resolve(result);
318
- }
319
- );
320
- });
321
- }
322
- // ...........................................................................
323
- /**
324
- * Lists all blobs with optional filtering and pagination.
325
- * @param options - Optional listing configuration
326
- * @returns Promise resolving to list of blobs
327
- */
328
- listBlobs(options) {
329
- return new Promise((resolve, reject) => {
330
- this._socket.emit(
331
- "listBlobs",
332
- options || {},
333
- (error, result) => {
334
- if (error) reject(error);
335
- else resolve(result);
336
- }
337
- );
338
- });
339
- }
340
- // ...........................................................................
341
- /**
342
- * Generates a signed URL for temporary blob access.
343
- * @param blobId - The unique identifier of the blob
344
- * @param expiresIn - Expiration time in seconds
345
- * @param permissions - Permissions for the URL
346
- * @returns Promise resolving to signed URL
347
- */
348
- generateSignedUrl(blobId, expiresIn, permissions) {
349
- return new Promise((resolve, reject) => {
350
- this._socket.emit(
351
- "generateSignedUrl",
352
- blobId,
353
- expiresIn,
354
- permissions,
355
- (error, url) => {
356
- if (error) reject(error);
357
- else resolve(url);
358
- }
359
- );
360
- });
361
- }
362
- }
363
- class PeerSocketMock {
364
- constructor(_bs) {
365
- this._bs = _bs;
366
- }
367
- _listenersMap = /* @__PURE__ */ new Map();
368
- connected = false;
369
- disconnected = true;
370
- // ............................................................................
371
- /**
372
- * Removes a specific listener for the specified event.
373
- * @param eventName - The event name
374
- * @param listener - The listener function to remove
375
- * @returns This socket instance for chaining
376
- */
377
- off(eventName, listener) {
378
- const listeners = this._listenersMap.get(eventName) || [];
379
- const index = listeners.indexOf(listener);
380
- if (index !== -1) {
381
- listeners.splice(index, 1);
382
- this._listenersMap.set(eventName, listeners);
383
- }
384
- return this;
385
- }
386
- // ............................................................................
387
- /**
388
- * Removes all listeners for the specified event, or all listeners if no event is specified.
389
- * @param eventName - Optional event name
390
- * @returns This socket instance for chaining
391
- */
392
- removeAllListeners(eventName) {
393
- if (eventName) {
394
- this._listenersMap.delete(eventName);
395
- } else {
396
- this._listenersMap.clear();
397
- }
398
- return this;
399
- }
400
- // ............................................................................
401
- /**
402
- * Registers an event listener for the specified event.
403
- * @param eventName - The event name
404
- * @param listener - The listener function to register
405
- * @returns This socket instance for chaining
406
- */
407
- on(eventName, listener) {
408
- if (!this._listenersMap.has(eventName)) {
409
- this._listenersMap.set(eventName, []);
410
- }
411
- this._listenersMap.get(eventName).push(listener);
412
- return this;
413
- }
414
- // ...........................................................................
415
- /**
416
- * Simulates a connection event.
417
- */
418
- connect() {
419
- this.connected = true;
420
- this.disconnected = false;
421
- const listeners = this._listenersMap.get("connect") || [];
422
- for (const cb of listeners) {
423
- cb({});
424
- }
425
- return this;
426
- }
427
- // ...........................................................................
428
- /**
429
- * Simulates a disconnection event.
430
- */
431
- disconnect() {
432
- this.connected = false;
433
- this.disconnected = true;
434
- const listeners = this._listenersMap.get("disconnect") || [];
435
- for (const cb of listeners) {
436
- cb({});
437
- }
438
- return this;
439
- }
440
- // ............................................................................
441
- /**
442
- * Emits an event, invoking the corresponding method on the Bs instance.
443
- * @param eventName - The event name
444
- * @param args - Event arguments
445
- * @returns True if the event was handled
446
- */
447
- emit(eventName, ...args) {
448
- const fn = this._bs[eventName];
449
- if (typeof fn !== "function") {
450
- throw new Error(`Event ${eventName.toString()} not supported`);
451
- }
452
- const cb = args[args.length - 1];
453
- fn.apply(this._bs, args.slice(0, -1)).then((result) => {
454
- cb(null, result);
455
- }).catch((err) => {
456
- cb(err);
457
- });
458
- return true;
459
- }
460
- }
461
- class BsMulti {
462
- constructor(_stores) {
463
- this._stores = _stores;
464
- }
465
- // ...........................................................................
466
- /**
467
- * Initializes the BsMulti by assigning IDs to all underlying Bs instances.
468
- * All underlying Bs instances must already be initialized.
469
- */
470
- async init() {
471
- for (let idx = 0; idx < this._stores.length; idx++) {
472
- this._stores[idx] = { ...this._stores[idx], id: `bs-${idx}` };
473
- }
474
- return Promise.resolve();
475
- }
476
- // ...........................................................................
477
- /**
478
- * Stores a blob in all writable Bs instances in parallel.
479
- * @param content - The blob content to store
480
- * @returns Promise resolving to blob properties from the first successful write
481
- */
482
- async setBlob(content) {
483
- if (this.writables.length === 0) {
484
- throw new Error("No writable Bs available");
485
- }
486
- const writes = this.writables.map(({ bs }) => bs.setBlob(content));
487
- const results = await Promise.all(writes);
488
- return results[0];
489
- }
490
- // ...........................................................................
491
- /**
492
- * Retrieves a blob from the highest priority readable Bs instance.
493
- * Hot-swaps the blob to all writable instances for caching.
494
- * @param blobId - The blob identifier
495
- * @param options - Download options
496
- * @returns Promise resolving to blob content and properties
497
- */
498
- async getBlob(blobId, options) {
499
- if (this.readables.length === 0) {
500
- throw new Error("No readable Bs available");
501
- }
502
- let result;
503
- let readFrom = "";
504
- const errors = [];
505
- for (const readable of this.readables) {
506
- try {
507
- result = await readable.bs.getBlob(blobId, options);
508
- readFrom = readable.id ?? "";
509
- break;
510
- } catch (e) {
511
- errors.push(e);
512
- continue;
513
- }
514
- }
515
- if (!result) {
516
- const notFoundErrors = errors.filter(
517
- (err) => err.message.includes("Blob not found")
518
- );
519
- if (notFoundErrors.length === errors.length) {
520
- throw new Error(`Blob not found: ${blobId}`);
521
- } else {
522
- throw errors[0];
523
- }
524
- }
525
- if (this.writables.length > 0) {
526
- const hotSwapWrites = this.writables.filter((writable) => writable.id !== readFrom).map(({ bs }) => bs.setBlob(result.content).catch(() => {
527
- }));
528
- await Promise.all(hotSwapWrites);
529
- }
530
- return result;
531
- }
532
- // ...........................................................................
533
- /**
534
- * Retrieves a blob as a ReadableStream from the highest priority readable Bs instance.
535
- * @param blobId - The blob identifier
536
- * @returns Promise resolving to a ReadableStream
537
- */
538
- async getBlobStream(blobId) {
539
- if (this.readables.length === 0) {
540
- throw new Error("No readable Bs available");
541
- }
542
- const errors = [];
543
- for (const readable of this.readables) {
544
- try {
545
- return await readable.bs.getBlobStream(blobId);
546
- } catch (e) {
547
- errors.push(e);
548
- continue;
549
- }
550
- }
551
- const notFoundErrors = errors.filter(
552
- (err) => err.message.includes("Blob not found")
553
- );
554
- if (notFoundErrors.length === errors.length) {
555
- throw new Error(`Blob not found: ${blobId}`);
556
- } else {
557
- throw errors[0];
558
- }
559
- }
560
- // ...........................................................................
561
- /**
562
- * Deletes a blob from all writable Bs instances in parallel.
563
- * @param blobId - The blob identifier
564
- */
565
- async deleteBlob(blobId) {
566
- if (this.writables.length === 0) {
567
- throw new Error("No writable Bs available");
568
- }
569
- const deletes = this.writables.map(({ bs }) => bs.deleteBlob(blobId));
570
- await Promise.all(deletes);
571
- }
572
- // ...........................................................................
573
- /**
574
- * Checks if a blob exists in any readable Bs instance.
575
- * @param blobId - The blob identifier
576
- * @returns Promise resolving to true if blob exists in any readable
577
- */
578
- async blobExists(blobId) {
579
- if (this.readables.length === 0) {
580
- throw new Error("No readable Bs available");
581
- }
582
- for (const readable of this.readables) {
583
- try {
584
- const exists = await readable.bs.blobExists(blobId);
585
- if (exists) {
586
- return true;
587
- }
588
- } catch {
589
- continue;
590
- }
591
- }
592
- return false;
593
- }
594
- // ...........................................................................
595
- /**
596
- * Gets blob properties from the highest priority readable Bs instance.
597
- * @param blobId - The blob identifier
598
- * @returns Promise resolving to blob properties
599
- */
600
- async getBlobProperties(blobId) {
601
- if (this.readables.length === 0) {
602
- throw new Error("No readable Bs available");
603
- }
604
- const errors = [];
605
- for (const readable of this.readables) {
606
- try {
607
- return await readable.bs.getBlobProperties(blobId);
608
- } catch (e) {
609
- errors.push(e);
610
- continue;
611
- }
612
- }
613
- const notFoundErrors = errors.filter(
614
- (err) => err.message.includes("Blob not found")
615
- );
616
- if (notFoundErrors.length === errors.length) {
617
- throw new Error(`Blob not found: ${blobId}`);
618
- } else {
619
- throw errors[0];
620
- }
621
- }
622
- // ...........................................................................
623
- /**
624
- * Lists blobs by merging results from all readable Bs instances.
625
- * Deduplicates by blobId (content-addressable).
626
- * @param options - Listing options
627
- * @returns Promise resolving to list of blobs
628
- */
629
- async listBlobs(options) {
630
- if (this.readables.length === 0) {
631
- throw new Error("No readable Bs available");
632
- }
633
- const blobMap = /* @__PURE__ */ new Map();
634
- for (const readable of this.readables) {
635
- try {
636
- let continuationToken2;
637
- do {
638
- const result = await readable.bs.listBlobs({
639
- prefix: options?.prefix,
640
- // Apply prefix filter during collection
641
- continuationToken: continuationToken2,
642
- maxResults: 1e3
643
- // Fetch in chunks from each store
644
- });
645
- for (const blob of result.blobs) {
646
- if (!blobMap.has(blob.blobId)) {
647
- blobMap.set(blob.blobId, blob);
648
- }
649
- }
650
- continuationToken2 = result.continuationToken;
651
- } while (continuationToken2);
652
- } catch {
653
- continue;
654
- }
655
- }
656
- const blobs = Array.from(blobMap.values());
657
- blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
658
- const maxResults = options?.maxResults ?? blobs.length;
659
- let startIndex = 0;
660
- if (options?.continuationToken) {
661
- const tokenIndex = blobs.findIndex(
662
- (blob) => blob.blobId === options.continuationToken
663
- );
664
- startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
665
- }
666
- const endIndex = Math.min(startIndex + maxResults, blobs.length);
667
- const pageBlobs = blobs.slice(startIndex, endIndex);
668
- const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
669
- return {
670
- blobs: pageBlobs,
671
- continuationToken
672
- };
673
- }
674
- // ...........................................................................
675
- /**
676
- * Generates a signed URL from the highest priority readable Bs instance.
677
- * @param blobId - The blob identifier
678
- * @param expiresIn - Expiration time in seconds
679
- * @param permissions - Access permissions
680
- * @returns Promise resolving to signed URL
681
- */
682
- async generateSignedUrl(blobId, expiresIn, permissions = "read") {
683
- if (this.readables.length === 0) {
684
- throw new Error("No readable Bs available");
685
- }
686
- const errors = [];
687
- for (const readable of this.readables) {
688
- try {
689
- return await readable.bs.generateSignedUrl(
690
- blobId,
691
- expiresIn,
692
- permissions
693
- );
694
- } catch (e) {
695
- errors.push(e);
696
- continue;
697
- }
698
- }
699
- const notFoundErrors = errors.filter(
700
- (err) => err.message.includes("Blob not found")
701
- );
702
- if (notFoundErrors.length === errors.length) {
703
- throw new Error(`Blob not found: ${blobId}`);
704
- } else {
705
- throw errors[0];
706
- }
707
- }
708
- // ...........................................................................
709
- /**
710
- * Gets the list of underlying readable Bs instances, sorted by priority.
711
- */
712
- get readables() {
713
- return this._stores.filter((store) => store.read).sort((a, b) => a.priority - b.priority);
714
- }
715
- // ...........................................................................
716
- /**
717
- * Gets the list of underlying writable Bs instances, sorted by priority.
718
- */
719
- get writables() {
720
- return this._stores.filter((store) => store.write).sort((a, b) => a.priority - b.priority);
721
- }
722
- // ...........................................................................
723
- /**
724
- * Example: Local cache (BsMem) + Remote server (BsPeer)
725
- */
726
- static example = async () => {
727
- const bsRemoteMem = new BsMem();
728
- const bsRemoteSocket = new PeerSocketMock(bsRemoteMem);
729
- const bsRemote = new BsPeer(bsRemoteSocket);
730
- await bsRemote.init();
731
- const bsLocal = new BsMem();
732
- const stores = [
733
- { bs: bsLocal, priority: 0, read: true, write: true },
734
- // Cache first
735
- { bs: bsRemote, priority: 1, read: true, write: false }
736
- // Remote fallback
737
- ];
738
- const bsMulti = new BsMulti(stores);
739
- await bsMulti.init();
740
- return bsMulti;
741
- };
742
- }
743
- class BsPeerBridge {
744
- constructor(_bs, _socket) {
745
- this._bs = _bs;
746
- this._socket = _socket;
747
- }
748
- _eventHandlers = /* @__PURE__ */ new Map();
749
- _handleConnectBound = this._handleConnect.bind(this);
750
- _handleDisconnectBound = this._handleDisconnect.bind(this);
751
- /**
752
- * Starts the bridge by setting up connection event handlers and
753
- * automatically registering all Bs methods.
754
- */
755
- start() {
756
- this._socket.on("connect", this._handleConnectBound);
757
- this._socket.on("disconnect", this._handleDisconnectBound);
758
- this._registerBsMethods();
759
- }
760
- /**
761
- * Stops the bridge by removing all event handlers.
762
- */
763
- stop() {
764
- this._socket.off("connect", this._handleConnectBound);
765
- this._socket.off("disconnect", this._handleDisconnectBound);
766
- for (const [eventName, handler] of this._eventHandlers) {
767
- this._socket.off(eventName, handler);
768
- }
769
- this._eventHandlers.clear();
770
- }
771
- /**
772
- * Automatically registers all Bs interface methods as socket event handlers.
773
- */
774
- _registerBsMethods() {
775
- const bsMethods = [
776
- "getBlob",
777
- "getBlobStream",
778
- "blobExists",
779
- "getBlobProperties",
780
- "listBlobs"
781
- ];
782
- for (const methodName of bsMethods) {
783
- this.registerEvent(methodName);
784
- }
785
- }
786
- /**
787
- * Registers a socket event to be translated to a Bs method call.
788
- * @param eventName - The socket event name (should match a Bs method name)
789
- * @param bsMethodName - (Optional) The Bs method name if different from eventName
790
- */
791
- registerEvent(eventName, bsMethodName) {
792
- const methodName = bsMethodName || eventName;
793
- const handler = (...args) => {
794
- const callback = args[args.length - 1];
795
- const methodArgs = args.slice(0, -1);
796
- const bsMethod = this._bs[methodName];
797
- if (typeof bsMethod !== "function") {
798
- const error = new Error(
799
- `Method "${methodName}" not found on Bs instance`
800
- );
801
- if (typeof callback === "function") {
802
- callback(error, null);
803
- }
804
- return;
805
- }
806
- bsMethod.apply(this._bs, methodArgs).then((result) => {
807
- if (typeof callback === "function") {
808
- callback(null, result);
809
- }
810
- }).catch((error) => {
811
- if (typeof callback === "function") {
812
- callback(error, null);
813
- }
814
- });
815
- };
816
- this._eventHandlers.set(eventName, handler);
817
- this._socket.on(eventName, handler);
818
- }
819
- /**
820
- * Registers multiple socket events at once.
821
- * @param eventNames - Array of event names to register
822
- */
823
- registerEvents(eventNames) {
824
- for (const eventName of eventNames) {
825
- this.registerEvent(eventName);
826
- }
827
- }
828
- /**
829
- * Unregisters a socket event handler.
830
- * @param eventName - The event name to unregister
831
- */
832
- unregisterEvent(eventName) {
833
- const handler = this._eventHandlers.get(eventName);
834
- if (handler) {
835
- this._socket.off(eventName, handler);
836
- this._eventHandlers.delete(eventName);
837
- }
838
- }
839
- /**
840
- * Emits a result back through the socket.
841
- * @param eventName - The event name to emit
842
- * @param data - The data to send
843
- */
844
- emitToSocket(eventName, ...data) {
845
- this._socket.emit(eventName, ...data);
846
- }
847
- /**
848
- * Calls a Bs method directly and emits the result through the socket.
849
- * @param bsMethodName - The Bs method to call
850
- * @param socketEventName - The socket event to emit with the result
851
- * @param args - Arguments to pass to the Bs method
852
- */
853
- async callBsAndEmit(bsMethodName, socketEventName, ...args) {
854
- try {
855
- const bsMethod = this._bs[bsMethodName];
856
- if (typeof bsMethod !== "function") {
857
- throw new Error(`Method "${bsMethodName}" not found on Bs instance`);
858
- }
859
- const result = await bsMethod.apply(this._bs, args);
860
- this._socket.emit(socketEventName, null, result);
861
- } catch (error) {
862
- this._socket.emit(socketEventName, error, null);
863
- }
864
- }
865
- /* v8 ignore next -- @preserve */
866
- _handleConnect() {
867
- }
868
- /* v8 ignore next -- @preserve */
869
- _handleDisconnect() {
870
- }
871
- /**
872
- * Gets the current socket instance.
873
- */
874
- get socket() {
875
- return this._socket;
876
- }
877
- /**
878
- * Gets the current Bs instance.
879
- */
880
- get bs() {
881
- return this._bs;
882
- }
883
- /**
884
- * Returns whether the socket is currently connected.
885
- */
886
- get isConnected() {
887
- return this._socket.connected;
888
- }
889
- }
890
- class BsServer {
891
- constructor(_bs) {
892
- this._bs = _bs;
893
- }
894
- _sockets = [];
895
- // ...........................................................................
896
- /**
897
- * Adds a socket to the BsServer instance.
898
- * @param socket - The socket to add.
899
- */
900
- async addSocket(socket) {
901
- await this._addTransportLayer(socket);
902
- this._sockets.push(socket);
903
- }
904
- // ...........................................................................
905
- /**
906
- * Removes a socket from the BsServer instance.
907
- * @param socket - The socket to remove.
908
- */
909
- removeSocket(socket) {
910
- this._sockets = this._sockets.filter((s) => s !== socket);
911
- }
912
- // ...........................................................................
913
- /**
914
- * Adds a transport layer to the given socket.
915
- * @param socket - The socket to add the transport layer to.
916
- */
917
- async _addTransportLayer(socket) {
918
- const methods = this._generateTransportLayer(this._bs);
919
- for (const [key, fn] of Object.entries(methods)) {
920
- socket.on(key, (...args) => {
921
- const cb = args[args.length - 1];
922
- fn.apply(this, args.slice(0, -1)).then((result) => {
923
- cb(null, result);
924
- }).catch((err) => {
925
- cb(err);
926
- });
927
- });
928
- }
929
- }
930
- // ...........................................................................
931
- /**
932
- * Generates a transport layer object for the given Bs instance.
933
- * @param bs - The Bs instance to generate the transport layer for.
934
- * @returns An object containing methods that correspond to the Bs interface.
935
- */
936
- _generateTransportLayer = (bs) => ({
937
- setBlob: (content) => bs.setBlob(content),
938
- getBlob: (blobId, options) => bs.getBlob(blobId, options),
939
- getBlobStream: (blobId) => bs.getBlobStream(blobId),
940
- deleteBlob: (blobId) => bs.deleteBlob(blobId),
941
- blobExists: (blobId) => bs.blobExists(blobId),
942
- getBlobProperties: (blobId) => bs.getBlobProperties(blobId),
943
- listBlobs: (options) => bs.listBlobs(options),
944
- generateSignedUrl: (blobId, expiresIn, permissions) => bs.generateSignedUrl(blobId, expiresIn, permissions)
945
- });
946
- }
947
9
  class BaseNode {
948
10
  constructor(_localIo) {
949
11
  this._localIo = _localIo;
@@ -1159,6 +221,11 @@ class Client extends BaseNode {
1159
221
  _syncConfig;
1160
222
  _clientIdentity;
1161
223
  _peerInitTimeoutMs;
224
+ // Connection state
225
+ _isConnected = true;
226
+ _disconnectCallbacks = [];
227
+ _reconnectCallbacks = [];
228
+ _connectionCleanup;
1162
229
  /**
1163
230
  * Initializes Io and Bs multis and their peer bridges.
1164
231
  * @returns The initialized Io implementation.
@@ -1171,6 +238,7 @@ class Client extends BaseNode {
1171
238
  if (this._route) {
1172
239
  this._setupDbAndConnector();
1173
240
  }
241
+ this._registerConnectionHandlers();
1174
242
  await this.ready();
1175
243
  this._logger.info("Client", "Client initialized successfully", {
1176
244
  hasRoute: !!this._route,
@@ -1196,6 +264,12 @@ class Client extends BaseNode {
1196
264
  */
1197
265
  async tearDown() {
1198
266
  this._logger.info("Client", "Tearing down client");
267
+ if (this._connectionCleanup) {
268
+ this._connectionCleanup();
269
+ this._connectionCleanup = void 0;
270
+ }
271
+ this._disconnectCallbacks = [];
272
+ this._reconnectCallbacks = [];
1199
273
  if (this._ioMulti && this._ioMulti.isOpen) {
1200
274
  this._ioMulti.close();
1201
275
  }
@@ -1244,6 +318,29 @@ class Client extends BaseNode {
1244
318
  get logger() {
1245
319
  return this._logger;
1246
320
  }
321
+ /**
322
+ * Whether the client is currently connected to the server.
323
+ * Tracks socket-level connection state via `disconnect` and `connect` events.
324
+ */
325
+ get isConnected() {
326
+ return this._isConnected;
327
+ }
328
+ /**
329
+ * Registers a callback that fires when the socket disconnects.
330
+ * The callback receives the disconnect reason string.
331
+ * @param callback - Invoked with the disconnect reason
332
+ */
333
+ onDisconnect(callback) {
334
+ this._disconnectCallbacks.push(callback);
335
+ }
336
+ /**
337
+ * Registers a callback that fires when the socket reconnects
338
+ * after a previous disconnect.
339
+ * @param callback - Invoked on reconnection
340
+ */
341
+ onReconnect(callback) {
342
+ this._reconnectCallbacks.push(callback);
343
+ }
1247
344
  /**
1248
345
  * Creates Db and Connector from the route and IoMulti.
1249
346
  * Called during init() when a route was provided.
@@ -1263,6 +360,44 @@ class Client extends BaseNode {
1263
360
  );
1264
361
  this._logger.info("Client", "Db and Connector created");
1265
362
  }
363
+ /**
364
+ * Registers socket-level disconnect/connect listeners.
365
+ * Logs state transitions and invokes registered callbacks.
366
+ * The `connect` callback only fires on RE-connections (not the initial connect).
367
+ */
368
+ _registerConnectionHandlers() {
369
+ const sockets = normalizeSocketBundle(this._socketToServer);
370
+ const socket = sockets.ioUp;
371
+ const disconnectHandler = (...args) => {
372
+ const reason = typeof args[0] === "string" ? args[0] : "unknown";
373
+ this._isConnected = false;
374
+ this._logger.warn("Client", "Disconnected from server", { reason });
375
+ for (const cb of this._disconnectCallbacks) {
376
+ try {
377
+ cb(reason);
378
+ } catch {
379
+ }
380
+ }
381
+ };
382
+ const reconnectHandler = () => {
383
+ if (!this._isConnected) {
384
+ this._isConnected = true;
385
+ this._logger.info("Client", "Reconnected to server");
386
+ for (const cb of this._reconnectCallbacks) {
387
+ try {
388
+ cb();
389
+ } catch {
390
+ }
391
+ }
392
+ }
393
+ };
394
+ socket.on("disconnect", disconnectHandler);
395
+ socket.on("connect", reconnectHandler);
396
+ this._connectionCleanup = () => {
397
+ socket.off("disconnect", disconnectHandler);
398
+ socket.off("connect", reconnectHandler);
399
+ };
400
+ }
1266
401
  /**
1267
402
  * Builds the Io multi with local and peer layers.
1268
403
  */
@@ -2212,6 +1347,280 @@ class Server extends BaseNode {
2212
1347
  return new Server(route, io, bs).addSocket(socket);
2213
1348
  }
2214
1349
  }
1350
+ class Node {
1351
+ constructor(_config, _deps) {
1352
+ this._config = _config;
1353
+ this._deps = _deps;
1354
+ this._logger = _config.logger ?? noopLogger;
1355
+ const networkConfig = {
1356
+ domain: _config.domain,
1357
+ port: _config.port,
1358
+ identityDir: _config.identityDir,
1359
+ broadcast: _config.network?.broadcast ?? {
1360
+ enabled: true,
1361
+ port: 41234
1362
+ },
1363
+ cloud: _config.network?.cloud ?? { enabled: false, endpoint: "" },
1364
+ static: _config.network?.static ?? {},
1365
+ probing: _config.network?.probing ?? { enabled: true }
1366
+ };
1367
+ this._networkManager = new NetworkManager(
1368
+ networkConfig,
1369
+ _deps.networkManagerOptions
1370
+ );
1371
+ }
1372
+ _networkManager;
1373
+ _server;
1374
+ _client;
1375
+ _hubTransport;
1376
+ _clientSocket;
1377
+ _agentHandle;
1378
+ _ioMem;
1379
+ _bsMem;
1380
+ _role = "unassigned";
1381
+ _running = false;
1382
+ _transitioning;
1383
+ _listeners = /* @__PURE__ */ new Map();
1384
+ _logger;
1385
+ // .........................................................................
1386
+ // Lifecycle
1387
+ // .........................................................................
1388
+ /**
1389
+ * Start the node. Begins network discovery and role assignment.
1390
+ */
1391
+ async start() {
1392
+ if (this._running) return;
1393
+ this._ioMem = new IoMem();
1394
+ await this._ioMem.init();
1395
+ await this._ioMem.isReady();
1396
+ this._bsMem = new BsMem();
1397
+ this._running = true;
1398
+ this._networkManager.on("role-changed", this._onRoleChanged);
1399
+ await this._networkManager.start();
1400
+ }
1401
+ /**
1402
+ * Stop the node. Tears down Server/Client and network discovery.
1403
+ */
1404
+ async stop() {
1405
+ if (!this._running) return;
1406
+ this._running = false;
1407
+ this._networkManager.off("role-changed", this._onRoleChanged);
1408
+ if (this._transitioning) {
1409
+ await this._transitioning;
1410
+ this._transitioning = void 0;
1411
+ }
1412
+ await this._tearDownCurrentRole();
1413
+ await this._networkManager.stop();
1414
+ this._role = "unassigned";
1415
+ this._emit("stopped");
1416
+ }
1417
+ // .........................................................................
1418
+ // State
1419
+ // .........................................................................
1420
+ /** This node's current role. */
1421
+ get role() {
1422
+ return this._role;
1423
+ }
1424
+ /** Current network topology snapshot. */
1425
+ get topology() {
1426
+ return this._networkManager.getTopology();
1427
+ }
1428
+ /** The Io instance (from Server or Client), or undefined if unassigned. */
1429
+ get io() {
1430
+ return this._server?.io ?? this._client?.io;
1431
+ }
1432
+ /** The Bs instance (from Server or Client), or undefined if unassigned. */
1433
+ get bs() {
1434
+ return this._server?.bs ?? this._client?.bs;
1435
+ }
1436
+ /** The Server instance when this node is the hub, or undefined. */
1437
+ get server() {
1438
+ return this._server;
1439
+ }
1440
+ /** The Client instance when this node is a client, or undefined. */
1441
+ get client() {
1442
+ return this._client;
1443
+ }
1444
+ /** The socket used to connect to the hub (defined when role is `'client'`). */
1445
+ get socket() {
1446
+ return this._clientSocket;
1447
+ }
1448
+ /** Whether the node is currently running. */
1449
+ get isRunning() {
1450
+ return this._running;
1451
+ }
1452
+ /** The underlying NetworkManager. */
1453
+ get networkManager() {
1454
+ return this._networkManager;
1455
+ }
1456
+ // .........................................................................
1457
+ // Events
1458
+ // .........................................................................
1459
+ /**
1460
+ * Subscribe to node events.
1461
+ * @param event - Event name
1462
+ * @param cb - Callback
1463
+ */
1464
+ on(event, cb) {
1465
+ let set = this._listeners.get(event);
1466
+ if (!set) {
1467
+ set = /* @__PURE__ */ new Set();
1468
+ this._listeners.set(event, set);
1469
+ }
1470
+ set.add(cb);
1471
+ }
1472
+ /**
1473
+ * Unsubscribe from node events.
1474
+ * @param event - Event name
1475
+ * @param cb - Callback
1476
+ */
1477
+ off(event, cb) {
1478
+ const set = this._listeners.get(event);
1479
+ if (!set) return;
1480
+ set.delete(cb);
1481
+ }
1482
+ // .........................................................................
1483
+ // Role transitions
1484
+ // .........................................................................
1485
+ _onRoleChanged = (event) => {
1486
+ if (!this._running) return;
1487
+ const { current } = event;
1488
+ if (current === this._role) return;
1489
+ const prev = this._transitioning ?? Promise.resolve();
1490
+ this._transitioning = prev.then(() => this._performTransition(event));
1491
+ };
1492
+ async _performTransition(event) {
1493
+ if (!this._running) return;
1494
+ const { current } = event;
1495
+ if (current === this._role) return;
1496
+ this._logger.info("Node", `Role changing: ${this._role} → ${current}`);
1497
+ await this._tearDownCurrentRole();
1498
+ this._role = current;
1499
+ this._emit("role-changed", event);
1500
+ switch (current) {
1501
+ case "hub":
1502
+ await this._becomeHub();
1503
+ break;
1504
+ case "client":
1505
+ await this._becomeClient();
1506
+ break;
1507
+ }
1508
+ }
1509
+ async _becomeHub() {
1510
+ await this._ioMem.init();
1511
+ await this._ioMem.isReady();
1512
+ this._server = new Server(this._config.route, this._ioMem, this._bsMem, {
1513
+ ...this._config.serverOptions,
1514
+ logger: this._logger
1515
+ });
1516
+ await this._server.init();
1517
+ try {
1518
+ this._hubTransport = await this._deps.createHubTransport(
1519
+ this._config.port
1520
+ );
1521
+ this._hubTransport.onConnection(async (socket) => {
1522
+ if (!this._server) return;
1523
+ await this._server.addSocket(socket);
1524
+ });
1525
+ this._logger.info("Node", "Now hub — accepting connections");
1526
+ } catch (err) {
1527
+ this._logger.error(
1528
+ "Node",
1529
+ `Hub transport failed (no incoming connections): ${err}`
1530
+ );
1531
+ }
1532
+ const ctx = { role: "hub", server: this._server };
1533
+ this._emit("ready", ctx);
1534
+ await this._startAgent(ctx);
1535
+ }
1536
+ async _becomeClient() {
1537
+ const topology = this._networkManager.getTopology();
1538
+ const hubAddress = topology.hubAddress;
1539
+ if (!hubAddress) {
1540
+ this._logger.warn(
1541
+ "Node",
1542
+ "Cannot become client: no hub address in topology"
1543
+ );
1544
+ return;
1545
+ }
1546
+ try {
1547
+ this._clientSocket = await this._deps.createClientTransport(hubAddress);
1548
+ } catch (err) {
1549
+ this._logger.error(
1550
+ "Node",
1551
+ `Client transport to ${hubAddress} failed: ${err}`
1552
+ );
1553
+ return;
1554
+ }
1555
+ await this._ioMem.init();
1556
+ await this._ioMem.isReady();
1557
+ this._client = new Client(
1558
+ this._clientSocket,
1559
+ this._ioMem,
1560
+ this._bsMem,
1561
+ this._config.route,
1562
+ {
1563
+ ...this._config.clientOptions,
1564
+ logger: this._logger
1565
+ }
1566
+ );
1567
+ await this._client.init();
1568
+ this._logger.info("Node", `Now client — connected to hub ${hubAddress}`);
1569
+ const ctx = {
1570
+ role: "client",
1571
+ client: this._client,
1572
+ socket: this._clientSocket
1573
+ };
1574
+ this._emit("ready", ctx);
1575
+ await this._startAgent(ctx);
1576
+ }
1577
+ // .........................................................................
1578
+ // Internal
1579
+ // .........................................................................
1580
+ async _tearDownCurrentRole() {
1581
+ await this._stopAgent();
1582
+ if (this._server) {
1583
+ await this._server.tearDown();
1584
+ this._server = void 0;
1585
+ }
1586
+ if (this._hubTransport) {
1587
+ await this._hubTransport.close();
1588
+ this._hubTransport = void 0;
1589
+ }
1590
+ if (this._client) {
1591
+ await this._client.tearDown();
1592
+ this._client = void 0;
1593
+ }
1594
+ this._clientSocket = void 0;
1595
+ }
1596
+ async _startAgent(ctx) {
1597
+ if (this._deps.createAgent) {
1598
+ try {
1599
+ this._agentHandle = await this._deps.createAgent(ctx);
1600
+ } catch (err) {
1601
+ this._logger.error("Node", `createAgent failed: ${err}`);
1602
+ }
1603
+ }
1604
+ }
1605
+ async _stopAgent() {
1606
+ if (this._agentHandle) {
1607
+ const handle = this._agentHandle;
1608
+ this._agentHandle = void 0;
1609
+ try {
1610
+ await handle.stop();
1611
+ } catch (err) {
1612
+ this._logger.error("Node", `Agent stop failed: ${err}`);
1613
+ }
1614
+ }
1615
+ }
1616
+ _emit(event, ...args) {
1617
+ const set = this._listeners.get(event);
1618
+ if (!set) return;
1619
+ for (const cb of set) {
1620
+ cb(...args);
1621
+ }
1622
+ }
1623
+ }
2215
1624
  class SocketIoBridge {
2216
1625
  constructor(_socket) {
2217
1626
  this._socket = _socket;
@@ -2271,6 +1680,7 @@ export {
2271
1680
  ConsoleLogger,
2272
1681
  FileLogger,
2273
1682
  FilteredLogger,
1683
+ Node,
2274
1684
  NoopLogger,
2275
1685
  Server,
2276
1686
  SocketIoBridge,