@rljson/db 0.0.13 → 0.0.14
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 +62 -0
- package/README.public.md +73 -0
- package/dist/README.architecture.md +62 -0
- package/dist/README.public.md +73 -0
- package/dist/connector/connector.d.ts +71 -10
- package/dist/db.d.ts +23 -1
- package/dist/db.js +250 -24
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/package.json +17 -17
package/dist/db.js
CHANGED
|
@@ -1,75 +1,251 @@
|
|
|
1
|
-
import { timeId, Route, createInsertHistoryTableCfg, Validate, BaseValidator, treeFromObject, isTimeId, getTimeIdTimestamp, createSliceIdsTableCfg, createLayerTableCfg, createCakeTableCfg } from "@rljson/rljson";
|
|
1
|
+
import { timeId, syncEvents, clientId, Route, createInsertHistoryTableCfg, Validate, BaseValidator, treeFromObject, isTimeId, getTimeIdTimestamp, createSliceIdsTableCfg, createLayerTableCfg, createCakeTableCfg } from "@rljson/rljson";
|
|
2
2
|
import { rmhsh, hsh, Hash, hip } from "@rljson/hash";
|
|
3
3
|
import { equals, merge } from "@rljson/json";
|
|
4
4
|
import { IoMem } from "@rljson/io";
|
|
5
5
|
import { traverse } from "object-traversal";
|
|
6
6
|
import { compileExpression } from "filtrex";
|
|
7
7
|
class Connector {
|
|
8
|
-
constructor(_db, _route, _socket) {
|
|
8
|
+
constructor(_db, _route, _socket, syncConfig, clientIdentity) {
|
|
9
9
|
this._db = _db;
|
|
10
10
|
this._route = _route;
|
|
11
11
|
this._socket = _socket;
|
|
12
12
|
this._origin = timeId();
|
|
13
|
+
this._syncConfig = syncConfig;
|
|
14
|
+
this._events = syncEvents(this._route.flat);
|
|
15
|
+
if (clientIdentity) {
|
|
16
|
+
this._clientId = clientIdentity;
|
|
17
|
+
} else if (syncConfig?.includeClientIdentity) {
|
|
18
|
+
this._clientId = clientId();
|
|
19
|
+
}
|
|
20
|
+
this._maxDedup = syncConfig?.maxDedupSetSize ?? 1e4;
|
|
13
21
|
this._init();
|
|
14
22
|
}
|
|
15
23
|
_origin;
|
|
16
24
|
_callbacks = [];
|
|
17
25
|
_isListening = false;
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
// Two-generation dedup sets — bounded memory
|
|
27
|
+
_sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
28
|
+
_sentRefsPrevious = /* @__PURE__ */ new Set();
|
|
29
|
+
_receivedRefsCurrent = /* @__PURE__ */ new Set();
|
|
30
|
+
_receivedRefsPrevious = /* @__PURE__ */ new Set();
|
|
31
|
+
_maxDedup;
|
|
32
|
+
// Sync protocol state
|
|
33
|
+
_syncConfig;
|
|
34
|
+
_clientId;
|
|
35
|
+
_events;
|
|
36
|
+
_seq = 0;
|
|
37
|
+
_lastPredecessors = [];
|
|
38
|
+
_peerSeqs = /* @__PURE__ */ new Map();
|
|
39
|
+
// ...........................................................................
|
|
40
|
+
/**
|
|
41
|
+
* Sends a ref to the server via the socket.
|
|
42
|
+
* Enriches the payload based on SyncConfig flags.
|
|
43
|
+
* @param ref - The ref to send
|
|
44
|
+
*/
|
|
20
45
|
send(ref) {
|
|
21
|
-
if (this.
|
|
22
|
-
this.
|
|
23
|
-
|
|
46
|
+
if (this._hasSentRef(ref) || this._hasReceivedRef(ref)) return;
|
|
47
|
+
this._addSentRef(ref);
|
|
48
|
+
const payload = {
|
|
24
49
|
o: this._origin,
|
|
25
50
|
r: ref
|
|
51
|
+
};
|
|
52
|
+
if (this._syncConfig?.includeClientIdentity && this._clientId) {
|
|
53
|
+
payload.c = this._clientId;
|
|
54
|
+
payload.t = Date.now();
|
|
55
|
+
}
|
|
56
|
+
if (this._syncConfig?.causalOrdering) {
|
|
57
|
+
payload.seq = ++this._seq;
|
|
58
|
+
if (this._lastPredecessors.length > 0) {
|
|
59
|
+
payload.p = [...this._lastPredecessors];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.socket.emit(this._events.ref, payload);
|
|
63
|
+
}
|
|
64
|
+
// ...........................................................................
|
|
65
|
+
/**
|
|
66
|
+
* Sends a ref and waits for server acknowledgment.
|
|
67
|
+
* Only meaningful when `syncConfig.requireAck` is `true`.
|
|
68
|
+
* @param ref - The ref to send
|
|
69
|
+
* @returns A promise that resolves with the AckPayload
|
|
70
|
+
*/
|
|
71
|
+
async sendWithAck(ref) {
|
|
72
|
+
const timeoutMs = this._syncConfig?.ackTimeoutMs ?? 1e4;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
this._socket.off(this._events.ack, handler);
|
|
76
|
+
reject(new Error(`ACK timeout for ref ${ref} after ${timeoutMs}ms`));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
const handler = (ack) => {
|
|
79
|
+
if (ack.r === ref) {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
this._socket.off(this._events.ack, handler);
|
|
82
|
+
resolve(ack);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
this._socket.on(this._events.ack, handler);
|
|
86
|
+
this.send(ref);
|
|
26
87
|
});
|
|
27
88
|
}
|
|
89
|
+
// ...........................................................................
|
|
90
|
+
/**
|
|
91
|
+
* Sets the causal predecessors for the next send.
|
|
92
|
+
* @param predecessors - The InsertHistory timeIds of causal predecessors
|
|
93
|
+
*/
|
|
94
|
+
setPredecessors(predecessors) {
|
|
95
|
+
this._lastPredecessors = predecessors;
|
|
96
|
+
}
|
|
97
|
+
// ...........................................................................
|
|
98
|
+
/**
|
|
99
|
+
* Registers a callback for incoming refs on this route.
|
|
100
|
+
*
|
|
101
|
+
* Incoming refs are processed through the full sync pipeline:
|
|
102
|
+
* origin filtering, dedup, gap detection, and ACK.
|
|
103
|
+
*
|
|
104
|
+
* @param callback - The callback to invoke with each deduplicated incoming ref
|
|
105
|
+
*/
|
|
28
106
|
listen(callback) {
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
107
|
+
this._callbacks.push(callback);
|
|
108
|
+
}
|
|
109
|
+
// ...........................................................................
|
|
110
|
+
/**
|
|
111
|
+
* Returns the current sequence number.
|
|
112
|
+
* Only meaningful when `causalOrdering` is enabled.
|
|
113
|
+
*/
|
|
114
|
+
get seq() {
|
|
115
|
+
return this._seq;
|
|
116
|
+
}
|
|
117
|
+
// ...........................................................................
|
|
118
|
+
/**
|
|
119
|
+
* Returns the stable client identity.
|
|
120
|
+
* Only available when `includeClientIdentity` is enabled.
|
|
121
|
+
*/
|
|
122
|
+
get clientIdentity() {
|
|
123
|
+
return this._clientId;
|
|
124
|
+
}
|
|
125
|
+
// ...........................................................................
|
|
126
|
+
/**
|
|
127
|
+
* Returns the sync configuration, if any.
|
|
128
|
+
*/
|
|
129
|
+
get syncConfig() {
|
|
130
|
+
return this._syncConfig;
|
|
131
|
+
}
|
|
132
|
+
// ...........................................................................
|
|
133
|
+
/**
|
|
134
|
+
* Returns the typed event names for this connector's route.
|
|
135
|
+
*/
|
|
136
|
+
get events() {
|
|
137
|
+
return this._events;
|
|
36
138
|
}
|
|
139
|
+
// ######################
|
|
140
|
+
// Private
|
|
141
|
+
// ######################
|
|
37
142
|
_init() {
|
|
38
143
|
this._registerSocketObserver();
|
|
144
|
+
this._registerBootstrapHandler();
|
|
39
145
|
this._registerDbObserver();
|
|
146
|
+
if (this._syncConfig?.causalOrdering) {
|
|
147
|
+
this._registerGapFillHandler();
|
|
148
|
+
}
|
|
40
149
|
this._isListening = true;
|
|
41
150
|
}
|
|
42
|
-
|
|
43
|
-
this._socket.removeAllListeners(this.
|
|
151
|
+
tearDown() {
|
|
152
|
+
this._socket.removeAllListeners(this._events.ref);
|
|
153
|
+
this._socket.removeAllListeners(this._events.bootstrap);
|
|
154
|
+
if (this._syncConfig?.causalOrdering) {
|
|
155
|
+
this._socket.removeAllListeners(this._events.gapFillRes);
|
|
156
|
+
}
|
|
157
|
+
if (this._syncConfig?.requireAck) {
|
|
158
|
+
this._socket.removeAllListeners(this._events.ack);
|
|
159
|
+
}
|
|
44
160
|
this._db.unregisterAllObservers(this._route);
|
|
45
161
|
this._isListening = false;
|
|
46
162
|
}
|
|
163
|
+
// ...........................................................................
|
|
164
|
+
// Two-generation dedup helpers
|
|
165
|
+
// ...........................................................................
|
|
166
|
+
_hasSentRef(ref) {
|
|
167
|
+
return this._sentRefsCurrent.has(ref) || this._sentRefsPrevious.has(ref);
|
|
168
|
+
}
|
|
169
|
+
_addSentRef(ref) {
|
|
170
|
+
this._sentRefsCurrent.add(ref);
|
|
171
|
+
if (this._sentRefsCurrent.size >= this._maxDedup) {
|
|
172
|
+
this._sentRefsPrevious = this._sentRefsCurrent;
|
|
173
|
+
this._sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_hasReceivedRef(ref) {
|
|
177
|
+
return this._receivedRefsCurrent.has(ref) || this._receivedRefsPrevious.has(ref);
|
|
178
|
+
}
|
|
179
|
+
_addReceivedRef(ref) {
|
|
180
|
+
this._receivedRefsCurrent.add(ref);
|
|
181
|
+
if (this._receivedRefsCurrent.size >= this._maxDedup) {
|
|
182
|
+
this._receivedRefsPrevious = this._receivedRefsCurrent;
|
|
183
|
+
this._receivedRefsCurrent = /* @__PURE__ */ new Set();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
47
186
|
_notifyCallbacks(ref) {
|
|
48
187
|
Promise.all(this._callbacks.map((cb) => cb(ref))).catch((err) => {
|
|
49
188
|
console.error(`Error notifying connector callbacks for ref ${ref}:`, err);
|
|
50
189
|
});
|
|
51
190
|
}
|
|
191
|
+
_processIncoming(payload) {
|
|
192
|
+
const ref = payload.r;
|
|
193
|
+
if (this._hasReceivedRef(ref)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (this._syncConfig?.causalOrdering && payload.seq != null && payload.c) {
|
|
197
|
+
const lastSeq = this._peerSeqs.get(payload.c) ?? 0;
|
|
198
|
+
if (payload.seq > lastSeq + 1) {
|
|
199
|
+
const gapReq = {
|
|
200
|
+
route: this._route.flat,
|
|
201
|
+
afterSeq: lastSeq
|
|
202
|
+
};
|
|
203
|
+
this._socket.emit(this._events.gapFillReq, gapReq);
|
|
204
|
+
}
|
|
205
|
+
this._peerSeqs.set(payload.c, payload.seq);
|
|
206
|
+
}
|
|
207
|
+
this._addReceivedRef(ref);
|
|
208
|
+
this._notifyCallbacks(ref);
|
|
209
|
+
if (this._syncConfig?.requireAck) {
|
|
210
|
+
this._socket.emit(this._events.ackClient, { r: ref });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
52
213
|
_registerSocketObserver() {
|
|
53
|
-
this.socket.on(this.
|
|
214
|
+
this.socket.on(this._events.ref, (p) => {
|
|
54
215
|
if (p.o === this._origin) {
|
|
55
216
|
return;
|
|
56
217
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
218
|
+
this._processIncoming(p);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
_registerGapFillHandler() {
|
|
222
|
+
this._socket.on(this._events.gapFillRes, (res) => {
|
|
223
|
+
for (const p of res.refs) {
|
|
224
|
+
this._processIncoming(p);
|
|
60
225
|
}
|
|
61
|
-
|
|
62
|
-
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Listens for bootstrap messages from the server.
|
|
230
|
+
* The server sends the latest ref on connect and optionally via heartbeat.
|
|
231
|
+
* _processIncoming handles dedup so already-seen refs are filtered out.
|
|
232
|
+
*/
|
|
233
|
+
_registerBootstrapHandler() {
|
|
234
|
+
this._socket.on(this._events.bootstrap, (p) => {
|
|
235
|
+
this._processIncoming(p);
|
|
63
236
|
});
|
|
64
237
|
}
|
|
65
238
|
_registerDbObserver() {
|
|
66
239
|
this._db.registerObserver(this._route, (ins) => {
|
|
67
240
|
return new Promise((resolve) => {
|
|
68
241
|
const ref = ins[this.route.root.tableKey + "Ref"];
|
|
69
|
-
if (this.
|
|
242
|
+
if (this._hasSentRef(ref)) {
|
|
70
243
|
resolve();
|
|
71
244
|
return;
|
|
72
245
|
}
|
|
246
|
+
if (this._syncConfig?.causalOrdering && ins.previous?.length) {
|
|
247
|
+
this._lastPredecessors = [...ins.previous];
|
|
248
|
+
}
|
|
73
249
|
this.send(ref);
|
|
74
250
|
resolve();
|
|
75
251
|
});
|
|
@@ -2134,7 +2310,7 @@ class ColumnFilterProcessor {
|
|
|
2134
2310
|
}
|
|
2135
2311
|
}
|
|
2136
2312
|
// ...........................................................................
|
|
2137
|
-
/* v8 ignore stop */
|
|
2313
|
+
/* v8 ignore stop -- @preserve */
|
|
2138
2314
|
static operatorsForType(type) {
|
|
2139
2315
|
switch (type) {
|
|
2140
2316
|
case "string":
|
|
@@ -3585,6 +3761,56 @@ class Db {
|
|
|
3585
3761
|
return insertHistoryRow;
|
|
3586
3762
|
}
|
|
3587
3763
|
// ...........................................................................
|
|
3764
|
+
/**
|
|
3765
|
+
* Insert pre-decomposed tree nodes into a tree table.
|
|
3766
|
+
*
|
|
3767
|
+
* Unlike `insert()`, which expects a plain nested object and decomposes it
|
|
3768
|
+
* via `treeFromObject()`, this method accepts an array of already-decomposed
|
|
3769
|
+
* `Tree` nodes (e.g. from FsScanner). The **root node must be the last
|
|
3770
|
+
* element** in the array.
|
|
3771
|
+
*
|
|
3772
|
+
* The method goes through the full insert pipeline:
|
|
3773
|
+
* 1. Writes each node via TreeController
|
|
3774
|
+
* 2. Creates an InsertHistoryRow automatically
|
|
3775
|
+
* 3. Calls `notify.notify()` so Connector observers fire
|
|
3776
|
+
*
|
|
3777
|
+
* @param treeKey - The tree table key (must end with "Tree")
|
|
3778
|
+
* @param trees - Pre-decomposed Tree nodes, root LAST
|
|
3779
|
+
* @param options - Optional: skip notification or history
|
|
3780
|
+
* @returns The InsertHistoryRow for the root node
|
|
3781
|
+
*/
|
|
3782
|
+
async insertTrees(treeKey, trees, options) {
|
|
3783
|
+
if (!trees || trees.length === 0) {
|
|
3784
|
+
throw new Error(
|
|
3785
|
+
"Db.insertTrees: trees array must contain at least one node."
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
const controller = await this.getController(treeKey);
|
|
3789
|
+
const writePromises = trees.map(
|
|
3790
|
+
(tree) => controller.insert("add", tree, "db.insertTrees")
|
|
3791
|
+
);
|
|
3792
|
+
const writeResults = await Promise.all(writePromises);
|
|
3793
|
+
const lastResult = writeResults[writeResults.length - 1];
|
|
3794
|
+
if (!lastResult || lastResult.length === 0) {
|
|
3795
|
+
throw new Error(
|
|
3796
|
+
`Db.insertTrees: TreeController returned no result for root node of table "${treeKey}".`
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3799
|
+
const rootResult = lastResult[0];
|
|
3800
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
3801
|
+
const result = {
|
|
3802
|
+
...rootResult,
|
|
3803
|
+
route: route.flat
|
|
3804
|
+
};
|
|
3805
|
+
if (!options?.skipHistory) {
|
|
3806
|
+
await this._writeInsertHistory(treeKey, result);
|
|
3807
|
+
}
|
|
3808
|
+
if (!options?.skipNotification) {
|
|
3809
|
+
this.notify.notify(route, result);
|
|
3810
|
+
}
|
|
3811
|
+
return [result];
|
|
3812
|
+
}
|
|
3813
|
+
// ...........................................................................
|
|
3588
3814
|
/**
|
|
3589
3815
|
* Recursively runs controllers based on the route of the Insert
|
|
3590
3816
|
* @param insert - The Insert to run
|