@rljson/db 0.0.12 → 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.
@@ -6,9 +6,58 @@
6
6
 
7
7
  ## Table of contents <!-- omit in toc -->
8
8
 
9
+ - [Tree INSERT operations failing with empty results](#tree-insert-operations-failing-with-empty-results)
9
10
  - [Vscode Windows: Debugging is not working](#vscode-windows-debugging-is-not-working)
10
11
  - [GitHub actions: Cannot find module @rollup/rollup-linux-x64-gnu](#github-actions-cannot-find-module-rolluprollup-linux-x64-gnu)
11
12
 
13
+ ## Tree INSERT operations failing with empty results
14
+
15
+ Date: 2026-01-28
16
+
17
+ ### Symptoms
18
+
19
+ - Tree INSERT operations complete without errors
20
+ - GET queries after INSERT return empty results or only root node
21
+ - Cell length is 1 instead of expected count
22
+ - Navigation stops at root level
23
+
24
+ ### Root Cause
25
+
26
+ The `treeFromObject` function from `@rljson/rljson` v0.0.74+ creates an explicit root node with `id='root'` at the end of the tree array. When inserting an already-isolated subtree (from `isolate()`), this created a double-root structure:
27
+
28
+ ```
29
+ Auto-root (id='root')
30
+ └─ User-root (id='root')
31
+ └─ actual data nodes
32
+ ```
33
+
34
+ When `TreeController` navigates the tree, it stops at the first node with `id='root'` (the auto-root), which has no meaningful data.
35
+
36
+ ### Solution
37
+
38
+ ✅ **Fixed in current version**: The `db.ts` file now calls `treeFromObject` with `skipRootCreation: true` parameter:
39
+
40
+ ```typescript
41
+ const trees = treeFromObject(treeObject, true);
42
+ ```
43
+
44
+ This prevents the automatic root wrapper from being created during INSERT operations.
45
+
46
+ ### Verification
47
+
48
+ Run the tree INSERT tests:
49
+
50
+ ```bash
51
+ pnpm test --run --grep "insert on tree"
52
+ ```
53
+
54
+ All tree INSERT tests should pass:
55
+
56
+ - "insert on tree root node"
57
+ - "insert on tree deeper leaf"
58
+ - "insert on tree simple branch"
59
+ - "insert new child on branch"
60
+
12
61
  ## Vscode Windows: Debugging is not working
13
62
 
14
63
  Date: 2025-03-08
@@ -1,10 +1,7 @@
1
1
  import { Socket } from '@rljson/io';
2
- import { Route } from '@rljson/rljson';
2
+ import { AckPayload, ClientId, InsertHistoryTimeId, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
3
3
  import { Db } from '../db.ts';
4
- export type ConnectorPayload = {
5
- o: string;
6
- r: string;
7
- };
4
+ export type { ConnectorPayload } from '@rljson/rljson';
8
5
  export type ConnectorCallback = (ref: string) => Promise<any>;
9
6
  export declare class Connector {
10
7
  private readonly _db;
@@ -13,15 +10,79 @@ export declare class Connector {
13
10
  private _origin;
14
11
  private _callbacks;
15
12
  private _isListening;
16
- private _sentRefs;
17
- private _receivedRefs;
18
- constructor(_db: Db, _route: Route, _socket: Socket);
13
+ private _sentRefsCurrent;
14
+ private _sentRefsPrevious;
15
+ private _receivedRefsCurrent;
16
+ private _receivedRefsPrevious;
17
+ private readonly _maxDedup;
18
+ private readonly _syncConfig;
19
+ private readonly _clientId;
20
+ private readonly _events;
21
+ private _seq;
22
+ private _lastPredecessors;
23
+ private _peerSeqs;
24
+ constructor(_db: Db, _route: Route, _socket: Socket, syncConfig?: SyncConfig, clientIdentity?: ClientId);
25
+ /**
26
+ * Sends a ref to the server via the socket.
27
+ * Enriches the payload based on SyncConfig flags.
28
+ * @param ref - The ref to send
29
+ */
19
30
  send(ref: string): void;
20
- listen(callback: (editHistoryRef: string) => Promise<void>): void;
31
+ /**
32
+ * Sends a ref and waits for server acknowledgment.
33
+ * Only meaningful when `syncConfig.requireAck` is `true`.
34
+ * @param ref - The ref to send
35
+ * @returns A promise that resolves with the AckPayload
36
+ */
37
+ sendWithAck(ref: string): Promise<AckPayload>;
38
+ /**
39
+ * Sets the causal predecessors for the next send.
40
+ * @param predecessors - The InsertHistory timeIds of causal predecessors
41
+ */
42
+ setPredecessors(predecessors: InsertHistoryTimeId[]): void;
43
+ /**
44
+ * Registers a callback for incoming refs on this route.
45
+ *
46
+ * Incoming refs are processed through the full sync pipeline:
47
+ * origin filtering, dedup, gap detection, and ACK.
48
+ *
49
+ * @param callback - The callback to invoke with each deduplicated incoming ref
50
+ */
51
+ listen(callback: ConnectorCallback): void;
52
+ /**
53
+ * Returns the current sequence number.
54
+ * Only meaningful when `causalOrdering` is enabled.
55
+ */
56
+ get seq(): number;
57
+ /**
58
+ * Returns the stable client identity.
59
+ * Only available when `includeClientIdentity` is enabled.
60
+ */
61
+ get clientIdentity(): ClientId | undefined;
62
+ /**
63
+ * Returns the sync configuration, if any.
64
+ */
65
+ get syncConfig(): SyncConfig | undefined;
66
+ /**
67
+ * Returns the typed event names for this connector's route.
68
+ */
69
+ get events(): SyncEventNames;
21
70
  private _init;
22
- teardown(): void;
71
+ tearDown(): void;
72
+ private _hasSentRef;
73
+ private _addSentRef;
74
+ private _hasReceivedRef;
75
+ private _addReceivedRef;
23
76
  private _notifyCallbacks;
77
+ private _processIncoming;
24
78
  private _registerSocketObserver;
79
+ private _registerGapFillHandler;
80
+ /**
81
+ * Listens for bootstrap messages from the server.
82
+ * The server sends the latest ref on connect and optionally via heartbeat.
83
+ * _processIncoming handles dedup so already-seen refs are filtered out.
84
+ */
85
+ private _registerBootstrapHandler;
25
86
  private _registerDbObserver;
26
87
  get socket(): Socket;
27
88
  get route(): Route;
package/dist/db.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Io } from '@rljson/io';
2
2
  import { Json, JsonValue } from '@rljson/json';
3
- import { Edit, EditHistory, InsertHistoryRow, InsertHistoryTimeId, MultiEdit, Ref, Rljson, Route, SliceId, TableType } from '@rljson/rljson';
3
+ import { Edit, EditHistory, InsertHistoryRow, InsertHistoryTimeId, MultiEdit, Ref, Rljson, Route, SliceId, TableType, Tree } from '@rljson/rljson';
4
4
  import { Controller, ControllerChildProperty, ControllerRefs } from './controller/controller.ts';
5
5
  import { Core } from './core.ts';
6
6
  import { Join } from './join/join.ts';
@@ -90,6 +90,28 @@ export declare class Db {
90
90
  skipNotification?: boolean;
91
91
  skipHistory?: boolean;
92
92
  }): Promise<InsertHistoryRow<any>[]>;
93
+ /**
94
+ * Insert pre-decomposed tree nodes into a tree table.
95
+ *
96
+ * Unlike `insert()`, which expects a plain nested object and decomposes it
97
+ * via `treeFromObject()`, this method accepts an array of already-decomposed
98
+ * `Tree` nodes (e.g. from FsScanner). The **root node must be the last
99
+ * element** in the array.
100
+ *
101
+ * The method goes through the full insert pipeline:
102
+ * 1. Writes each node via TreeController
103
+ * 2. Creates an InsertHistoryRow automatically
104
+ * 3. Calls `notify.notify()` so Connector observers fire
105
+ *
106
+ * @param treeKey - The tree table key (must end with "Tree")
107
+ * @param trees - Pre-decomposed Tree nodes, root LAST
108
+ * @param options - Optional: skip notification or history
109
+ * @returns The InsertHistoryRow for the root node
110
+ */
111
+ insertTrees(treeKey: string, trees: Tree[], options?: {
112
+ skipNotification?: boolean;
113
+ skipHistory?: boolean;
114
+ }): Promise<InsertHistoryRow<any>[]>;
93
115
  /**
94
116
  * Recursively runs controllers based on the route of the Insert
95
117
  * @param insert - The Insert to run
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;
36
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;
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
  });
@@ -984,6 +1160,15 @@ class TreeController extends BaseController {
984
1160
  if (treeId && treeId !== tree.id) {
985
1161
  return { [this._tableKey]: { _data: [], _type: "trees" } };
986
1162
  }
1163
+ const shouldExpandChildren = path !== void 0;
1164
+ if (!shouldExpandChildren) {
1165
+ return {
1166
+ [this._tableKey]: {
1167
+ _data: [tree],
1168
+ _type: "trees"
1169
+ }
1170
+ };
1171
+ }
987
1172
  const children = [];
988
1173
  for (const childRef of tree.children ?? []) {
989
1174
  const child = await this.get(
@@ -1005,21 +1190,47 @@ class TreeController extends BaseController {
1005
1190
  if (trees.length === 0) {
1006
1191
  return {};
1007
1192
  }
1193
+ if (trees.length > 1e5) {
1194
+ throw new Error(
1195
+ `TreeController.buildTreeFromTrees: Tree size exceeds limit (${trees.length} > 100000 nodes). This may indicate a performance issue or data structure problem.`
1196
+ );
1197
+ }
1008
1198
  const treeMap = /* @__PURE__ */ new Map();
1009
1199
  for (const tree of trees) {
1010
1200
  treeMap.set(tree._hash, tree);
1011
1201
  }
1012
- const buildObject = (tree) => {
1202
+ const memo = /* @__PURE__ */ new Map();
1203
+ let buildObjectCallCount = 0;
1204
+ const MAX_ITERATIONS = 1e6;
1205
+ const buildObject = (tree, depth = 0) => {
1206
+ buildObjectCallCount++;
1207
+ if (buildObjectCallCount > MAX_ITERATIONS) {
1208
+ throw new Error(
1209
+ `TreeController.buildTreeFromTrees: Maximum iterations (${MAX_ITERATIONS}) exceeded. This likely indicates a bug. Processed ${buildObjectCallCount} nodes from ${trees.length} total.`
1210
+ );
1211
+ }
1212
+ if (depth > 1e4) {
1213
+ throw new Error(
1214
+ `TreeController.buildTreeFromTrees: Tree depth exceeds limit (${depth} > 10000). This may indicate a circular reference or extremely deep structure.`
1215
+ );
1216
+ }
1217
+ const hash = tree._hash;
1218
+ if (memo.has(hash)) {
1219
+ return memo.get(hash);
1220
+ }
1013
1221
  if (!tree.isParent || !tree.children || tree.children.length === 0) {
1014
- return tree;
1222
+ const result3 = tree;
1223
+ memo.set(hash, result3);
1224
+ return result3;
1015
1225
  }
1016
1226
  const result2 = {};
1017
1227
  for (const childHash of tree.children) {
1018
1228
  const childTree = treeMap.get(childHash);
1019
1229
  if (childTree && childTree.id) {
1020
- result2[childTree.id] = buildObject(childTree);
1230
+ result2[childTree.id] = buildObject(childTree, depth + 1);
1021
1231
  }
1022
1232
  }
1233
+ memo.set(hash, result2);
1023
1234
  return result2;
1024
1235
  };
1025
1236
  const referencedHashes = /* @__PURE__ */ new Set();
@@ -1105,6 +1316,9 @@ class TreeController extends BaseController {
1105
1316
  return cells;
1106
1317
  }
1107
1318
  async getChildRefs(where, filter) {
1319
+ if (typeof where === "string") {
1320
+ return [];
1321
+ }
1108
1322
  const childRefs = [];
1109
1323
  const { [this._tableKey]: table } = await this.get(where, filter);
1110
1324
  const trees = table._data;
@@ -1241,7 +1455,11 @@ class Core {
1241
1455
  // ...........................................................................
1242
1456
  /** Reads a specific row from a database table */
1243
1457
  async readRow(table, rowHash) {
1244
- return await this._io.readRows({ table, where: { _hash: rowHash } });
1458
+ const result = await this._io.readRows({
1459
+ table,
1460
+ where: { _hash: rowHash }
1461
+ });
1462
+ return result;
1245
1463
  }
1246
1464
  // ...........................................................................
1247
1465
  async readRows(table, where) {
@@ -2092,7 +2310,7 @@ class ColumnFilterProcessor {
2092
2310
  }
2093
2311
  }
2094
2312
  // ...........................................................................
2095
- /* v8 ignore stop */
2313
+ /* v8 ignore stop -- @preserve */
2096
2314
  static operatorsForType(type) {
2097
2315
  switch (type) {
2098
2316
  case "string":
@@ -2919,6 +3137,12 @@ class Db {
2919
3137
  * @returns - An Rljson object matching the route and filters
2920
3138
  */
2921
3139
  async _get(route, where, controllers, filter, sliceIds, routeAccumulator, options) {
3140
+ const depth = routeAccumulator ? routeAccumulator.flat.split("/").length : 1;
3141
+ if (depth > 100) {
3142
+ throw new Error(
3143
+ `Db._get: Maximum recursion depth (100) exceeded. This likely indicates a bug in route resolution or where clause construction. Current route: ${route.flat}, routeAccumulator: ${routeAccumulator?.flat}`
3144
+ );
3145
+ }
2922
3146
  const opts = options ?? {};
2923
3147
  const routeHasRefs = route.flat != route.flatWithoutRefs;
2924
3148
  const hasFilter = filter !== void 0 && filter.length > 0;
@@ -3537,6 +3761,56 @@ class Db {
3537
3761
  return insertHistoryRow;
3538
3762
  }
3539
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
+ // ...........................................................................
3540
3814
  /**
3541
3815
  * Recursively runs controllers based on the route of the Insert
3542
3816
  * @param insert - The Insert to run
@@ -3738,7 +4012,7 @@ class Db {
3738
4012
  `Db._insert: No tree data found for table "${nodeTableKey}" in route "${route.flat}".`
3739
4013
  );
3740
4014
  }
3741
- const trees = treeFromObject(treeObject);
4015
+ const trees = treeFromObject(treeObject, true);
3742
4016
  const writePromises = trees.map(
3743
4017
  (tree2) => runFn("add", tree2, "db.insert")
3744
4018
  );
@@ -3862,9 +4136,7 @@ class Db {
3862
4136
  const multiEditController = await this.getController(
3863
4137
  cakeKey + "MultiEdits"
3864
4138
  );
3865
- const { [cakeKey + "MultiEdits"]: result } = await multiEditController.get(
3866
- where
3867
- );
4139
+ const { [cakeKey + "MultiEdits"]: result } = await multiEditController.get(where);
3868
4140
  return result._data;
3869
4141
  }
3870
4142
  // ...........................................................................
@@ -6091,6 +6363,10 @@ export {
6091
6363
  exampleEditActionRowSort,
6092
6364
  exampleEditActionSetValue,
6093
6365
  exampleEditSetValueReferenced,
6366
+ inject,
6367
+ isolate,
6368
+ makeUnique,
6369
+ mergeTrees,
6094
6370
  staticExample
6095
6371
  };
6096
6372
  //# sourceMappingURL=db.js.map