@rljson/fs-agent 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4049 @@
1
+ import { BsMem as BsMem$1 } from "@rljson/bs";
2
+ import { timeId, Route, iterateTablesSync, iterateTables, throwOnInvalidTableCfg, validateRljsonAgainstTableCfg, createTreesTableCfg } from "@rljson/rljson";
3
+ import { stat, readFile, mkdir, writeFile, readdir, utimes, rm } from "fs/promises";
4
+ import { dirname, join, sep } from "path";
5
+ import { hip, hsh, hshBuffer } from "@rljson/hash";
6
+ import { watch } from "fs";
7
+ import { Db } from "@rljson/db";
8
+ import { copy, equals, merge } from "@rljson/json";
9
+ import { Readable } from "node:stream";
10
+ class FsBlobAdapter {
11
+ _bs;
12
+ constructor(bs) {
13
+ this._bs = bs || new BsMem$1();
14
+ }
15
+ /**
16
+ * Gets the blob storage instance
17
+ */
18
+ get bs() {
19
+ return this._bs;
20
+ }
21
+ /**
22
+ * Converts a file to a blob and returns metadata
23
+ * @param filePath - Absolute path to the file
24
+ * @param options - Conversion options
25
+ * @returns File metadata including blob ID
26
+ */
27
+ async fileToBlob(filePath, options = {}) {
28
+ const bs = options.bs || this._bs;
29
+ const includePath = options.includePath !== false;
30
+ const stats = await stat(filePath);
31
+ const content = await readFile(filePath);
32
+ const blobProps = await bs.setBlob(content);
33
+ const name = filePath.split("/").pop() || filePath.split("\\").pop() || "";
34
+ const metadata = {
35
+ name,
36
+ blobId: blobProps.blobId,
37
+ size: stats.size,
38
+ mtime: stats.mtime.getTime(),
39
+ path: includePath ? filePath : ""
40
+ };
41
+ return metadata;
42
+ }
43
+ /**
44
+ * Converts multiple files to blobs
45
+ * @param filePaths - Array of absolute file paths
46
+ * @param options - Conversion options
47
+ * @returns Array of file metadata
48
+ */
49
+ async filesToBlobs(filePaths, options = {}) {
50
+ const results = [];
51
+ for (const filePath of filePaths) {
52
+ const metadata = await this.fileToBlob(filePath, options);
53
+ results.push(metadata);
54
+ }
55
+ return results;
56
+ }
57
+ /**
58
+ * Writes a file from a blob using metadata
59
+ * @param metadata - File metadata including blob ID
60
+ * @param targetPath - Target path where file should be written
61
+ * @param options - Conversion options
62
+ */
63
+ async blobToFile(metadata, targetPath, options = {}) {
64
+ const bs = options.bs || this._bs;
65
+ const createDirs = options.createDirs !== false;
66
+ const preserveMtime = options.preserveMtime !== false;
67
+ const blob = await bs.getBlob(metadata.blobId);
68
+ if (createDirs) {
69
+ const dir = dirname(targetPath);
70
+ await mkdir(dir, { recursive: true });
71
+ }
72
+ await writeFile(targetPath, blob.content);
73
+ if (preserveMtime && metadata.mtime) {
74
+ const { utimes: utimes2 } = await import("fs/promises");
75
+ const mtime = new Date(metadata.mtime);
76
+ await utimes2(targetPath, mtime, mtime);
77
+ }
78
+ }
79
+ /**
80
+ * Writes multiple files from blobs
81
+ * @param metadataList - Array of file metadata
82
+ * @param targetDir - Target directory where files should be written
83
+ * @param options - Conversion options
84
+ */
85
+ async blobsToFiles(metadataList, targetDir, options = {}) {
86
+ for (const metadata of metadataList) {
87
+ const targetPath = `${targetDir}/${metadata.name}`;
88
+ await this.blobToFile(metadata, targetPath, options);
89
+ }
90
+ }
91
+ /**
92
+ * Retrieves file content from blob storage
93
+ * @param blobId - Blob ID
94
+ * @returns File content as Buffer
95
+ */
96
+ async getFileContent(blobId) {
97
+ const blob = await this._bs.getBlob(blobId);
98
+ return blob.content;
99
+ }
100
+ /**
101
+ * Checks if a blob exists in storage
102
+ * @param blobId - Blob ID to check
103
+ * @returns True if blob exists
104
+ */
105
+ async hasBlob(blobId) {
106
+ return await this._bs.blobExists(blobId);
107
+ }
108
+ /**
109
+ * Creates an example instance for testing
110
+ */
111
+ static example() {
112
+ return new FsBlobAdapter();
113
+ }
114
+ }
115
+ class FsDbAdapter {
116
+ constructor(db, treeKey) {
117
+ this.db = db;
118
+ this.treeKey = treeKey;
119
+ }
120
+ /**
121
+ * Store a filesystem tree in the database
122
+ * @param fsTree - The filesystem tree to store
123
+ * @param options - Storage options
124
+ * @returns The root tree reference
125
+ */
126
+ async storeFsTree(fsTree, options = {}) {
127
+ const { notify = false } = options;
128
+ if (!fsTree) {
129
+ throw new Error("fsTree cannot be null or undefined");
130
+ }
131
+ if (!fsTree.rootHash || typeof fsTree.rootHash !== "string") {
132
+ throw new Error(
133
+ `Invalid rootHash: expected non-empty string, got ${typeof fsTree.rootHash}`
134
+ );
135
+ }
136
+ if (!fsTree.trees || !(fsTree.trees instanceof Map)) {
137
+ throw new Error(
138
+ `Invalid trees: expected Map, got ${typeof fsTree.trees}`
139
+ );
140
+ }
141
+ if (fsTree.trees.size === 0) {
142
+ throw new Error(
143
+ "Cannot store empty tree: trees Map must contain at least one node"
144
+ );
145
+ }
146
+ if (!fsTree.trees.has(fsTree.rootHash)) {
147
+ throw new Error(
148
+ `Root hash "${fsTree.rootHash}" not found in trees Map. The tree structure may be corrupted.`
149
+ );
150
+ }
151
+ const rootTree = fsTree.trees.get(fsTree.rootHash);
152
+ const trees = Array.from(fsTree.trees.values()).filter(
153
+ (tree) => tree._hash !== fsTree.rootHash
154
+ );
155
+ trees.push(rootTree);
156
+ const treeTable = {
157
+ _type: "trees",
158
+ _data: trees
159
+ };
160
+ try {
161
+ await this.db.core.import({
162
+ [this.treeKey]: treeTable
163
+ });
164
+ } catch (error) {
165
+ throw new Error(
166
+ `Failed to import tree data into database: ${error instanceof Error ? error.message : String(error)}`
167
+ );
168
+ }
169
+ const treeRootRef = trees[trees.length - 1]._hash;
170
+ const historyRow = {
171
+ timeId: timeId(),
172
+ route: `/${this.treeKey}/${treeRootRef}`,
173
+ [`${this.treeKey}Ref`]: treeRootRef
174
+ };
175
+ const historyTable = {
176
+ _type: "insertHistory",
177
+ _data: [historyRow]
178
+ };
179
+ await this.db.core.import({
180
+ [`${this.treeKey}InsertHistory`]: historyTable
181
+ });
182
+ if (notify) {
183
+ const treeKeyRoute = Route.fromFlat(`/${this.treeKey}`);
184
+ this.db.notify.notify(treeKeyRoute, historyRow);
185
+ }
186
+ return treeRootRef;
187
+ }
188
+ /**
189
+ * Get the tree table key
190
+ */
191
+ getTreeKey() {
192
+ return this.treeKey;
193
+ }
194
+ /**
195
+ * Get the database instance
196
+ */
197
+ getDb() {
198
+ return this.db;
199
+ }
200
+ }
201
+ class FsScanner {
202
+ _rootPath;
203
+ _tree = null;
204
+ _watcher = null;
205
+ _changeCallbacks = [];
206
+ _options;
207
+ _bs;
208
+ _paused = false;
209
+ constructor(rootPath, options = {}) {
210
+ this._rootPath = rootPath;
211
+ this._options = {
212
+ ignore: options.ignore || ["node_modules", ".git", "dist", "coverage"],
213
+ maxDepth: options.maxDepth,
214
+ followSymlinks: options.followSymlinks ?? false,
215
+ bs: options.bs
216
+ };
217
+ this._bs = options.bs || new BsMem$1();
218
+ }
219
+ get tree() {
220
+ return this._tree;
221
+ }
222
+ get rootPath() {
223
+ return this._rootPath;
224
+ }
225
+ get bs() {
226
+ return this._bs;
227
+ }
228
+ async scan() {
229
+ try {
230
+ const stats = await stat(this._rootPath);
231
+ if (!stats.isDirectory()) {
232
+ throw new Error(
233
+ `Root path "${this._rootPath}" exists but is not a directory`
234
+ );
235
+ }
236
+ } catch (error) {
237
+ if (error.code === "ENOENT") {
238
+ throw new Error(
239
+ `Root path "${this._rootPath}" does not exist. Cannot scan non-existent directory.`
240
+ );
241
+ }
242
+ throw new Error(
243
+ `Cannot access root path "${this._rootPath}": ${error instanceof Error ? error.message : String(error)}`
244
+ );
245
+ }
246
+ const trees = /* @__PURE__ */ new Map();
247
+ let rootTree;
248
+ try {
249
+ rootTree = await this._scanDirectory(this._rootPath, ".", 0, trees);
250
+ } catch (error) {
251
+ throw new Error(
252
+ `Failed to scan directory "${this._rootPath}": ${error instanceof Error ? error.message : String(error)}`
253
+ );
254
+ }
255
+ hip(rootTree);
256
+ const rootHashStr = rootTree._hash;
257
+ if (!rootHashStr) {
258
+ throw new Error(
259
+ "Failed to generate hash for root tree. Tree structure may be invalid."
260
+ );
261
+ }
262
+ trees.set(rootHashStr, rootTree);
263
+ this._tree = {
264
+ rootHash: rootHashStr,
265
+ trees
266
+ };
267
+ return this._tree;
268
+ }
269
+ async _scanDirectory(absolutePath, relativePath, depth, trees) {
270
+ const stats = await stat(absolutePath);
271
+ const entries = await readdir(absolutePath, { withFileTypes: true });
272
+ const childRefs = [];
273
+ for (const entry of entries) {
274
+ if (this._shouldIgnore(entry.name)) {
275
+ continue;
276
+ }
277
+ const childPath = join(absolutePath, entry.name);
278
+ const childRelPath = relativePath === "." ? entry.name : `${relativePath}/${entry.name}`;
279
+ if (entry.isSymbolicLink() && !this._options.followSymlinks) {
280
+ continue;
281
+ }
282
+ if (this._options.maxDepth !== void 0 && depth >= this._options.maxDepth) {
283
+ continue;
284
+ }
285
+ const childStats = await stat(childPath);
286
+ if (entry.isDirectory()) {
287
+ const childTree = await this._scanDirectory(
288
+ childPath,
289
+ childRelPath,
290
+ depth + 1,
291
+ trees
292
+ );
293
+ hip(childTree);
294
+ const childHashStr = childTree._hash;
295
+ trees.set(childHashStr, childTree);
296
+ childRefs.push(childHashStr);
297
+ } else if (entry.isFile()) {
298
+ let fileContent;
299
+ try {
300
+ fileContent = await readFile(childPath);
301
+ } catch (error) {
302
+ throw new Error(
303
+ `Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
304
+ );
305
+ }
306
+ let blobProps;
307
+ try {
308
+ blobProps = await this._bs.setBlob(fileContent);
309
+ } catch (error) {
310
+ throw new Error(
311
+ `Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
312
+ );
313
+ }
314
+ if (!blobProps || !blobProps.blobId) {
315
+ throw new Error(
316
+ `Blob storage returned invalid blobId for file "${childRelPath}"`
317
+ );
318
+ }
319
+ const fileMeta = {
320
+ name: entry.name,
321
+ type: "file",
322
+ path: childPath,
323
+ relativePath: childRelPath,
324
+ size: childStats.size,
325
+ mtime: childStats.mtime.getTime(),
326
+ blobId: blobProps.blobId
327
+ // Link to content in Bs
328
+ };
329
+ const fileTree = {
330
+ id: entry.name,
331
+ isParent: false,
332
+ meta: fileMeta,
333
+ children: null
334
+ };
335
+ hip(fileTree);
336
+ const fileTreeHashStr = fileTree._hash;
337
+ trees.set(fileTreeHashStr, fileTree);
338
+ childRefs.push(fileTreeHashStr);
339
+ }
340
+ }
341
+ const dirName = relativePath === "." ? (
342
+ /* v8 ignore next -- @preserve */
343
+ this._rootPath.split(sep).pop() || ""
344
+ ) : (
345
+ /* v8 ignore next -- @preserve */
346
+ relativePath.split("/").pop() || ""
347
+ );
348
+ const dirMeta = {
349
+ name: dirName,
350
+ type: "directory",
351
+ path: absolutePath,
352
+ relativePath,
353
+ mtime: stats.mtime.getTime()
354
+ };
355
+ const dirTree = {
356
+ id: dirName,
357
+ isParent: childRefs.length > 0,
358
+ meta: dirMeta,
359
+ children: childRefs.length > 0 ? childRefs : null
360
+ };
361
+ return dirTree;
362
+ }
363
+ _shouldIgnore(name) {
364
+ if (!this._options.ignore) {
365
+ return false;
366
+ }
367
+ for (const pattern of this._options.ignore) {
368
+ if (name === pattern || name.startsWith(pattern)) {
369
+ return true;
370
+ }
371
+ }
372
+ return false;
373
+ }
374
+ async watch() {
375
+ if (this._watcher) {
376
+ throw new Error("Already watching. Call stopWatch() first.");
377
+ }
378
+ if (!this._tree) {
379
+ await this.scan();
380
+ }
381
+ this._watcher = watch(
382
+ this._rootPath,
383
+ { recursive: true },
384
+ /* v8 ignore next -- @preserve */
385
+ async (eventType, filename) => {
386
+ if (!filename) return;
387
+ if (this._shouldIgnore(filename)) {
388
+ return;
389
+ }
390
+ await this._handleFileChange(eventType, filename);
391
+ }
392
+ );
393
+ }
394
+ async _handleFileChange(_eventType, filename) {
395
+ if (this._paused) {
396
+ return;
397
+ }
398
+ const relativePath = filename.replace(/\\/g, "/");
399
+ try {
400
+ await stat(join(this._rootPath, filename));
401
+ const existingTree = this._findTreeByPath(relativePath);
402
+ if (!existingTree) {
403
+ await this.scan();
404
+ await this._notifyChange({
405
+ type: "added",
406
+ path: relativePath
407
+ });
408
+ } else {
409
+ await this.scan();
410
+ await this._notifyChange({
411
+ type: "modified",
412
+ path: relativePath
413
+ });
414
+ }
415
+ } catch {
416
+ try {
417
+ await stat(this._rootPath);
418
+ await this.scan();
419
+ await this._notifyChange({
420
+ type: "deleted",
421
+ path: relativePath
422
+ });
423
+ } catch {
424
+ this.stopWatch();
425
+ }
426
+ }
427
+ }
428
+ _findTreeByPath(relativePath) {
429
+ if (!this._tree) return void 0;
430
+ for (const tree of this._tree.trees.values()) {
431
+ const meta = tree.meta;
432
+ if (meta && meta.relativePath === relativePath) {
433
+ return tree;
434
+ }
435
+ }
436
+ return void 0;
437
+ }
438
+ async _notifyChange(change) {
439
+ if (this._paused) {
440
+ return;
441
+ }
442
+ for (const callback of this._changeCallbacks) {
443
+ await callback(change);
444
+ }
445
+ }
446
+ onChange(callback) {
447
+ this._changeCallbacks.push(callback);
448
+ }
449
+ offChange(callback) {
450
+ this._changeCallbacks = this._changeCallbacks.filter(
451
+ (cb) => cb !== callback
452
+ );
453
+ }
454
+ stopWatch() {
455
+ if (this._watcher) {
456
+ this._watcher.close();
457
+ this._watcher = null;
458
+ }
459
+ }
460
+ /**
461
+ * Temporarily pause file change notifications
462
+ * Used to prevent loops when updating filesystem from external source
463
+ */
464
+ pauseWatch() {
465
+ this._paused = true;
466
+ }
467
+ /**
468
+ * Resume file change notifications
469
+ */
470
+ resumeWatch() {
471
+ this._paused = false;
472
+ }
473
+ getTreeByHash(treeHash) {
474
+ return this._tree?.trees.get(treeHash);
475
+ }
476
+ getTreeByPath(relativePath) {
477
+ return this._findTreeByPath(relativePath);
478
+ }
479
+ getAllTrees() {
480
+ if (!this._tree) {
481
+ return [];
482
+ }
483
+ return Array.from(this._tree.trees.values());
484
+ }
485
+ getChildren(treeHash) {
486
+ const tree = this.getTreeByHash(treeHash);
487
+ if (!tree || !tree.children) {
488
+ return [];
489
+ }
490
+ return tree.children.map((childHash) => this.getTreeByHash(childHash)).filter((t) => t !== void 0);
491
+ }
492
+ getRootTree() {
493
+ if (!this._tree) return void 0;
494
+ return this._tree.trees.get(this._tree.rootHash);
495
+ }
496
+ static example() {
497
+ return new FsScanner(process.cwd(), {
498
+ ignore: ["node_modules", ".git", "dist", "coverage"]
499
+ });
500
+ }
501
+ }
502
+ class FsAgent {
503
+ _scanner;
504
+ _adapter;
505
+ _rootPath;
506
+ _bs;
507
+ _db;
508
+ _treeKey;
509
+ _stopSync;
510
+ _stopSyncFromDb;
511
+ _lastSentRef;
512
+ constructor(rootPath, bs, options = {}) {
513
+ this._rootPath = rootPath;
514
+ this._bs = bs || new BsMem$1();
515
+ this._db = options.db;
516
+ this._treeKey = options.treeKey;
517
+ this._scanner = new FsScanner(rootPath, { ...options, bs: this._bs });
518
+ this._adapter = new FsBlobAdapter(this._bs);
519
+ if (this._db && this._treeKey) {
520
+ this._startAutoSync().catch(() => {
521
+ });
522
+ this._startAutoSyncFromDb(options.bidirectional || false).catch(() => {
523
+ });
524
+ }
525
+ }
526
+ /**
527
+ * Gets the root path
528
+ */
529
+ get rootPath() {
530
+ return this._rootPath;
531
+ }
532
+ /**
533
+ * Gets the blob storage instance
534
+ */
535
+ get bs() {
536
+ return this._bs;
537
+ }
538
+ /**
539
+ * Gets the scanner instance
540
+ */
541
+ get scanner() {
542
+ return this._scanner;
543
+ }
544
+ /**
545
+ * Gets the adapter instance
546
+ */
547
+ get adapter() {
548
+ return this._adapter;
549
+ }
550
+ /**
551
+ * Starts automatic syncing to database
552
+ * Note: Auto-sync requires Connector which is not available in constructor.
553
+ * Consider using syncToDb() directly instead of constructor options.
554
+ */
555
+ async _startAutoSync() {
556
+ if (!this._db || !this._treeKey) {
557
+ return;
558
+ }
559
+ throw new Error(
560
+ "Auto-sync from constructor is not supported. Use syncToDb() method directly with a Connector instance."
561
+ );
562
+ }
563
+ /**
564
+ * Starts automatic syncing from database
565
+ * @param bidirectional - Whether bidirectional sync is enabled
566
+ * Note: Auto-sync requires Connector which is not available in constructor.
567
+ * Consider using syncFromDb() directly instead of constructor options.
568
+ */
569
+ async _startAutoSyncFromDb(bidirectional) {
570
+ if (!this._db || !this._treeKey || !bidirectional) {
571
+ return;
572
+ }
573
+ throw new Error(
574
+ "Auto-sync from constructor is not supported. Use syncFromDb() method directly with a Connector instance."
575
+ );
576
+ }
577
+ /**
578
+ * Stops automatic syncing and cleans up resources
579
+ */
580
+ dispose() {
581
+ if (this._stopSync) {
582
+ this._stopSync();
583
+ this._stopSync = void 0;
584
+ }
585
+ if (this._stopSyncFromDb) {
586
+ this._stopSyncFromDb();
587
+ this._stopSyncFromDb = void 0;
588
+ }
589
+ }
590
+ /**
591
+ * Extracts filesystem into tree structure with file content in blobs
592
+ * File content is stored in Bs, tree structure returned with blobIds embedded
593
+ * @returns Tree structure with blobIds in file metadata
594
+ */
595
+ async extract() {
596
+ const tree = await this._scanner.scan();
597
+ return tree;
598
+ }
599
+ /**
600
+ * Restores filesystem from tree structure and blob storage
601
+ * @param tree - Tree structure with blobIds in file metadata
602
+ * @param targetPath - Optional target path (defaults to rootPath)
603
+ * @param options - Restore options
604
+ */
605
+ async restore(tree, targetPath, options) {
606
+ const target = targetPath || this._rootPath;
607
+ const { expectedDirs, expectedFiles } = this._collectExpectedPaths(
608
+ tree,
609
+ target
610
+ );
611
+ await this._restoreTree(tree.rootHash, tree.trees, target);
612
+ if (options?.cleanTarget) {
613
+ await this._pruneExtraneous(target, expectedDirs, expectedFiles);
614
+ }
615
+ }
616
+ /**
617
+ * Recursively restores a tree node and its children
618
+ * @param treeHash - Hash of the tree node to restore
619
+ * @param trees - Map of all tree nodes
620
+ * @param targetPath - Target directory path
621
+ */
622
+ async _restoreTree(treeHash, trees, targetPath) {
623
+ const treeNode = trees.get(treeHash);
624
+ if (!treeNode) {
625
+ throw new Error(`Tree node not found: ${treeHash}`);
626
+ }
627
+ const meta = treeNode.meta;
628
+ if (!meta) {
629
+ throw new Error(`Tree node is missing meta for hash: ${treeHash}`);
630
+ }
631
+ if (meta.type === "file") {
632
+ const filePath = join(targetPath, meta.relativePath);
633
+ if (meta.blobId) {
634
+ let fileBlob;
635
+ try {
636
+ fileBlob = await this._bs.getBlob(meta.blobId);
637
+ } catch (error) {
638
+ throw new Error(
639
+ `Failed to retrieve blob for file "${meta.relativePath}" (blobId: ${meta.blobId}): ${error instanceof Error ? error.message : String(error)}`
640
+ );
641
+ }
642
+ if (!fileBlob || !fileBlob.content) {
643
+ throw new Error(
644
+ `Missing blob content for file "${meta.relativePath}" (blobId: ${meta.blobId}). The blob may have been deleted or not synced properly.`
645
+ );
646
+ }
647
+ await mkdir(dirname(filePath), { recursive: true });
648
+ await writeFile(filePath, fileBlob.content);
649
+ if (meta.mtime) {
650
+ const mtime = new Date(meta.mtime);
651
+ await utimes(filePath, mtime, mtime);
652
+ }
653
+ }
654
+ } else if (meta.type === "directory") {
655
+ const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
656
+ await mkdir(dirPath, { recursive: true });
657
+ if (treeNode.children && Array.isArray(treeNode.children)) {
658
+ for (const childHash of treeNode.children) {
659
+ await this._restoreTree(childHash, trees, targetPath);
660
+ }
661
+ }
662
+ }
663
+ }
664
+ /**
665
+ * Gets the current tree structure
666
+ */
667
+ getTree() {
668
+ return this._scanner.tree;
669
+ }
670
+ /**
671
+ * Checks if a blob exists in storage
672
+ * @param blobId - Blob ID to check
673
+ */
674
+ async hasBlob(blobId) {
675
+ return await this._adapter.hasBlob(blobId);
676
+ }
677
+ /**
678
+ * Gets file content from blob storage
679
+ * @param blobId - Blob ID
680
+ */
681
+ async getFileContent(blobId) {
682
+ return await this._adapter.getFileContent(blobId);
683
+ }
684
+ /**
685
+ * Extracts and stores filesystem tree in database
686
+ * Reads from filesystem, stores trees in DB and blobs in Bs
687
+ * @param db - Database instance
688
+ * @param treeKey - Tree table key
689
+ * @param options - Storage options
690
+ * @returns The root tree reference
691
+ */
692
+ async storeInDb(db, treeKey, options) {
693
+ const tree = await this.extract();
694
+ if (!tree || !tree.rootHash || !tree.trees) {
695
+ throw new Error(
696
+ "Cannot store empty or invalid tree in database. Ensure the filesystem has been scanned and contains valid data."
697
+ );
698
+ }
699
+ if (tree.trees.size === 0) {
700
+ throw new Error(
701
+ "Cannot store tree with no nodes. The tree structure is empty."
702
+ );
703
+ }
704
+ const dbAdapter = new FsDbAdapter(db, treeKey);
705
+ return await dbAdapter.storeFsTree(tree, options);
706
+ }
707
+ /**
708
+ * Recursively fetches all tree nodes starting from a root hash
709
+ * Trees are stored as separate rows with parent-child relationships
710
+ * This method follows the tree structure and fetches all related nodes
711
+ * @param db - Database instance
712
+ * @param route - Route to tree table
713
+ * @param treeKey - Tree table key
714
+ * @param rootHash - Hash of the root node to start fetching from
715
+ * @returns Array of all tree nodes in the tree
716
+ */
717
+ async _fetchTreeRecursively(db, route, treeKey, rootHash) {
718
+ const fetchedNodes = /* @__PURE__ */ new Map();
719
+ const nodesToFetch = /* @__PURE__ */ new Set([rootHash]);
720
+ const processed = /* @__PURE__ */ new Set();
721
+ while (nodesToFetch.size > 0) {
722
+ const currentHash = Array.from(nodesToFetch)[0];
723
+ nodesToFetch.delete(currentHash);
724
+ if (processed.has(currentHash)) {
725
+ continue;
726
+ }
727
+ processed.add(currentHash);
728
+ let result;
729
+ try {
730
+ result = await db.get(route, { _hash: currentHash });
731
+ } catch {
732
+ continue;
733
+ }
734
+ const treeData = result?.rljson?.[treeKey];
735
+ if (!treeData || !treeData._data) {
736
+ continue;
737
+ }
738
+ const dataArray = Array.isArray(treeData._data) ? treeData._data : Object.values(treeData._data);
739
+ for (const node of dataArray) {
740
+ if (!node._hash) continue;
741
+ fetchedNodes.set(node._hash, node);
742
+ if (node.children && Array.isArray(node.children)) {
743
+ for (const childHash of node.children) {
744
+ if (typeof childHash === "string" && !processed.has(childHash)) {
745
+ nodesToFetch.add(childHash);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
751
+ return Array.from(fetchedNodes.values());
752
+ }
753
+ /**
754
+ * Loads tree from database and restores to filesystem
755
+ * Writes to filesystem from DB trees and Bs blobs
756
+ * @param db - Database instance
757
+ * @param treeKey - Tree table key
758
+ * @param rootRef - Root tree reference (hash)
759
+ * @param targetPath - Optional target path (defaults to rootPath)
760
+ * @param options - Restore options
761
+ */
762
+ async loadFromDb(db, treeKey, rootRef, targetPath, options) {
763
+ if (!rootRef || rootRef.trim() === "") {
764
+ throw new Error("rootRef cannot be empty");
765
+ }
766
+ const route = Route.fromFlat(treeKey);
767
+ const allNodes = await this._fetchTreeRecursively(
768
+ db,
769
+ route,
770
+ treeKey,
771
+ rootRef
772
+ );
773
+ if (allNodes.length === 0) {
774
+ throw new Error(
775
+ `No tree nodes found for ${treeKey}@${rootRef}. The tree may have been deleted or the reference is invalid.`
776
+ );
777
+ }
778
+ const trees = /* @__PURE__ */ new Map();
779
+ for (const treeNode of allNodes) {
780
+ if (treeNode._hash) {
781
+ trees.set(treeNode._hash, treeNode);
782
+ }
783
+ }
784
+ if (!trees.has(rootRef)) {
785
+ throw new Error(
786
+ `Root tree node "${rootRef}" not found in tree data. Available hashes: ${Array.from(trees.keys()).slice(0, 5).join(", ")}${trees.size > 5 ? "..." : ""}`
787
+ );
788
+ }
789
+ const fsTree = {
790
+ rootHash: rootRef,
791
+ trees
792
+ };
793
+ await this.restore(fsTree, targetPath, options);
794
+ }
795
+ /**
796
+ * Collects expected file and directory paths for cleanup
797
+ * @param tree - Tree structure to evaluate
798
+ * @param target - Filesystem root where the tree will be restored
799
+ */
800
+ _collectExpectedPaths(tree, target) {
801
+ const expectedDirs = /* @__PURE__ */ new Set([target]);
802
+ const expectedFiles = /* @__PURE__ */ new Set();
803
+ for (const [, node] of tree.trees) {
804
+ const meta = node?.meta;
805
+ if (!meta) {
806
+ continue;
807
+ }
808
+ if (meta.type === "directory") {
809
+ const dirPath = meta.relativePath === "." ? target : join(target, meta.relativePath);
810
+ expectedDirs.add(dirPath);
811
+ } else if (meta.type === "file") {
812
+ const filePath = join(target, meta.relativePath);
813
+ expectedFiles.add(filePath);
814
+ expectedDirs.add(dirname(filePath));
815
+ }
816
+ }
817
+ return { expectedDirs, expectedFiles };
818
+ }
819
+ /**
820
+ * Remove files/dirs not present in the expected sets
821
+ * @param currentDir - Directory currently being inspected
822
+ * @param expectedDirs - Allowed directory paths
823
+ * @param expectedFiles - Allowed file paths
824
+ */
825
+ async _pruneExtraneous(currentDir, expectedDirs, expectedFiles) {
826
+ const entries = await readdir(currentDir, { withFileTypes: true });
827
+ for (const entry of entries) {
828
+ const fullPath = join(currentDir, entry.name);
829
+ if (entry.isDirectory()) {
830
+ if (!expectedDirs.has(fullPath)) {
831
+ await rm(fullPath, { recursive: true, force: true });
832
+ continue;
833
+ }
834
+ await this._pruneExtraneous(fullPath, expectedDirs, expectedFiles);
835
+ } else {
836
+ if (!expectedFiles.has(fullPath)) {
837
+ await rm(fullPath, { force: true });
838
+ }
839
+ }
840
+ }
841
+ }
842
+ /**
843
+ * Watches filesystem for changes and syncs to database
844
+ * Uses Connector for socket-based broadcast
845
+ * @param db - Database instance
846
+ * @param connector - Connector instance for socket-based sync
847
+ * @param treeKey - Tree table key
848
+ * @param options - Storage options (e.g., notify)
849
+ * @returns Function to stop watching
850
+ */
851
+ async syncToDb(db, connector, treeKey, options) {
852
+ const initialRef = await this.storeInDb(db, treeKey, options);
853
+ if (initialRef) {
854
+ this._lastSentRef = initialRef;
855
+ connector.send(initialRef);
856
+ }
857
+ const syncCallback = async () => {
858
+ const tree = this._scanner.tree;
859
+ if (tree) {
860
+ const dbAdapter = new FsDbAdapter(db, treeKey);
861
+ const ref = await dbAdapter.storeFsTree(tree, options);
862
+ this._lastSentRef = ref;
863
+ if (ref) {
864
+ connector.send(ref);
865
+ }
866
+ }
867
+ };
868
+ this._scanner.onChange(syncCallback);
869
+ await this._scanner.watch();
870
+ return () => {
871
+ this._scanner.offChange(syncCallback);
872
+ this._scanner.stopWatch();
873
+ };
874
+ }
875
+ /**
876
+ * Watches database for tree changes and syncs to filesystem
877
+ * Uses Connector for socket-based notifications
878
+ * @param db - Database instance
879
+ * @param connector - Connector instance for socket-based sync
880
+ * @param treeKey - Tree table key
881
+ * @param restoreOptions - Restore options (e.g., cleanTarget)
882
+ * @returns Function to stop watching
883
+ */
884
+ async syncFromDb(db, connector, treeKey, restoreOptions) {
885
+ if (!this._scanner["_watcher"]) {
886
+ await this._scanner.watch();
887
+ }
888
+ const syncCallback = async (treeRef) => {
889
+ if (!treeRef || typeof treeRef !== "string") {
890
+ return;
891
+ }
892
+ if (treeRef === this._lastSentRef) {
893
+ return;
894
+ }
895
+ this._scanner.pauseWatch();
896
+ try {
897
+ await this.loadFromDb(db, treeKey, treeRef, void 0, restoreOptions);
898
+ } catch {
899
+ } finally {
900
+ this._scanner.resumeWatch();
901
+ }
902
+ };
903
+ connector.listen(syncCallback);
904
+ return () => {
905
+ connector.teardown();
906
+ };
907
+ }
908
+ /** Example instance for test purposes */
909
+ static get example() {
910
+ return new FsAgent(process.cwd());
911
+ }
912
+ }
913
+ var __defProp = Object.defineProperty;
914
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
915
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
916
+ class IsReady {
917
+ constructor() {
918
+ __publicField(this, "_state", false);
919
+ __publicField(this, "_callbacks", []);
920
+ }
921
+ static get instance() {
922
+ return new IsReady();
923
+ }
924
+ static get example() {
925
+ return new IsReady();
926
+ }
927
+ get state() {
928
+ return this._state;
929
+ }
930
+ resolve() {
931
+ this._state = true;
932
+ this._callbacks.forEach((callback) => {
933
+ callback();
934
+ });
935
+ this._callbacks = [];
936
+ }
937
+ reset() {
938
+ this._state = false;
939
+ }
940
+ get promise() {
941
+ return this._isReady();
942
+ }
943
+ _isReady() {
944
+ return new Promise((resolve) => {
945
+ if (this._state) {
946
+ resolve();
947
+ } else {
948
+ this._callbacks.push(() => {
949
+ resolve();
950
+ });
951
+ }
952
+ });
953
+ }
954
+ }
955
+ class IoTools {
956
+ /**
957
+ * Constructor
958
+ * @param io The Io interface to use
959
+ */
960
+ constructor(io) {
961
+ this.io = io;
962
+ }
963
+ /**
964
+ * Returns the table configuration of the tableCfgs table.
965
+ */
966
+ static get tableCfgsTableCfg() {
967
+ const tableCfg = hip({
968
+ _hash: "",
969
+ key: "tableCfgs",
970
+ type: "tableCfgs",
971
+ isHead: false,
972
+ isRoot: false,
973
+ isShared: true,
974
+ previous: "",
975
+ columns: [
976
+ {
977
+ key: "_hash",
978
+ type: "string",
979
+ titleShort: "Hash",
980
+ titleLong: "Row Hash"
981
+ },
982
+ {
983
+ key: "key",
984
+ type: "string",
985
+ titleShort: "Key",
986
+ titleLong: "Table Key"
987
+ },
988
+ {
989
+ key: "type",
990
+ type: "string",
991
+ titleShort: "Type",
992
+ titleLong: "Content Type"
993
+ },
994
+ {
995
+ key: "isHead",
996
+ type: "boolean",
997
+ titleShort: "Is Head",
998
+ titleLong: "Is Head Table"
999
+ },
1000
+ {
1001
+ key: "isRoot",
1002
+ type: "boolean",
1003
+ titleShort: "Is Root",
1004
+ titleLong: "Is Root Table"
1005
+ },
1006
+ {
1007
+ key: "isShared",
1008
+ type: "boolean",
1009
+ titleShort: "Is Shared",
1010
+ titleLong: "Is Shared Table"
1011
+ },
1012
+ {
1013
+ key: "previous",
1014
+ type: "string",
1015
+ titleShort: "Previous",
1016
+ titleLong: "Previous Table Configuration Hash"
1017
+ },
1018
+ {
1019
+ key: "columns",
1020
+ type: "jsonArray",
1021
+ titleShort: "Columns",
1022
+ titleLong: "Column Configurations"
1023
+ }
1024
+ ]
1025
+ });
1026
+ return tableCfg;
1027
+ }
1028
+ /**
1029
+ * Initializes the revisions table.
1030
+ */
1031
+ initRevisionsTable = async () => {
1032
+ const tableCfg = {
1033
+ key: "revisions",
1034
+ type: "revisions",
1035
+ isHead: true,
1036
+ isRoot: true,
1037
+ isShared: false,
1038
+ columns: [
1039
+ {
1040
+ key: "_hash",
1041
+ type: "string",
1042
+ titleShort: "Hash",
1043
+ titleLong: "Row Hash"
1044
+ },
1045
+ {
1046
+ key: "table",
1047
+ type: "string",
1048
+ titleShort: "Table",
1049
+ titleLong: "Table Key"
1050
+ },
1051
+ {
1052
+ key: "predecessor",
1053
+ type: "string",
1054
+ titleShort: "Predecessor",
1055
+ titleLong: "Predecessor Revision Hash"
1056
+ },
1057
+ {
1058
+ key: "successor",
1059
+ type: "string",
1060
+ titleShort: "Successor",
1061
+ titleLong: "Successor Revision Hash"
1062
+ },
1063
+ {
1064
+ key: "timestamp",
1065
+ type: "number",
1066
+ titleShort: "Timestamp",
1067
+ titleLong: "Revision Timestamp"
1068
+ },
1069
+ {
1070
+ key: "id",
1071
+ type: "string",
1072
+ titleShort: "ID",
1073
+ titleLong: "Revision ID"
1074
+ }
1075
+ ]
1076
+ };
1077
+ await this.io.createOrExtendTable({ tableCfg });
1078
+ };
1079
+ /**
1080
+ * Example object for test purposes
1081
+ * @returns An instance of io tools
1082
+ */
1083
+ static example = async () => {
1084
+ const io = await IoMem.example();
1085
+ await io.init();
1086
+ await io.isReady();
1087
+ return new IoTools(io);
1088
+ };
1089
+ /**
1090
+ * Throws if the table does not exist
1091
+ */
1092
+ async throwWhenTableDoesNotExist(table) {
1093
+ const exists = await this.io.tableExists(table);
1094
+ if (!exists) {
1095
+ throw new Error(`Table "${table}" not found`);
1096
+ }
1097
+ }
1098
+ /**
1099
+ * Throws if any of the tables in rljson do not exist
1100
+ * @param rljson - The Rljson object to check
1101
+ */
1102
+ async throwWhenTablesDoNotExist(rljson) {
1103
+ try {
1104
+ await iterateTables(rljson, async (tableKey) => {
1105
+ const exists = await this.io.tableExists(tableKey);
1106
+ if (!exists) {
1107
+ throw new Error(`Table "${tableKey}" not found`);
1108
+ }
1109
+ });
1110
+ } catch (e) {
1111
+ const missingTables = e.map((e2) => e2.tableKey);
1112
+ throw new Error(
1113
+ `The following tables do not exist: ${missingTables.join(", ")}`
1114
+ );
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Returns the current table cfgs of all tables
1119
+ * @returns The table configuration of all tables
1120
+ */
1121
+ async tableCfgs() {
1122
+ const tables = await this.io.rawTableCfgs();
1123
+ const newestVersion = {};
1124
+ for (let i = tables.length - 1; i >= 0; i--) {
1125
+ const table = tables[i];
1126
+ const existing = newestVersion[table.key];
1127
+ if (!existing || existing.columns.length < table.columns.length) {
1128
+ newestVersion[table.key] = table;
1129
+ }
1130
+ }
1131
+ const resultData = Object.values(newestVersion).sort((a, b) => {
1132
+ if (a.key < b.key) {
1133
+ return -1;
1134
+ }
1135
+ if (a.key > b.key) {
1136
+ return 1;
1137
+ }
1138
+ return 0;
1139
+ });
1140
+ return resultData;
1141
+ }
1142
+ /**
1143
+ * Returns a list with all table names
1144
+ */
1145
+ async allTableKeys() {
1146
+ const result = (await this.tableCfgs()).map((e) => e.key);
1147
+ return result;
1148
+ }
1149
+ /**
1150
+ * Returns the configuration of a given table
1151
+ */
1152
+ async tableCfg(table) {
1153
+ const tableCfg = await this.tableCfgOrNull(table);
1154
+ if (!tableCfg) {
1155
+ throw new Error(`Table "${table}" not found`);
1156
+ }
1157
+ return tableCfg;
1158
+ }
1159
+ /**
1160
+ * Returns the configuration of a given table or null if it does not exist.
1161
+
1162
+ */
1163
+ async tableCfgOrNull(table) {
1164
+ const tableCfgs = await this.tableCfgs();
1165
+ const tableCfg = tableCfgs.find((e) => e.key === table);
1166
+ return tableCfg ?? null;
1167
+ }
1168
+ /**
1169
+ * Returns a list of all column names of a given table
1170
+ */
1171
+ async allColumnKeys(table) {
1172
+ const tableCfg = await this.tableCfg(table);
1173
+ const result = tableCfg.columns.map((column) => column.key);
1174
+ return result;
1175
+ }
1176
+ /**
1177
+ * Throws when a column does not exist in a given table
1178
+ * @param table - The table to check
1179
+ * @param columns - The column to check
1180
+ */
1181
+ async throwWhenColumnDoesNotExist(table, columns) {
1182
+ const tableCfg = await this.tableCfg(table);
1183
+ const columnKeys = tableCfg.columns.map((column) => column.key);
1184
+ const missingColumns = columns.filter(
1185
+ (column) => !columnKeys.includes(column)
1186
+ );
1187
+ if (missingColumns.length > 0) {
1188
+ throw new Error(
1189
+ `The following columns do not exist in table "${table}": ${missingColumns.join(
1190
+ ", "
1191
+ )}.`
1192
+ );
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Throws when a table update is not compatible with the current table
1197
+ * configuration.
1198
+ */
1199
+ async throwWhenTableIsNotCompatible(update) {
1200
+ const prefix = `Invalid update of table able "${update.key}"`;
1201
+ throwOnInvalidTableCfg(update);
1202
+ const existing = await this.tableCfgOrNull(update.key);
1203
+ if (existing) {
1204
+ if (existing.columns.length > update.columns.length) {
1205
+ const deletedColumnKeys = existing.columns.map((column) => column.key).filter(
1206
+ (key) => !update.columns.some((column) => column.key === key)
1207
+ );
1208
+ if (deletedColumnKeys.length > 0) {
1209
+ const deletedColumns = deletedColumnKeys.join(", ");
1210
+ throw new Error(
1211
+ `${prefix}: Columns must not be deleted. Deleted columns: ${deletedColumns}}`
1212
+ );
1213
+ }
1214
+ }
1215
+ for (let i = 0; i < existing.columns.length; i++) {
1216
+ const before = existing.columns[i].key;
1217
+ const after = update.columns[i].key;
1218
+ if (before !== after) {
1219
+ throw new Error(
1220
+ `${prefix}: Column keys must not change! Column "${before}" was renamed into "${after}".`
1221
+ );
1222
+ }
1223
+ }
1224
+ for (let i = 0; i < existing.columns.length; i++) {
1225
+ const column = existing.columns[i].key;
1226
+ const before = existing.columns[i].type;
1227
+ const after = update.columns[i].type;
1228
+ if (before !== after) {
1229
+ throw new Error(
1230
+ `${prefix}: Column types must not change! Type of column "${column}" was changed from "${before}" to ${after}.`
1231
+ );
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Throws if the data in the table do not match the table configuration
1238
+ */
1239
+ async throwWhenTableDataDoesNotMatchCfg(data) {
1240
+ const errors = [];
1241
+ await iterateTables(data, async (tableKey) => {
1242
+ const tableCfg = await this.tableCfg(tableKey);
1243
+ const table = data[tableKey];
1244
+ if (table._type === "tableCfgs") return;
1245
+ errors.push(...validateRljsonAgainstTableCfg(table._data, tableCfg));
1246
+ });
1247
+ if (errors.length > 0) {
1248
+ throw new Error(
1249
+ `Table data does not match the configuration.
1250
+
1251
+ Errors:
1252
+ ${errors.map((e) => `- ${e}`).join("\n")}`
1253
+ );
1254
+ }
1255
+ }
1256
+ /**
1257
+ * Sorts the data of a table by the hash and updates the table hash in place
1258
+ */
1259
+ sortTableDataAndUpdateHash(table) {
1260
+ table._data.sort((a, b) => {
1261
+ const hashA = a._hash;
1262
+ const hashB = b._hash;
1263
+ if (hashA < hashB) {
1264
+ return -1;
1265
+ }
1266
+ if (hashA > hashB) {
1267
+ return 1;
1268
+ }
1269
+ return 0;
1270
+ });
1271
+ table._hash = "";
1272
+ hip(table, {
1273
+ updateExistingHashes: false,
1274
+ throwOnWrongHashes: false
1275
+ });
1276
+ }
1277
+ }
1278
+ class IoMem {
1279
+ // ...........................................................................
1280
+ // Constructor & example
1281
+ init() {
1282
+ this._isOpen = true;
1283
+ return this._init();
1284
+ }
1285
+ close() {
1286
+ this._isOpen = false;
1287
+ return Promise.resolve();
1288
+ }
1289
+ get isOpen() {
1290
+ return this._isOpen;
1291
+ }
1292
+ static example = async () => {
1293
+ const io = new IoMem();
1294
+ await io.init();
1295
+ return io;
1296
+ };
1297
+ // ...........................................................................
1298
+ // General
1299
+ isReady() {
1300
+ return this._isReady.promise;
1301
+ }
1302
+ // ...........................................................................
1303
+ // Dump
1304
+ dump() {
1305
+ return this._dump();
1306
+ }
1307
+ async dumpTable(request) {
1308
+ return this._dumpTable(request);
1309
+ }
1310
+ // ...........................................................................
1311
+ // Meta Data
1312
+ async contentType(request) {
1313
+ return this._contentType(request);
1314
+ }
1315
+ // ...........................................................................
1316
+ // Rows
1317
+ readRows(request) {
1318
+ return this._readRows(request);
1319
+ }
1320
+ async rowCount(table) {
1321
+ const tableData = this._mem[table];
1322
+ if (!tableData) {
1323
+ throw new Error(`Table "${table}" not found`);
1324
+ }
1325
+ return Promise.resolve(tableData._data.length);
1326
+ }
1327
+ // ...........................................................................
1328
+ // Write
1329
+ write(request) {
1330
+ return this._write(request);
1331
+ }
1332
+ // ...........................................................................
1333
+ // Table management
1334
+ async tableExists(tableKey) {
1335
+ const table = this._mem[tableKey];
1336
+ return table ? true : false;
1337
+ }
1338
+ createOrExtendTable(request) {
1339
+ return this._createOrExtendTable(request);
1340
+ }
1341
+ async rawTableCfgs() {
1342
+ const tables = this._mem.tableCfgs._data;
1343
+ return tables;
1344
+ }
1345
+ // ######################
1346
+ // Private
1347
+ // ######################
1348
+ _ioTools;
1349
+ _isReady = new IsReady();
1350
+ _isOpen = false;
1351
+ _mem = hip({});
1352
+ // ...........................................................................
1353
+ async _init() {
1354
+ this._ioTools = new IoTools(this);
1355
+ this._initTableCfgs();
1356
+ this._updateGlobalHash();
1357
+ await this._ioTools.initRevisionsTable();
1358
+ hsh(this._mem);
1359
+ this._isReady.resolve();
1360
+ }
1361
+ // ...........................................................................
1362
+ _initTableCfgs = () => {
1363
+ const tableCfg = IoTools.tableCfgsTableCfg;
1364
+ this._mem.tableCfgs = hip({
1365
+ _type: "tableCfgs",
1366
+ _data: [tableCfg],
1367
+ _tableCfg: tableCfg._hash
1368
+ });
1369
+ };
1370
+ // ...........................................................................
1371
+ _updateGlobalHash() {
1372
+ this._mem._hash = "";
1373
+ hip(this._mem, {
1374
+ updateExistingHashes: false
1375
+ });
1376
+ }
1377
+ // ...........................................................................
1378
+ _updateTableHash(tableKey) {
1379
+ const table = this._mem[tableKey];
1380
+ table._hash = "";
1381
+ hip(table, { updateExistingHashes: false });
1382
+ }
1383
+ // ...........................................................................
1384
+ async _createOrExtendTable(request) {
1385
+ const tableCfg = request.tableCfg;
1386
+ await this._ioTools.throwWhenTableIsNotCompatible(tableCfg);
1387
+ const { key } = tableCfg;
1388
+ const newConfig = hsh(tableCfg);
1389
+ const existingConfig = await this._ioTools.tableCfgOrNull(key);
1390
+ if (!existingConfig) {
1391
+ this._createTable(newConfig, key);
1392
+ } else {
1393
+ this._extendTable(existingConfig, newConfig);
1394
+ }
1395
+ }
1396
+ // ...........................................................................
1397
+ _createTable(newConfig, tableKey) {
1398
+ newConfig = hsh(newConfig);
1399
+ this._mem.tableCfgs._data.push(newConfig);
1400
+ this._ioTools.sortTableDataAndUpdateHash(this._mem.tableCfgs);
1401
+ const table = {
1402
+ _type: newConfig.type,
1403
+ _data: [],
1404
+ _tableCfg: newConfig._hash
1405
+ };
1406
+ this._mem[tableKey] ??= hip(table);
1407
+ this._updateTableHash(tableKey);
1408
+ this._updateGlobalHash();
1409
+ }
1410
+ // ...........................................................................
1411
+ _extendTable(existingConfig, newConfig) {
1412
+ if (existingConfig.columns.length === newConfig.columns.length) {
1413
+ return;
1414
+ }
1415
+ newConfig = hsh(newConfig);
1416
+ this._mem.tableCfgs._data.push(newConfig);
1417
+ this._ioTools.sortTableDataAndUpdateHash(this._mem.tableCfgs);
1418
+ const table = this._mem[newConfig.key];
1419
+ table._tableCfg = newConfig._hash;
1420
+ this._updateTableHash(newConfig.key);
1421
+ this._updateGlobalHash();
1422
+ }
1423
+ // ...........................................................................
1424
+ async _dump() {
1425
+ return copy(this._mem);
1426
+ }
1427
+ // ...........................................................................
1428
+ async _dumpTable(request) {
1429
+ await this._ioTools.throwWhenTableDoesNotExist(request.table);
1430
+ const table = this._mem[request.table];
1431
+ return {
1432
+ [request.table]: copy(table)
1433
+ };
1434
+ }
1435
+ // ...........................................................................
1436
+ async _contentType(request) {
1437
+ await this._ioTools.throwWhenTableDoesNotExist(request.table);
1438
+ return this._mem[request.table]._type;
1439
+ }
1440
+ // ...........................................................................
1441
+ async _write(request) {
1442
+ const addedData = hsh(request.data);
1443
+ this._removeNullValues(addedData);
1444
+ const tables = Object.keys(addedData);
1445
+ hsh(addedData);
1446
+ await this._ioTools.throwWhenTablesDoNotExist(request.data);
1447
+ await this._ioTools.throwWhenTableDataDoesNotMatchCfg(request.data);
1448
+ for (const table of tables) {
1449
+ if (table.startsWith("_")) {
1450
+ continue;
1451
+ }
1452
+ const oldTable = this._mem[table];
1453
+ const newTable = addedData[table];
1454
+ for (const item of newTable._data) {
1455
+ const hash = item._hash;
1456
+ const exists = oldTable._data.find((i) => i._hash === hash);
1457
+ if (!exists) {
1458
+ oldTable._data.push(item);
1459
+ }
1460
+ }
1461
+ this._ioTools.sortTableDataAndUpdateHash(oldTable);
1462
+ }
1463
+ this._updateGlobalHash();
1464
+ }
1465
+ // ...........................................................................
1466
+ async _readRows(request) {
1467
+ await this._ioTools.throwWhenTableDoesNotExist(request.table);
1468
+ await this._ioTools.throwWhenColumnDoesNotExist(
1469
+ request.table,
1470
+ Object.keys(request.where)
1471
+ );
1472
+ const table = this._mem[request.table];
1473
+ const tableDataFiltered = table._data.filter((row) => {
1474
+ for (const column in request.where) {
1475
+ const a = row[column];
1476
+ const b = request.where[column];
1477
+ if (b === null && a === void 0) {
1478
+ return true;
1479
+ }
1480
+ if (!equals(a, b)) {
1481
+ return false;
1482
+ }
1483
+ }
1484
+ return true;
1485
+ });
1486
+ const tableFiltered = {
1487
+ _type: table._type,
1488
+ _data: tableDataFiltered
1489
+ };
1490
+ this._ioTools.sortTableDataAndUpdateHash(tableFiltered);
1491
+ const result = {
1492
+ [request.table]: tableFiltered
1493
+ };
1494
+ return result;
1495
+ }
1496
+ _removeNullValues(rljson) {
1497
+ iterateTablesSync(rljson, (table) => {
1498
+ const data = rljson[table]._data;
1499
+ for (const row of data) {
1500
+ for (const key in row) {
1501
+ if (row[key] === null) {
1502
+ delete row[key];
1503
+ }
1504
+ }
1505
+ }
1506
+ });
1507
+ }
1508
+ }
1509
+ let PeerSocketMock$1 = class PeerSocketMock {
1510
+ constructor(_io) {
1511
+ this._io = _io;
1512
+ }
1513
+ _listenersMap = /* @__PURE__ */ new Map();
1514
+ connected = false;
1515
+ disconnected = true;
1516
+ // ............................................................................
1517
+ /**
1518
+ * Removes a specific listener for the specified event.
1519
+ * @param eventName - The name of the event.
1520
+ * @param listener - The callback function to remove.
1521
+ * @returns The PeerSocketMock instance for chaining.
1522
+ */
1523
+ off(eventName, listener) {
1524
+ const listeners = this._listenersMap.get(eventName) || [];
1525
+ const index = listeners.indexOf(listener);
1526
+ if (index !== -1) {
1527
+ listeners.splice(index, 1);
1528
+ this._listenersMap.set(eventName, listeners);
1529
+ }
1530
+ return this;
1531
+ }
1532
+ // ............................................................................
1533
+ /**
1534
+ * Removes all listeners for the specified event, or all listeners if no event is specified.
1535
+ * @param eventName - (Optional) The name of the event.
1536
+ * @returns The PeerSocketMock instance for chaining.
1537
+ */
1538
+ removeAllListeners(eventName) {
1539
+ if (eventName) {
1540
+ this._listenersMap.delete(eventName);
1541
+ } else {
1542
+ this._listenersMap.clear();
1543
+ }
1544
+ return this;
1545
+ }
1546
+ // ............................................................................
1547
+ /**
1548
+ * Registers an event listener for the specified event.
1549
+ * @param eventName - The name of the event to listen for.
1550
+ * @param listener - The callback function to invoke when the event is emitted.
1551
+ * @returns The PeerSocketMock instance for chaining.
1552
+ */
1553
+ on(eventName, listener) {
1554
+ if (!this._listenersMap.has(eventName)) {
1555
+ this._listenersMap.set(eventName, []);
1556
+ }
1557
+ this._listenersMap.get(eventName).push(listener);
1558
+ return this;
1559
+ }
1560
+ // ...........................................................................
1561
+ /**
1562
+ * Simulates a connection event.
1563
+ *
1564
+ * Emits the 'connect' event to all registered listeners.
1565
+ */
1566
+ connect() {
1567
+ this.connected = true;
1568
+ this.disconnected = false;
1569
+ const listeners = this._listenersMap.get("connect") || [];
1570
+ for (const cb of listeners) {
1571
+ cb({});
1572
+ }
1573
+ }
1574
+ // ...........................................................................
1575
+ /**
1576
+ * Simulates a disconnection event.
1577
+ *
1578
+ * Emits the 'disconnect' event to all registered listeners.
1579
+ */
1580
+ disconnect() {
1581
+ this.connected = false;
1582
+ this.disconnected = true;
1583
+ const listeners = this._listenersMap.get("disconnect") || [];
1584
+ for (const cb of listeners) {
1585
+ cb({});
1586
+ }
1587
+ }
1588
+ // ............................................................................
1589
+ /**
1590
+ * Emits an event, invoking the corresponding method on the Io instance.
1591
+ * @param eventName - The name of the event to emit.
1592
+ * @param args - The arguments to pass to the event listener.
1593
+ * @returns
1594
+ */
1595
+ emit(eventName, ...args) {
1596
+ const fn = this._io[eventName];
1597
+ if (typeof fn !== "function") {
1598
+ throw new Error(`Event ${eventName.toString()} not supported`);
1599
+ }
1600
+ const cb = args[args.length - 1];
1601
+ fn.apply(this._io, args.slice(0, -1)).then((result) => {
1602
+ cb(result, null);
1603
+ }).catch((err) => {
1604
+ cb(null, err);
1605
+ });
1606
+ return true;
1607
+ }
1608
+ };
1609
+ class IoPeer {
1610
+ constructor(_socket) {
1611
+ this._socket = _socket;
1612
+ }
1613
+ isOpen = false;
1614
+ // ...........................................................................
1615
+ /**
1616
+ *
1617
+ * Initializes the Peer connection.
1618
+ * @returns
1619
+ */
1620
+ async init() {
1621
+ this._socket.on("connect", () => {
1622
+ this.isOpen = true;
1623
+ });
1624
+ this._socket.on("disconnect", () => {
1625
+ this.isOpen = false;
1626
+ });
1627
+ this._socket.connect();
1628
+ return new Promise((resolve) => {
1629
+ if (this._socket.connected) {
1630
+ this.isOpen = true;
1631
+ resolve();
1632
+ } else {
1633
+ this._socket.on("connect", () => {
1634
+ resolve();
1635
+ });
1636
+ }
1637
+ });
1638
+ }
1639
+ // ...........................................................................
1640
+ /**
1641
+ * Closes the Peer connection.
1642
+ * @returns
1643
+ */
1644
+ async close() {
1645
+ if (!this._socket.connected) return;
1646
+ return new Promise((resolve) => {
1647
+ this._socket.on("disconnect", () => {
1648
+ resolve();
1649
+ });
1650
+ this._socket.disconnect();
1651
+ });
1652
+ }
1653
+ // ...........................................................................
1654
+ /**
1655
+ * Returns a promise that resolves once the Peer connection is ready.
1656
+ * @returns
1657
+ */
1658
+ async isReady() {
1659
+ if (!!this._socket && this._socket.connected === true) this.isOpen = true;
1660
+ else this.isOpen = false;
1661
+ return !!this.isOpen ? Promise.resolve() : Promise.reject();
1662
+ }
1663
+ // ...........................................................................
1664
+ /**
1665
+ * Dumps the entire database content.
1666
+ * @returns A promise that resolves to the dumped database content.
1667
+ */
1668
+ async dump() {
1669
+ return new Promise((resolve) => {
1670
+ this._socket.emit("dump", (data) => {
1671
+ resolve(data);
1672
+ });
1673
+ });
1674
+ }
1675
+ // ...........................................................................
1676
+ /**
1677
+ * Dumps a specific table from the database.
1678
+ * @param request An object containing the table name to dump.
1679
+ * @returns A promise that resolves to the dumped table data.
1680
+ */
1681
+ dumpTable(request) {
1682
+ return new Promise((resolve, reject) => {
1683
+ this._socket.emit("dumpTable", request, (data, error) => {
1684
+ if (error) reject(error);
1685
+ resolve(data);
1686
+ });
1687
+ });
1688
+ }
1689
+ // ...........................................................................
1690
+ /**
1691
+ * Gets the content type of a specific table.
1692
+ * @param request An object containing the table name to get the content type for.
1693
+ * @returns A promise that resolves to the content type of the specified table.
1694
+ */
1695
+ contentType(request) {
1696
+ return new Promise((resolve, reject) => {
1697
+ this._socket.emit(
1698
+ "contentType",
1699
+ request,
1700
+ (data, error) => {
1701
+ if (error) reject(error);
1702
+ resolve(data);
1703
+ }
1704
+ );
1705
+ });
1706
+ }
1707
+ // ...........................................................................
1708
+ /**
1709
+ * Checks if a specific table exists in the database.
1710
+ * @param tableKey The key of the table to check for existence.
1711
+ * @returns A promise that resolves to true if the table exists, false otherwise.
1712
+ */
1713
+ tableExists(tableKey) {
1714
+ return new Promise((resolve) => {
1715
+ this._socket.emit("tableExists", tableKey, (exists) => {
1716
+ resolve(exists);
1717
+ });
1718
+ });
1719
+ }
1720
+ // ...........................................................................
1721
+ /**
1722
+ * Creates or extends a table with the given configuration.
1723
+ * @param request An object containing the table configuration.
1724
+ * @returns A promise that resolves once the table is created or extended.
1725
+ */
1726
+ createOrExtendTable(request) {
1727
+ return new Promise((resolve, reject) => {
1728
+ this._socket.emit(
1729
+ "createOrExtendTable",
1730
+ request,
1731
+ (_, error) => {
1732
+ if (error) reject(error);
1733
+ resolve();
1734
+ }
1735
+ );
1736
+ });
1737
+ }
1738
+ // ...........................................................................
1739
+ /**
1740
+ * Retrieves the raw table configurations from the database.
1741
+ * @returns A promise that resolves to an array of table configurations.
1742
+ */
1743
+ rawTableCfgs() {
1744
+ return new Promise((resolve) => {
1745
+ this._socket.emit("rawTableCfgs", (data) => {
1746
+ resolve(data);
1747
+ });
1748
+ });
1749
+ }
1750
+ // ...........................................................................
1751
+ /**
1752
+ * Writes data to the database.
1753
+ * @param request An object containing the data to write.
1754
+ * @returns A promise that resolves once the data is written.
1755
+ */
1756
+ write(request) {
1757
+ return new Promise((resolve, reject) => {
1758
+ this._socket.emit("write", request, (_, error) => {
1759
+ if (error) reject(error);
1760
+ resolve();
1761
+ });
1762
+ });
1763
+ }
1764
+ // ...........................................................................
1765
+ /**
1766
+ * Reads rows from a specific table that match the given criteria.
1767
+ * @param request An object containing the table name and the criteria for selecting rows.
1768
+ * @returns A promise that resolves to the selected rows.
1769
+ */
1770
+ readRows(request) {
1771
+ return new Promise((resolve, reject) => {
1772
+ this._socket.emit(
1773
+ "readRows",
1774
+ request,
1775
+ (result, error) => {
1776
+ if (error) reject(error);
1777
+ resolve(result);
1778
+ }
1779
+ );
1780
+ });
1781
+ }
1782
+ // ...........................................................................
1783
+ /**
1784
+ * Retrieves the number of rows in a specific table.
1785
+ * @param table The name of the table to count rows in.
1786
+ * @returns A promise that resolves to the number of rows in the specified table.
1787
+ */
1788
+ rowCount(table) {
1789
+ return new Promise((resolve, reject) => {
1790
+ this._socket.emit("rowCount", table, (count, error) => {
1791
+ if (error) reject(error);
1792
+ resolve(count);
1793
+ });
1794
+ });
1795
+ }
1796
+ // ...........................................................................
1797
+ static example = async () => {
1798
+ const ioMem = await IoMem.example();
1799
+ const socket = new PeerSocketMock$1(ioMem);
1800
+ const io = new IoPeer(socket);
1801
+ await io.init();
1802
+ return io;
1803
+ };
1804
+ }
1805
+ class IoMulti {
1806
+ constructor(_ios) {
1807
+ this._ios = _ios;
1808
+ }
1809
+ isOpen = false;
1810
+ // ...........................................................................
1811
+ /**
1812
+ *
1813
+ * Initializes all underlying Io instances.
1814
+ * @returns
1815
+ */
1816
+ async init() {
1817
+ for (let idx = 0; idx < this._ios.length; idx++) {
1818
+ const { io } = this._ios[idx];
1819
+ if (io.isOpen === false) {
1820
+ throw new Error(
1821
+ "All underlying Io instances must be initialized before initializing IoMulti"
1822
+ );
1823
+ }
1824
+ this._ios[idx] = { ...this._ios[idx], id: `io-${idx}` };
1825
+ }
1826
+ this.isOpen = true;
1827
+ return Promise.resolve();
1828
+ }
1829
+ // ...........................................................................
1830
+ /**
1831
+ * Closes all underlying Io instances.
1832
+ * @returns
1833
+ */
1834
+ async close() {
1835
+ await Promise.all(this._ios.map((ioMultiIo) => ioMultiIo.io.close()));
1836
+ this.isOpen = false;
1837
+ return Promise.resolve();
1838
+ }
1839
+ // ...........................................................................
1840
+ /**
1841
+ * Returns a promise that resolves once all underlying Io instances are ready.
1842
+ * @returns
1843
+ */
1844
+ isReady() {
1845
+ return Promise.all(
1846
+ this._ios.map((ioMultiIo) => ioMultiIo.io.isReady())
1847
+ ).then(() => Promise.resolve());
1848
+ }
1849
+ // ...........................................................................
1850
+ /**
1851
+ * Dumps the entire database content by merging dumps from all dumpable underlying Io instances.
1852
+ * @returns
1853
+ */
1854
+ async dump() {
1855
+ if (this.dumpables.length === 0) {
1856
+ throw new Error("No dumpable Io available");
1857
+ }
1858
+ const dumps = await Promise.all(
1859
+ this.dumpables.map(({ io: dumpable }) => dumpable.dump())
1860
+ );
1861
+ return merge(...dumps);
1862
+ }
1863
+ // ...........................................................................
1864
+ /**
1865
+ * Dumps a specific table by merging dumps from all dumpable underlying Io instances that contain the table.
1866
+ * @param request An object containing the table name to dump.
1867
+ * @returns A promise that resolves to the dumped table data.
1868
+ */
1869
+ async dumpTable(request) {
1870
+ if (this.dumpables.length === 0) {
1871
+ throw new Error("No dumpable Io available");
1872
+ }
1873
+ const dumps = [];
1874
+ for (const { io: dumpable } of this.dumpables) {
1875
+ try {
1876
+ const dump = await dumpable.dumpTable(request);
1877
+ dumps.push(dump);
1878
+ } catch {
1879
+ continue;
1880
+ }
1881
+ }
1882
+ if (dumps.length === 0) {
1883
+ throw new Error(`Table "${request.table}" not found`);
1884
+ }
1885
+ return merge(...dumps);
1886
+ }
1887
+ // ...........................................................................
1888
+ /**
1889
+ * Retrieves the content type of a specific table from the first underlying readable Io instance that contains the table.
1890
+ * @param request An object containing the table name.
1891
+ * @returns A promise that resolves to the content type of the table.
1892
+ */
1893
+ async contentType(request) {
1894
+ if (this.readables.length === 0) {
1895
+ throw new Error("No readable Io available");
1896
+ }
1897
+ for (const { io: readable } of this.readables) {
1898
+ return readable.contentType(request);
1899
+ }
1900
+ throw new Error(`Table "${request.table}" not found`);
1901
+ }
1902
+ // ...........................................................................
1903
+ /**
1904
+ * Checks if a specific table exists in any of the underlying readable Io instances.
1905
+ * @param tableKey The key of the table to check.
1906
+ * @returns A promise that resolves to true if the table exists in any readable Io, false otherwise.
1907
+ */
1908
+ async tableExists(tableKey) {
1909
+ if (this.readables.length === 0) {
1910
+ throw new Error("No readable Io available");
1911
+ }
1912
+ for (let i = 0; i < this.readables.length; i++) {
1913
+ const readable = this.readables[i].io;
1914
+ const exists = await readable.tableExists(tableKey);
1915
+ if (exists) {
1916
+ return true;
1917
+ }
1918
+ }
1919
+ return false;
1920
+ }
1921
+ // ...........................................................................
1922
+ /**
1923
+ * Creates or extends a table in all underlying writable Io instances.
1924
+ * @param request An object containing the table configuration.
1925
+ * @returns A promise that resolves once the table has been created or extended in all writable Io instances.
1926
+ */
1927
+ createOrExtendTable(request) {
1928
+ if (this.writables.length === 0) {
1929
+ throw new Error("No writable Io available");
1930
+ }
1931
+ const creations = this.writables.map(
1932
+ ({ io: writable }) => writable.createOrExtendTable(request)
1933
+ );
1934
+ return Promise.all(creations).then(() => Promise.resolve());
1935
+ }
1936
+ // ...........................................................................
1937
+ /**
1938
+ * Retrieves the raw table configurations from the highest priority underlying readable Io instance.
1939
+ * @returns A promise that resolves to an array of table configurations.
1940
+ */
1941
+ async rawTableCfgs() {
1942
+ if (this.readables.length === 0) {
1943
+ throw new Error("No readable Io available");
1944
+ }
1945
+ const rawTableCfgs = /* @__PURE__ */ new Map();
1946
+ for (const { io: readable } of this.readables) {
1947
+ const cfgs = await readable.rawTableCfgs();
1948
+ if (cfgs.length > 0) {
1949
+ for (const tableCfg of cfgs) {
1950
+ if (!rawTableCfgs.has(tableCfg.key)) {
1951
+ rawTableCfgs.set(tableCfg.key, tableCfg);
1952
+ }
1953
+ }
1954
+ }
1955
+ }
1956
+ return Array.from(rawTableCfgs.values());
1957
+ }
1958
+ // ...........................................................................
1959
+ /**
1960
+ * Writes data to all underlying writable Io instances.
1961
+ * @param request - An object containing the data to write.
1962
+ * @returns A promise that resolves once the data has been written to all writable Io instances.
1963
+ */
1964
+ write(request) {
1965
+ if (this.writables.length === 0) {
1966
+ throw new Error("No writable Io available");
1967
+ }
1968
+ const writes = this.writables.map(
1969
+ ({ io: writable }) => writable.write(request)
1970
+ );
1971
+ return Promise.all(writes).then(() => Promise.resolve());
1972
+ }
1973
+ // ...........................................................................
1974
+ /**
1975
+ * Reads rows from a specific table by merging rows from all underlying readable Io instances.
1976
+ * @param request An object containing the table name and where clause.
1977
+ * @returns A promise that resolves to the read rows.
1978
+ */
1979
+ async readRows(request) {
1980
+ if (this.readables.length === 0) {
1981
+ throw new Error("No readable Io available");
1982
+ }
1983
+ let tableExistsAny = false;
1984
+ const rows = /* @__PURE__ */ new Map();
1985
+ let type = void 0;
1986
+ let readFrom = "";
1987
+ const errors = [];
1988
+ for (const readable of this.readables) {
1989
+ let tableRows = [];
1990
+ let tableType;
1991
+ try {
1992
+ const { [request.table]: tableData } = await readable.io.readRows(
1993
+ request
1994
+ );
1995
+ tableRows = tableData._data;
1996
+ tableType = tableData._type;
1997
+ tableExistsAny = true;
1998
+ readFrom = readable.id ?? "";
1999
+ } catch (e) {
2000
+ errors.push(e);
2001
+ continue;
2002
+ }
2003
+ type ??= tableType;
2004
+ if (tableRows.length === 0) {
2005
+ continue;
2006
+ }
2007
+ for (const tableRow of tableRows) {
2008
+ const ref = tableRow._hash;
2009
+ rows.set(ref, tableRow);
2010
+ }
2011
+ break;
2012
+ }
2013
+ if (!tableExistsAny) {
2014
+ if (errors.length === 0) {
2015
+ throw new Error(`Table "${request.table}" not found`);
2016
+ } else {
2017
+ const preciseErrors = errors.filter(
2018
+ (err) => !err.message.includes(`Table "${request.table}" not found`)
2019
+ );
2020
+ if (preciseErrors.length > 0) {
2021
+ throw preciseErrors[0];
2022
+ } else {
2023
+ throw errors[0];
2024
+ }
2025
+ }
2026
+ } else {
2027
+ const rljson = {
2028
+ [request.table]: hip({ _data: Array.from(rows.values()), _type: type })
2029
+ };
2030
+ if (this.writables.length > 0 && rows.size > 0) {
2031
+ for (const writeable of this.writables) {
2032
+ if (writeable.id === readFrom) {
2033
+ continue;
2034
+ }
2035
+ try {
2036
+ await writeable.io.write({
2037
+ data: rljson
2038
+ });
2039
+ } catch {
2040
+ continue;
2041
+ }
2042
+ }
2043
+ }
2044
+ return rljson;
2045
+ }
2046
+ }
2047
+ // ...........................................................................
2048
+ /**
2049
+ * Retrieves the row count of a specific table by aggregating row counts from all dumpable underlying Io instances.
2050
+ * @param table The name of the table.
2051
+ * @returns A promise that resolves to the row count of the table.
2052
+ */
2053
+ async rowCount(table) {
2054
+ if (this.dumpables.length === 0) {
2055
+ throw new Error("No dumpable Io available");
2056
+ }
2057
+ const dumpTable = await this.dumpTable({ table });
2058
+ const tableData = dumpTable[table];
2059
+ if (!tableData) {
2060
+ throw new Error(`Table "${table}" not found`);
2061
+ }
2062
+ return Promise.resolve(tableData._data.length);
2063
+ }
2064
+ // ...........................................................................
2065
+ /**
2066
+ * Gets the list of underlying readable Io instances, sorted by priority.
2067
+ */
2068
+ get readables() {
2069
+ return this._ios.filter((ioMultiIo) => ioMultiIo.read).sort((a, b) => a.priority - b.priority);
2070
+ }
2071
+ // ...........................................................................
2072
+ /**
2073
+ * Gets the list of underlying writable Io instances, sorted by priority.
2074
+ */
2075
+ get writables() {
2076
+ return this._ios.filter((ioMultiIo) => ioMultiIo.write).sort((a, b) => a.priority - b.priority);
2077
+ }
2078
+ // ...........................................................................
2079
+ /**
2080
+ * Gets the list of underlying dumpable Io instances, sorted by priority.
2081
+ */
2082
+ get dumpables() {
2083
+ return this._ios.filter((ioMultiIo) => ioMultiIo.dump).sort((a, b) => a.priority - b.priority);
2084
+ }
2085
+ // ...........................................................................
2086
+ static example = async () => {
2087
+ const ioPeerMem = await IoMem.example();
2088
+ await ioPeerMem.init();
2089
+ const ioPeerSocket = new PeerSocketMock$1(ioPeerMem);
2090
+ const ioPeer = new IoPeer(ioPeerSocket);
2091
+ await ioPeer.init();
2092
+ const ioMem = await IoMem.example();
2093
+ await ioMem.init();
2094
+ const ios = [
2095
+ { io: ioPeer, priority: 1, read: true, write: false, dump: false },
2096
+ { io: ioMem, priority: 0, read: true, write: true, dump: true }
2097
+ ];
2098
+ const ioMulti = new IoMulti(ios);
2099
+ await ioMulti.init();
2100
+ return ioMulti;
2101
+ };
2102
+ }
2103
+ class IoPeerBridge {
2104
+ constructor(_io, _socket) {
2105
+ this._io = _io;
2106
+ this._socket = _socket;
2107
+ }
2108
+ _eventHandlers = /* @__PURE__ */ new Map();
2109
+ /**
2110
+ * Starts the bridge by setting up connection event handlers and
2111
+ * automatically registering all Io methods.
2112
+ */
2113
+ start() {
2114
+ this._socket.on("connect", () => this._handleConnect());
2115
+ this._socket.on("disconnect", () => this._handleDisconnect());
2116
+ this._registerIoMethods();
2117
+ }
2118
+ /**
2119
+ * Stops the bridge by removing all event handlers.
2120
+ */
2121
+ stop() {
2122
+ this._socket.off("connect", () => this._handleConnect());
2123
+ this._socket.off("disconnect", () => this._handleDisconnect());
2124
+ for (const [eventName, handler] of this._eventHandlers) {
2125
+ this._socket.off(eventName, handler);
2126
+ }
2127
+ this._eventHandlers.clear();
2128
+ }
2129
+ /**
2130
+ * Automatically registers all Io interface methods as socket event handlers.
2131
+ */
2132
+ _registerIoMethods() {
2133
+ const ioMethods = [
2134
+ "init",
2135
+ "isReady",
2136
+ "close",
2137
+ "tableExists",
2138
+ "createOrExtendTable",
2139
+ "write",
2140
+ "readRows",
2141
+ "rowCount",
2142
+ "dumpTable",
2143
+ "dump",
2144
+ "contentType",
2145
+ "rawTableCfgs"
2146
+ ];
2147
+ for (const methodName of ioMethods) {
2148
+ this.registerEvent(methodName);
2149
+ }
2150
+ }
2151
+ /**
2152
+ * Registers a socket event to be translated to an Io method call.
2153
+ *
2154
+ * @param eventName - The socket event name (should match an Io method name)
2155
+ * @param ioMethodName - (Optional) The Io method name if different from eventName
2156
+ */
2157
+ registerEvent(eventName, ioMethodName) {
2158
+ const methodName = ioMethodName || eventName;
2159
+ const handler = (...args) => {
2160
+ const callback = args[args.length - 1];
2161
+ const methodArgs = args.slice(0, -1);
2162
+ const ioMethod = this._io[methodName];
2163
+ if (typeof ioMethod !== "function") {
2164
+ const error = new Error(
2165
+ `Method "${methodName}" not found on Io instance`
2166
+ );
2167
+ if (typeof callback === "function") {
2168
+ callback(null, error);
2169
+ }
2170
+ return;
2171
+ }
2172
+ ioMethod.apply(this._io, methodArgs).then((result) => {
2173
+ if (typeof callback === "function") {
2174
+ callback(result, null);
2175
+ }
2176
+ }).catch((error) => {
2177
+ if (typeof callback === "function") {
2178
+ callback(null, error);
2179
+ }
2180
+ });
2181
+ };
2182
+ this._eventHandlers.set(eventName, handler);
2183
+ this._socket.on(eventName, handler);
2184
+ }
2185
+ /**
2186
+ * Registers multiple socket events at once.
2187
+ *
2188
+ * @param eventNames - Array of event names to register
2189
+ */
2190
+ registerEvents(eventNames) {
2191
+ for (const eventName of eventNames) {
2192
+ this.registerEvent(eventName);
2193
+ }
2194
+ }
2195
+ /**
2196
+ * Unregisters a socket event handler.
2197
+ *
2198
+ * @param eventName - The event name to unregister
2199
+ */
2200
+ unregisterEvent(eventName) {
2201
+ const handler = this._eventHandlers.get(eventName);
2202
+ if (handler) {
2203
+ this._socket.off(eventName, handler);
2204
+ this._eventHandlers.delete(eventName);
2205
+ }
2206
+ }
2207
+ /**
2208
+ * Emits a result back through the socket.
2209
+ *
2210
+ * @param eventName - The event name to emit
2211
+ * @param data - The data to send
2212
+ */
2213
+ emitToSocket(eventName, ...data) {
2214
+ this._socket.emit(eventName, ...data);
2215
+ }
2216
+ /**
2217
+ * Calls an Io method directly and emits the result through the socket.
2218
+ *
2219
+ * @param ioMethodName - The Io method to call
2220
+ * @param socketEventName - The socket event to emit with the result
2221
+ * @param args - Arguments to pass to the Io method
2222
+ */
2223
+ async callIoAndEmit(ioMethodName, socketEventName, ...args) {
2224
+ try {
2225
+ const ioMethod = this._io[ioMethodName];
2226
+ if (typeof ioMethod !== "function") {
2227
+ throw new Error(`Method "${ioMethodName}" not found on Io instance`);
2228
+ }
2229
+ const result = await ioMethod.apply(this._io, args);
2230
+ this._socket.emit(socketEventName, result, null);
2231
+ } catch (error) {
2232
+ this._socket.emit(socketEventName, null, error);
2233
+ }
2234
+ }
2235
+ /* v8 ignore next -- @preserve */
2236
+ _handleConnect() {
2237
+ }
2238
+ /* v8 ignore next -- @preserve */
2239
+ _handleDisconnect() {
2240
+ }
2241
+ /**
2242
+ * Gets the current socket instance.
2243
+ */
2244
+ get socket() {
2245
+ return this._socket;
2246
+ }
2247
+ /**
2248
+ * Gets the current Io instance.
2249
+ */
2250
+ get io() {
2251
+ return this._io;
2252
+ }
2253
+ /**
2254
+ * Returns whether the socket is currently connected.
2255
+ */
2256
+ get isConnected() {
2257
+ return this._socket.connected;
2258
+ }
2259
+ }
2260
+ class IoServer {
2261
+ constructor(_io) {
2262
+ this._io = _io;
2263
+ }
2264
+ _sockets = [];
2265
+ // ...........................................................................
2266
+ /**
2267
+ * Adds a socket to the IoServer instance.
2268
+ * @param socket - The socket to add.
2269
+ */
2270
+ async addSocket(socket) {
2271
+ await this._addTransportLayer(socket);
2272
+ this._sockets.push(socket);
2273
+ }
2274
+ // ...........................................................................
2275
+ /**
2276
+ * Removes a transport layer from the given socket.
2277
+ * @param socket - The socket to remove the transport layer from.
2278
+ */
2279
+ removeSocket(socket) {
2280
+ this._sockets = this._sockets.filter((s) => s !== socket);
2281
+ }
2282
+ // ...........................................................................
2283
+ /**
2284
+ * Adds a transport layer to the given socket.
2285
+ * @param socket - The socket to add the transport layer to.
2286
+ */
2287
+ async _addTransportLayer(socket) {
2288
+ const crud = this._generateTransportLayerCRUD(this._io);
2289
+ for (const [key, fn] of Object.entries(crud)) {
2290
+ socket.on(key, (...args) => {
2291
+ const cb = args[args.length - 1];
2292
+ fn.apply(this, args.slice(0, -1)).then((result) => {
2293
+ cb(result, null);
2294
+ }).catch((err) => {
2295
+ cb(null, err);
2296
+ });
2297
+ });
2298
+ }
2299
+ }
2300
+ // ...........................................................................
2301
+ /**
2302
+ * Creates or extends a table with the given configuration.
2303
+ * @param request - An object containing the table configuration.
2304
+ */
2305
+ async createOrExtendTable(request) {
2306
+ return this._io.createOrExtendTable(request);
2307
+ }
2308
+ // ...........................................................................
2309
+ /**
2310
+ * Generates a transport layer object for the given Io instance.
2311
+ * @param io - The Io instance to generate the transport layer for.
2312
+ * @returns An object containing methods that correspond to the Io interface.
2313
+ */
2314
+ _generateTransportLayerCRUD = (io) => ({
2315
+ init: () => io.init(),
2316
+ close: () => io.close(),
2317
+ isOpen: () => new Promise((resolve) => resolve(io.isOpen)),
2318
+ isReady: () => io.isReady(),
2319
+ dump: () => io.dump(),
2320
+ dumpTable: (request) => io.dumpTable(request),
2321
+ contentType: (request) => io.contentType(request),
2322
+ tableExists: (tableKey) => io.tableExists(tableKey),
2323
+ createOrExtendTable: (request) => this.createOrExtendTable(request),
2324
+ rawTableCfgs: () => io.rawTableCfgs(),
2325
+ write: (request) => io.write(request),
2326
+ readRows: (request) => io.readRows(request),
2327
+ rowCount: (table) => io.rowCount(table)
2328
+ });
2329
+ }
2330
+ class SocketMock {
2331
+ connected = false;
2332
+ disconnected = true;
2333
+ _listeners = /* @__PURE__ */ new Map();
2334
+ _onceListeners = /* @__PURE__ */ new Map();
2335
+ connect() {
2336
+ if (!this.connected) {
2337
+ this.connected = true;
2338
+ this.disconnected = false;
2339
+ this.emit("connect");
2340
+ }
2341
+ }
2342
+ disconnect() {
2343
+ if (this.connected) {
2344
+ this.connected = false;
2345
+ this.disconnected = true;
2346
+ this.emit("disconnect");
2347
+ }
2348
+ }
2349
+ on(eventName, listener) {
2350
+ if (!this._listeners.has(eventName)) {
2351
+ this._listeners.set(eventName, []);
2352
+ }
2353
+ this._listeners.get(eventName).push(listener);
2354
+ return this;
2355
+ }
2356
+ once(eventName, listener) {
2357
+ if (!this._onceListeners.has(eventName)) {
2358
+ this._onceListeners.set(eventName, []);
2359
+ }
2360
+ this._onceListeners.get(eventName).push(listener);
2361
+ return this;
2362
+ }
2363
+ off(eventName, listener) {
2364
+ if (listener) {
2365
+ const regularListeners = this._listeners.get(eventName);
2366
+ if (regularListeners) {
2367
+ const index = regularListeners.indexOf(listener);
2368
+ if (index > -1) {
2369
+ regularListeners.splice(index, 1);
2370
+ }
2371
+ }
2372
+ const onceListeners = this._onceListeners.get(eventName);
2373
+ if (onceListeners) {
2374
+ const index = onceListeners.indexOf(listener);
2375
+ if (index > -1) {
2376
+ onceListeners.splice(index, 1);
2377
+ }
2378
+ }
2379
+ } else {
2380
+ this._listeners.delete(eventName);
2381
+ this._onceListeners.delete(eventName);
2382
+ }
2383
+ return this;
2384
+ }
2385
+ emit(eventName, ...args) {
2386
+ let hasListeners = false;
2387
+ const regularListeners = this._listeners.get(eventName);
2388
+ if (regularListeners && regularListeners.length > 0) {
2389
+ hasListeners = true;
2390
+ [...regularListeners].forEach((listener) => {
2391
+ try {
2392
+ listener(...args);
2393
+ } catch (error) {
2394
+ console.error(
2395
+ `Error in listener for event ${String(eventName)}:`,
2396
+ error
2397
+ );
2398
+ }
2399
+ });
2400
+ }
2401
+ const onceListeners = this._onceListeners.get(eventName);
2402
+ if (onceListeners && onceListeners.length > 0) {
2403
+ hasListeners = true;
2404
+ const listenersToCall = [...onceListeners];
2405
+ this._onceListeners.delete(eventName);
2406
+ listenersToCall.forEach((listener) => {
2407
+ try {
2408
+ listener(...args);
2409
+ } catch (error) {
2410
+ console.error(
2411
+ `Error in once listener for event ${String(eventName)}:`,
2412
+ error
2413
+ );
2414
+ }
2415
+ });
2416
+ }
2417
+ return hasListeners;
2418
+ }
2419
+ removeAllListeners(eventName) {
2420
+ if (eventName !== void 0) {
2421
+ this._listeners.delete(eventName);
2422
+ this._onceListeners.delete(eventName);
2423
+ } else {
2424
+ this._listeners.clear();
2425
+ this._onceListeners.clear();
2426
+ }
2427
+ return this;
2428
+ }
2429
+ listenerCount(eventName) {
2430
+ const regularCount = this._listeners.get(eventName)?.length || 0;
2431
+ const onceCount = this._onceListeners.get(eventName)?.length || 0;
2432
+ return regularCount + onceCount;
2433
+ }
2434
+ listeners(eventName) {
2435
+ const regularListeners = this._listeners.get(eventName) || [];
2436
+ const onceListeners = this._onceListeners.get(eventName) || [];
2437
+ return [...regularListeners, ...onceListeners];
2438
+ }
2439
+ eventNames() {
2440
+ const allEvents = /* @__PURE__ */ new Set([
2441
+ ...this._listeners.keys(),
2442
+ ...this._onceListeners.keys()
2443
+ ]);
2444
+ return Array.from(allEvents);
2445
+ }
2446
+ // Test helper methods
2447
+ reset() {
2448
+ this.connected = false;
2449
+ this.disconnected = true;
2450
+ this.removeAllListeners();
2451
+ }
2452
+ simulateError(error) {
2453
+ this.emit("error", error);
2454
+ }
2455
+ simulateMessage(message) {
2456
+ this.emit("message", message);
2457
+ }
2458
+ // Get internal state for testing
2459
+ getListeners() {
2460
+ return new Map(this._listeners);
2461
+ }
2462
+ getOnceListeners() {
2463
+ return new Map(this._onceListeners);
2464
+ }
2465
+ }
2466
+ function createSocketPair() {
2467
+ const socketA = new DirectionalSocketMock();
2468
+ const socketB = new DirectionalSocketMock();
2469
+ socketA._setPeer(socketB);
2470
+ socketB._setPeer(socketA);
2471
+ return [socketA, socketB];
2472
+ }
2473
+ class DirectionalSocketMock {
2474
+ connected = false;
2475
+ disconnected = true;
2476
+ _peer;
2477
+ _listeners = /* @__PURE__ */ new Map();
2478
+ _onceListeners = /* @__PURE__ */ new Map();
2479
+ _setPeer(peer) {
2480
+ this._peer = peer;
2481
+ }
2482
+ connect() {
2483
+ if (!this.connected) {
2484
+ this.connected = true;
2485
+ this.disconnected = false;
2486
+ this._triggerLocal("connect");
2487
+ if (this._peer) {
2488
+ this._peer._triggerLocal("connect");
2489
+ }
2490
+ }
2491
+ }
2492
+ disconnect() {
2493
+ if (this.connected) {
2494
+ this.connected = false;
2495
+ this.disconnected = true;
2496
+ this._triggerLocal("disconnect");
2497
+ if (this._peer) {
2498
+ this._peer._triggerLocal("disconnect");
2499
+ }
2500
+ }
2501
+ }
2502
+ on(eventName, listener) {
2503
+ if (!this._listeners.has(eventName)) {
2504
+ this._listeners.set(eventName, []);
2505
+ }
2506
+ this._listeners.get(eventName).push(listener);
2507
+ return this;
2508
+ }
2509
+ once(eventName, listener) {
2510
+ if (!this._onceListeners.has(eventName)) {
2511
+ this._onceListeners.set(eventName, []);
2512
+ }
2513
+ this._onceListeners.get(eventName).push(listener);
2514
+ return this;
2515
+ }
2516
+ off(eventName, listener) {
2517
+ if (listener) {
2518
+ const regularListeners = this._listeners.get(eventName);
2519
+ if (regularListeners) {
2520
+ const index = regularListeners.indexOf(listener);
2521
+ if (index > -1) regularListeners.splice(index, 1);
2522
+ }
2523
+ const onceListeners = this._onceListeners.get(eventName);
2524
+ if (onceListeners) {
2525
+ const index = onceListeners.indexOf(listener);
2526
+ if (index > -1) onceListeners.splice(index, 1);
2527
+ }
2528
+ } else {
2529
+ this._listeners.delete(eventName);
2530
+ this._onceListeners.delete(eventName);
2531
+ }
2532
+ return this;
2533
+ }
2534
+ /**
2535
+ * Emits an event to the PEER socket (cross-socket emission).
2536
+ * This is the key difference from SocketMock - emit() sends to the other side,
2537
+ * not to local listeners.
2538
+ *
2539
+ * Implements Socket.IO acknowledgement pattern: the last argument can be a callback
2540
+ * that the peer will invoke to send a response back.
2541
+ */
2542
+ emit(eventName, ...args) {
2543
+ if (!this._peer) {
2544
+ console.warn(
2545
+ `DirectionalSocketMock.emit: No peer connected for event ${String(eventName)}`
2546
+ );
2547
+ return false;
2548
+ }
2549
+ this._peer._triggerLocal(eventName, ...args);
2550
+ return true;
2551
+ }
2552
+ /**
2553
+ * Triggers listeners on THIS socket (local emission).
2554
+ * Used internally when receiving events from peer.
2555
+ */
2556
+ _triggerLocal(eventName, ...args) {
2557
+ const regularListeners = this._listeners.get(eventName);
2558
+ if (regularListeners) {
2559
+ [...regularListeners].forEach((listener) => {
2560
+ try {
2561
+ listener(...args);
2562
+ } catch (error) {
2563
+ console.error(`Error in listener for ${String(eventName)}:`, error);
2564
+ }
2565
+ });
2566
+ }
2567
+ const onceListeners = this._onceListeners.get(eventName);
2568
+ if (onceListeners) {
2569
+ const listenersToCall = [...onceListeners];
2570
+ this._onceListeners.delete(eventName);
2571
+ listenersToCall.forEach((listener) => {
2572
+ try {
2573
+ listener(...args);
2574
+ } catch (error) {
2575
+ console.error(
2576
+ `Error in once listener for ${String(eventName)}:`,
2577
+ error
2578
+ );
2579
+ }
2580
+ });
2581
+ }
2582
+ }
2583
+ removeAllListeners(eventName) {
2584
+ if (eventName !== void 0) {
2585
+ this._listeners.delete(eventName);
2586
+ this._onceListeners.delete(eventName);
2587
+ } else {
2588
+ this._listeners.clear();
2589
+ this._onceListeners.clear();
2590
+ }
2591
+ return this;
2592
+ }
2593
+ listenerCount(eventName) {
2594
+ const regularCount = this._listeners.get(eventName)?.length || 0;
2595
+ const onceCount = this._onceListeners.get(eventName)?.length || 0;
2596
+ return regularCount + onceCount;
2597
+ }
2598
+ listeners(eventName) {
2599
+ const regularListeners = this._listeners.get(eventName) || [];
2600
+ const onceListeners = this._onceListeners.get(eventName) || [];
2601
+ return [...regularListeners, ...onceListeners];
2602
+ }
2603
+ eventNames() {
2604
+ const allEvents = /* @__PURE__ */ new Set([
2605
+ ...this._listeners.keys(),
2606
+ ...this._onceListeners.keys()
2607
+ ]);
2608
+ return Array.from(allEvents);
2609
+ }
2610
+ }
2611
+ class BsMem {
2612
+ blobs = /* @__PURE__ */ new Map();
2613
+ /**
2614
+ * Convert content to Buffer
2615
+ * @param content - Content to convert (Buffer, string, or ReadableStream)
2616
+ */
2617
+ async toBuffer(content) {
2618
+ if (Buffer.isBuffer(content)) {
2619
+ return content;
2620
+ }
2621
+ if (typeof content === "string") {
2622
+ return Buffer.from(content, "utf8");
2623
+ }
2624
+ const reader = content.getReader();
2625
+ const chunks = [];
2626
+ while (true) {
2627
+ const { done, value } = await reader.read();
2628
+ if (done) break;
2629
+ chunks.push(value);
2630
+ }
2631
+ return Buffer.concat(chunks);
2632
+ }
2633
+ async setBlob(content) {
2634
+ const buffer = await this.toBuffer(content);
2635
+ const blobId = hshBuffer(buffer);
2636
+ const existing = this.blobs.get(blobId);
2637
+ if (existing) {
2638
+ return existing.properties;
2639
+ }
2640
+ const properties = {
2641
+ blobId,
2642
+ size: buffer.length,
2643
+ createdAt: /* @__PURE__ */ new Date()
2644
+ };
2645
+ this.blobs.set(blobId, {
2646
+ content: buffer,
2647
+ properties
2648
+ });
2649
+ return properties;
2650
+ }
2651
+ async getBlob(blobId, options) {
2652
+ const stored = this.blobs.get(blobId);
2653
+ if (!stored) {
2654
+ throw new Error(`Blob not found: ${blobId}`);
2655
+ }
2656
+ let content = stored.content;
2657
+ if (options?.range) {
2658
+ const { start, end } = options.range;
2659
+ content = stored.content.subarray(start, end);
2660
+ }
2661
+ return {
2662
+ content,
2663
+ properties: stored.properties
2664
+ };
2665
+ }
2666
+ async getBlobStream(blobId) {
2667
+ const stored = this.blobs.get(blobId);
2668
+ if (!stored) {
2669
+ throw new Error(`Blob not found: ${blobId}`);
2670
+ }
2671
+ const nodeStream = Readable.from(stored.content);
2672
+ return Readable.toWeb(nodeStream);
2673
+ }
2674
+ async deleteBlob(blobId) {
2675
+ const deleted = this.blobs.delete(blobId);
2676
+ if (!deleted) {
2677
+ throw new Error(`Blob not found: ${blobId}`);
2678
+ }
2679
+ }
2680
+ async blobExists(blobId) {
2681
+ return this.blobs.has(blobId);
2682
+ }
2683
+ async getBlobProperties(blobId) {
2684
+ const stored = this.blobs.get(blobId);
2685
+ if (!stored) {
2686
+ throw new Error(`Blob not found: ${blobId}`);
2687
+ }
2688
+ return stored.properties;
2689
+ }
2690
+ async listBlobs(options) {
2691
+ let blobs = Array.from(this.blobs.values()).map(
2692
+ (stored) => stored.properties
2693
+ );
2694
+ if (options?.prefix) {
2695
+ blobs = blobs.filter((blob) => blob.blobId.startsWith(options.prefix));
2696
+ }
2697
+ blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
2698
+ const maxResults = options?.maxResults ?? blobs.length;
2699
+ let startIndex = 0;
2700
+ if (options?.continuationToken) {
2701
+ const tokenIndex = blobs.findIndex(
2702
+ (blob) => blob.blobId === options.continuationToken
2703
+ );
2704
+ startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
2705
+ }
2706
+ const endIndex = Math.min(startIndex + maxResults, blobs.length);
2707
+ const pageBlobs = blobs.slice(startIndex, endIndex);
2708
+ const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
2709
+ return {
2710
+ blobs: pageBlobs,
2711
+ continuationToken
2712
+ };
2713
+ }
2714
+ async generateSignedUrl(blobId, expiresIn, permissions) {
2715
+ if (!this.blobs.has(blobId)) {
2716
+ throw new Error(`Blob not found: ${blobId}`);
2717
+ }
2718
+ const expires = Date.now() + expiresIn * 1e3;
2719
+ const perm = permissions ?? "read";
2720
+ return `mem://${blobId}?expires=${expires}&permissions=${perm}`;
2721
+ }
2722
+ /**
2723
+ * Clear all blobs from storage (useful for testing)
2724
+ */
2725
+ clear() {
2726
+ this.blobs.clear();
2727
+ }
2728
+ /**
2729
+ * Get the number of blobs in storage
2730
+ */
2731
+ get size() {
2732
+ return this.blobs.size;
2733
+ }
2734
+ }
2735
+ class BsPeer {
2736
+ constructor(_socket) {
2737
+ this._socket = _socket;
2738
+ }
2739
+ isOpen = false;
2740
+ // ...........................................................................
2741
+ /**
2742
+ * Initializes the Peer connection.
2743
+ */
2744
+ async init() {
2745
+ this._socket.on("connect", () => {
2746
+ this.isOpen = true;
2747
+ });
2748
+ this._socket.on("disconnect", () => {
2749
+ this.isOpen = false;
2750
+ });
2751
+ this._socket.connect();
2752
+ return new Promise((resolve) => {
2753
+ if (this._socket.connected) {
2754
+ this.isOpen = true;
2755
+ resolve();
2756
+ } else {
2757
+ this._socket.on("connect", () => {
2758
+ resolve();
2759
+ });
2760
+ }
2761
+ });
2762
+ }
2763
+ // ...........................................................................
2764
+ /**
2765
+ * Closes the Peer connection.
2766
+ */
2767
+ async close() {
2768
+ if (!this._socket.connected) return;
2769
+ return new Promise((resolve) => {
2770
+ this._socket.on("disconnect", () => {
2771
+ resolve();
2772
+ });
2773
+ this._socket.disconnect();
2774
+ });
2775
+ }
2776
+ // ...........................................................................
2777
+ /**
2778
+ * Returns a promise that resolves once the Peer connection is ready.
2779
+ */
2780
+ async isReady() {
2781
+ if (!!this._socket && this._socket.connected === true) this.isOpen = true;
2782
+ else this.isOpen = false;
2783
+ return !!this.isOpen ? Promise.resolve() : Promise.reject();
2784
+ }
2785
+ // ...........................................................................
2786
+ /**
2787
+ * Stores a blob from Buffer, string, or ReadableStream and returns properties.
2788
+ * @param content - The blob content to store
2789
+ * @returns Promise resolving to blob properties
2790
+ */
2791
+ setBlob(content) {
2792
+ return new Promise((resolve, reject) => {
2793
+ if (content instanceof ReadableStream) {
2794
+ const reader = content.getReader();
2795
+ const chunks = [];
2796
+ const readStream = async () => {
2797
+ try {
2798
+ while (true) {
2799
+ const { done, value } = await reader.read();
2800
+ if (done) break;
2801
+ chunks.push(value);
2802
+ }
2803
+ const totalLength = chunks.reduce(
2804
+ (sum, chunk) => sum + chunk.length,
2805
+ 0
2806
+ );
2807
+ const buffer = Buffer.concat(
2808
+ chunks.map((chunk) => Buffer.from(chunk)),
2809
+ totalLength
2810
+ );
2811
+ this._socket.emit(
2812
+ "setBlob",
2813
+ buffer,
2814
+ (error, result) => {
2815
+ if (error) reject(error);
2816
+ else resolve(result);
2817
+ }
2818
+ );
2819
+ } catch (err) {
2820
+ reject(err);
2821
+ }
2822
+ };
2823
+ readStream();
2824
+ } else {
2825
+ this._socket.emit(
2826
+ "setBlob",
2827
+ content,
2828
+ (error, result) => {
2829
+ if (error) reject(error);
2830
+ else resolve(result);
2831
+ }
2832
+ );
2833
+ }
2834
+ });
2835
+ }
2836
+ // ...........................................................................
2837
+ /**
2838
+ * Retrieves a blob by its ID as a Buffer.
2839
+ * @param blobId - The unique identifier of the blob
2840
+ * @param options - Download options
2841
+ * @returns Promise resolving to blob content and properties
2842
+ */
2843
+ getBlob(blobId, options) {
2844
+ return new Promise((resolve, reject) => {
2845
+ this._socket.emit(
2846
+ "getBlob",
2847
+ blobId,
2848
+ options,
2849
+ (error, result) => {
2850
+ if (error) reject(error);
2851
+ else resolve(result);
2852
+ }
2853
+ );
2854
+ });
2855
+ }
2856
+ // ...........................................................................
2857
+ /**
2858
+ * Retrieves a blob by its ID as a ReadableStream.
2859
+ * @param blobId - The unique identifier of the blob
2860
+ * @returns Promise resolving to readable stream
2861
+ */
2862
+ getBlobStream(blobId) {
2863
+ return new Promise((resolve, reject) => {
2864
+ this._socket.emit(
2865
+ "getBlobStream",
2866
+ blobId,
2867
+ (error, result) => {
2868
+ if (error) reject(error);
2869
+ else resolve(result);
2870
+ }
2871
+ );
2872
+ });
2873
+ }
2874
+ // ...........................................................................
2875
+ /**
2876
+ * Deletes a blob by its ID.
2877
+ * @param blobId - The unique identifier of the blob
2878
+ * @returns Promise that resolves when deletion is complete
2879
+ */
2880
+ deleteBlob(blobId) {
2881
+ return new Promise((resolve, reject) => {
2882
+ this._socket.emit("deleteBlob", blobId, (error) => {
2883
+ if (error) reject(error);
2884
+ else resolve();
2885
+ });
2886
+ });
2887
+ }
2888
+ // ...........................................................................
2889
+ /**
2890
+ * Checks if a blob exists.
2891
+ * @param blobId - The unique identifier of the blob
2892
+ * @returns Promise resolving to true if blob exists
2893
+ */
2894
+ blobExists(blobId) {
2895
+ return new Promise((resolve, reject) => {
2896
+ this._socket.emit(
2897
+ "blobExists",
2898
+ blobId,
2899
+ (error, exists) => {
2900
+ if (error) reject(error);
2901
+ else resolve(exists);
2902
+ }
2903
+ );
2904
+ });
2905
+ }
2906
+ // ...........................................................................
2907
+ /**
2908
+ * Gets blob properties (size, createdAt) without retrieving content.
2909
+ * @param blobId - The unique identifier of the blob
2910
+ * @returns Promise resolving to blob properties
2911
+ */
2912
+ getBlobProperties(blobId) {
2913
+ return new Promise((resolve, reject) => {
2914
+ this._socket.emit(
2915
+ "getBlobProperties",
2916
+ blobId,
2917
+ (error, result) => {
2918
+ if (error) reject(error);
2919
+ else resolve(result);
2920
+ }
2921
+ );
2922
+ });
2923
+ }
2924
+ // ...........................................................................
2925
+ /**
2926
+ * Lists all blobs with optional filtering and pagination.
2927
+ * @param options - Optional listing configuration
2928
+ * @returns Promise resolving to list of blobs
2929
+ */
2930
+ listBlobs(options) {
2931
+ return new Promise((resolve, reject) => {
2932
+ this._socket.emit(
2933
+ "listBlobs",
2934
+ options || {},
2935
+ (error, result) => {
2936
+ if (error) reject(error);
2937
+ else resolve(result);
2938
+ }
2939
+ );
2940
+ });
2941
+ }
2942
+ // ...........................................................................
2943
+ /**
2944
+ * Generates a signed URL for temporary blob access.
2945
+ * @param blobId - The unique identifier of the blob
2946
+ * @param expiresIn - Expiration time in seconds
2947
+ * @param permissions - Permissions for the URL
2948
+ * @returns Promise resolving to signed URL
2949
+ */
2950
+ generateSignedUrl(blobId, expiresIn, permissions) {
2951
+ return new Promise((resolve, reject) => {
2952
+ this._socket.emit(
2953
+ "generateSignedUrl",
2954
+ blobId,
2955
+ expiresIn,
2956
+ permissions,
2957
+ (error, url) => {
2958
+ if (error) reject(error);
2959
+ else resolve(url);
2960
+ }
2961
+ );
2962
+ });
2963
+ }
2964
+ }
2965
+ class PeerSocketMock2 {
2966
+ constructor(_bs) {
2967
+ this._bs = _bs;
2968
+ }
2969
+ _listenersMap = /* @__PURE__ */ new Map();
2970
+ connected = false;
2971
+ disconnected = true;
2972
+ // ............................................................................
2973
+ /**
2974
+ * Removes a specific listener for the specified event.
2975
+ * @param eventName - The event name
2976
+ * @param listener - The listener function to remove
2977
+ * @returns This socket instance for chaining
2978
+ */
2979
+ off(eventName, listener) {
2980
+ const listeners = this._listenersMap.get(eventName) || [];
2981
+ const index = listeners.indexOf(listener);
2982
+ if (index !== -1) {
2983
+ listeners.splice(index, 1);
2984
+ this._listenersMap.set(eventName, listeners);
2985
+ }
2986
+ return this;
2987
+ }
2988
+ // ............................................................................
2989
+ /**
2990
+ * Removes all listeners for the specified event, or all listeners if no event is specified.
2991
+ * @param eventName - Optional event name
2992
+ * @returns This socket instance for chaining
2993
+ */
2994
+ removeAllListeners(eventName) {
2995
+ if (eventName) {
2996
+ this._listenersMap.delete(eventName);
2997
+ } else {
2998
+ this._listenersMap.clear();
2999
+ }
3000
+ return this;
3001
+ }
3002
+ // ............................................................................
3003
+ /**
3004
+ * Registers an event listener for the specified event.
3005
+ * @param eventName - The event name
3006
+ * @param listener - The listener function to register
3007
+ * @returns This socket instance for chaining
3008
+ */
3009
+ on(eventName, listener) {
3010
+ if (!this._listenersMap.has(eventName)) {
3011
+ this._listenersMap.set(eventName, []);
3012
+ }
3013
+ this._listenersMap.get(eventName).push(listener);
3014
+ return this;
3015
+ }
3016
+ // ...........................................................................
3017
+ /**
3018
+ * Simulates a connection event.
3019
+ */
3020
+ connect() {
3021
+ this.connected = true;
3022
+ this.disconnected = false;
3023
+ const listeners = this._listenersMap.get("connect") || [];
3024
+ for (const cb of listeners) {
3025
+ cb({});
3026
+ }
3027
+ return this;
3028
+ }
3029
+ // ...........................................................................
3030
+ /**
3031
+ * Simulates a disconnection event.
3032
+ */
3033
+ disconnect() {
3034
+ this.connected = false;
3035
+ this.disconnected = true;
3036
+ const listeners = this._listenersMap.get("disconnect") || [];
3037
+ for (const cb of listeners) {
3038
+ cb({});
3039
+ }
3040
+ return this;
3041
+ }
3042
+ // ............................................................................
3043
+ /**
3044
+ * Emits an event, invoking the corresponding method on the Bs instance.
3045
+ * @param eventName - The event name
3046
+ * @param args - Event arguments
3047
+ * @returns True if the event was handled
3048
+ */
3049
+ emit(eventName, ...args) {
3050
+ const fn = this._bs[eventName];
3051
+ if (typeof fn !== "function") {
3052
+ throw new Error(`Event ${eventName.toString()} not supported`);
3053
+ }
3054
+ const cb = args[args.length - 1];
3055
+ fn.apply(this._bs, args.slice(0, -1)).then((result) => {
3056
+ cb(null, result);
3057
+ }).catch((err) => {
3058
+ cb(err);
3059
+ });
3060
+ return true;
3061
+ }
3062
+ }
3063
+ class BsMulti {
3064
+ constructor(_stores) {
3065
+ this._stores = _stores;
3066
+ }
3067
+ // ...........................................................................
3068
+ /**
3069
+ * Initializes the BsMulti by assigning IDs to all underlying Bs instances.
3070
+ * All underlying Bs instances must already be initialized.
3071
+ */
3072
+ async init() {
3073
+ for (let idx = 0; idx < this._stores.length; idx++) {
3074
+ this._stores[idx] = { ...this._stores[idx], id: `bs-${idx}` };
3075
+ }
3076
+ return Promise.resolve();
3077
+ }
3078
+ // ...........................................................................
3079
+ /**
3080
+ * Stores a blob in all writable Bs instances in parallel.
3081
+ * @param content - The blob content to store
3082
+ * @returns Promise resolving to blob properties from the first successful write
3083
+ */
3084
+ async setBlob(content) {
3085
+ if (this.writables.length === 0) {
3086
+ throw new Error("No writable Bs available");
3087
+ }
3088
+ const writes = this.writables.map(({ bs }) => bs.setBlob(content));
3089
+ const results = await Promise.all(writes);
3090
+ return results[0];
3091
+ }
3092
+ // ...........................................................................
3093
+ /**
3094
+ * Retrieves a blob from the highest priority readable Bs instance.
3095
+ * Hot-swaps the blob to all writable instances for caching.
3096
+ * @param blobId - The blob identifier
3097
+ * @param options - Download options
3098
+ * @returns Promise resolving to blob content and properties
3099
+ */
3100
+ async getBlob(blobId, options) {
3101
+ if (this.readables.length === 0) {
3102
+ throw new Error("No readable Bs available");
3103
+ }
3104
+ let result;
3105
+ let readFrom = "";
3106
+ const errors = [];
3107
+ for (const readable of this.readables) {
3108
+ try {
3109
+ result = await readable.bs.getBlob(blobId, options);
3110
+ readFrom = readable.id ?? "";
3111
+ break;
3112
+ } catch (e) {
3113
+ errors.push(e);
3114
+ continue;
3115
+ }
3116
+ }
3117
+ if (!result) {
3118
+ const notFoundErrors = errors.filter(
3119
+ (err) => err.message.includes("Blob not found")
3120
+ );
3121
+ if (notFoundErrors.length === errors.length) {
3122
+ throw new Error(`Blob not found: ${blobId}`);
3123
+ } else {
3124
+ throw errors[0];
3125
+ }
3126
+ }
3127
+ if (this.writables.length > 0) {
3128
+ const hotSwapWrites = this.writables.filter((writable) => writable.id !== readFrom).map(({ bs }) => bs.setBlob(result.content).catch(() => {
3129
+ }));
3130
+ await Promise.all(hotSwapWrites);
3131
+ }
3132
+ return result;
3133
+ }
3134
+ // ...........................................................................
3135
+ /**
3136
+ * Retrieves a blob as a ReadableStream from the highest priority readable Bs instance.
3137
+ * @param blobId - The blob identifier
3138
+ * @returns Promise resolving to a ReadableStream
3139
+ */
3140
+ async getBlobStream(blobId) {
3141
+ if (this.readables.length === 0) {
3142
+ throw new Error("No readable Bs available");
3143
+ }
3144
+ const errors = [];
3145
+ for (const readable of this.readables) {
3146
+ try {
3147
+ return await readable.bs.getBlobStream(blobId);
3148
+ } catch (e) {
3149
+ errors.push(e);
3150
+ continue;
3151
+ }
3152
+ }
3153
+ const notFoundErrors = errors.filter(
3154
+ (err) => err.message.includes("Blob not found")
3155
+ );
3156
+ if (notFoundErrors.length === errors.length) {
3157
+ throw new Error(`Blob not found: ${blobId}`);
3158
+ } else {
3159
+ throw errors[0];
3160
+ }
3161
+ }
3162
+ // ...........................................................................
3163
+ /**
3164
+ * Deletes a blob from all writable Bs instances in parallel.
3165
+ * @param blobId - The blob identifier
3166
+ */
3167
+ async deleteBlob(blobId) {
3168
+ if (this.writables.length === 0) {
3169
+ throw new Error("No writable Bs available");
3170
+ }
3171
+ const deletes = this.writables.map(({ bs }) => bs.deleteBlob(blobId));
3172
+ await Promise.all(deletes);
3173
+ }
3174
+ // ...........................................................................
3175
+ /**
3176
+ * Checks if a blob exists in any readable Bs instance.
3177
+ * @param blobId - The blob identifier
3178
+ * @returns Promise resolving to true if blob exists in any readable
3179
+ */
3180
+ async blobExists(blobId) {
3181
+ if (this.readables.length === 0) {
3182
+ throw new Error("No readable Bs available");
3183
+ }
3184
+ for (const readable of this.readables) {
3185
+ try {
3186
+ const exists = await readable.bs.blobExists(blobId);
3187
+ if (exists) {
3188
+ return true;
3189
+ }
3190
+ } catch {
3191
+ continue;
3192
+ }
3193
+ }
3194
+ return false;
3195
+ }
3196
+ // ...........................................................................
3197
+ /**
3198
+ * Gets blob properties from the highest priority readable Bs instance.
3199
+ * @param blobId - The blob identifier
3200
+ * @returns Promise resolving to blob properties
3201
+ */
3202
+ async getBlobProperties(blobId) {
3203
+ if (this.readables.length === 0) {
3204
+ throw new Error("No readable Bs available");
3205
+ }
3206
+ const errors = [];
3207
+ for (const readable of this.readables) {
3208
+ try {
3209
+ return await readable.bs.getBlobProperties(blobId);
3210
+ } catch (e) {
3211
+ errors.push(e);
3212
+ continue;
3213
+ }
3214
+ }
3215
+ const notFoundErrors = errors.filter(
3216
+ (err) => err.message.includes("Blob not found")
3217
+ );
3218
+ if (notFoundErrors.length === errors.length) {
3219
+ throw new Error(`Blob not found: ${blobId}`);
3220
+ } else {
3221
+ throw errors[0];
3222
+ }
3223
+ }
3224
+ // ...........................................................................
3225
+ /**
3226
+ * Lists blobs by merging results from all readable Bs instances.
3227
+ * Deduplicates by blobId (content-addressable).
3228
+ * @param options - Listing options
3229
+ * @returns Promise resolving to list of blobs
3230
+ */
3231
+ async listBlobs(options) {
3232
+ if (this.readables.length === 0) {
3233
+ throw new Error("No readable Bs available");
3234
+ }
3235
+ const blobMap = /* @__PURE__ */ new Map();
3236
+ for (const readable of this.readables) {
3237
+ try {
3238
+ let continuationToken2;
3239
+ do {
3240
+ const result = await readable.bs.listBlobs({
3241
+ prefix: options?.prefix,
3242
+ // Apply prefix filter during collection
3243
+ continuationToken: continuationToken2,
3244
+ maxResults: 1e3
3245
+ // Fetch in chunks from each store
3246
+ });
3247
+ for (const blob of result.blobs) {
3248
+ if (!blobMap.has(blob.blobId)) {
3249
+ blobMap.set(blob.blobId, blob);
3250
+ }
3251
+ }
3252
+ continuationToken2 = result.continuationToken;
3253
+ } while (continuationToken2);
3254
+ } catch {
3255
+ continue;
3256
+ }
3257
+ }
3258
+ const blobs = Array.from(blobMap.values());
3259
+ blobs.sort((a, b) => a.blobId.localeCompare(b.blobId));
3260
+ const maxResults = options?.maxResults ?? blobs.length;
3261
+ let startIndex = 0;
3262
+ if (options?.continuationToken) {
3263
+ const tokenIndex = blobs.findIndex(
3264
+ (blob) => blob.blobId === options.continuationToken
3265
+ );
3266
+ startIndex = tokenIndex === -1 ? 0 : tokenIndex + 1;
3267
+ }
3268
+ const endIndex = Math.min(startIndex + maxResults, blobs.length);
3269
+ const pageBlobs = blobs.slice(startIndex, endIndex);
3270
+ const continuationToken = endIndex < blobs.length ? pageBlobs[pageBlobs.length - 1]?.blobId : void 0;
3271
+ return {
3272
+ blobs: pageBlobs,
3273
+ continuationToken
3274
+ };
3275
+ }
3276
+ // ...........................................................................
3277
+ /**
3278
+ * Generates a signed URL from the highest priority readable Bs instance.
3279
+ * @param blobId - The blob identifier
3280
+ * @param expiresIn - Expiration time in seconds
3281
+ * @param permissions - Access permissions
3282
+ * @returns Promise resolving to signed URL
3283
+ */
3284
+ async generateSignedUrl(blobId, expiresIn, permissions = "read") {
3285
+ if (this.readables.length === 0) {
3286
+ throw new Error("No readable Bs available");
3287
+ }
3288
+ const errors = [];
3289
+ for (const readable of this.readables) {
3290
+ try {
3291
+ return await readable.bs.generateSignedUrl(
3292
+ blobId,
3293
+ expiresIn,
3294
+ permissions
3295
+ );
3296
+ } catch (e) {
3297
+ errors.push(e);
3298
+ continue;
3299
+ }
3300
+ }
3301
+ const notFoundErrors = errors.filter(
3302
+ (err) => err.message.includes("Blob not found")
3303
+ );
3304
+ if (notFoundErrors.length === errors.length) {
3305
+ throw new Error(`Blob not found: ${blobId}`);
3306
+ } else {
3307
+ throw errors[0];
3308
+ }
3309
+ }
3310
+ // ...........................................................................
3311
+ /**
3312
+ * Gets the list of underlying readable Bs instances, sorted by priority.
3313
+ */
3314
+ get readables() {
3315
+ return this._stores.filter((store) => store.read).sort((a, b) => a.priority - b.priority);
3316
+ }
3317
+ // ...........................................................................
3318
+ /**
3319
+ * Gets the list of underlying writable Bs instances, sorted by priority.
3320
+ */
3321
+ get writables() {
3322
+ return this._stores.filter((store) => store.write).sort((a, b) => a.priority - b.priority);
3323
+ }
3324
+ // ...........................................................................
3325
+ /**
3326
+ * Example: Local cache (BsMem) + Remote server (BsPeer)
3327
+ */
3328
+ static example = async () => {
3329
+ const bsRemoteMem = new BsMem();
3330
+ const bsRemoteSocket = new PeerSocketMock2(bsRemoteMem);
3331
+ const bsRemote = new BsPeer(bsRemoteSocket);
3332
+ await bsRemote.init();
3333
+ const bsLocal = new BsMem();
3334
+ const stores = [
3335
+ { bs: bsLocal, priority: 0, read: true, write: true },
3336
+ // Cache first
3337
+ { bs: bsRemote, priority: 1, read: true, write: false }
3338
+ // Remote fallback
3339
+ ];
3340
+ const bsMulti = new BsMulti(stores);
3341
+ await bsMulti.init();
3342
+ return bsMulti;
3343
+ };
3344
+ }
3345
+ class BsPeerBridge {
3346
+ constructor(_bs, _socket) {
3347
+ this._bs = _bs;
3348
+ this._socket = _socket;
3349
+ }
3350
+ _eventHandlers = /* @__PURE__ */ new Map();
3351
+ _handleConnectBound = this._handleConnect.bind(this);
3352
+ _handleDisconnectBound = this._handleDisconnect.bind(this);
3353
+ /**
3354
+ * Starts the bridge by setting up connection event handlers and
3355
+ * automatically registering all Bs methods.
3356
+ */
3357
+ start() {
3358
+ this._socket.on("connect", this._handleConnectBound);
3359
+ this._socket.on("disconnect", this._handleDisconnectBound);
3360
+ this._registerBsMethods();
3361
+ }
3362
+ /**
3363
+ * Stops the bridge by removing all event handlers.
3364
+ */
3365
+ stop() {
3366
+ this._socket.off("connect", this._handleConnectBound);
3367
+ this._socket.off("disconnect", this._handleDisconnectBound);
3368
+ for (const [eventName, handler] of this._eventHandlers) {
3369
+ this._socket.off(eventName, handler);
3370
+ }
3371
+ this._eventHandlers.clear();
3372
+ }
3373
+ /**
3374
+ * Automatically registers all Bs interface methods as socket event handlers.
3375
+ */
3376
+ _registerBsMethods() {
3377
+ const bsMethods = [
3378
+ "getBlob",
3379
+ "getBlobStream",
3380
+ "blobExists",
3381
+ "getBlobProperties",
3382
+ "listBlobs"
3383
+ ];
3384
+ for (const methodName of bsMethods) {
3385
+ this.registerEvent(methodName);
3386
+ }
3387
+ }
3388
+ /**
3389
+ * Registers a socket event to be translated to a Bs method call.
3390
+ * @param eventName - The socket event name (should match a Bs method name)
3391
+ * @param bsMethodName - (Optional) The Bs method name if different from eventName
3392
+ */
3393
+ registerEvent(eventName, bsMethodName) {
3394
+ const methodName = bsMethodName || eventName;
3395
+ const handler = (...args) => {
3396
+ const callback = args[args.length - 1];
3397
+ const methodArgs = args.slice(0, -1);
3398
+ const bsMethod = this._bs[methodName];
3399
+ if (typeof bsMethod !== "function") {
3400
+ const error = new Error(
3401
+ `Method "${methodName}" not found on Bs instance`
3402
+ );
3403
+ if (typeof callback === "function") {
3404
+ callback(error, null);
3405
+ }
3406
+ return;
3407
+ }
3408
+ bsMethod.apply(this._bs, methodArgs).then((result) => {
3409
+ if (typeof callback === "function") {
3410
+ callback(null, result);
3411
+ }
3412
+ }).catch((error) => {
3413
+ if (typeof callback === "function") {
3414
+ callback(error, null);
3415
+ }
3416
+ });
3417
+ };
3418
+ this._eventHandlers.set(eventName, handler);
3419
+ this._socket.on(eventName, handler);
3420
+ }
3421
+ /**
3422
+ * Registers multiple socket events at once.
3423
+ * @param eventNames - Array of event names to register
3424
+ */
3425
+ registerEvents(eventNames) {
3426
+ for (const eventName of eventNames) {
3427
+ this.registerEvent(eventName);
3428
+ }
3429
+ }
3430
+ /**
3431
+ * Unregisters a socket event handler.
3432
+ * @param eventName - The event name to unregister
3433
+ */
3434
+ unregisterEvent(eventName) {
3435
+ const handler = this._eventHandlers.get(eventName);
3436
+ if (handler) {
3437
+ this._socket.off(eventName, handler);
3438
+ this._eventHandlers.delete(eventName);
3439
+ }
3440
+ }
3441
+ /**
3442
+ * Emits a result back through the socket.
3443
+ * @param eventName - The event name to emit
3444
+ * @param data - The data to send
3445
+ */
3446
+ emitToSocket(eventName, ...data) {
3447
+ this._socket.emit(eventName, ...data);
3448
+ }
3449
+ /**
3450
+ * Calls a Bs method directly and emits the result through the socket.
3451
+ * @param bsMethodName - The Bs method to call
3452
+ * @param socketEventName - The socket event to emit with the result
3453
+ * @param args - Arguments to pass to the Bs method
3454
+ */
3455
+ async callBsAndEmit(bsMethodName, socketEventName, ...args) {
3456
+ try {
3457
+ const bsMethod = this._bs[bsMethodName];
3458
+ if (typeof bsMethod !== "function") {
3459
+ throw new Error(`Method "${bsMethodName}" not found on Bs instance`);
3460
+ }
3461
+ const result = await bsMethod.apply(this._bs, args);
3462
+ this._socket.emit(socketEventName, null, result);
3463
+ } catch (error) {
3464
+ this._socket.emit(socketEventName, error, null);
3465
+ }
3466
+ }
3467
+ /* v8 ignore next -- @preserve */
3468
+ _handleConnect() {
3469
+ }
3470
+ /* v8 ignore next -- @preserve */
3471
+ _handleDisconnect() {
3472
+ }
3473
+ /**
3474
+ * Gets the current socket instance.
3475
+ */
3476
+ get socket() {
3477
+ return this._socket;
3478
+ }
3479
+ /**
3480
+ * Gets the current Bs instance.
3481
+ */
3482
+ get bs() {
3483
+ return this._bs;
3484
+ }
3485
+ /**
3486
+ * Returns whether the socket is currently connected.
3487
+ */
3488
+ get isConnected() {
3489
+ return this._socket.connected;
3490
+ }
3491
+ }
3492
+ class BsServer {
3493
+ constructor(_bs) {
3494
+ this._bs = _bs;
3495
+ }
3496
+ _sockets = [];
3497
+ // ...........................................................................
3498
+ /**
3499
+ * Adds a socket to the BsServer instance.
3500
+ * @param socket - The socket to add.
3501
+ */
3502
+ async addSocket(socket) {
3503
+ await this._addTransportLayer(socket);
3504
+ this._sockets.push(socket);
3505
+ }
3506
+ // ...........................................................................
3507
+ /**
3508
+ * Removes a socket from the BsServer instance.
3509
+ * @param socket - The socket to remove.
3510
+ */
3511
+ removeSocket(socket) {
3512
+ this._sockets = this._sockets.filter((s) => s !== socket);
3513
+ }
3514
+ // ...........................................................................
3515
+ /**
3516
+ * Adds a transport layer to the given socket.
3517
+ * @param socket - The socket to add the transport layer to.
3518
+ */
3519
+ async _addTransportLayer(socket) {
3520
+ const methods = this._generateTransportLayer(this._bs);
3521
+ for (const [key, fn] of Object.entries(methods)) {
3522
+ socket.on(key, (...args) => {
3523
+ const cb = args[args.length - 1];
3524
+ fn.apply(this, args.slice(0, -1)).then((result) => {
3525
+ cb(null, result);
3526
+ }).catch((err) => {
3527
+ cb(err);
3528
+ });
3529
+ });
3530
+ }
3531
+ }
3532
+ // ...........................................................................
3533
+ /**
3534
+ * Generates a transport layer object for the given Bs instance.
3535
+ * @param bs - The Bs instance to generate the transport layer for.
3536
+ * @returns An object containing methods that correspond to the Bs interface.
3537
+ */
3538
+ _generateTransportLayer = (bs) => ({
3539
+ setBlob: (content) => bs.setBlob(content),
3540
+ getBlob: (blobId, options) => bs.getBlob(blobId, options),
3541
+ getBlobStream: (blobId) => bs.getBlobStream(blobId),
3542
+ deleteBlob: (blobId) => bs.deleteBlob(blobId),
3543
+ blobExists: (blobId) => bs.blobExists(blobId),
3544
+ getBlobProperties: (blobId) => bs.getBlobProperties(blobId),
3545
+ listBlobs: (options) => bs.listBlobs(options),
3546
+ generateSignedUrl: (blobId, expiresIn, permissions) => bs.generateSignedUrl(blobId, expiresIn, permissions)
3547
+ });
3548
+ }
3549
+ class BaseNode {
3550
+ constructor(_localIo) {
3551
+ this._localIo = _localIo;
3552
+ if (!_localIo.isOpen) {
3553
+ throw new Error("Local Io must be initialized and open");
3554
+ }
3555
+ this._localDb = new Db(this._localIo);
3556
+ }
3557
+ _localDb;
3558
+ // ...........................................................................
3559
+ /**
3560
+ * Creates tables in the local Db.
3561
+ * @param cfgs - Table configurations
3562
+ * @param cfgs.withInsertHistory - TableCfgs for tables with InsertHistory
3563
+ * @param cfgs.withoutInsertHistory - TableCfgs for tables without InsertHistory
3564
+ */
3565
+ async createTables(cfgs) {
3566
+ if (!this._localDb) throw new Error("Local Db not initialized");
3567
+ for (const tableCfg of cfgs.withoutInsertHistory || []) {
3568
+ await this._localDb.core.createTable(tableCfg);
3569
+ }
3570
+ for (const tableCfg of cfgs.withInsertHistory || []) {
3571
+ await this._localDb.core.createTableWithInsertHistory(tableCfg);
3572
+ }
3573
+ }
3574
+ // ...........................................................................
3575
+ /**
3576
+ * Imports Rljson data into the local Db.
3577
+ * @param data - Rljson data to import
3578
+ */
3579
+ /* v8 ignore next -- @preserve */
3580
+ async import(data) {
3581
+ if (!this._localDb) throw new Error("Local Db not initialized");
3582
+ await this._localDb.core.import(data);
3583
+ }
3584
+ }
3585
+ function normalizeSocketBundle(socket) {
3586
+ const bundle = socket;
3587
+ if (bundle.ioUp && bundle.ioDown && bundle.bsUp && bundle.bsDown) {
3588
+ return bundle;
3589
+ }
3590
+ const single = socket;
3591
+ return {
3592
+ ioUp: single,
3593
+ ioDown: single,
3594
+ bsUp: single,
3595
+ bsDown: single
3596
+ };
3597
+ }
3598
+ class Client extends BaseNode {
3599
+ // ...........................................................................
3600
+ /**
3601
+ * Creates a Client instance
3602
+ * @param _socketToServer - Socket or namespace bundle to connect to server
3603
+ * @param _localIo - Local Io for local storage
3604
+ * @param _localBs - Local Bs for local blob storage
3605
+ */
3606
+ constructor(_socketToServer, _localIo, _localBs) {
3607
+ super(_localIo);
3608
+ this._socketToServer = _socketToServer;
3609
+ this._localIo = _localIo;
3610
+ this._localBs = _localBs;
3611
+ }
3612
+ _ioMultiIos = [];
3613
+ _ioMulti;
3614
+ _bsMultiBss = [];
3615
+ _bsMulti;
3616
+ /**
3617
+ * Initializes Io and Bs multis and their peer bridges.
3618
+ * @returns The initialized Io implementation.
3619
+ */
3620
+ async init() {
3621
+ await this._setupIo();
3622
+ await this._setupBs();
3623
+ await this.ready();
3624
+ return this._ioMulti;
3625
+ }
3626
+ /**
3627
+ * Resolves once the Io implementation is ready.
3628
+ */
3629
+ async ready() {
3630
+ if (this._ioMulti) {
3631
+ await this._ioMulti.isReady();
3632
+ }
3633
+ }
3634
+ /**
3635
+ * Closes client resources and clears internal state.
3636
+ */
3637
+ async tearDown() {
3638
+ if (this._ioMulti && this._ioMulti.isOpen) {
3639
+ this._ioMulti.close();
3640
+ }
3641
+ if (this._bsMulti) ;
3642
+ this._ioMultiIos = [];
3643
+ this._bsMultiBss = [];
3644
+ this._ioMulti = void 0;
3645
+ this._bsMulti = void 0;
3646
+ }
3647
+ /**
3648
+ * Returns the Io implementation.
3649
+ */
3650
+ get io() {
3651
+ return this._ioMulti;
3652
+ }
3653
+ /**
3654
+ * Returns the Bs implementation.
3655
+ */
3656
+ get bs() {
3657
+ return this._bsMulti;
3658
+ }
3659
+ /**
3660
+ * Builds the Io multi with local and peer layers.
3661
+ */
3662
+ async _setupIo() {
3663
+ const sockets = normalizeSocketBundle(this._socketToServer);
3664
+ this._ioMultiIos.push({
3665
+ io: this._localIo,
3666
+ dump: true,
3667
+ read: true,
3668
+ write: true,
3669
+ priority: 1
3670
+ });
3671
+ const ioPeerBridge = new IoPeerBridge(this._localIo, sockets.ioUp);
3672
+ ioPeerBridge.start();
3673
+ const ioPeer = await this._createIoPeer(sockets.ioDown);
3674
+ this._ioMultiIos.push({
3675
+ io: ioPeer,
3676
+ dump: false,
3677
+ read: true,
3678
+ write: false,
3679
+ priority: 2
3680
+ });
3681
+ this._ioMulti = new IoMulti(this._ioMultiIos);
3682
+ await this._ioMulti.init();
3683
+ await this._ioMulti.isReady();
3684
+ }
3685
+ /**
3686
+ * Builds the Bs multi with local and peer layers.
3687
+ */
3688
+ async _setupBs() {
3689
+ const sockets = normalizeSocketBundle(this._socketToServer);
3690
+ this._bsMultiBss.push({
3691
+ bs: this._localBs,
3692
+ read: true,
3693
+ write: true,
3694
+ priority: 1
3695
+ });
3696
+ const bsPeerBridge = new BsPeerBridge(this._localBs, sockets.bsUp);
3697
+ bsPeerBridge.start();
3698
+ const bsPeer = await this._createBsPeer(sockets.bsDown);
3699
+ this._bsMultiBss.push({
3700
+ bs: bsPeer,
3701
+ read: true,
3702
+ write: false,
3703
+ priority: 2
3704
+ });
3705
+ this._bsMulti = new BsMulti(this._bsMultiBss);
3706
+ await this._bsMulti.init();
3707
+ }
3708
+ /**
3709
+ * Creates and initializes a downstream Io peer.
3710
+ * @param socket - Downstream socket to the server Io namespace.
3711
+ */
3712
+ async _createIoPeer(socket) {
3713
+ const ioPeer = new IoPeer(socket);
3714
+ await ioPeer.init();
3715
+ await ioPeer.isReady();
3716
+ return ioPeer;
3717
+ }
3718
+ /**
3719
+ * Creates and initializes a downstream Bs peer.
3720
+ * @param socket - Downstream socket to the server Bs namespace.
3721
+ */
3722
+ async _createBsPeer(socket) {
3723
+ const bsPeer = new BsPeer(socket);
3724
+ await bsPeer.init();
3725
+ return bsPeer;
3726
+ }
3727
+ }
3728
+ class Server extends BaseNode {
3729
+ constructor(_route, _localIo, _localBs) {
3730
+ super(_localIo);
3731
+ this._route = _route;
3732
+ this._localIo = _localIo;
3733
+ this._localBs = _localBs;
3734
+ const ioMultiIoLocal = {
3735
+ io: this._localIo,
3736
+ dump: true,
3737
+ read: true,
3738
+ write: true,
3739
+ priority: 1
3740
+ };
3741
+ this._ios.push(ioMultiIoLocal);
3742
+ this._ioMulti = new IoMulti(this._ios);
3743
+ this._ioServer = new IoServer(this._ioMulti);
3744
+ const bsMultiBsLocal = {
3745
+ bs: this._localBs,
3746
+ read: true,
3747
+ write: true,
3748
+ priority: 1
3749
+ };
3750
+ this._bss.push(bsMultiBsLocal);
3751
+ this._bsMulti = new BsMulti(this._bss);
3752
+ this._bsServer = new BsServer(this._bsMulti);
3753
+ }
3754
+ // Map of connected clients
3755
+ // socket => Push: Send new Refs through Route
3756
+ // io => Pull: Read from Clients Io
3757
+ _clients = /* @__PURE__ */ new Map();
3758
+ _ios = [];
3759
+ _ioMulti;
3760
+ // Storage => Let Clients read from Servers Io
3761
+ _ioServer;
3762
+ _bss = [];
3763
+ _bsMulti;
3764
+ // Storage => Let Clients read from Servers Bs
3765
+ _bsServer;
3766
+ // To avoid rebroadcasting the same edit refs multiple times
3767
+ _multicastedRefs = /* @__PURE__ */ new Set();
3768
+ _refreshPromise;
3769
+ _pendingSockets = [];
3770
+ /**
3771
+ * Initializes Io and Bs multis on the server.
3772
+ */
3773
+ async init() {
3774
+ await this._ioMulti.init();
3775
+ await this._ioMulti.isReady();
3776
+ await this._bsMulti.init();
3777
+ await this.ready();
3778
+ }
3779
+ /**
3780
+ * Resolves once the Io implementation is ready.
3781
+ */
3782
+ async ready() {
3783
+ await this._ioMulti.isReady();
3784
+ }
3785
+ /**
3786
+ * Adds a client socket, rebuilds multis, and refreshes servers.
3787
+ * @param socket - Client socket to register.
3788
+ * @returns The server instance.
3789
+ */
3790
+ async addSocket(socket) {
3791
+ const sockets = normalizeSocketBundle(socket);
3792
+ const clientId = `client_${this._clients.size}_${Math.random().toString(36).slice(2)}`;
3793
+ const ioUp = sockets.ioUp;
3794
+ const ioDown = sockets.ioDown;
3795
+ const bsUp = sockets.bsUp;
3796
+ const bsDown = sockets.bsDown;
3797
+ ioUp.__clientId = clientId;
3798
+ ioDown.__clientId = clientId;
3799
+ bsUp.__clientId = clientId;
3800
+ bsDown.__clientId = clientId;
3801
+ const ioPeer = await this._createIoPeer(ioUp);
3802
+ const bsPeer = await this._createBsPeer(bsUp);
3803
+ this._registerClient(
3804
+ clientId,
3805
+ { ioUp, ioDown, bsUp, bsDown },
3806
+ ioPeer,
3807
+ bsPeer
3808
+ );
3809
+ this._pendingSockets.push({ ioDown, bsDown });
3810
+ this._queueIoPeer(ioPeer);
3811
+ this._queueBsPeer(bsPeer);
3812
+ await this._queueRefresh();
3813
+ this._removeAllListeners();
3814
+ this._multicastRefs();
3815
+ return this;
3816
+ }
3817
+ // ...........................................................................
3818
+ /**
3819
+ * Removes all listeners from all connected clients.
3820
+ */
3821
+ _removeAllListeners() {
3822
+ for (const { ioUp } of this._clients.values()) {
3823
+ ioUp.removeAllListeners(this._route.flat);
3824
+ }
3825
+ }
3826
+ // ...........................................................................
3827
+ /**
3828
+ * Broadcasts incoming payloads from any client to all other connected clients.
3829
+ * Ensures the sender is filtered out when broadcasting.
3830
+ */
3831
+ _multicastRefs = () => {
3832
+ for (const [clientIdA, { ioUp: socketA }] of this._clients.entries()) {
3833
+ socketA.on(this._route.flat, (payload) => {
3834
+ const ref = payload.r;
3835
+ if (this._multicastedRefs.has(ref)) {
3836
+ return;
3837
+ }
3838
+ this._multicastedRefs.add(ref);
3839
+ const p = payload;
3840
+ if (p && p.__origin) {
3841
+ return;
3842
+ }
3843
+ for (const [
3844
+ clientIdB,
3845
+ { ioDown: socketB }
3846
+ ] of this._clients.entries()) {
3847
+ if (clientIdA !== clientIdB) {
3848
+ const forwarded = Object.assign({}, payload, {
3849
+ __origin: clientIdA
3850
+ });
3851
+ socketB.emit(this._route.flat, forwarded);
3852
+ }
3853
+ }
3854
+ });
3855
+ }
3856
+ };
3857
+ get route() {
3858
+ return this._route;
3859
+ }
3860
+ /**
3861
+ * Returns the Io implementation.
3862
+ */
3863
+ get io() {
3864
+ return this._ioMulti;
3865
+ }
3866
+ /**
3867
+ * Returns the Bs implementation.
3868
+ */
3869
+ get bs() {
3870
+ return this._bsMulti;
3871
+ }
3872
+ /**
3873
+ * Returns the connected clients map.
3874
+ */
3875
+ get clients() {
3876
+ return this._clients;
3877
+ }
3878
+ /**
3879
+ * Creates and initializes a downstream Io peer for a socket.
3880
+ * @param socket - Client socket to bind the peer to.
3881
+ */
3882
+ async _createIoPeer(socket) {
3883
+ const ioPeer = new IoPeer(socket);
3884
+ await ioPeer.init();
3885
+ await ioPeer.isReady();
3886
+ return ioPeer;
3887
+ }
3888
+ /**
3889
+ * Creates and initializes a downstream Bs peer for a socket.
3890
+ * @param socket - Client socket to bind the peer to.
3891
+ */
3892
+ async _createBsPeer(socket) {
3893
+ const bsPeer = new BsPeer(socket);
3894
+ await bsPeer.init();
3895
+ return bsPeer;
3896
+ }
3897
+ /**
3898
+ * Registers the client socket and peers.
3899
+ * @param clientId - Stable client identifier.
3900
+ * @param sockets - Directional sockets to register.
3901
+ * @param io - Io peer associated with the client.
3902
+ * @param bs - Bs peer associated with the client.
3903
+ */
3904
+ _registerClient(clientId, sockets, io, bs) {
3905
+ this._clients.set(clientId, {
3906
+ ioUp: sockets.ioUp,
3907
+ ioDown: sockets.ioDown,
3908
+ bsUp: sockets.bsUp,
3909
+ bsDown: sockets.bsDown,
3910
+ io,
3911
+ bs
3912
+ });
3913
+ }
3914
+ /**
3915
+ * Queues an Io peer for inclusion in the Io multi.
3916
+ * @param ioPeer - Io peer to add.
3917
+ */
3918
+ _queueIoPeer(ioPeer) {
3919
+ this._ios.push({
3920
+ io: ioPeer,
3921
+ dump: false,
3922
+ read: true,
3923
+ write: false,
3924
+ priority: 2
3925
+ });
3926
+ }
3927
+ /**
3928
+ * Queues a Bs peer for inclusion in the Bs multi.
3929
+ * @param bsPeer - Bs peer to add.
3930
+ */
3931
+ _queueBsPeer(bsPeer) {
3932
+ this._bss.push({
3933
+ bs: bsPeer,
3934
+ read: true,
3935
+ write: false,
3936
+ priority: 2
3937
+ });
3938
+ }
3939
+ /**
3940
+ * Rebuilds Io and Bs multis from queued peers.
3941
+ */
3942
+ async _rebuildMultis() {
3943
+ this._ioMulti = new IoMulti(this._ios);
3944
+ await this._ioMulti.init();
3945
+ await this._ioMulti.isReady();
3946
+ this._bsMulti = new BsMulti(this._bss);
3947
+ await this._bsMulti.init();
3948
+ }
3949
+ /**
3950
+ * Recreates servers and reattaches sockets.
3951
+ */
3952
+ async _refreshServers() {
3953
+ this._ioServer._io = this._ioMulti;
3954
+ this._bsServer._bs = this._bsMulti;
3955
+ for (const pending of this._pendingSockets) {
3956
+ await this._ioServer.addSocket(pending.ioDown);
3957
+ await this._bsServer.addSocket(pending.bsDown);
3958
+ }
3959
+ this._pendingSockets = [];
3960
+ }
3961
+ /**
3962
+ * Batches multi/server refreshes into a single queued task.
3963
+ */
3964
+ _queueRefresh() {
3965
+ if (!this._refreshPromise) {
3966
+ this._refreshPromise = Promise.resolve().then(async () => {
3967
+ await this._rebuildMultis();
3968
+ await this._refreshServers();
3969
+ }).finally(() => {
3970
+ this._refreshPromise = void 0;
3971
+ });
3972
+ }
3973
+ return this._refreshPromise;
3974
+ }
3975
+ /** Example instance for test purposes */
3976
+ static async example() {
3977
+ const route = Route.fromFlat("example.route");
3978
+ const io = new IoMem();
3979
+ await io.init();
3980
+ await io.isReady();
3981
+ const bs = new BsMem();
3982
+ const socket = new SocketMock();
3983
+ socket.connect();
3984
+ return new Server(route, io, bs).addSocket(socket);
3985
+ }
3986
+ }
3987
+ async function createSharedTreeTable(io, treeKey) {
3988
+ const db = new Db(io);
3989
+ const treeCfg = createTreesTableCfg(treeKey);
3990
+ await db.core.createTableWithInsertHistory(treeCfg);
3991
+ }
3992
+ async function runClientServerSetup(opts = {}) {
3993
+ const baseDir = opts.baseDir || join(process.cwd(), "demo", "client-server");
3994
+ const folderA = join(baseDir, "folder-a");
3995
+ const folderB = join(baseDir, "folder-b");
3996
+ const treeKey = opts.treeKey || "sharedTree";
3997
+ await rm(baseDir, { recursive: true, force: true });
3998
+ await mkdir(folderA, { recursive: true });
3999
+ await mkdir(folderB, { recursive: true });
4000
+ const route = Route.fromFlat(`/${treeKey}`);
4001
+ const serverIo = new IoMem();
4002
+ await serverIo.init();
4003
+ await serverIo.isReady();
4004
+ const { BsMem: BsMem2 } = await import("@rljson/bs");
4005
+ const serverBsMem = new BsMem2();
4006
+ await createSharedTreeTable(serverIo, treeKey);
4007
+ const server = new Server(route, serverIo, serverBsMem);
4008
+ await server.init();
4009
+ const [serverSocketA, clientSocketA] = createSocketPair();
4010
+ serverSocketA.connect();
4011
+ await server.addSocket(serverSocketA);
4012
+ const localIoA = new IoMem();
4013
+ await localIoA.init();
4014
+ await localIoA.isReady();
4015
+ await createSharedTreeTable(localIoA, treeKey);
4016
+ const localBsA = new BsMem2();
4017
+ const clientA = new Client(clientSocketA, localIoA, localBsA);
4018
+ await clientA.init();
4019
+ const clientDbA = new Db(clientA.io);
4020
+ const agentA = new FsAgent(folderA, clientA.bs);
4021
+ const [serverSocketB, clientSocketB] = createSocketPair();
4022
+ serverSocketB.connect();
4023
+ await server.addSocket(serverSocketB);
4024
+ const localIoB = new IoMem();
4025
+ await localIoB.init();
4026
+ await localIoB.isReady();
4027
+ await createSharedTreeTable(localIoB, treeKey);
4028
+ const localBsB = new BsMem2();
4029
+ const clientB = new Client(clientSocketB, localIoB, localBsB);
4030
+ await clientB.init();
4031
+ const clientDbB = new Db(clientB.io);
4032
+ const agentB = new FsAgent(folderB, clientB.bs);
4033
+ const helloPathA = join(folderA, "hello.txt");
4034
+ await writeFile(helloPathA, "Hello from Client A");
4035
+ const rootRef = await agentA.storeInDb(clientDbA, treeKey, { notify: false });
4036
+ await agentB.loadFromDb(clientDbB, treeKey, rootRef);
4037
+ const contentB = await readFile(join(folderB, "hello.txt"), "utf8");
4038
+ const cleanup = async () => {
4039
+ await rm(baseDir, { recursive: true, force: true });
4040
+ };
4041
+ return { baseDir, folderA, folderB, contentB, cleanup };
4042
+ }
4043
+ export {
4044
+ FsAgent,
4045
+ FsBlobAdapter,
4046
+ FsDbAdapter,
4047
+ FsScanner,
4048
+ runClientServerSetup
4049
+ };