@rljson/bs 0.0.18 → 0.0.20

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/bs.js CHANGED
@@ -124,6 +124,1112 @@ class BsMem {
124
124
  return this.blobs.size;
125
125
  }
126
126
  }
127
+ class BsPeer {
128
+ constructor(_socket) {
129
+ this._socket = _socket;
130
+ }
131
+ isOpen = false;
132
+ // ...........................................................................
133
+ /**
134
+ * Initializes the Peer connection.
135
+ */
136
+ async init() {
137
+ this._socket.on("connect", () => {
138
+ this.isOpen = true;
139
+ });
140
+ this._socket.on("disconnect", () => {
141
+ this.isOpen = false;
142
+ });
143
+ this._socket.connect();
144
+ return new Promise((resolve) => {
145
+ if (this._socket.connected) {
146
+ this.isOpen = true;
147
+ resolve();
148
+ } else {
149
+ this._socket.on("connect", () => {
150
+ resolve();
151
+ });
152
+ }
153
+ });
154
+ }
155
+ // ...........................................................................
156
+ /**
157
+ * Closes the Peer connection.
158
+ */
159
+ async close() {
160
+ if (!this._socket.connected) return;
161
+ return new Promise((resolve) => {
162
+ this._socket.on("disconnect", () => {
163
+ resolve();
164
+ });
165
+ this._socket.disconnect();
166
+ });
167
+ }
168
+ // ...........................................................................
169
+ /**
170
+ * Returns a promise that resolves once the Peer connection is ready.
171
+ */
172
+ async isReady() {
173
+ if (!!this._socket && this._socket.connected === true) this.isOpen = true;
174
+ else this.isOpen = false;
175
+ return !!this.isOpen ? Promise.resolve() : Promise.reject();
176
+ }
177
+ // ...........................................................................
178
+ /**
179
+ * Stores a blob from Buffer, string, or ReadableStream and returns properties.
180
+ * @param content - The blob content to store
181
+ * @returns Promise resolving to blob properties
182
+ */
183
+ setBlob(content) {
184
+ return new Promise((resolve, reject) => {
185
+ if (content instanceof ReadableStream) {
186
+ const reader = content.getReader();
187
+ const chunks = [];
188
+ const readStream = async () => {
189
+ try {
190
+ while (true) {
191
+ const { done, value } = await reader.read();
192
+ if (done) break;
193
+ chunks.push(value);
194
+ }
195
+ const totalLength = chunks.reduce(
196
+ (sum, chunk) => sum + chunk.length,
197
+ 0
198
+ );
199
+ const buffer = Buffer.concat(
200
+ chunks.map((chunk) => Buffer.from(chunk)),
201
+ totalLength
202
+ );
203
+ this._socket.emit(
204
+ "setBlob",
205
+ buffer,
206
+ (error, result) => {
207
+ if (error) reject(error);
208
+ else resolve(result);
209
+ }
210
+ );
211
+ } catch (err) {
212
+ reject(err);
213
+ }
214
+ };
215
+ readStream();
216
+ } else {
217
+ this._socket.emit(
218
+ "setBlob",
219
+ content,
220
+ (error, result) => {
221
+ if (error) reject(error);
222
+ else resolve(result);
223
+ }
224
+ );
225
+ }
226
+ });
227
+ }
228
+ // ...........................................................................
229
+ /**
230
+ * Retrieves a blob by its ID as a Buffer.
231
+ * @param blobId - The unique identifier of the blob
232
+ * @param options - Download options
233
+ * @returns Promise resolving to blob content and properties
234
+ */
235
+ getBlob(blobId, options) {
236
+ return new Promise((resolve, reject) => {
237
+ this._socket.emit(
238
+ "getBlob",
239
+ blobId,
240
+ options,
241
+ (error, result) => {
242
+ if (error) reject(error);
243
+ else resolve(result);
244
+ }
245
+ );
246
+ });
247
+ }
248
+ // ...........................................................................
249
+ /**
250
+ * Retrieves a blob by its ID as a ReadableStream.
251
+ * @param blobId - The unique identifier of the blob
252
+ * @returns Promise resolving to readable stream
253
+ */
254
+ getBlobStream(blobId) {
255
+ return new Promise((resolve, reject) => {
256
+ this._socket.emit(
257
+ "getBlobStream",
258
+ blobId,
259
+ (error, result) => {
260
+ if (error) reject(error);
261
+ else resolve(result);
262
+ }
263
+ );
264
+ });
265
+ }
266
+ // ...........................................................................
267
+ /**
268
+ * Deletes a blob by its ID.
269
+ * @param blobId - The unique identifier of the blob
270
+ * @returns Promise that resolves when deletion is complete
271
+ */
272
+ deleteBlob(blobId) {
273
+ return new Promise((resolve, reject) => {
274
+ this._socket.emit("deleteBlob", blobId, (error) => {
275
+ if (error) reject(error);
276
+ else resolve();
277
+ });
278
+ });
279
+ }
280
+ // ...........................................................................
281
+ /**
282
+ * Checks if a blob exists.
283
+ * @param blobId - The unique identifier of the blob
284
+ * @returns Promise resolving to true if blob exists
285
+ */
286
+ blobExists(blobId) {
287
+ return new Promise((resolve, reject) => {
288
+ this._socket.emit(
289
+ "blobExists",
290
+ blobId,
291
+ (error, exists) => {
292
+ if (error) reject(error);
293
+ else resolve(exists);
294
+ }
295
+ );
296
+ });
297
+ }
298
+ // ...........................................................................
299
+ /**
300
+ * Gets blob properties (size, createdAt) without retrieving content.
301
+ * @param blobId - The unique identifier of the blob
302
+ * @returns Promise resolving to blob properties
303
+ */
304
+ getBlobProperties(blobId) {
305
+ return new Promise((resolve, reject) => {
306
+ this._socket.emit(
307
+ "getBlobProperties",
308
+ blobId,
309
+ (error, result) => {
310
+ if (error) reject(error);
311
+ else resolve(result);
312
+ }
313
+ );
314
+ });
315
+ }
316
+ // ...........................................................................
317
+ /**
318
+ * Lists all blobs with optional filtering and pagination.
319
+ * @param options - Optional listing configuration
320
+ * @returns Promise resolving to list of blobs
321
+ */
322
+ listBlobs(options) {
323
+ return new Promise((resolve, reject) => {
324
+ this._socket.emit(
325
+ "listBlobs",
326
+ options || {},
327
+ (error, result) => {
328
+ if (error) reject(error);
329
+ else resolve(result);
330
+ }
331
+ );
332
+ });
333
+ }
334
+ // ...........................................................................
335
+ /**
336
+ * Generates a signed URL for temporary blob access.
337
+ * @param blobId - The unique identifier of the blob
338
+ * @param expiresIn - Expiration time in seconds
339
+ * @param permissions - Permissions for the URL
340
+ * @returns Promise resolving to signed URL
341
+ */
342
+ generateSignedUrl(blobId, expiresIn, permissions) {
343
+ return new Promise((resolve, reject) => {
344
+ this._socket.emit(
345
+ "generateSignedUrl",
346
+ blobId,
347
+ expiresIn,
348
+ permissions,
349
+ (error, url) => {
350
+ if (error) reject(error);
351
+ else resolve(url);
352
+ }
353
+ );
354
+ });
355
+ }
356
+ }
357
+ class PeerSocketMock {
358
+ constructor(_bs) {
359
+ this._bs = _bs;
360
+ }
361
+ _listenersMap = /* @__PURE__ */ new Map();
362
+ connected = false;
363
+ disconnected = true;
364
+ // ............................................................................
365
+ /**
366
+ * Removes a specific listener for the specified event.
367
+ * @param eventName - The event name
368
+ * @param listener - The listener function to remove
369
+ * @returns This socket instance for chaining
370
+ */
371
+ off(eventName, listener) {
372
+ const listeners = this._listenersMap.get(eventName) || [];
373
+ const index = listeners.indexOf(listener);
374
+ if (index !== -1) {
375
+ listeners.splice(index, 1);
376
+ this._listenersMap.set(eventName, listeners);
377
+ }
378
+ return this;
379
+ }
380
+ // ............................................................................
381
+ /**
382
+ * Removes all listeners for the specified event, or all listeners if no event is specified.
383
+ * @param eventName - Optional event name
384
+ * @returns This socket instance for chaining
385
+ */
386
+ removeAllListeners(eventName) {
387
+ if (eventName) {
388
+ this._listenersMap.delete(eventName);
389
+ } else {
390
+ this._listenersMap.clear();
391
+ }
392
+ return this;
393
+ }
394
+ // ............................................................................
395
+ /**
396
+ * Registers an event listener for the specified event.
397
+ * @param eventName - The event name
398
+ * @param listener - The listener function to register
399
+ * @returns This socket instance for chaining
400
+ */
401
+ on(eventName, listener) {
402
+ if (!this._listenersMap.has(eventName)) {
403
+ this._listenersMap.set(eventName, []);
404
+ }
405
+ this._listenersMap.get(eventName).push(listener);
406
+ return this;
407
+ }
408
+ // ...........................................................................
409
+ /**
410
+ * Simulates a connection event.
411
+ */
412
+ connect() {
413
+ this.connected = true;
414
+ this.disconnected = false;
415
+ const listeners = this._listenersMap.get("connect") || [];
416
+ for (const cb of listeners) {
417
+ cb({});
418
+ }
419
+ return this;
420
+ }
421
+ // ...........................................................................
422
+ /**
423
+ * Simulates a disconnection event.
424
+ */
425
+ disconnect() {
426
+ this.connected = false;
427
+ this.disconnected = true;
428
+ const listeners = this._listenersMap.get("disconnect") || [];
429
+ for (const cb of listeners) {
430
+ cb({});
431
+ }
432
+ return this;
433
+ }
434
+ // ............................................................................
435
+ /**
436
+ * Emits an event, invoking the corresponding method on the Bs instance.
437
+ * @param eventName - The event name
438
+ * @param args - Event arguments
439
+ * @returns True if the event was handled
440
+ */
441
+ emit(eventName, ...args) {
442
+ const fn = this._bs[eventName];
443
+ if (typeof fn !== "function") {
444
+ throw new Error(`Event ${eventName.toString()} not supported`);
445
+ }
446
+ const cb = args[args.length - 1];
447
+ fn.apply(this._bs, args.slice(0, -1)).then((result) => {
448
+ cb(null, result);
449
+ }).catch((err) => {
450
+ cb(err);
451
+ });
452
+ return true;
453
+ }
454
+ }
455
+ class BsMulti {
456
+ constructor(_stores) {
457
+ this._stores = _stores;
458
+ }
459
+ // ...........................................................................
460
+ /**
461
+ * Initializes the BsMulti by assigning IDs to all underlying Bs instances.
462
+ * All underlying Bs instances must already be initialized.
463
+ */
464
+ async init() {
465
+ for (let idx = 0; idx < this._stores.length; idx++) {
466
+ this._stores[idx] = { ...this._stores[idx], id: `bs-${idx}` };
467
+ }
468
+ return Promise.resolve();
469
+ }
470
+ // ...........................................................................
471
+ /**
472
+ * Stores a blob in all writable Bs instances in parallel.
473
+ * @param content - The blob content to store
474
+ * @returns Promise resolving to blob properties from the first successful write
475
+ */
476
+ async setBlob(content) {
477
+ if (this.writables.length === 0) {
478
+ throw new Error("No writable Bs available");
479
+ }
480
+ const writes = this.writables.map(({ bs }) => bs.setBlob(content));
481
+ const results = await Promise.all(writes);
482
+ return results[0];
483
+ }
484
+ // ...........................................................................
485
+ /**
486
+ * Retrieves a blob from the highest priority readable Bs instance.
487
+ * Hot-swaps the blob to all writable instances for caching.
488
+ * @param blobId - The blob identifier
489
+ * @param options - Download options
490
+ * @returns Promise resolving to blob content and properties
491
+ */
492
+ async getBlob(blobId, options) {
493
+ if (this.readables.length === 0) {
494
+ throw new Error("No readable Bs available");
495
+ }
496
+ let result;
497
+ let readFrom = "";
498
+ const errors = [];
499
+ for (const readable of this.readables) {
500
+ try {
501
+ result = await readable.bs.getBlob(blobId, options);
502
+ readFrom = readable.id ?? "";
503
+ break;
504
+ } catch (e) {
505
+ errors.push(e);
506
+ continue;
507
+ }
508
+ }
509
+ if (!result) {
510
+ const notFoundErrors = errors.filter(
511
+ (err) => err.message.includes("Blob not found")
512
+ );
513
+ if (notFoundErrors.length === errors.length) {
514
+ throw new Error(`Blob not found: ${blobId}`);
515
+ } else {
516
+ throw errors[0];
517
+ }
518
+ }
519
+ if (this.writables.length > 0) {
520
+ const hotSwapWrites = this.writables.filter((writable) => writable.id !== readFrom).map(({ bs }) => bs.setBlob(result.content).catch(() => {
521
+ }));
522
+ await Promise.all(hotSwapWrites);
523
+ }
524
+ return result;
525
+ }
526
+ // ...........................................................................
527
+ /**
528
+ * Retrieves a blob as a ReadableStream from the highest priority readable Bs instance.
529
+ * @param blobId - The blob identifier
530
+ * @returns Promise resolving to a ReadableStream
531
+ */
532
+ async getBlobStream(blobId) {
533
+ if (this.readables.length === 0) {
534
+ throw new Error("No readable Bs available");
535
+ }
536
+ const errors = [];
537
+ for (const readable of this.readables) {
538
+ try {
539
+ return await readable.bs.getBlobStream(blobId);
540
+ } catch (e) {
541
+ errors.push(e);
542
+ continue;
543
+ }
544
+ }
545
+ const notFoundErrors = errors.filter(
546
+ (err) => err.message.includes("Blob not found")
547
+ );
548
+ if (notFoundErrors.length === errors.length) {
549
+ throw new Error(`Blob not found: ${blobId}`);
550
+ } else {
551
+ throw errors[0];
552
+ }
553
+ }
554
+ // ...........................................................................
555
+ /**
556
+ * Deletes a blob from all writable Bs instances in parallel.
557
+ * @param blobId - The blob identifier
558
+ */
559
+ async deleteBlob(blobId) {
560
+ if (this.writables.length === 0) {
561
+ throw new Error("No writable Bs available");
562
+ }
563
+ const deletes = this.writables.map(({ bs }) => bs.deleteBlob(blobId));
564
+ await Promise.all(deletes);
565
+ }
566
+ // ...........................................................................
567
+ /**
568
+ * Checks if a blob exists in any readable Bs instance.
569
+ * @param blobId - The blob identifier
570
+ * @returns Promise resolving to true if blob exists in any readable
571
+ */
572
+ async blobExists(blobId) {
573
+ if (this.readables.length === 0) {
574
+ throw new Error("No readable Bs available");
575
+ }
576
+ for (const readable of this.readables) {
577
+ try {
578
+ const exists = await readable.bs.blobExists(blobId);
579
+ if (exists) {
580
+ return true;
581
+ }
582
+ } catch {
583
+ continue;
584
+ }
585
+ }
586
+ return false;
587
+ }
588
+ // ...........................................................................
589
+ /**
590
+ * Gets blob properties from the highest priority readable Bs instance.
591
+ * @param blobId - The blob identifier
592
+ * @returns Promise resolving to blob properties
593
+ */
594
+ async getBlobProperties(blobId) {
595
+ if (this.readables.length === 0) {
596
+ throw new Error("No readable Bs available");
597
+ }
598
+ const errors = [];
599
+ for (const readable of this.readables) {
600
+ try {
601
+ return await readable.bs.getBlobProperties(blobId);
602
+ } catch (e) {
603
+ errors.push(e);
604
+ continue;
605
+ }
606
+ }
607
+ const notFoundErrors = errors.filter(
608
+ (err) => err.message.includes("Blob not found")
609
+ );
610
+ if (notFoundErrors.length === errors.length) {
611
+ throw new Error(`Blob not found: ${blobId}`);
612
+ } else {
613
+ throw errors[0];
614
+ }
615
+ }
616
+ // ...........................................................................
617
+ /**
618
+ * Lists blobs by merging results from all readable Bs instances.
619
+ * Deduplicates by blobId (content-addressable).
620
+ * @param options - Listing options
621
+ * @returns Promise resolving to list of blobs
622
+ */
623
+ async listBlobs(options) {
624
+ if (this.readables.length === 0) {
625
+ throw new Error("No readable Bs available");
626
+ }
627
+ const blobMap = /* @__PURE__ */ new Map();
628
+ for (const readable of this.readables) {
629
+ try {
630
+ let continuationToken2;
631
+ do {
632
+ const result = await readable.bs.listBlobs({
633
+ prefix: options?.prefix,
634
+ // Apply prefix filter during collection
635
+ continuationToken: continuationToken2,
636
+ maxResults: 1e3
637
+ // Fetch in chunks from each store
638
+ });
639
+ for (const blob of result.blobs) {
640
+ if (!blobMap.has(blob.blobId)) {
641
+ blobMap.set(blob.blobId, blob);
642
+ }
643
+ }
644
+ continuationToken2 = result.continuationToken;
645
+ } while (continuationToken2);
646
+ } catch {
647
+ continue;
648
+ }
649
+ }
650
+ const blobs = Array.from(blobMap.values());
651
+ blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
652
+ const maxResults = options?.maxResults ?? blobs.length;
653
+ let startIndex = 0;
654
+ if (options?.continuationToken) {
655
+ const tokenIndex = blobs.findIndex(
656
+ (blob) => blob.blobId === options.continuationToken
657
+ );
658
+ startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
659
+ }
660
+ const endIndex = Math.min(startIndex + maxResults, blobs.length);
661
+ const pageBlobs = blobs.slice(startIndex, endIndex);
662
+ const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
663
+ return {
664
+ blobs: pageBlobs,
665
+ continuationToken
666
+ };
667
+ }
668
+ // ...........................................................................
669
+ /**
670
+ * Generates a signed URL from the highest priority readable Bs instance.
671
+ * @param blobId - The blob identifier
672
+ * @param expiresIn - Expiration time in seconds
673
+ * @param permissions - Access permissions
674
+ * @returns Promise resolving to signed URL
675
+ */
676
+ async generateSignedUrl(blobId, expiresIn, permissions = "read") {
677
+ if (this.readables.length === 0) {
678
+ throw new Error("No readable Bs available");
679
+ }
680
+ const errors = [];
681
+ for (const readable of this.readables) {
682
+ try {
683
+ return await readable.bs.generateSignedUrl(
684
+ blobId,
685
+ expiresIn,
686
+ permissions
687
+ );
688
+ } catch (e) {
689
+ errors.push(e);
690
+ continue;
691
+ }
692
+ }
693
+ const notFoundErrors = errors.filter(
694
+ (err) => err.message.includes("Blob not found")
695
+ );
696
+ if (notFoundErrors.length === errors.length) {
697
+ throw new Error(`Blob not found: ${blobId}`);
698
+ } else {
699
+ throw errors[0];
700
+ }
701
+ }
702
+ // ...........................................................................
703
+ /**
704
+ * Gets the list of underlying readable Bs instances, sorted by priority.
705
+ */
706
+ get readables() {
707
+ return this._stores.filter((store) => store.read).sort((a, b) => a.priority - b.priority);
708
+ }
709
+ // ...........................................................................
710
+ /**
711
+ * Gets the list of underlying writable Bs instances, sorted by priority.
712
+ */
713
+ get writables() {
714
+ return this._stores.filter((store) => store.write).sort((a, b) => a.priority - b.priority);
715
+ }
716
+ // ...........................................................................
717
+ /**
718
+ * Example: Local cache (BsMem) + Remote server (BsPeer)
719
+ */
720
+ static example = async () => {
721
+ const bsRemoteMem = new BsMem();
722
+ const bsRemoteSocket = new PeerSocketMock(bsRemoteMem);
723
+ const bsRemote = new BsPeer(bsRemoteSocket);
724
+ await bsRemote.init();
725
+ const bsLocal = new BsMem();
726
+ const stores = [
727
+ { bs: bsLocal, priority: 0, read: true, write: true },
728
+ // Cache first
729
+ { bs: bsRemote, priority: 1, read: true, write: false }
730
+ // Remote fallback
731
+ ];
732
+ const bsMulti = new BsMulti(stores);
733
+ await bsMulti.init();
734
+ return bsMulti;
735
+ };
736
+ }
737
+ class BsPeerBridge {
738
+ constructor(_bs, _socket) {
739
+ this._bs = _bs;
740
+ this._socket = _socket;
741
+ }
742
+ _eventHandlers = /* @__PURE__ */ new Map();
743
+ _handleConnectBound = this._handleConnect.bind(this);
744
+ _handleDisconnectBound = this._handleDisconnect.bind(this);
745
+ /**
746
+ * Starts the bridge by setting up connection event handlers and
747
+ * automatically registering all Bs methods.
748
+ */
749
+ start() {
750
+ this._socket.on("connect", this._handleConnectBound);
751
+ this._socket.on("disconnect", this._handleDisconnectBound);
752
+ this._registerBsMethods();
753
+ }
754
+ /**
755
+ * Stops the bridge by removing all event handlers.
756
+ */
757
+ stop() {
758
+ this._socket.off("connect", this._handleConnectBound);
759
+ this._socket.off("disconnect", this._handleDisconnectBound);
760
+ for (const [eventName, handler] of this._eventHandlers) {
761
+ this._socket.off(eventName, handler);
762
+ }
763
+ this._eventHandlers.clear();
764
+ }
765
+ /**
766
+ * Automatically registers all Bs interface methods as socket event handlers.
767
+ */
768
+ _registerBsMethods() {
769
+ const bsMethods = [
770
+ "getBlob",
771
+ "getBlobStream",
772
+ "blobExists",
773
+ "getBlobProperties",
774
+ "listBlobs"
775
+ ];
776
+ for (const methodName of bsMethods) {
777
+ this.registerEvent(methodName);
778
+ }
779
+ }
780
+ /**
781
+ * Registers a socket event to be translated to a Bs method call.
782
+ * @param eventName - The socket event name (should match a Bs method name)
783
+ * @param bsMethodName - (Optional) The Bs method name if different from eventName
784
+ */
785
+ registerEvent(eventName, bsMethodName) {
786
+ const methodName = bsMethodName || eventName;
787
+ const handler = (...args) => {
788
+ const callback = args[args.length - 1];
789
+ const methodArgs = args.slice(0, -1);
790
+ const bsMethod = this._bs[methodName];
791
+ if (typeof bsMethod !== "function") {
792
+ const error = new Error(
793
+ `Method "${methodName}" not found on Bs instance`
794
+ );
795
+ if (typeof callback === "function") {
796
+ callback(error, null);
797
+ }
798
+ return;
799
+ }
800
+ bsMethod.apply(this._bs, methodArgs).then((result) => {
801
+ if (typeof callback === "function") {
802
+ callback(null, result);
803
+ }
804
+ }).catch((error) => {
805
+ if (typeof callback === "function") {
806
+ callback(error, null);
807
+ }
808
+ });
809
+ };
810
+ this._eventHandlers.set(eventName, handler);
811
+ this._socket.on(eventName, handler);
812
+ }
813
+ /**
814
+ * Registers multiple socket events at once.
815
+ * @param eventNames - Array of event names to register
816
+ */
817
+ registerEvents(eventNames) {
818
+ for (const eventName of eventNames) {
819
+ this.registerEvent(eventName);
820
+ }
821
+ }
822
+ /**
823
+ * Unregisters a socket event handler.
824
+ * @param eventName - The event name to unregister
825
+ */
826
+ unregisterEvent(eventName) {
827
+ const handler = this._eventHandlers.get(eventName);
828
+ if (handler) {
829
+ this._socket.off(eventName, handler);
830
+ this._eventHandlers.delete(eventName);
831
+ }
832
+ }
833
+ /**
834
+ * Emits a result back through the socket.
835
+ * @param eventName - The event name to emit
836
+ * @param data - The data to send
837
+ */
838
+ emitToSocket(eventName, ...data) {
839
+ this._socket.emit(eventName, ...data);
840
+ }
841
+ /**
842
+ * Calls a Bs method directly and emits the result through the socket.
843
+ * @param bsMethodName - The Bs method to call
844
+ * @param socketEventName - The socket event to emit with the result
845
+ * @param args - Arguments to pass to the Bs method
846
+ */
847
+ async callBsAndEmit(bsMethodName, socketEventName, ...args) {
848
+ try {
849
+ const bsMethod = this._bs[bsMethodName];
850
+ if (typeof bsMethod !== "function") {
851
+ throw new Error(`Method "${bsMethodName}" not found on Bs instance`);
852
+ }
853
+ const result = await bsMethod.apply(this._bs, args);
854
+ this._socket.emit(socketEventName, null, result);
855
+ } catch (error) {
856
+ this._socket.emit(socketEventName, error, null);
857
+ }
858
+ }
859
+ /* v8 ignore next -- @preserve */
860
+ _handleConnect() {
861
+ }
862
+ /* v8 ignore next -- @preserve */
863
+ _handleDisconnect() {
864
+ }
865
+ /**
866
+ * Gets the current socket instance.
867
+ */
868
+ get socket() {
869
+ return this._socket;
870
+ }
871
+ /**
872
+ * Gets the current Bs instance.
873
+ */
874
+ get bs() {
875
+ return this._bs;
876
+ }
877
+ /**
878
+ * Returns whether the socket is currently connected.
879
+ */
880
+ get isConnected() {
881
+ return this._socket.connected;
882
+ }
883
+ }
884
+ class BsServer {
885
+ constructor(_bs) {
886
+ this._bs = _bs;
887
+ }
888
+ _sockets = [];
889
+ // ...........................................................................
890
+ /**
891
+ * Adds a socket to the BsServer instance.
892
+ * @param socket - The socket to add.
893
+ */
894
+ async addSocket(socket) {
895
+ await this._addTransportLayer(socket);
896
+ this._sockets.push(socket);
897
+ }
898
+ // ...........................................................................
899
+ /**
900
+ * Removes a socket from the BsServer instance.
901
+ * @param socket - The socket to remove.
902
+ */
903
+ removeSocket(socket) {
904
+ this._sockets = this._sockets.filter((s) => s !== socket);
905
+ }
906
+ // ...........................................................................
907
+ /**
908
+ * Adds a transport layer to the given socket.
909
+ * @param socket - The socket to add the transport layer to.
910
+ */
911
+ async _addTransportLayer(socket) {
912
+ const methods = this._generateTransportLayer(this._bs);
913
+ for (const [key, fn] of Object.entries(methods)) {
914
+ socket.on(key, (...args) => {
915
+ const cb = args[args.length - 1];
916
+ fn.apply(this, args.slice(0, -1)).then((result) => {
917
+ cb(null, result);
918
+ }).catch((err) => {
919
+ cb(err);
920
+ });
921
+ });
922
+ }
923
+ }
924
+ // ...........................................................................
925
+ /**
926
+ * Generates a transport layer object for the given Bs instance.
927
+ * @param bs - The Bs instance to generate the transport layer for.
928
+ * @returns An object containing methods that correspond to the Bs interface.
929
+ */
930
+ _generateTransportLayer = (bs) => ({
931
+ setBlob: (content) => bs.setBlob(content),
932
+ getBlob: (blobId, options) => bs.getBlob(blobId, options),
933
+ getBlobStream: (blobId) => bs.getBlobStream(blobId),
934
+ deleteBlob: (blobId) => bs.deleteBlob(blobId),
935
+ blobExists: (blobId) => bs.blobExists(blobId),
936
+ getBlobProperties: (blobId) => bs.getBlobProperties(blobId),
937
+ listBlobs: (options) => bs.listBlobs(options),
938
+ generateSignedUrl: (blobId, expiresIn, permissions) => bs.generateSignedUrl(blobId, expiresIn, permissions)
939
+ });
940
+ }
941
+ class SocketMock {
942
+ connected = false;
943
+ disconnected = true;
944
+ _listeners = /* @__PURE__ */ new Map();
945
+ _onceListeners = /* @__PURE__ */ new Map();
946
+ connect() {
947
+ if (!this.connected) {
948
+ this.connected = true;
949
+ this.disconnected = false;
950
+ this.emit("connect");
951
+ }
952
+ }
953
+ disconnect() {
954
+ if (this.connected) {
955
+ this.connected = false;
956
+ this.disconnected = true;
957
+ this.emit("disconnect");
958
+ }
959
+ }
960
+ on(eventName, listener) {
961
+ if (!this._listeners.has(eventName)) {
962
+ this._listeners.set(eventName, []);
963
+ }
964
+ this._listeners.get(eventName).push(listener);
965
+ return this;
966
+ }
967
+ once(eventName, listener) {
968
+ if (!this._onceListeners.has(eventName)) {
969
+ this._onceListeners.set(eventName, []);
970
+ }
971
+ this._onceListeners.get(eventName).push(listener);
972
+ return this;
973
+ }
974
+ off(eventName, listener) {
975
+ if (listener) {
976
+ const regularListeners = this._listeners.get(eventName);
977
+ if (regularListeners) {
978
+ const index = regularListeners.indexOf(listener);
979
+ if (index > -1) {
980
+ regularListeners.splice(index, 1);
981
+ }
982
+ }
983
+ const onceListeners = this._onceListeners.get(eventName);
984
+ if (onceListeners) {
985
+ const index = onceListeners.indexOf(listener);
986
+ if (index > -1) {
987
+ onceListeners.splice(index, 1);
988
+ }
989
+ }
990
+ } else {
991
+ this._listeners.delete(eventName);
992
+ this._onceListeners.delete(eventName);
993
+ }
994
+ return this;
995
+ }
996
+ emit(eventName, ...args) {
997
+ let hasListeners = false;
998
+ const regularListeners = this._listeners.get(eventName);
999
+ if (regularListeners && regularListeners.length > 0) {
1000
+ hasListeners = true;
1001
+ [...regularListeners].forEach((listener) => {
1002
+ try {
1003
+ listener(...args);
1004
+ } catch (error) {
1005
+ console.error(
1006
+ `Error in listener for event ${String(eventName)}:`,
1007
+ error
1008
+ );
1009
+ }
1010
+ });
1011
+ }
1012
+ const onceListeners = this._onceListeners.get(eventName);
1013
+ if (onceListeners && onceListeners.length > 0) {
1014
+ hasListeners = true;
1015
+ const listenersToCall = [...onceListeners];
1016
+ this._onceListeners.delete(eventName);
1017
+ listenersToCall.forEach((listener) => {
1018
+ try {
1019
+ listener(...args);
1020
+ } catch (error) {
1021
+ console.error(
1022
+ `Error in once listener for event ${String(eventName)}:`,
1023
+ error
1024
+ );
1025
+ }
1026
+ });
1027
+ }
1028
+ return hasListeners;
1029
+ }
1030
+ removeAllListeners(eventName) {
1031
+ if (eventName !== void 0) {
1032
+ this._listeners.delete(eventName);
1033
+ this._onceListeners.delete(eventName);
1034
+ } else {
1035
+ this._listeners.clear();
1036
+ this._onceListeners.clear();
1037
+ }
1038
+ return this;
1039
+ }
1040
+ listenerCount(eventName) {
1041
+ const regularCount = this._listeners.get(eventName)?.length || 0;
1042
+ const onceCount = this._onceListeners.get(eventName)?.length || 0;
1043
+ return regularCount + onceCount;
1044
+ }
1045
+ listeners(eventName) {
1046
+ const regularListeners = this._listeners.get(eventName) || [];
1047
+ const onceListeners = this._onceListeners.get(eventName) || [];
1048
+ return [...regularListeners, ...onceListeners];
1049
+ }
1050
+ eventNames() {
1051
+ const allEvents = /* @__PURE__ */ new Set([
1052
+ ...this._listeners.keys(),
1053
+ ...this._onceListeners.keys()
1054
+ ]);
1055
+ return Array.from(allEvents);
1056
+ }
1057
+ // Test helper methods
1058
+ reset() {
1059
+ this.connected = false;
1060
+ this.disconnected = true;
1061
+ this.removeAllListeners();
1062
+ }
1063
+ simulateError(error) {
1064
+ this.emit("error", error);
1065
+ }
1066
+ simulateMessage(message) {
1067
+ this.emit("message", message);
1068
+ }
1069
+ // Get internal state for testing
1070
+ getListeners() {
1071
+ return new Map(this._listeners);
1072
+ }
1073
+ getOnceListeners() {
1074
+ return new Map(this._onceListeners);
1075
+ }
1076
+ }
1077
+ function createSocketPair() {
1078
+ const socketA = new DirectionalSocketMock();
1079
+ const socketB = new DirectionalSocketMock();
1080
+ socketA._setPeer(socketB);
1081
+ socketB._setPeer(socketA);
1082
+ return [socketA, socketB];
1083
+ }
1084
+ class DirectionalSocketMock {
1085
+ connected = false;
1086
+ disconnected = true;
1087
+ _peer;
1088
+ _listeners = /* @__PURE__ */ new Map();
1089
+ _onceListeners = /* @__PURE__ */ new Map();
1090
+ _setPeer(peer) {
1091
+ this._peer = peer;
1092
+ }
1093
+ connect() {
1094
+ if (!this.connected) {
1095
+ this.connected = true;
1096
+ this.disconnected = false;
1097
+ this._triggerLocal("connect");
1098
+ if (this._peer) {
1099
+ this._peer._triggerLocal("connect");
1100
+ }
1101
+ }
1102
+ }
1103
+ disconnect() {
1104
+ if (this.connected) {
1105
+ this.connected = false;
1106
+ this.disconnected = true;
1107
+ this._triggerLocal("disconnect");
1108
+ if (this._peer) {
1109
+ this._peer._triggerLocal("disconnect");
1110
+ }
1111
+ }
1112
+ }
1113
+ on(eventName, listener) {
1114
+ if (!this._listeners.has(eventName)) {
1115
+ this._listeners.set(eventName, []);
1116
+ }
1117
+ this._listeners.get(eventName).push(listener);
1118
+ return this;
1119
+ }
1120
+ once(eventName, listener) {
1121
+ if (!this._onceListeners.has(eventName)) {
1122
+ this._onceListeners.set(eventName, []);
1123
+ }
1124
+ this._onceListeners.get(eventName).push(listener);
1125
+ return this;
1126
+ }
1127
+ off(eventName, listener) {
1128
+ if (listener) {
1129
+ const regularListeners = this._listeners.get(eventName);
1130
+ if (regularListeners) {
1131
+ const index = regularListeners.indexOf(listener);
1132
+ if (index > -1) regularListeners.splice(index, 1);
1133
+ }
1134
+ const onceListeners = this._onceListeners.get(eventName);
1135
+ if (onceListeners) {
1136
+ const index = onceListeners.indexOf(listener);
1137
+ if (index > -1) onceListeners.splice(index, 1);
1138
+ }
1139
+ } else {
1140
+ this._listeners.delete(eventName);
1141
+ this._onceListeners.delete(eventName);
1142
+ }
1143
+ return this;
1144
+ }
1145
+ /**
1146
+ * Emits an event to the PEER socket (cross-socket emission).
1147
+ * This is the key difference from SocketMock - emit() sends to the other side,
1148
+ * not to local listeners.
1149
+ *
1150
+ * Implements Socket.IO acknowledgement pattern: the last argument can be a callback
1151
+ * that the peer will invoke to send a response back.
1152
+ * @param eventName - The event name to emit
1153
+ * @param args - Arguments to pass with the event
1154
+ */
1155
+ emit(eventName, ...args) {
1156
+ if (!this._peer) {
1157
+ console.warn(
1158
+ `DirectionalSocketMock.emit: No peer connected for event ${String(eventName)}`
1159
+ );
1160
+ return false;
1161
+ }
1162
+ this._peer._triggerLocal(eventName, ...args);
1163
+ return true;
1164
+ }
1165
+ /**
1166
+ * Triggers listeners on THIS socket (local emission).
1167
+ * Used internally when receiving events from peer.
1168
+ * @param eventName - The event name to trigger
1169
+ * @param args - Arguments to pass to the listeners
1170
+ */
1171
+ _triggerLocal(eventName, ...args) {
1172
+ const regularListeners = this._listeners.get(eventName);
1173
+ if (regularListeners) {
1174
+ [...regularListeners].forEach((listener) => {
1175
+ try {
1176
+ listener(...args);
1177
+ } catch (error) {
1178
+ console.error(`Error in listener for ${String(eventName)}:`, error);
1179
+ }
1180
+ });
1181
+ }
1182
+ const onceListeners = this._onceListeners.get(eventName);
1183
+ if (onceListeners) {
1184
+ const listenersToCall = [...onceListeners];
1185
+ this._onceListeners.delete(eventName);
1186
+ listenersToCall.forEach((listener) => {
1187
+ try {
1188
+ listener(...args);
1189
+ } catch (error) {
1190
+ console.error(
1191
+ `Error in once listener for ${String(eventName)}:`,
1192
+ error
1193
+ );
1194
+ }
1195
+ });
1196
+ }
1197
+ }
1198
+ removeAllListeners(eventName) {
1199
+ if (eventName !== void 0) {
1200
+ this._listeners.delete(eventName);
1201
+ this._onceListeners.delete(eventName);
1202
+ } else {
1203
+ this._listeners.clear();
1204
+ this._onceListeners.clear();
1205
+ }
1206
+ return this;
1207
+ }
1208
+ listenerCount(eventName) {
1209
+ const regularCount = this._listeners.get(eventName)?.length || 0;
1210
+ const onceCount = this._onceListeners.get(eventName)?.length || 0;
1211
+ return regularCount + onceCount;
1212
+ }
1213
+ listeners(eventName) {
1214
+ const regularListeners = this._listeners.get(eventName) || [];
1215
+ const onceListeners = this._onceListeners.get(eventName) || [];
1216
+ return [...regularListeners, ...onceListeners];
1217
+ }
1218
+ eventNames() {
1219
+ const allEvents = /* @__PURE__ */ new Set([
1220
+ ...this._listeners.keys(),
1221
+ ...this._onceListeners.keys()
1222
+ ]);
1223
+ return Array.from(allEvents);
1224
+ }
1225
+ }
127
1226
  export {
128
- BsMem
1227
+ BsMem,
1228
+ BsMulti,
1229
+ BsPeer,
1230
+ BsPeerBridge,
1231
+ BsServer,
1232
+ PeerSocketMock,
1233
+ SocketMock,
1234
+ createSocketPair
129
1235
  };