@rljson/server 0.0.3

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 ADDED
@@ -0,0 +1,1403 @@
1
+ import { hshBuffer } from "@rljson/hash";
2
+ import { Readable } from "node:stream";
3
+ import { IoPeerBridge, IoMulti, IoPeer, IoServer, IoMem, SocketMock } from "@rljson/io";
4
+ import { Db } from "@rljson/db";
5
+ import { Route } from "@rljson/rljson";
6
+ class BsMem {
7
+ blobs = /* @__PURE__ */ new Map();
8
+ /**
9
+ * Convert content to Buffer
10
+ * @param content - Content to convert (Buffer, string, or ReadableStream)
11
+ */
12
+ async toBuffer(content) {
13
+ if (Buffer.isBuffer(content)) {
14
+ return content;
15
+ }
16
+ if (typeof content === "string") {
17
+ return Buffer.from(content, "utf8");
18
+ }
19
+ const reader = content.getReader();
20
+ const chunks = [];
21
+ while (true) {
22
+ const { done, value } = await reader.read();
23
+ if (done) break;
24
+ chunks.push(value);
25
+ }
26
+ return Buffer.concat(chunks);
27
+ }
28
+ async setBlob(content) {
29
+ const buffer = await this.toBuffer(content);
30
+ const blobId = hshBuffer(buffer);
31
+ const existing = this.blobs.get(blobId);
32
+ if (existing) {
33
+ return existing.properties;
34
+ }
35
+ const properties = {
36
+ blobId,
37
+ size: buffer.length,
38
+ createdAt: /* @__PURE__ */ new Date()
39
+ };
40
+ this.blobs.set(blobId, {
41
+ content: buffer,
42
+ properties
43
+ });
44
+ return properties;
45
+ }
46
+ async getBlob(blobId, options) {
47
+ const stored = this.blobs.get(blobId);
48
+ if (!stored) {
49
+ throw new Error(`Blob not found: ${blobId}`);
50
+ }
51
+ let content = stored.content;
52
+ if (options?.range) {
53
+ const { start, end } = options.range;
54
+ content = stored.content.subarray(start, end);
55
+ }
56
+ return {
57
+ content,
58
+ properties: stored.properties
59
+ };
60
+ }
61
+ async getBlobStream(blobId) {
62
+ const stored = this.blobs.get(blobId);
63
+ if (!stored) {
64
+ throw new Error(`Blob not found: ${blobId}`);
65
+ }
66
+ const nodeStream = Readable.from(stored.content);
67
+ return Readable.toWeb(nodeStream);
68
+ }
69
+ async deleteBlob(blobId) {
70
+ const deleted = this.blobs.delete(blobId);
71
+ if (!deleted) {
72
+ throw new Error(`Blob not found: ${blobId}`);
73
+ }
74
+ }
75
+ async blobExists(blobId) {
76
+ return this.blobs.has(blobId);
77
+ }
78
+ async getBlobProperties(blobId) {
79
+ const stored = this.blobs.get(blobId);
80
+ if (!stored) {
81
+ throw new Error(`Blob not found: ${blobId}`);
82
+ }
83
+ return stored.properties;
84
+ }
85
+ async listBlobs(options) {
86
+ let blobs = Array.from(this.blobs.values()).map(
87
+ (stored) => stored.properties
88
+ );
89
+ if (options?.prefix) {
90
+ blobs = blobs.filter((blob) => blob.blobId.startsWith(options.prefix));
91
+ }
92
+ blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
93
+ const maxResults = options?.maxResults ?? blobs.length;
94
+ let startIndex = 0;
95
+ if (options?.continuationToken) {
96
+ const tokenIndex = blobs.findIndex(
97
+ (blob) => blob.blobId === options.continuationToken
98
+ );
99
+ startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
100
+ }
101
+ const endIndex = Math.min(startIndex + maxResults, blobs.length);
102
+ const pageBlobs = blobs.slice(startIndex, endIndex);
103
+ const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
104
+ return {
105
+ blobs: pageBlobs,
106
+ continuationToken
107
+ };
108
+ }
109
+ async generateSignedUrl(blobId, expiresIn, permissions) {
110
+ if (!this.blobs.has(blobId)) {
111
+ throw new Error(`Blob not found: ${blobId}`);
112
+ }
113
+ const expires = Date.now() + expiresIn * 1e3;
114
+ const perm = permissions ?? "read";
115
+ return `mem://${blobId}?expires=${expires}&permissions=${perm}`;
116
+ }
117
+ /**
118
+ * Clear all blobs from storage (useful for testing)
119
+ */
120
+ clear() {
121
+ this.blobs.clear();
122
+ }
123
+ /**
124
+ * Get the number of blobs in storage
125
+ */
126
+ get size() {
127
+ return this.blobs.size;
128
+ }
129
+ }
130
+ class BsPeer {
131
+ constructor(_socket) {
132
+ this._socket = _socket;
133
+ }
134
+ isOpen = false;
135
+ // ...........................................................................
136
+ /**
137
+ * Initializes the Peer connection.
138
+ */
139
+ async init() {
140
+ this._socket.on("connect", () => {
141
+ this.isOpen = true;
142
+ });
143
+ this._socket.on("disconnect", () => {
144
+ this.isOpen = false;
145
+ });
146
+ this._socket.connect();
147
+ return new Promise((resolve) => {
148
+ if (this._socket.connected) {
149
+ this.isOpen = true;
150
+ resolve();
151
+ } else {
152
+ this._socket.on("connect", () => {
153
+ resolve();
154
+ });
155
+ }
156
+ });
157
+ }
158
+ // ...........................................................................
159
+ /**
160
+ * Closes the Peer connection.
161
+ */
162
+ async close() {
163
+ if (!this._socket.connected) return;
164
+ return new Promise((resolve) => {
165
+ this._socket.on("disconnect", () => {
166
+ resolve();
167
+ });
168
+ this._socket.disconnect();
169
+ });
170
+ }
171
+ // ...........................................................................
172
+ /**
173
+ * Returns a promise that resolves once the Peer connection is ready.
174
+ */
175
+ async isReady() {
176
+ if (!!this._socket && this._socket.connected === true) this.isOpen = true;
177
+ else this.isOpen = false;
178
+ return !!this.isOpen ? Promise.resolve() : Promise.reject();
179
+ }
180
+ // ...........................................................................
181
+ /**
182
+ * Stores a blob from Buffer, string, or ReadableStream and returns properties.
183
+ * @param content - The blob content to store
184
+ * @returns Promise resolving to blob properties
185
+ */
186
+ setBlob(content) {
187
+ return new Promise((resolve, reject) => {
188
+ if (content instanceof ReadableStream) {
189
+ const reader = content.getReader();
190
+ const chunks = [];
191
+ const readStream = async () => {
192
+ try {
193
+ while (true) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ chunks.push(value);
197
+ }
198
+ const totalLength = chunks.reduce(
199
+ (sum, chunk) => sum + chunk.length,
200
+ 0
201
+ );
202
+ const buffer = Buffer.concat(
203
+ chunks.map((chunk) => Buffer.from(chunk)),
204
+ totalLength
205
+ );
206
+ this._socket.emit(
207
+ "setBlob",
208
+ buffer,
209
+ (error, result) => {
210
+ if (error) reject(error);
211
+ else resolve(result);
212
+ }
213
+ );
214
+ } catch (err) {
215
+ reject(err);
216
+ }
217
+ };
218
+ readStream();
219
+ } else {
220
+ this._socket.emit(
221
+ "setBlob",
222
+ content,
223
+ (error, result) => {
224
+ if (error) reject(error);
225
+ else resolve(result);
226
+ }
227
+ );
228
+ }
229
+ });
230
+ }
231
+ // ...........................................................................
232
+ /**
233
+ * Retrieves a blob by its ID as a Buffer.
234
+ * @param blobId - The unique identifier of the blob
235
+ * @param options - Download options
236
+ * @returns Promise resolving to blob content and properties
237
+ */
238
+ getBlob(blobId, options) {
239
+ return new Promise((resolve, reject) => {
240
+ this._socket.emit(
241
+ "getBlob",
242
+ blobId,
243
+ options,
244
+ (error, result) => {
245
+ if (error) reject(error);
246
+ else resolve(result);
247
+ }
248
+ );
249
+ });
250
+ }
251
+ // ...........................................................................
252
+ /**
253
+ * Retrieves a blob by its ID as a ReadableStream.
254
+ * @param blobId - The unique identifier of the blob
255
+ * @returns Promise resolving to readable stream
256
+ */
257
+ getBlobStream(blobId) {
258
+ return new Promise((resolve, reject) => {
259
+ this._socket.emit(
260
+ "getBlobStream",
261
+ blobId,
262
+ (error, result) => {
263
+ if (error) reject(error);
264
+ else resolve(result);
265
+ }
266
+ );
267
+ });
268
+ }
269
+ // ...........................................................................
270
+ /**
271
+ * Deletes a blob by its ID.
272
+ * @param blobId - The unique identifier of the blob
273
+ * @returns Promise that resolves when deletion is complete
274
+ */
275
+ deleteBlob(blobId) {
276
+ return new Promise((resolve, reject) => {
277
+ this._socket.emit("deleteBlob", blobId, (error) => {
278
+ if (error) reject(error);
279
+ else resolve();
280
+ });
281
+ });
282
+ }
283
+ // ...........................................................................
284
+ /**
285
+ * Checks if a blob exists.
286
+ * @param blobId - The unique identifier of the blob
287
+ * @returns Promise resolving to true if blob exists
288
+ */
289
+ blobExists(blobId) {
290
+ return new Promise((resolve, reject) => {
291
+ this._socket.emit(
292
+ "blobExists",
293
+ blobId,
294
+ (error, exists) => {
295
+ if (error) reject(error);
296
+ else resolve(exists);
297
+ }
298
+ );
299
+ });
300
+ }
301
+ // ...........................................................................
302
+ /**
303
+ * Gets blob properties (size, createdAt) without retrieving content.
304
+ * @param blobId - The unique identifier of the blob
305
+ * @returns Promise resolving to blob properties
306
+ */
307
+ getBlobProperties(blobId) {
308
+ return new Promise((resolve, reject) => {
309
+ this._socket.emit(
310
+ "getBlobProperties",
311
+ blobId,
312
+ (error, result) => {
313
+ if (error) reject(error);
314
+ else resolve(result);
315
+ }
316
+ );
317
+ });
318
+ }
319
+ // ...........................................................................
320
+ /**
321
+ * Lists all blobs with optional filtering and pagination.
322
+ * @param options - Optional listing configuration
323
+ * @returns Promise resolving to list of blobs
324
+ */
325
+ listBlobs(options) {
326
+ return new Promise((resolve, reject) => {
327
+ this._socket.emit(
328
+ "listBlobs",
329
+ options || {},
330
+ (error, result) => {
331
+ if (error) reject(error);
332
+ else resolve(result);
333
+ }
334
+ );
335
+ });
336
+ }
337
+ // ...........................................................................
338
+ /**
339
+ * Generates a signed URL for temporary blob access.
340
+ * @param blobId - The unique identifier of the blob
341
+ * @param expiresIn - Expiration time in seconds
342
+ * @param permissions - Permissions for the URL
343
+ * @returns Promise resolving to signed URL
344
+ */
345
+ generateSignedUrl(blobId, expiresIn, permissions) {
346
+ return new Promise((resolve, reject) => {
347
+ this._socket.emit(
348
+ "generateSignedUrl",
349
+ blobId,
350
+ expiresIn,
351
+ permissions,
352
+ (error, url) => {
353
+ if (error) reject(error);
354
+ else resolve(url);
355
+ }
356
+ );
357
+ });
358
+ }
359
+ }
360
+ class PeerSocketMock {
361
+ constructor(_bs) {
362
+ this._bs = _bs;
363
+ }
364
+ _listenersMap = /* @__PURE__ */ new Map();
365
+ connected = false;
366
+ disconnected = true;
367
+ // ............................................................................
368
+ /**
369
+ * Removes a specific listener for the specified event.
370
+ * @param eventName - The event name
371
+ * @param listener - The listener function to remove
372
+ * @returns This socket instance for chaining
373
+ */
374
+ off(eventName, listener) {
375
+ const listeners = this._listenersMap.get(eventName) || [];
376
+ const index = listeners.indexOf(listener);
377
+ if (index !== -1) {
378
+ listeners.splice(index, 1);
379
+ this._listenersMap.set(eventName, listeners);
380
+ }
381
+ return this;
382
+ }
383
+ // ............................................................................
384
+ /**
385
+ * Removes all listeners for the specified event, or all listeners if no event is specified.
386
+ * @param eventName - Optional event name
387
+ * @returns This socket instance for chaining
388
+ */
389
+ removeAllListeners(eventName) {
390
+ if (eventName) {
391
+ this._listenersMap.delete(eventName);
392
+ } else {
393
+ this._listenersMap.clear();
394
+ }
395
+ return this;
396
+ }
397
+ // ............................................................................
398
+ /**
399
+ * Registers an event listener for the specified event.
400
+ * @param eventName - The event name
401
+ * @param listener - The listener function to register
402
+ * @returns This socket instance for chaining
403
+ */
404
+ on(eventName, listener) {
405
+ if (!this._listenersMap.has(eventName)) {
406
+ this._listenersMap.set(eventName, []);
407
+ }
408
+ this._listenersMap.get(eventName).push(listener);
409
+ return this;
410
+ }
411
+ // ...........................................................................
412
+ /**
413
+ * Simulates a connection event.
414
+ */
415
+ connect() {
416
+ this.connected = true;
417
+ this.disconnected = false;
418
+ const listeners = this._listenersMap.get("connect") || [];
419
+ for (const cb of listeners) {
420
+ cb({});
421
+ }
422
+ return this;
423
+ }
424
+ // ...........................................................................
425
+ /**
426
+ * Simulates a disconnection event.
427
+ */
428
+ disconnect() {
429
+ this.connected = false;
430
+ this.disconnected = true;
431
+ const listeners = this._listenersMap.get("disconnect") || [];
432
+ for (const cb of listeners) {
433
+ cb({});
434
+ }
435
+ return this;
436
+ }
437
+ // ............................................................................
438
+ /**
439
+ * Emits an event, invoking the corresponding method on the Bs instance.
440
+ * @param eventName - The event name
441
+ * @param args - Event arguments
442
+ * @returns True if the event was handled
443
+ */
444
+ emit(eventName, ...args) {
445
+ const fn = this._bs[eventName];
446
+ if (typeof fn !== "function") {
447
+ throw new Error(`Event ${eventName.toString()} not supported`);
448
+ }
449
+ const cb = args[args.length - 1];
450
+ fn.apply(this._bs, args.slice(0, -1)).then((result) => {
451
+ cb(null, result);
452
+ }).catch((err) => {
453
+ cb(err);
454
+ });
455
+ return true;
456
+ }
457
+ }
458
+ class BsMulti {
459
+ constructor(_stores) {
460
+ this._stores = _stores;
461
+ }
462
+ // ...........................................................................
463
+ /**
464
+ * Initializes the BsMulti by assigning IDs to all underlying Bs instances.
465
+ * All underlying Bs instances must already be initialized.
466
+ */
467
+ async init() {
468
+ for (let idx = 0; idx < this._stores.length; idx++) {
469
+ this._stores[idx] = { ...this._stores[idx], id: `bs-${idx}` };
470
+ }
471
+ return Promise.resolve();
472
+ }
473
+ // ...........................................................................
474
+ /**
475
+ * Stores a blob in all writable Bs instances in parallel.
476
+ * @param content - The blob content to store
477
+ * @returns Promise resolving to blob properties from the first successful write
478
+ */
479
+ async setBlob(content) {
480
+ if (this.writables.length === 0) {
481
+ throw new Error("No writable Bs available");
482
+ }
483
+ const writes = this.writables.map(({ bs }) => bs.setBlob(content));
484
+ const results = await Promise.all(writes);
485
+ return results[0];
486
+ }
487
+ // ...........................................................................
488
+ /**
489
+ * Retrieves a blob from the highest priority readable Bs instance.
490
+ * Hot-swaps the blob to all writable instances for caching.
491
+ * @param blobId - The blob identifier
492
+ * @param options - Download options
493
+ * @returns Promise resolving to blob content and properties
494
+ */
495
+ async getBlob(blobId, options) {
496
+ if (this.readables.length === 0) {
497
+ throw new Error("No readable Bs available");
498
+ }
499
+ let result;
500
+ let readFrom = "";
501
+ const errors = [];
502
+ for (const readable of this.readables) {
503
+ try {
504
+ result = await readable.bs.getBlob(blobId, options);
505
+ readFrom = readable.id ?? "";
506
+ break;
507
+ } catch (e) {
508
+ errors.push(e);
509
+ continue;
510
+ }
511
+ }
512
+ if (!result) {
513
+ const notFoundErrors = errors.filter(
514
+ (err) => err.message.includes("Blob not found")
515
+ );
516
+ if (notFoundErrors.length === errors.length) {
517
+ throw new Error(`Blob not found: ${blobId}`);
518
+ } else {
519
+ throw errors[0];
520
+ }
521
+ }
522
+ if (this.writables.length > 0) {
523
+ const hotSwapWrites = this.writables.filter((writable) => writable.id !== readFrom).map(({ bs }) => bs.setBlob(result.content).catch(() => {
524
+ }));
525
+ await Promise.all(hotSwapWrites);
526
+ }
527
+ return result;
528
+ }
529
+ // ...........................................................................
530
+ /**
531
+ * Retrieves a blob as a ReadableStream from the highest priority readable Bs instance.
532
+ * @param blobId - The blob identifier
533
+ * @returns Promise resolving to a ReadableStream
534
+ */
535
+ async getBlobStream(blobId) {
536
+ if (this.readables.length === 0) {
537
+ throw new Error("No readable Bs available");
538
+ }
539
+ const errors = [];
540
+ for (const readable of this.readables) {
541
+ try {
542
+ return await readable.bs.getBlobStream(blobId);
543
+ } catch (e) {
544
+ errors.push(e);
545
+ continue;
546
+ }
547
+ }
548
+ const notFoundErrors = errors.filter(
549
+ (err) => err.message.includes("Blob not found")
550
+ );
551
+ if (notFoundErrors.length === errors.length) {
552
+ throw new Error(`Blob not found: ${blobId}`);
553
+ } else {
554
+ throw errors[0];
555
+ }
556
+ }
557
+ // ...........................................................................
558
+ /**
559
+ * Deletes a blob from all writable Bs instances in parallel.
560
+ * @param blobId - The blob identifier
561
+ */
562
+ async deleteBlob(blobId) {
563
+ if (this.writables.length === 0) {
564
+ throw new Error("No writable Bs available");
565
+ }
566
+ const deletes = this.writables.map(({ bs }) => bs.deleteBlob(blobId));
567
+ await Promise.all(deletes);
568
+ }
569
+ // ...........................................................................
570
+ /**
571
+ * Checks if a blob exists in any readable Bs instance.
572
+ * @param blobId - The blob identifier
573
+ * @returns Promise resolving to true if blob exists in any readable
574
+ */
575
+ async blobExists(blobId) {
576
+ if (this.readables.length === 0) {
577
+ throw new Error("No readable Bs available");
578
+ }
579
+ for (const readable of this.readables) {
580
+ try {
581
+ const exists = await readable.bs.blobExists(blobId);
582
+ if (exists) {
583
+ return true;
584
+ }
585
+ } catch {
586
+ continue;
587
+ }
588
+ }
589
+ return false;
590
+ }
591
+ // ...........................................................................
592
+ /**
593
+ * Gets blob properties from the highest priority readable Bs instance.
594
+ * @param blobId - The blob identifier
595
+ * @returns Promise resolving to blob properties
596
+ */
597
+ async getBlobProperties(blobId) {
598
+ if (this.readables.length === 0) {
599
+ throw new Error("No readable Bs available");
600
+ }
601
+ const errors = [];
602
+ for (const readable of this.readables) {
603
+ try {
604
+ return await readable.bs.getBlobProperties(blobId);
605
+ } catch (e) {
606
+ errors.push(e);
607
+ continue;
608
+ }
609
+ }
610
+ const notFoundErrors = errors.filter(
611
+ (err) => err.message.includes("Blob not found")
612
+ );
613
+ if (notFoundErrors.length === errors.length) {
614
+ throw new Error(`Blob not found: ${blobId}`);
615
+ } else {
616
+ throw errors[0];
617
+ }
618
+ }
619
+ // ...........................................................................
620
+ /**
621
+ * Lists blobs by merging results from all readable Bs instances.
622
+ * Deduplicates by blobId (content-addressable).
623
+ * @param options - Listing options
624
+ * @returns Promise resolving to list of blobs
625
+ */
626
+ async listBlobs(options) {
627
+ if (this.readables.length === 0) {
628
+ throw new Error("No readable Bs available");
629
+ }
630
+ const blobMap = /* @__PURE__ */ new Map();
631
+ for (const readable of this.readables) {
632
+ try {
633
+ let continuationToken2;
634
+ do {
635
+ const result = await readable.bs.listBlobs({
636
+ prefix: options?.prefix,
637
+ // Apply prefix filter during collection
638
+ continuationToken: continuationToken2,
639
+ maxResults: 1e3
640
+ // Fetch in chunks from each store
641
+ });
642
+ for (const blob of result.blobs) {
643
+ if (!blobMap.has(blob.blobId)) {
644
+ blobMap.set(blob.blobId, blob);
645
+ }
646
+ }
647
+ continuationToken2 = result.continuationToken;
648
+ } while (continuationToken2);
649
+ } catch {
650
+ continue;
651
+ }
652
+ }
653
+ const blobs = Array.from(blobMap.values());
654
+ blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
655
+ const maxResults = options?.maxResults ?? blobs.length;
656
+ let startIndex = 0;
657
+ if (options?.continuationToken) {
658
+ const tokenIndex = blobs.findIndex(
659
+ (blob) => blob.blobId === options.continuationToken
660
+ );
661
+ startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
662
+ }
663
+ const endIndex = Math.min(startIndex + maxResults, blobs.length);
664
+ const pageBlobs = blobs.slice(startIndex, endIndex);
665
+ const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
666
+ return {
667
+ blobs: pageBlobs,
668
+ continuationToken
669
+ };
670
+ }
671
+ // ...........................................................................
672
+ /**
673
+ * Generates a signed URL from the highest priority readable Bs instance.
674
+ * @param blobId - The blob identifier
675
+ * @param expiresIn - Expiration time in seconds
676
+ * @param permissions - Access permissions
677
+ * @returns Promise resolving to signed URL
678
+ */
679
+ async generateSignedUrl(blobId, expiresIn, permissions = "read") {
680
+ if (this.readables.length === 0) {
681
+ throw new Error("No readable Bs available");
682
+ }
683
+ const errors = [];
684
+ for (const readable of this.readables) {
685
+ try {
686
+ return await readable.bs.generateSignedUrl(
687
+ blobId,
688
+ expiresIn,
689
+ permissions
690
+ );
691
+ } catch (e) {
692
+ errors.push(e);
693
+ continue;
694
+ }
695
+ }
696
+ const notFoundErrors = errors.filter(
697
+ (err) => err.message.includes("Blob not found")
698
+ );
699
+ if (notFoundErrors.length === errors.length) {
700
+ throw new Error(`Blob not found: ${blobId}`);
701
+ } else {
702
+ throw errors[0];
703
+ }
704
+ }
705
+ // ...........................................................................
706
+ /**
707
+ * Gets the list of underlying readable Bs instances, sorted by priority.
708
+ */
709
+ get readables() {
710
+ return this._stores.filter((store) => store.read).sort((a, b) => a.priority - b.priority);
711
+ }
712
+ // ...........................................................................
713
+ /**
714
+ * Gets the list of underlying writable Bs instances, sorted by priority.
715
+ */
716
+ get writables() {
717
+ return this._stores.filter((store) => store.write).sort((a, b) => a.priority - b.priority);
718
+ }
719
+ // ...........................................................................
720
+ /**
721
+ * Example: Local cache (BsMem) + Remote server (BsPeer)
722
+ */
723
+ static example = async () => {
724
+ const bsRemoteMem = new BsMem();
725
+ const bsRemoteSocket = new PeerSocketMock(bsRemoteMem);
726
+ const bsRemote = new BsPeer(bsRemoteSocket);
727
+ await bsRemote.init();
728
+ const bsLocal = new BsMem();
729
+ const stores = [
730
+ { bs: bsLocal, priority: 0, read: true, write: true },
731
+ // Cache first
732
+ { bs: bsRemote, priority: 1, read: true, write: false }
733
+ // Remote fallback
734
+ ];
735
+ const bsMulti = new BsMulti(stores);
736
+ await bsMulti.init();
737
+ return bsMulti;
738
+ };
739
+ }
740
+ class BsPeerBridge {
741
+ constructor(_bs, _socket) {
742
+ this._bs = _bs;
743
+ this._socket = _socket;
744
+ }
745
+ _eventHandlers = /* @__PURE__ */ new Map();
746
+ /**
747
+ * Starts the bridge by setting up connection event handlers and
748
+ * automatically registering all Bs methods.
749
+ */
750
+ start() {
751
+ this._socket.on("connect", () => this._handleConnect());
752
+ this._socket.on("disconnect", () => this._handleDisconnect());
753
+ this._registerBsMethods();
754
+ }
755
+ /**
756
+ * Stops the bridge by removing all event handlers.
757
+ */
758
+ stop() {
759
+ this._socket.off("connect", () => this._handleConnect());
760
+ this._socket.off("disconnect", () => this._handleDisconnect());
761
+ for (const [eventName, handler] of this._eventHandlers) {
762
+ this._socket.off(eventName, handler);
763
+ }
764
+ this._eventHandlers.clear();
765
+ }
766
+ /**
767
+ * Automatically registers all Bs interface methods as socket event handlers.
768
+ */
769
+ _registerBsMethods() {
770
+ const bsMethods = [
771
+ "setBlob",
772
+ "getBlob",
773
+ "getBlobStream",
774
+ "deleteBlob",
775
+ "blobExists",
776
+ "getBlobProperties",
777
+ "listBlobs",
778
+ "generateSignedUrl"
779
+ ];
780
+ for (const methodName of bsMethods) {
781
+ this.registerEvent(methodName);
782
+ }
783
+ }
784
+ /**
785
+ * Registers a socket event to be translated to a Bs method call.
786
+ * @param eventName - The socket event name (should match a Bs method name)
787
+ * @param bsMethodName - (Optional) The Bs method name if different from eventName
788
+ */
789
+ registerEvent(eventName, bsMethodName) {
790
+ const methodName = bsMethodName || eventName;
791
+ const handler = (...args) => {
792
+ const callback = args[args.length - 1];
793
+ const methodArgs = args.slice(0, -1);
794
+ const bsMethod = this._bs[methodName];
795
+ if (typeof bsMethod !== "function") {
796
+ const error = new Error(
797
+ `Method "${methodName}" not found on Bs instance`
798
+ );
799
+ if (typeof callback === "function") {
800
+ callback(null, error);
801
+ }
802
+ return;
803
+ }
804
+ bsMethod.apply(this._bs, methodArgs).then((result) => {
805
+ if (typeof callback === "function") {
806
+ callback(result, null);
807
+ }
808
+ }).catch((error) => {
809
+ if (typeof callback === "function") {
810
+ callback(null, error);
811
+ }
812
+ });
813
+ };
814
+ this._eventHandlers.set(eventName, handler);
815
+ this._socket.on(eventName, handler);
816
+ }
817
+ /**
818
+ * Registers multiple socket events at once.
819
+ * @param eventNames - Array of event names to register
820
+ */
821
+ registerEvents(eventNames) {
822
+ for (const eventName of eventNames) {
823
+ this.registerEvent(eventName);
824
+ }
825
+ }
826
+ /**
827
+ * Unregisters a socket event handler.
828
+ * @param eventName - The event name to unregister
829
+ */
830
+ unregisterEvent(eventName) {
831
+ const handler = this._eventHandlers.get(eventName);
832
+ if (handler) {
833
+ this._socket.off(eventName, handler);
834
+ this._eventHandlers.delete(eventName);
835
+ }
836
+ }
837
+ /**
838
+ * Emits a result back through the socket.
839
+ * @param eventName - The event name to emit
840
+ * @param data - The data to send
841
+ */
842
+ emitToSocket(eventName, ...data) {
843
+ this._socket.emit(eventName, ...data);
844
+ }
845
+ /**
846
+ * Calls a Bs method directly and emits the result through the socket.
847
+ * @param bsMethodName - The Bs method to call
848
+ * @param socketEventName - The socket event to emit with the result
849
+ * @param args - Arguments to pass to the Bs method
850
+ */
851
+ async callBsAndEmit(bsMethodName, socketEventName, ...args) {
852
+ try {
853
+ const bsMethod = this._bs[bsMethodName];
854
+ if (typeof bsMethod !== "function") {
855
+ throw new Error(`Method "${bsMethodName}" not found on Bs instance`);
856
+ }
857
+ const result = await bsMethod.apply(this._bs, args);
858
+ this._socket.emit(socketEventName, result, null);
859
+ } catch (error) {
860
+ this._socket.emit(socketEventName, null, error);
861
+ }
862
+ }
863
+ /* v8 ignore next -- @preserve */
864
+ _handleConnect() {
865
+ }
866
+ /* v8 ignore next -- @preserve */
867
+ _handleDisconnect() {
868
+ }
869
+ /**
870
+ * Gets the current socket instance.
871
+ */
872
+ get socket() {
873
+ return this._socket;
874
+ }
875
+ /**
876
+ * Gets the current Bs instance.
877
+ */
878
+ get bs() {
879
+ return this._bs;
880
+ }
881
+ /**
882
+ * Returns whether the socket is currently connected.
883
+ */
884
+ get isConnected() {
885
+ return this._socket.connected;
886
+ }
887
+ }
888
+ class BsServer {
889
+ constructor(_bs) {
890
+ this._bs = _bs;
891
+ }
892
+ _sockets = [];
893
+ // ...........................................................................
894
+ /**
895
+ * Adds a socket to the BsServer instance.
896
+ * @param socket - The socket to add.
897
+ */
898
+ async addSocket(socket) {
899
+ await this._addTransportLayer(socket);
900
+ this._sockets.push(socket);
901
+ }
902
+ // ...........................................................................
903
+ /**
904
+ * Removes a socket from the BsServer instance.
905
+ * @param socket - The socket to remove.
906
+ */
907
+ removeSocket(socket) {
908
+ this._sockets = this._sockets.filter((s) => s !== socket);
909
+ }
910
+ // ...........................................................................
911
+ /**
912
+ * Adds a transport layer to the given socket.
913
+ * @param socket - The socket to add the transport layer to.
914
+ */
915
+ async _addTransportLayer(socket) {
916
+ const methods = this._generateTransportLayer(this._bs);
917
+ for (const [key, fn] of Object.entries(methods)) {
918
+ socket.on(key, (...args) => {
919
+ const cb = args[args.length - 1];
920
+ fn.apply(this, args.slice(0, -1)).then((result) => {
921
+ cb(null, result);
922
+ }).catch((err) => {
923
+ cb(err);
924
+ });
925
+ });
926
+ }
927
+ }
928
+ // ...........................................................................
929
+ /**
930
+ * Generates a transport layer object for the given Bs instance.
931
+ * @param bs - The Bs instance to generate the transport layer for.
932
+ * @returns An object containing methods that correspond to the Bs interface.
933
+ */
934
+ _generateTransportLayer = (bs) => ({
935
+ setBlob: (content) => bs.setBlob(content),
936
+ getBlob: (blobId, options) => bs.getBlob(blobId, options),
937
+ getBlobStream: (blobId) => bs.getBlobStream(blobId),
938
+ deleteBlob: (blobId) => bs.deleteBlob(blobId),
939
+ blobExists: (blobId) => bs.blobExists(blobId),
940
+ getBlobProperties: (blobId) => bs.getBlobProperties(blobId),
941
+ listBlobs: (options) => bs.listBlobs(options),
942
+ generateSignedUrl: (blobId, expiresIn, permissions) => bs.generateSignedUrl(blobId, expiresIn, permissions)
943
+ });
944
+ }
945
+ class BaseNode {
946
+ constructor(_localIo) {
947
+ this._localIo = _localIo;
948
+ if (!_localIo.isOpen) {
949
+ throw new Error("Local Io must be initialized and open");
950
+ }
951
+ this._localDb = new Db(this._localIo);
952
+ }
953
+ _localDb;
954
+ // ...........................................................................
955
+ /**
956
+ * Creates tables in the local Db.
957
+ * @param cfgs - Table configurations
958
+ * @param cfgs.withInsertHistory - TableCfgs for tables with InsertHistory
959
+ * @param cfgs.withoutInsertHistory - TableCfgs for tables without InsertHistory
960
+ */
961
+ async createTables(cfgs) {
962
+ if (!this._localDb) throw new Error("Local Db not initialized");
963
+ for (const tableCfg of cfgs.withoutInsertHistory || []) {
964
+ await this._localDb.core.createTable(tableCfg);
965
+ }
966
+ for (const tableCfg of cfgs.withInsertHistory || []) {
967
+ await this._localDb.core.createTableWithInsertHistory(tableCfg);
968
+ }
969
+ }
970
+ // ...........................................................................
971
+ /**
972
+ * Imports Rljson data into the local Db.
973
+ * @param data - Rljson data to import
974
+ */
975
+ /* v8 ignore next -- @preserve */
976
+ async import(data) {
977
+ if (!this._localDb) throw new Error("Local Db not initialized");
978
+ await this._localDb.core.import(data);
979
+ }
980
+ }
981
+ class Client extends BaseNode {
982
+ // ...........................................................................
983
+ /**
984
+ * Creates a Client instance
985
+ * @param _socketToServer - Socket to connect to server
986
+ * @param _localIo - Local Io for local storage
987
+ * @param _localBs - Local Bs for local blob storage
988
+ */
989
+ constructor(_socketToServer, _localIo, _localBs) {
990
+ super(_localIo);
991
+ this._socketToServer = _socketToServer;
992
+ this._localIo = _localIo;
993
+ this._localBs = _localBs;
994
+ }
995
+ _ioMultiIos = [];
996
+ _ioMulti;
997
+ _bsMultiBss = [];
998
+ _bsMulti;
999
+ /**
1000
+ * Initializes Io and Bs multis and their peer bridges.
1001
+ * @returns The initialized Io implementation.
1002
+ */
1003
+ async init() {
1004
+ await this._setupIo();
1005
+ await this._setupBs();
1006
+ await this.ready();
1007
+ return this._ioMulti;
1008
+ }
1009
+ /**
1010
+ * Resolves once the Io implementation is ready.
1011
+ */
1012
+ async ready() {
1013
+ if (this._ioMulti) {
1014
+ await this._ioMulti.isReady();
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Closes client resources and clears internal state.
1019
+ */
1020
+ async tearDown() {
1021
+ if (this._ioMulti && this._ioMulti.isOpen) {
1022
+ this._ioMulti.close();
1023
+ }
1024
+ if (this._bsMulti) ;
1025
+ this._ioMultiIos = [];
1026
+ this._bsMultiBss = [];
1027
+ this._ioMulti = void 0;
1028
+ this._bsMulti = void 0;
1029
+ }
1030
+ /**
1031
+ * Returns the Io implementation.
1032
+ */
1033
+ get io() {
1034
+ return this._ioMulti;
1035
+ }
1036
+ /**
1037
+ * Returns the Bs implementation.
1038
+ */
1039
+ get bs() {
1040
+ return this._bsMulti;
1041
+ }
1042
+ /**
1043
+ * Builds the Io multi with local and peer layers.
1044
+ */
1045
+ async _setupIo() {
1046
+ this._ioMultiIos.push({
1047
+ io: this._localIo,
1048
+ dump: true,
1049
+ read: true,
1050
+ write: true,
1051
+ priority: 1
1052
+ });
1053
+ const ioPeerBridge = new IoPeerBridge(this._localIo, this._socketToServer);
1054
+ ioPeerBridge.start();
1055
+ const ioPeer = await this._createIoPeer();
1056
+ this._ioMultiIos.push({
1057
+ io: ioPeer,
1058
+ dump: false,
1059
+ read: true,
1060
+ write: false,
1061
+ priority: 2
1062
+ });
1063
+ this._ioMulti = new IoMulti(this._ioMultiIos);
1064
+ await this._ioMulti.init();
1065
+ await this._ioMulti.isReady();
1066
+ }
1067
+ /**
1068
+ * Builds the Bs multi with local and peer layers.
1069
+ */
1070
+ async _setupBs() {
1071
+ this._bsMultiBss.push({
1072
+ bs: this._localBs,
1073
+ read: true,
1074
+ write: true,
1075
+ priority: 1
1076
+ });
1077
+ const bsPeerBridge = new BsPeerBridge(this._localBs, this._socketToServer);
1078
+ bsPeerBridge.start();
1079
+ const bsPeer = await this._createBsPeer();
1080
+ this._bsMultiBss.push({
1081
+ bs: bsPeer,
1082
+ read: true,
1083
+ write: false,
1084
+ priority: 2
1085
+ });
1086
+ this._bsMulti = new BsMulti(this._bsMultiBss);
1087
+ await this._bsMulti.init();
1088
+ }
1089
+ /**
1090
+ * Creates and initializes a downstream Io peer.
1091
+ */
1092
+ async _createIoPeer() {
1093
+ const ioPeer = new IoPeer(this._socketToServer);
1094
+ await ioPeer.init();
1095
+ await ioPeer.isReady();
1096
+ return ioPeer;
1097
+ }
1098
+ /**
1099
+ * Creates and initializes a downstream Bs peer.
1100
+ */
1101
+ async _createBsPeer() {
1102
+ const bsPeer = new BsPeer(this._socketToServer);
1103
+ await bsPeer.init();
1104
+ return bsPeer;
1105
+ }
1106
+ }
1107
+ class Server extends BaseNode {
1108
+ constructor(_route, _localIo, _localBs) {
1109
+ super(_localIo);
1110
+ this._route = _route;
1111
+ this._localIo = _localIo;
1112
+ this._localBs = _localBs;
1113
+ const ioMultiIoLocal = {
1114
+ io: this._localIo,
1115
+ dump: true,
1116
+ read: true,
1117
+ write: true,
1118
+ priority: 1
1119
+ };
1120
+ this._ios.push(ioMultiIoLocal);
1121
+ this._ioMulti = new IoMulti(this._ios);
1122
+ this._ioServer = new IoServer(this._ioMulti);
1123
+ const bsMultiBsLocal = {
1124
+ bs: this._localBs,
1125
+ read: true,
1126
+ write: true,
1127
+ priority: 1
1128
+ };
1129
+ this._bss.push(bsMultiBsLocal);
1130
+ this._bsMulti = new BsMulti(this._bss);
1131
+ this._bsServer = new BsServer(this._bsMulti);
1132
+ }
1133
+ // Map of connected clients
1134
+ // socket => Push: Send new Refs through Route
1135
+ // io => Pull: Read from Clients Io
1136
+ _clients = /* @__PURE__ */ new Map();
1137
+ _ios = [];
1138
+ _ioMulti;
1139
+ // Storage => Let Clients read from Servers Io
1140
+ _ioServer;
1141
+ _bss = [];
1142
+ _bsMulti;
1143
+ // Storage => Let Clients read from Servers Bs
1144
+ _bsServer;
1145
+ // To avoid rebroadcasting the same edit refs multiple times
1146
+ _multicastedRefs = /* @__PURE__ */ new Set();
1147
+ _refreshPromise;
1148
+ _pendingSockets = [];
1149
+ /**
1150
+ * Initializes Io and Bs multis on the server.
1151
+ */
1152
+ async init() {
1153
+ await this._ioMulti.init();
1154
+ await this._ioMulti.isReady();
1155
+ await this._bsMulti.init();
1156
+ await this.ready();
1157
+ }
1158
+ /**
1159
+ * Resolves once the Io implementation is ready.
1160
+ */
1161
+ async ready() {
1162
+ await this._ioMulti.isReady();
1163
+ }
1164
+ /**
1165
+ * Adds a client socket, rebuilds multis, and refreshes servers.
1166
+ * @param socket - Client socket to register.
1167
+ * @returns The server instance.
1168
+ */
1169
+ async addSocket(socket) {
1170
+ const clientId = `client_${this._clients.size}_${Math.random().toString(36).slice(2)}`;
1171
+ socket.__clientId = clientId;
1172
+ const ioPeer = await this._createIoPeer(socket);
1173
+ const bsPeer = await this._createBsPeer(socket);
1174
+ this._registerClient(clientId, socket, ioPeer, bsPeer);
1175
+ this._pendingSockets.push(socket);
1176
+ this._queueIoPeer(ioPeer);
1177
+ this._queueBsPeer(bsPeer);
1178
+ await this._queueRefresh();
1179
+ this._removeAllListeners();
1180
+ this._multicastRefs();
1181
+ return this;
1182
+ }
1183
+ // ...........................................................................
1184
+ /**
1185
+ * Removes all listeners from all connected clients.
1186
+ */
1187
+ _removeAllListeners() {
1188
+ for (const { socket } of this._clients.values()) {
1189
+ socket.removeAllListeners(this._route.flat);
1190
+ }
1191
+ }
1192
+ // ...........................................................................
1193
+ /**
1194
+ * Broadcasts incoming payloads from any client to all other connected clients.
1195
+ */
1196
+ _multicastRefs = () => {
1197
+ for (const { socket: socketA } of this._clients.values()) {
1198
+ socketA.on(this._route.flat, (payload) => {
1199
+ const ref = payload.r;
1200
+ if (this._multicastedRefs.has(ref)) {
1201
+ return;
1202
+ }
1203
+ this._multicastedRefs.add(ref);
1204
+ const p = payload;
1205
+ if (p && p.__origin) {
1206
+ return;
1207
+ }
1208
+ for (const { socket: socketB } of this._clients.values()) {
1209
+ if (socketA !== socketB) {
1210
+ const forwarded = Object.assign({}, payload, {
1211
+ __origin: socketA.__clientId
1212
+ });
1213
+ socketB.emit(this._route.flat, forwarded);
1214
+ }
1215
+ }
1216
+ });
1217
+ }
1218
+ };
1219
+ get route() {
1220
+ return this._route;
1221
+ }
1222
+ /**
1223
+ * Returns the Io implementation.
1224
+ */
1225
+ get io() {
1226
+ return this._ioMulti;
1227
+ }
1228
+ /**
1229
+ * Returns the Bs implementation.
1230
+ */
1231
+ get bs() {
1232
+ return this._bsMulti;
1233
+ }
1234
+ /**
1235
+ * Returns the connected clients map.
1236
+ */
1237
+ get clients() {
1238
+ return this._clients;
1239
+ }
1240
+ /**
1241
+ * Creates and initializes a downstream Io peer for a socket.
1242
+ * @param socket - Client socket to bind the peer to.
1243
+ */
1244
+ async _createIoPeer(socket) {
1245
+ const ioPeer = new IoPeer(socket);
1246
+ await ioPeer.init();
1247
+ await ioPeer.isReady();
1248
+ return ioPeer;
1249
+ }
1250
+ /**
1251
+ * Creates and initializes a downstream Bs peer for a socket.
1252
+ * @param socket - Client socket to bind the peer to.
1253
+ */
1254
+ async _createBsPeer(socket) {
1255
+ const bsPeer = new BsPeer(socket);
1256
+ await bsPeer.init();
1257
+ return bsPeer;
1258
+ }
1259
+ /**
1260
+ * Registers the client socket and peers.
1261
+ * @param clientId - Stable client identifier.
1262
+ * @param socket - Client socket to register.
1263
+ * @param io - Io peer associated with the client.
1264
+ * @param bs - Bs peer associated with the client.
1265
+ */
1266
+ _registerClient(clientId, socket, io, bs) {
1267
+ this._clients.set(clientId, {
1268
+ socket,
1269
+ io,
1270
+ bs
1271
+ });
1272
+ }
1273
+ /**
1274
+ * Queues an Io peer for inclusion in the Io multi.
1275
+ * @param ioPeer - Io peer to add.
1276
+ */
1277
+ _queueIoPeer(ioPeer) {
1278
+ this._ios.push({
1279
+ io: ioPeer,
1280
+ dump: false,
1281
+ read: true,
1282
+ write: false,
1283
+ priority: 2
1284
+ });
1285
+ }
1286
+ /**
1287
+ * Queues a Bs peer for inclusion in the Bs multi.
1288
+ * @param bsPeer - Bs peer to add.
1289
+ */
1290
+ _queueBsPeer(bsPeer) {
1291
+ this._bss.push({
1292
+ bs: bsPeer,
1293
+ read: true,
1294
+ write: false,
1295
+ priority: 2
1296
+ });
1297
+ }
1298
+ /**
1299
+ * Rebuilds Io and Bs multis from queued peers.
1300
+ */
1301
+ async _rebuildMultis() {
1302
+ this._ioMulti = new IoMulti(this._ios);
1303
+ await this._ioMulti.init();
1304
+ await this._ioMulti.isReady();
1305
+ this._bsMulti = new BsMulti(this._bss);
1306
+ await this._bsMulti.init();
1307
+ }
1308
+ /**
1309
+ * Recreates servers and reattaches sockets.
1310
+ */
1311
+ async _refreshServers() {
1312
+ this._ioServer._io = this._ioMulti;
1313
+ this._bsServer._bs = this._bsMulti;
1314
+ for (const socket of this._pendingSockets) {
1315
+ await this._ioServer.addSocket(socket);
1316
+ await this._bsServer.addSocket(socket);
1317
+ }
1318
+ this._pendingSockets = [];
1319
+ }
1320
+ /**
1321
+ * Batches multi/server refreshes into a single queued task.
1322
+ */
1323
+ _queueRefresh() {
1324
+ if (!this._refreshPromise) {
1325
+ this._refreshPromise = Promise.resolve().then(async () => {
1326
+ await this._rebuildMultis();
1327
+ await this._refreshServers();
1328
+ }).finally(() => {
1329
+ this._refreshPromise = void 0;
1330
+ });
1331
+ }
1332
+ return this._refreshPromise;
1333
+ }
1334
+ /** Example instance for test purposes */
1335
+ static async example() {
1336
+ const route = Route.fromFlat("example.route");
1337
+ const io = new IoMem();
1338
+ await io.init();
1339
+ await io.isReady();
1340
+ const bs = new BsMem();
1341
+ const socket = new SocketMock();
1342
+ socket.connect();
1343
+ return new Server(route, io, bs).addSocket(socket);
1344
+ }
1345
+ }
1346
+ class SocketIoBridge {
1347
+ constructor(_socket) {
1348
+ this._socket = _socket;
1349
+ if ("setMaxListeners" in this._socket) {
1350
+ this._socket.setMaxListeners(0);
1351
+ }
1352
+ }
1353
+ get connected() {
1354
+ return this._socket.connected;
1355
+ }
1356
+ /* v8 ignore next -- @preserve */
1357
+ get disconnected() {
1358
+ return this._socket.disconnected;
1359
+ }
1360
+ /**
1361
+ * Connect the socket.
1362
+ * Note: Socket.IO server sockets don't have a connect() method as they are
1363
+ * already connected when created. This is a no-op for server sockets.
1364
+ * For client sockets, this calls the underlying connect() method.
1365
+ */
1366
+ connect() {
1367
+ if ("connect" in this._socket && typeof this._socket.connect === "function") {
1368
+ this._socket.connect();
1369
+ }
1370
+ }
1371
+ disconnect() {
1372
+ this._socket.disconnect();
1373
+ }
1374
+ on(eventName, listener) {
1375
+ this._socket.on(eventName, listener);
1376
+ return this;
1377
+ }
1378
+ emit(eventName, ...args) {
1379
+ this._socket.emit(eventName, ...args);
1380
+ return this;
1381
+ }
1382
+ /* v8 ignore next -- @preserve */
1383
+ off(eventName, listener) {
1384
+ this._socket.off(eventName, listener);
1385
+ return this;
1386
+ }
1387
+ removeAllListeners(eventName) {
1388
+ this._socket.removeAllListeners(eventName);
1389
+ return this;
1390
+ }
1391
+ /**
1392
+ * Get the underlying Socket.IO socket instance.
1393
+ */
1394
+ /* v8 ignore next -- @preserve */
1395
+ get rawSocket() {
1396
+ return this._socket;
1397
+ }
1398
+ }
1399
+ export {
1400
+ Client,
1401
+ Server,
1402
+ SocketIoBridge
1403
+ };