@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/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
- _sentRefs = /* @__PURE__ */ new Set();
19
- _receivedRefs = /* @__PURE__ */ new Set();
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._sentRefs.has(ref) || this._receivedRefs.has(ref)) return;
22
- this._sentRefs.add(ref);
23
- this.socket.emit(this.route.flat, {
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._socket.on(this._route.flat, async (payload) => {
30
- try {
31
- await callback(payload.r);
32
- } catch (error) {
33
- console.error("Error in connector listener callback:", error);
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
- teardown() {
43
- this._socket.removeAllListeners(this._route.flat);
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.route.flat, (p) => {
214
+ this.socket.on(this._events.ref, (p) => {
54
215
  if (p.o === this._origin) {
55
216
  return;
56
217
  }
57
- const ref = p.r;
58
- if (this._receivedRefs.has(ref)) {
59
- return;
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
- this._receivedRefs.add(p.r);
62
- this._notifyCallbacks(p.r);
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._sentRefs.has(ref)) {
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