@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.
- package/README.architecture.md +921 -1
- package/README.contributors.md +534 -20
- package/README.public.md +1016 -4
- package/README.trouble.md +49 -0
- package/dist/README.architecture.md +921 -1
- package/dist/README.contributors.md +534 -20
- package/dist/README.public.md +1016 -4
- package/dist/README.trouble.md +49 -0
- package/dist/connector/connector.d.ts +71 -10
- package/dist/db.d.ts +23 -1
- package/dist/db.js +308 -32
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/package.json +20 -20
package/dist/README.trouble.md
CHANGED
|
@@ -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
|
|
17
|
-
private
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|