@rljson/fs-agent 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.architecture.md +47 -0
- package/README.contributors.md +4 -0
- package/README.public.md +59 -2
- package/dist/README.architecture.md +47 -0
- package/dist/README.contributors.md +4 -0
- package/dist/README.public.md +59 -2
- package/dist/client-server/example-from-client.d.ts +1 -0
- package/dist/client-server/live-client-server-socketio.d.ts +1 -0
- package/dist/fs-agent.d.ts +137 -1
- package/dist/fs-agent.js +289 -3150
- package/dist/fs-db-adapter.d.ts +10 -3
- package/dist/index.d.ts +1 -1
- package/package.json +19 -17
package/dist/fs-agent.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { BsMem
|
|
2
|
-
import {
|
|
1
|
+
import { BsMem } from "@rljson/bs";
|
|
2
|
+
import { Route, createTreesTableCfg } from "@rljson/rljson";
|
|
3
3
|
import { stat, readFile, mkdir, writeFile, readdir, utimes, rm } from "fs/promises";
|
|
4
4
|
import { dirname, join, sep } from "path";
|
|
5
|
-
import { hip
|
|
5
|
+
import { hip } from "@rljson/hash";
|
|
6
6
|
import { watch } from "fs";
|
|
7
7
|
import { Db } from "@rljson/db";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { IoMem, createSocketPair } from "@rljson/io";
|
|
9
|
+
import { Server, Client } from "@rljson/server";
|
|
10
10
|
class FsBlobAdapter {
|
|
11
11
|
_bs;
|
|
12
12
|
constructor(bs) {
|
|
13
|
-
this._bs = bs || new BsMem
|
|
13
|
+
this._bs = bs || new BsMem();
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Gets the blob storage instance
|
|
@@ -124,7 +124,6 @@ class FsDbAdapter {
|
|
|
124
124
|
* @returns The root tree reference
|
|
125
125
|
*/
|
|
126
126
|
async storeFsTree(fsTree, options = {}) {
|
|
127
|
-
const { notify = false } = options;
|
|
128
127
|
if (!fsTree) {
|
|
129
128
|
throw new Error("fsTree cannot be null or undefined");
|
|
130
129
|
}
|
|
@@ -153,37 +152,10 @@ class FsDbAdapter {
|
|
|
153
152
|
(tree) => tree._hash !== fsTree.rootHash
|
|
154
153
|
);
|
|
155
154
|
trees.push(rootTree);
|
|
156
|
-
const
|
|
157
|
-
|
|
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
|
|
155
|
+
const results = await this.db.insertTrees(this.treeKey, trees, {
|
|
156
|
+
skipNotification: options.skipNotification
|
|
181
157
|
});
|
|
182
|
-
|
|
183
|
-
const treeKeyRoute = Route.fromFlat(`/${this.treeKey}`);
|
|
184
|
-
this.db.notify.notify(treeKeyRoute, historyRow);
|
|
185
|
-
}
|
|
186
|
-
return treeRootRef;
|
|
158
|
+
return results[0][`${this.treeKey}Ref`];
|
|
187
159
|
}
|
|
188
160
|
/**
|
|
189
161
|
* Get the tree table key
|
|
@@ -214,7 +186,7 @@ class FsScanner {
|
|
|
214
186
|
followSymlinks: options.followSymlinks ?? false,
|
|
215
187
|
bs: options.bs
|
|
216
188
|
};
|
|
217
|
-
this._bs = options.bs || new BsMem
|
|
189
|
+
this._bs = options.bs || new BsMem();
|
|
218
190
|
}
|
|
219
191
|
get tree() {
|
|
220
192
|
return this._tree;
|
|
@@ -499,6 +471,14 @@ class FsScanner {
|
|
|
499
471
|
});
|
|
500
472
|
}
|
|
501
473
|
}
|
|
474
|
+
const DEFAULT_TIMEOUTS = {
|
|
475
|
+
dbQuery: 1e4,
|
|
476
|
+
fetchTree: 2e4,
|
|
477
|
+
extract: 15e3,
|
|
478
|
+
restore: 15e3,
|
|
479
|
+
syncCallback: 25e3,
|
|
480
|
+
debounceMs: 300
|
|
481
|
+
};
|
|
502
482
|
class FsAgent {
|
|
503
483
|
_scanner;
|
|
504
484
|
_adapter;
|
|
@@ -509,11 +489,15 @@ class FsAgent {
|
|
|
509
489
|
_stopSync;
|
|
510
490
|
_stopSyncFromDb;
|
|
511
491
|
_lastSentRef;
|
|
492
|
+
/** Content fingerprint of the last tree we broadcasted (paths+blobIds) */
|
|
493
|
+
_lastSentContentKey;
|
|
494
|
+
_timeouts;
|
|
512
495
|
constructor(rootPath, bs, options = {}) {
|
|
513
496
|
this._rootPath = rootPath;
|
|
514
|
-
this._bs = bs || new BsMem
|
|
497
|
+
this._bs = bs || new BsMem();
|
|
515
498
|
this._db = options.db;
|
|
516
499
|
this._treeKey = options.treeKey;
|
|
500
|
+
this._timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
|
|
517
501
|
this._scanner = new FsScanner(rootPath, { ...options, bs: this._bs });
|
|
518
502
|
this._adapter = new FsBlobAdapter(this._bs);
|
|
519
503
|
if (this._db && this._treeKey) {
|
|
@@ -547,6 +531,51 @@ class FsAgent {
|
|
|
547
531
|
get adapter() {
|
|
548
532
|
return this._adapter;
|
|
549
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Gets the current timeout configuration
|
|
536
|
+
*/
|
|
537
|
+
get timeouts() {
|
|
538
|
+
return this._timeouts;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Wraps a promise with a timeout.
|
|
542
|
+
* Rejects with a descriptive error if the promise does not settle
|
|
543
|
+
* within the given number of milliseconds.
|
|
544
|
+
* @param promise - The promise to guard
|
|
545
|
+
* @param ms - Maximum allowed time in milliseconds
|
|
546
|
+
* @param label - Human-readable label included in the error message
|
|
547
|
+
*/
|
|
548
|
+
static _withTimeout(promise, ms, label) {
|
|
549
|
+
return new Promise((resolve, reject) => {
|
|
550
|
+
const timer = setTimeout(() => {
|
|
551
|
+
reject(new Error(`Timeout after ${ms}ms: ${label}`));
|
|
552
|
+
}, ms);
|
|
553
|
+
promise.then(
|
|
554
|
+
(value) => {
|
|
555
|
+
clearTimeout(timer);
|
|
556
|
+
resolve(value);
|
|
557
|
+
},
|
|
558
|
+
(err) => {
|
|
559
|
+
clearTimeout(timer);
|
|
560
|
+
reject(err);
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Sends a ref through the connector.
|
|
567
|
+
* Uses `sendWithAck()` when the connector has `requireAck` enabled,
|
|
568
|
+
* otherwise falls back to fire-and-forget `send()`.
|
|
569
|
+
* @param connector - The Connector to send through
|
|
570
|
+
* @param ref - The ref to broadcast
|
|
571
|
+
*/
|
|
572
|
+
async _sendRef(connector, ref) {
|
|
573
|
+
if (connector.syncConfig?.requireAck) {
|
|
574
|
+
await connector.sendWithAck(ref);
|
|
575
|
+
} else {
|
|
576
|
+
connector.send(ref);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
550
579
|
/**
|
|
551
580
|
* Starts automatic syncing to database
|
|
552
581
|
* Note: Auto-sync requires Connector which is not available in constructor.
|
|
@@ -727,8 +756,15 @@ class FsAgent {
|
|
|
727
756
|
processed.add(currentHash);
|
|
728
757
|
let result;
|
|
729
758
|
try {
|
|
730
|
-
result = await
|
|
731
|
-
|
|
759
|
+
result = await FsAgent._withTimeout(
|
|
760
|
+
db.get(route, { _hash: currentHash }),
|
|
761
|
+
this._timeouts.dbQuery,
|
|
762
|
+
`db.get(${treeKey}, _hash=${currentHash.slice(0, 8)}…)`
|
|
763
|
+
);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (error instanceof Error && error.message.startsWith("Timeout")) {
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
732
768
|
continue;
|
|
733
769
|
}
|
|
734
770
|
const treeData = result?.rljson?.[treeKey];
|
|
@@ -751,24 +787,22 @@ class FsAgent {
|
|
|
751
787
|
return Array.from(fetchedNodes.values());
|
|
752
788
|
}
|
|
753
789
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
790
|
+
* Fetches tree from database without restoring to filesystem.
|
|
791
|
+
* Separated from loadFromDb to allow content comparison before restore.
|
|
756
792
|
* @param db - Database instance
|
|
757
793
|
* @param treeKey - Tree table key
|
|
758
794
|
* @param rootRef - Root tree reference (hash)
|
|
759
|
-
* @
|
|
760
|
-
* @param options - Restore options
|
|
795
|
+
* @returns FsTree structure ready for restore
|
|
761
796
|
*/
|
|
762
|
-
async
|
|
797
|
+
async _fetchTreeFromDb(db, treeKey, rootRef) {
|
|
763
798
|
if (!rootRef || rootRef.trim() === "") {
|
|
764
799
|
throw new Error("rootRef cannot be empty");
|
|
765
800
|
}
|
|
766
801
|
const route = Route.fromFlat(treeKey);
|
|
767
|
-
const allNodes = await
|
|
768
|
-
db,
|
|
769
|
-
|
|
770
|
-
treeKey,
|
|
771
|
-
rootRef
|
|
802
|
+
const allNodes = await FsAgent._withTimeout(
|
|
803
|
+
this._fetchTreeRecursively(db, route, treeKey, rootRef),
|
|
804
|
+
this._timeouts.fetchTree,
|
|
805
|
+
`fetchTree(${treeKey}@${rootRef.slice(0, 8)}…)`
|
|
772
806
|
);
|
|
773
807
|
if (allNodes.length === 0) {
|
|
774
808
|
throw new Error(
|
|
@@ -786,10 +820,19 @@ class FsAgent {
|
|
|
786
820
|
`Root tree node "${rootRef}" not found in tree data. Available hashes: ${Array.from(trees.keys()).slice(0, 5).join(", ")}${trees.size > 5 ? "..." : ""}`
|
|
787
821
|
);
|
|
788
822
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
823
|
+
return { rootHash: rootRef, trees };
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Loads tree from database and restores to filesystem
|
|
827
|
+
* Writes to filesystem from DB trees and Bs blobs
|
|
828
|
+
* @param db - Database instance
|
|
829
|
+
* @param treeKey - Tree table key
|
|
830
|
+
* @param rootRef - Root tree reference (hash)
|
|
831
|
+
* @param targetPath - Optional target path (defaults to rootPath)
|
|
832
|
+
* @param options - Restore options
|
|
833
|
+
*/
|
|
834
|
+
async loadFromDb(db, treeKey, rootRef, targetPath, options) {
|
|
835
|
+
const fsTree = await this._fetchTreeFromDb(db, treeKey, rootRef);
|
|
793
836
|
await this.restore(fsTree, targetPath, options);
|
|
794
837
|
}
|
|
795
838
|
/**
|
|
@@ -845,3143 +888,239 @@ class FsAgent {
|
|
|
845
888
|
* @param db - Database instance
|
|
846
889
|
* @param connector - Connector instance for socket-based sync
|
|
847
890
|
* @param treeKey - Tree table key
|
|
848
|
-
* @param options - Storage options (e.g.,
|
|
891
|
+
* @param options - Storage options (e.g., skipNotification)
|
|
849
892
|
* @returns Function to stop watching
|
|
850
893
|
*/
|
|
851
894
|
async syncToDb(db, connector, treeKey, options) {
|
|
852
|
-
const initialRef = await
|
|
895
|
+
const initialRef = await FsAgent._withTimeout(
|
|
896
|
+
this.storeInDb(db, treeKey, options),
|
|
897
|
+
this._timeouts.fetchTree,
|
|
898
|
+
`syncToDb → initial storeInDb(${treeKey})`
|
|
899
|
+
);
|
|
853
900
|
if (initialRef) {
|
|
854
901
|
this._lastSentRef = initialRef;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
902
|
+
const currentTree = this._scanner.tree;
|
|
903
|
+
if (currentTree) {
|
|
904
|
+
this._lastSentContentKey = this._contentKeyFromTree(currentTree);
|
|
905
|
+
}
|
|
906
|
+
await this._sendRef(connector, initialRef);
|
|
907
|
+
}
|
|
908
|
+
let debounceTimer = null;
|
|
909
|
+
const debouncedSync = () => {
|
|
910
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
911
|
+
debounceTimer = setTimeout(async () => {
|
|
912
|
+
debounceTimer = null;
|
|
913
|
+
const tree = this._scanner.tree;
|
|
914
|
+
if (tree) {
|
|
915
|
+
try {
|
|
916
|
+
const contentKey = this._contentKeyFromTree(tree);
|
|
917
|
+
if (contentKey === this._lastSentContentKey) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
921
|
+
const ref = await FsAgent._withTimeout(
|
|
922
|
+
dbAdapter.storeFsTree(tree, options),
|
|
923
|
+
this._timeouts.fetchTree,
|
|
924
|
+
`syncToDb → storeFsTree(${treeKey})`
|
|
925
|
+
);
|
|
926
|
+
if (ref === this._lastSentRef) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this._lastSentRef = ref;
|
|
930
|
+
this._lastSentContentKey = contentKey;
|
|
931
|
+
if (ref) {
|
|
932
|
+
await this._sendRef(connector, ref);
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
865
936
|
}
|
|
866
|
-
}
|
|
937
|
+
}, this._timeouts.debounceMs);
|
|
867
938
|
};
|
|
868
|
-
this._scanner.onChange(
|
|
939
|
+
this._scanner.onChange(debouncedSync);
|
|
869
940
|
await this._scanner.watch();
|
|
870
941
|
return () => {
|
|
871
|
-
|
|
942
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
943
|
+
this._scanner.offChange(debouncedSync);
|
|
872
944
|
this._scanner.stopWatch();
|
|
873
945
|
};
|
|
874
946
|
}
|
|
875
947
|
/**
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
* @param
|
|
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
|
|
948
|
+
* Builds a map of relativePath → blobId for all files in a tree.
|
|
949
|
+
* Used to compare trees by content rather than by hash (which includes mtime).
|
|
950
|
+
* @param tree - Tree structure to extract file content map from
|
|
883
951
|
*/
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
});
|
|
952
|
+
_getFileContentMap(tree) {
|
|
953
|
+
const map = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const [, node] of tree.trees) {
|
|
955
|
+
const meta = node?.meta;
|
|
956
|
+
if (meta?.type === "file") {
|
|
957
|
+
map.set(meta.relativePath, meta.blobId ?? "");
|
|
958
|
+
} else if (meta?.type === "directory" && meta.relativePath !== ".") {
|
|
959
|
+
map.set(meta.relativePath, "<dir>");
|
|
951
960
|
}
|
|
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
961
|
}
|
|
962
|
+
return map;
|
|
1116
963
|
}
|
|
1117
964
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
965
|
+
* Derives a deterministic string key from a content map so that two trees
|
|
966
|
+
* with identical file paths + blobIds produce the same key regardless of
|
|
967
|
+
* mtime differences.
|
|
968
|
+
* @param map - Content map (relativePath → blobId)
|
|
1120
969
|
*/
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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;
|
|
970
|
+
_contentKeyFromMap(map) {
|
|
971
|
+
const sorted = Array.from(map.entries()).sort(
|
|
972
|
+
(a, b) => a[0].localeCompare(b[0])
|
|
973
|
+
);
|
|
974
|
+
return sorted.map(([p, b]) => `${p}:${b}`).join("\n");
|
|
1141
975
|
}
|
|
1142
976
|
/**
|
|
1143
|
-
*
|
|
977
|
+
* Derives a deterministic content key from an FsTree.
|
|
978
|
+
* @param tree - Tree structure to derive content key from
|
|
1144
979
|
*/
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return result;
|
|
980
|
+
_contentKeyFromTree(tree) {
|
|
981
|
+
return this._contentKeyFromMap(this._getFileContentMap(tree));
|
|
1148
982
|
}
|
|
1149
983
|
/**
|
|
1150
|
-
*
|
|
984
|
+
* Compares two trees by file content (relativePath + blobId).
|
|
985
|
+
* Ignores mtime differences — trees are equivalent if they have the same
|
|
986
|
+
* files with the same content. This prevents bounce-back restores from
|
|
987
|
+
* destroying locally-created files during bidirectional sync.
|
|
988
|
+
* @param a - First tree to compare
|
|
989
|
+
* @param b - Second tree to compare
|
|
1151
990
|
*/
|
|
1152
|
-
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
991
|
+
_treesHaveEquivalentContent(a, b) {
|
|
992
|
+
const aFiles = this._getFileContentMap(a);
|
|
993
|
+
const bFiles = this._getFileContentMap(b);
|
|
994
|
+
if (aFiles.size !== bFiles.size) return false;
|
|
995
|
+
for (const [path, blobId] of aFiles) {
|
|
996
|
+
if (bFiles.get(path) !== blobId) return false;
|
|
1156
997
|
}
|
|
1157
|
-
return
|
|
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;
|
|
998
|
+
return true;
|
|
1175
999
|
}
|
|
1176
1000
|
/**
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
* @param
|
|
1001
|
+
* Watches database for tree changes and syncs to filesystem
|
|
1002
|
+
* Uses Connector for socket-based notifications
|
|
1003
|
+
* @param db - Database instance
|
|
1004
|
+
* @param connector - Connector instance for socket-based sync
|
|
1005
|
+
* @param treeKey - Tree table key
|
|
1006
|
+
* @param restoreOptions - Restore options (e.g., cleanTarget)
|
|
1007
|
+
* @returns Function to stop watching
|
|
1180
1008
|
*/
|
|
1181
|
-
async
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
);
|
|
1009
|
+
async syncFromDb(db, connector, treeKey, restoreOptions) {
|
|
1010
|
+
if (!this._scanner["_watcher"]) {
|
|
1011
|
+
await this._scanner.watch();
|
|
1193
1012
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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)
|
|
1013
|
+
let pendingRef = null;
|
|
1014
|
+
let fromDbTimer = null;
|
|
1015
|
+
const processRef = async (treeRef) => {
|
|
1016
|
+
this._scanner.pauseWatch();
|
|
1017
|
+
try {
|
|
1018
|
+
const incomingTree = await FsAgent._withTimeout(
|
|
1019
|
+
this._fetchTreeFromDb(db, treeKey, treeRef),
|
|
1020
|
+
this._timeouts.fetchTree,
|
|
1021
|
+
`syncFromDb → fetchTree(${treeKey}@${treeRef.slice(0, 8)}…)`
|
|
1207
1022
|
);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1023
|
+
const currentTree = await FsAgent._withTimeout(
|
|
1024
|
+
this.extract(),
|
|
1025
|
+
this._timeouts.extract,
|
|
1026
|
+
`syncFromDb → extract(${this._rootPath})`
|
|
1027
|
+
);
|
|
1028
|
+
if (this._treesHaveEquivalentContent(currentTree, incomingTree)) {
|
|
1029
|
+
return;
|
|
1213
1030
|
}
|
|
1031
|
+
await FsAgent._withTimeout(
|
|
1032
|
+
this.restore(incomingTree, void 0, restoreOptions),
|
|
1033
|
+
this._timeouts.restore,
|
|
1034
|
+
`syncFromDb → restore(${treeKey})`
|
|
1035
|
+
);
|
|
1036
|
+
const postRestoreTree = await this._scanner.scan();
|
|
1037
|
+
const dbAdapter = new FsDbAdapter(db, treeKey);
|
|
1038
|
+
const postRestoreRef = await dbAdapter.storeFsTree(postRestoreTree, {
|
|
1039
|
+
skipNotification: true
|
|
1040
|
+
});
|
|
1041
|
+
this._lastSentRef = postRestoreRef;
|
|
1042
|
+
this._lastSentContentKey = this._contentKeyFromTree(postRestoreTree);
|
|
1043
|
+
} catch {
|
|
1044
|
+
} finally {
|
|
1045
|
+
this._scanner.resumeWatch();
|
|
1214
1046
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
throw new Error(
|
|
1220
|
-
`${prefix}: Column keys must not change! Column "${before}" was renamed into "${after}".`
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1047
|
+
};
|
|
1048
|
+
const syncCallback = async (treeRef) => {
|
|
1049
|
+
if (!treeRef || typeof treeRef !== "string") {
|
|
1050
|
+
return;
|
|
1223
1051
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
);
|
|
1052
|
+
pendingRef = treeRef;
|
|
1053
|
+
if (fromDbTimer) clearTimeout(fromDbTimer);
|
|
1054
|
+
fromDbTimer = setTimeout(async () => {
|
|
1055
|
+
fromDbTimer = null;
|
|
1056
|
+
const ref = pendingRef;
|
|
1057
|
+
pendingRef = null;
|
|
1058
|
+
if (ref) {
|
|
1059
|
+
await processRef(ref);
|
|
1232
1060
|
}
|
|
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
|
|
1061
|
+
}, this._timeouts.debounceMs);
|
|
1405
1062
|
};
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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)
|
|
1063
|
+
connector.listen(syncCallback);
|
|
1064
|
+
return () => {
|
|
1065
|
+
if (fromDbTimer) clearTimeout(fromDbTimer);
|
|
1066
|
+
connector.tearDown();
|
|
1433
1067
|
};
|
|
1434
1068
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
await
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1069
|
+
/**
|
|
1070
|
+
* Creates a fully configured FsAgent from a Client instance.
|
|
1071
|
+
* This factory method provides a simplified API where sync methods don't require
|
|
1072
|
+
* db, connector, and treeKey parameters - they are stored internally.
|
|
1073
|
+
* @param filePath - Directory path to sync
|
|
1074
|
+
* @param treeKey - Tree table key (route will be `/${treeKey}`)
|
|
1075
|
+
* @param client - Client instance with io and bs properties
|
|
1076
|
+
* @param socket - Socket instance for connector communication
|
|
1077
|
+
* @param options - Optional FsAgent options (db and treeKey are set automatically).
|
|
1078
|
+
* `syncConfig` and `clientIdentity` from these options are forwarded to
|
|
1079
|
+
* the Connector so that a single config origin governs all layers.
|
|
1080
|
+
* @returns Configured FsAgent instance with simplified sync API
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```typescript
|
|
1083
|
+
* const syncConfig: SyncConfig = { requireAck: true, maxDedupSetSize: 5000 };
|
|
1084
|
+
* const agent = await FsAgent.fromClient(
|
|
1085
|
+
* './my-folder', 'sharedTree', client, socket, { syncConfig },
|
|
1086
|
+
* );
|
|
1087
|
+
* // Simplified sync methods - no db/connector/treeKey needed
|
|
1088
|
+
* await agent.syncToDbSimple();
|
|
1089
|
+
* await agent.syncFromDbSimple({ cleanTarget: true });
|
|
1090
|
+
* // Original methods still work
|
|
1091
|
+
* await agent.syncToDb(db, connector, treeKey);
|
|
1092
|
+
* ```
|
|
1093
|
+
*/
|
|
1094
|
+
static async fromClient(filePath, treeKey, client, socket, options) {
|
|
1095
|
+
if (!client.io) {
|
|
1096
|
+
throw new Error("Client.io is not initialized");
|
|
1097
|
+
}
|
|
1098
|
+
if (!client.bs) {
|
|
1099
|
+
throw new Error("Client.bs is not initialized");
|
|
1100
|
+
}
|
|
1101
|
+
const { Db: Db2, Connector } = await import("@rljson/db");
|
|
1102
|
+
const db = new Db2(client.io);
|
|
1103
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
1104
|
+
const connector = new Connector(
|
|
1105
|
+
db,
|
|
1106
|
+
route,
|
|
1107
|
+
socket,
|
|
1108
|
+
options?.syncConfig,
|
|
1109
|
+
options?.clientIdentity
|
|
1471
1110
|
);
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
|
1111
|
+
const agent = new FsAgent(filePath, client.bs, options);
|
|
1112
|
+
const enhancedAgent = agent;
|
|
1113
|
+
enhancedAgent.syncToDbSimple = async (syncOptions) => {
|
|
1114
|
+
return agent.syncToDb(db, connector, treeKey, syncOptions);
|
|
1489
1115
|
};
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
[request.table]: tableFiltered
|
|
1116
|
+
enhancedAgent.syncFromDbSimple = async (restoreOpts) => {
|
|
1117
|
+
return agent.syncFromDb(db, connector, treeKey, restoreOpts);
|
|
1493
1118
|
};
|
|
1494
|
-
return
|
|
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;
|
|
1119
|
+
return enhancedAgent;
|
|
3974
1120
|
}
|
|
3975
1121
|
/** Example instance for test purposes */
|
|
3976
|
-
static
|
|
3977
|
-
|
|
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);
|
|
1122
|
+
static get example() {
|
|
1123
|
+
return new FsAgent(process.cwd());
|
|
3985
1124
|
}
|
|
3986
1125
|
}
|
|
3987
1126
|
async function createSharedTreeTable(io, treeKey) {
|
|
@@ -4032,7 +1171,7 @@ async function runClientServerSetup(opts = {}) {
|
|
|
4032
1171
|
const agentB = new FsAgent(folderB, clientB.bs);
|
|
4033
1172
|
const helloPathA = join(folderA, "hello.txt");
|
|
4034
1173
|
await writeFile(helloPathA, "Hello from Client A");
|
|
4035
|
-
const rootRef = await agentA.storeInDb(clientDbA, treeKey, {
|
|
1174
|
+
const rootRef = await agentA.storeInDb(clientDbA, treeKey, { skipNotification: true });
|
|
4036
1175
|
await agentB.loadFromDb(clientDbB, treeKey, rootRef);
|
|
4037
1176
|
const contentB = await readFile(join(folderB, "hello.txt"), "utf8");
|
|
4038
1177
|
const cleanup = async () => {
|