@rljson/db 0.0.13 → 0.0.15
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 +85 -10
- package/dist/db.d.ts +58 -1
- package/dist/db.js +369 -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,276 @@
|
|
|
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 = [];
|
|
25
|
+
_conflictCallbacks = [];
|
|
17
26
|
_isListening = false;
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
// Two-generation dedup sets — bounded memory
|
|
28
|
+
_sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
29
|
+
_sentRefsPrevious = /* @__PURE__ */ new Set();
|
|
30
|
+
_receivedRefsCurrent = /* @__PURE__ */ new Set();
|
|
31
|
+
_receivedRefsPrevious = /* @__PURE__ */ new Set();
|
|
32
|
+
_maxDedup;
|
|
33
|
+
// Sync protocol state
|
|
34
|
+
_syncConfig;
|
|
35
|
+
_clientId;
|
|
36
|
+
_events;
|
|
37
|
+
_seq = 0;
|
|
38
|
+
_lastPredecessors = [];
|
|
39
|
+
_peerSeqs = /* @__PURE__ */ new Map();
|
|
40
|
+
// ...........................................................................
|
|
41
|
+
/**
|
|
42
|
+
* Sends a ref to the server via the socket.
|
|
43
|
+
* Enriches the payload based on SyncConfig flags.
|
|
44
|
+
* @param ref - The ref to send
|
|
45
|
+
*/
|
|
20
46
|
send(ref) {
|
|
21
|
-
if (this.
|
|
22
|
-
this.
|
|
23
|
-
|
|
47
|
+
if (this._hasSentRef(ref) || this._hasReceivedRef(ref)) return;
|
|
48
|
+
this._addSentRef(ref);
|
|
49
|
+
const payload = {
|
|
24
50
|
o: this._origin,
|
|
25
51
|
r: ref
|
|
52
|
+
};
|
|
53
|
+
if (this._syncConfig?.includeClientIdentity && this._clientId) {
|
|
54
|
+
payload.c = this._clientId;
|
|
55
|
+
payload.t = Date.now();
|
|
56
|
+
}
|
|
57
|
+
if (this._syncConfig?.causalOrdering) {
|
|
58
|
+
payload.seq = ++this._seq;
|
|
59
|
+
if (this._lastPredecessors.length > 0) {
|
|
60
|
+
payload.p = [...this._lastPredecessors];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
this.socket.emit(this._events.ref, payload);
|
|
64
|
+
}
|
|
65
|
+
// ...........................................................................
|
|
66
|
+
/**
|
|
67
|
+
* Sends a ref and waits for server acknowledgment.
|
|
68
|
+
* Only meaningful when `syncConfig.requireAck` is `true`.
|
|
69
|
+
* @param ref - The ref to send
|
|
70
|
+
* @returns A promise that resolves with the AckPayload
|
|
71
|
+
*/
|
|
72
|
+
async sendWithAck(ref) {
|
|
73
|
+
const timeoutMs = this._syncConfig?.ackTimeoutMs ?? 1e4;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
this._socket.off(this._events.ack, handler);
|
|
77
|
+
reject(new Error(`ACK timeout for ref ${ref} after ${timeoutMs}ms`));
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
const handler = (ack) => {
|
|
80
|
+
if (ack.r === ref) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
this._socket.off(this._events.ack, handler);
|
|
83
|
+
resolve(ack);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
this._socket.on(this._events.ack, handler);
|
|
87
|
+
this.send(ref);
|
|
26
88
|
});
|
|
27
89
|
}
|
|
90
|
+
// ...........................................................................
|
|
91
|
+
/**
|
|
92
|
+
* Sets the causal predecessors for the next send.
|
|
93
|
+
* @param predecessors - The InsertHistory timeIds of causal predecessors
|
|
94
|
+
*/
|
|
95
|
+
setPredecessors(predecessors) {
|
|
96
|
+
this._lastPredecessors = predecessors;
|
|
97
|
+
}
|
|
98
|
+
// ...........................................................................
|
|
99
|
+
/**
|
|
100
|
+
* Registers a callback for incoming refs on this route.
|
|
101
|
+
*
|
|
102
|
+
* Incoming refs are processed through the full sync pipeline:
|
|
103
|
+
* origin filtering, dedup, gap detection, and ACK.
|
|
104
|
+
*
|
|
105
|
+
* @param callback - The callback to invoke with each deduplicated incoming ref
|
|
106
|
+
*/
|
|
28
107
|
listen(callback) {
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
108
|
+
this._callbacks.push(callback);
|
|
109
|
+
}
|
|
110
|
+
// ...........................................................................
|
|
111
|
+
/**
|
|
112
|
+
* Registers a callback that fires when a DAG conflict is detected.
|
|
113
|
+
*
|
|
114
|
+
* A conflict occurs when the InsertHistory for this route's table
|
|
115
|
+
* has multiple "tips" (leaf nodes), indicating concurrent writes
|
|
116
|
+
* from different clients that have not yet been merged.
|
|
117
|
+
*
|
|
118
|
+
* Detection-only: the callback receives a `Conflict` object
|
|
119
|
+
* describing the branches. Resolution is left to upper layers.
|
|
120
|
+
* @param callback - Invoked with the detected Conflict
|
|
121
|
+
*/
|
|
122
|
+
onConflict(callback) {
|
|
123
|
+
this._conflictCallbacks.push(callback);
|
|
124
|
+
}
|
|
125
|
+
// ...........................................................................
|
|
126
|
+
/**
|
|
127
|
+
* Returns the current sequence number.
|
|
128
|
+
* Only meaningful when `causalOrdering` is enabled.
|
|
129
|
+
*/
|
|
130
|
+
get seq() {
|
|
131
|
+
return this._seq;
|
|
132
|
+
}
|
|
133
|
+
// ...........................................................................
|
|
134
|
+
/**
|
|
135
|
+
* Returns the stable client identity.
|
|
136
|
+
* Only available when `includeClientIdentity` is enabled.
|
|
137
|
+
*/
|
|
138
|
+
get clientIdentity() {
|
|
139
|
+
return this._clientId;
|
|
140
|
+
}
|
|
141
|
+
// ...........................................................................
|
|
142
|
+
/**
|
|
143
|
+
* Returns the sync configuration, if any.
|
|
144
|
+
*/
|
|
145
|
+
get syncConfig() {
|
|
146
|
+
return this._syncConfig;
|
|
147
|
+
}
|
|
148
|
+
// ...........................................................................
|
|
149
|
+
/**
|
|
150
|
+
* Returns the typed event names for this connector's route.
|
|
151
|
+
*/
|
|
152
|
+
get events() {
|
|
153
|
+
return this._events;
|
|
36
154
|
}
|
|
155
|
+
// ######################
|
|
156
|
+
// Private
|
|
157
|
+
// ######################
|
|
37
158
|
_init() {
|
|
38
159
|
this._registerSocketObserver();
|
|
160
|
+
this._registerBootstrapHandler();
|
|
39
161
|
this._registerDbObserver();
|
|
162
|
+
this._registerConflictObserver();
|
|
163
|
+
if (this._syncConfig?.causalOrdering) {
|
|
164
|
+
this._registerGapFillHandler();
|
|
165
|
+
}
|
|
40
166
|
this._isListening = true;
|
|
41
167
|
}
|
|
42
|
-
|
|
43
|
-
this._socket.removeAllListeners(this.
|
|
168
|
+
tearDown() {
|
|
169
|
+
this._socket.removeAllListeners(this._events.ref);
|
|
170
|
+
this._socket.removeAllListeners(this._events.bootstrap);
|
|
171
|
+
if (this._syncConfig?.causalOrdering) {
|
|
172
|
+
this._socket.removeAllListeners(this._events.gapFillRes);
|
|
173
|
+
}
|
|
174
|
+
if (this._syncConfig?.requireAck) {
|
|
175
|
+
this._socket.removeAllListeners(this._events.ack);
|
|
176
|
+
}
|
|
44
177
|
this._db.unregisterAllObservers(this._route);
|
|
178
|
+
this._db.unregisterAllConflictObservers(this._route);
|
|
45
179
|
this._isListening = false;
|
|
46
180
|
}
|
|
181
|
+
// ...........................................................................
|
|
182
|
+
// Two-generation dedup helpers
|
|
183
|
+
// ...........................................................................
|
|
184
|
+
_hasSentRef(ref) {
|
|
185
|
+
return this._sentRefsCurrent.has(ref) || this._sentRefsPrevious.has(ref);
|
|
186
|
+
}
|
|
187
|
+
_addSentRef(ref) {
|
|
188
|
+
this._sentRefsCurrent.add(ref);
|
|
189
|
+
if (this._sentRefsCurrent.size >= this._maxDedup) {
|
|
190
|
+
this._sentRefsPrevious = this._sentRefsCurrent;
|
|
191
|
+
this._sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
_hasReceivedRef(ref) {
|
|
195
|
+
return this._receivedRefsCurrent.has(ref) || this._receivedRefsPrevious.has(ref);
|
|
196
|
+
}
|
|
197
|
+
_addReceivedRef(ref) {
|
|
198
|
+
this._receivedRefsCurrent.add(ref);
|
|
199
|
+
if (this._receivedRefsCurrent.size >= this._maxDedup) {
|
|
200
|
+
this._receivedRefsPrevious = this._receivedRefsCurrent;
|
|
201
|
+
this._receivedRefsCurrent = /* @__PURE__ */ new Set();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
47
204
|
_notifyCallbacks(ref) {
|
|
48
205
|
Promise.all(this._callbacks.map((cb) => cb(ref))).catch((err) => {
|
|
49
206
|
console.error(`Error notifying connector callbacks for ref ${ref}:`, err);
|
|
50
207
|
});
|
|
51
208
|
}
|
|
209
|
+
_processIncoming(payload) {
|
|
210
|
+
const ref = payload.r;
|
|
211
|
+
if (this._hasReceivedRef(ref)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this._syncConfig?.causalOrdering && payload.seq != null && payload.c) {
|
|
215
|
+
const lastSeq = this._peerSeqs.get(payload.c) ?? 0;
|
|
216
|
+
if (payload.seq > lastSeq + 1) {
|
|
217
|
+
const gapReq = {
|
|
218
|
+
route: this._route.flat,
|
|
219
|
+
afterSeq: lastSeq
|
|
220
|
+
};
|
|
221
|
+
this._socket.emit(this._events.gapFillReq, gapReq);
|
|
222
|
+
}
|
|
223
|
+
this._peerSeqs.set(payload.c, payload.seq);
|
|
224
|
+
}
|
|
225
|
+
this._addReceivedRef(ref);
|
|
226
|
+
this._notifyCallbacks(ref);
|
|
227
|
+
if (this._syncConfig?.requireAck) {
|
|
228
|
+
this._socket.emit(this._events.ackClient, { r: ref });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
52
231
|
_registerSocketObserver() {
|
|
53
|
-
this.socket.on(this.
|
|
232
|
+
this.socket.on(this._events.ref, (p) => {
|
|
54
233
|
if (p.o === this._origin) {
|
|
55
234
|
return;
|
|
56
235
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
236
|
+
this._processIncoming(p);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
_registerGapFillHandler() {
|
|
240
|
+
this._socket.on(this._events.gapFillRes, (res) => {
|
|
241
|
+
for (const p of res.refs) {
|
|
242
|
+
this._processIncoming(p);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Listens for bootstrap messages from the server.
|
|
248
|
+
* The server sends the latest ref on connect and optionally via heartbeat.
|
|
249
|
+
* _processIncoming handles dedup so already-seen refs are filtered out.
|
|
250
|
+
*/
|
|
251
|
+
_registerBootstrapHandler() {
|
|
252
|
+
this._socket.on(this._events.bootstrap, (p) => {
|
|
253
|
+
this._processIncoming(p);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
_registerConflictObserver() {
|
|
257
|
+
this._db.registerConflictObserver(this._route, (conflict) => {
|
|
258
|
+
for (const cb of this._conflictCallbacks) {
|
|
259
|
+
cb(conflict);
|
|
60
260
|
}
|
|
61
|
-
this._receivedRefs.add(p.r);
|
|
62
|
-
this._notifyCallbacks(p.r);
|
|
63
261
|
});
|
|
64
262
|
}
|
|
65
263
|
_registerDbObserver() {
|
|
66
264
|
this._db.registerObserver(this._route, (ins) => {
|
|
67
265
|
return new Promise((resolve) => {
|
|
68
266
|
const ref = ins[this.route.root.tableKey + "Ref"];
|
|
69
|
-
if (this.
|
|
267
|
+
if (this._hasSentRef(ref)) {
|
|
70
268
|
resolve();
|
|
71
269
|
return;
|
|
72
270
|
}
|
|
271
|
+
if (this._syncConfig?.causalOrdering && ins.previous?.length) {
|
|
272
|
+
this._lastPredecessors = [...ins.previous];
|
|
273
|
+
}
|
|
73
274
|
this.send(ref);
|
|
74
275
|
resolve();
|
|
75
276
|
});
|
|
@@ -2134,7 +2335,7 @@ class ColumnFilterProcessor {
|
|
|
2134
2335
|
}
|
|
2135
2336
|
}
|
|
2136
2337
|
// ...........................................................................
|
|
2137
|
-
/* v8 ignore stop */
|
|
2338
|
+
/* v8 ignore stop -- @preserve */
|
|
2138
2339
|
static operatorsForType(type) {
|
|
2139
2340
|
switch (type) {
|
|
2140
2341
|
case "string":
|
|
@@ -2916,6 +3117,7 @@ class Db {
|
|
|
2916
3117
|
* Notification system to register callbacks on data changes
|
|
2917
3118
|
*/
|
|
2918
3119
|
notify;
|
|
3120
|
+
_conflictCallbacks = /* @__PURE__ */ new Map();
|
|
2919
3121
|
_cache = /* @__PURE__ */ new Map();
|
|
2920
3122
|
// ...........................................................................
|
|
2921
3123
|
/**
|
|
@@ -3585,6 +3787,56 @@ class Db {
|
|
|
3585
3787
|
return insertHistoryRow;
|
|
3586
3788
|
}
|
|
3587
3789
|
// ...........................................................................
|
|
3790
|
+
/**
|
|
3791
|
+
* Insert pre-decomposed tree nodes into a tree table.
|
|
3792
|
+
*
|
|
3793
|
+
* Unlike `insert()`, which expects a plain nested object and decomposes it
|
|
3794
|
+
* via `treeFromObject()`, this method accepts an array of already-decomposed
|
|
3795
|
+
* `Tree` nodes (e.g. from FsScanner). The **root node must be the last
|
|
3796
|
+
* element** in the array.
|
|
3797
|
+
*
|
|
3798
|
+
* The method goes through the full insert pipeline:
|
|
3799
|
+
* 1. Writes each node via TreeController
|
|
3800
|
+
* 2. Creates an InsertHistoryRow automatically
|
|
3801
|
+
* 3. Calls `notify.notify()` so Connector observers fire
|
|
3802
|
+
*
|
|
3803
|
+
* @param treeKey - The tree table key (must end with "Tree")
|
|
3804
|
+
* @param trees - Pre-decomposed Tree nodes, root LAST
|
|
3805
|
+
* @param options - Optional: skip notification or history
|
|
3806
|
+
* @returns The InsertHistoryRow for the root node
|
|
3807
|
+
*/
|
|
3808
|
+
async insertTrees(treeKey, trees, options) {
|
|
3809
|
+
if (!trees || trees.length === 0) {
|
|
3810
|
+
throw new Error(
|
|
3811
|
+
"Db.insertTrees: trees array must contain at least one node."
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
const controller = await this.getController(treeKey);
|
|
3815
|
+
const writePromises = trees.map(
|
|
3816
|
+
(tree) => controller.insert("add", tree, "db.insertTrees")
|
|
3817
|
+
);
|
|
3818
|
+
const writeResults = await Promise.all(writePromises);
|
|
3819
|
+
const lastResult = writeResults[writeResults.length - 1];
|
|
3820
|
+
if (!lastResult || lastResult.length === 0) {
|
|
3821
|
+
throw new Error(
|
|
3822
|
+
`Db.insertTrees: TreeController returned no result for root node of table "${treeKey}".`
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
const rootResult = lastResult[0];
|
|
3826
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
3827
|
+
const result = {
|
|
3828
|
+
...rootResult,
|
|
3829
|
+
route: route.flat
|
|
3830
|
+
};
|
|
3831
|
+
if (!options?.skipHistory) {
|
|
3832
|
+
await this._writeInsertHistory(treeKey, result);
|
|
3833
|
+
}
|
|
3834
|
+
if (!options?.skipNotification) {
|
|
3835
|
+
this.notify.notify(route, result);
|
|
3836
|
+
}
|
|
3837
|
+
return [result];
|
|
3838
|
+
}
|
|
3839
|
+
// ...........................................................................
|
|
3588
3840
|
/**
|
|
3589
3841
|
* Recursively runs controllers based on the route of the Insert
|
|
3590
3842
|
* @param insert - The Insert to run
|
|
@@ -3835,6 +4087,44 @@ class Db {
|
|
|
3835
4087
|
this.notify.unregisterAll(route);
|
|
3836
4088
|
}
|
|
3837
4089
|
// ...........................................................................
|
|
4090
|
+
/**
|
|
4091
|
+
* Registers a callback to be called when a DAG conflict is detected
|
|
4092
|
+
* on the given route.
|
|
4093
|
+
* @param route - The route to register the conflict callback on
|
|
4094
|
+
* @param callback - The callback to invoke with the Conflict
|
|
4095
|
+
*/
|
|
4096
|
+
registerConflictObserver(route, callback) {
|
|
4097
|
+
const key = route.flat;
|
|
4098
|
+
this._conflictCallbacks.set(key, [
|
|
4099
|
+
...this._conflictCallbacks.get(key) || [],
|
|
4100
|
+
callback
|
|
4101
|
+
]);
|
|
4102
|
+
}
|
|
4103
|
+
// ...........................................................................
|
|
4104
|
+
/**
|
|
4105
|
+
* Unregisters a specific conflict callback from the given route.
|
|
4106
|
+
* @param route - The route to unregister the callback from
|
|
4107
|
+
* @param callback - The callback to remove
|
|
4108
|
+
*/
|
|
4109
|
+
unregisterConflictObserver(route, callback) {
|
|
4110
|
+
const key = route.flat;
|
|
4111
|
+
const callbacks = this._conflictCallbacks.get(key);
|
|
4112
|
+
if (callbacks) {
|
|
4113
|
+
this._conflictCallbacks.set(
|
|
4114
|
+
key,
|
|
4115
|
+
callbacks.filter((cb) => cb !== callback)
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
// ...........................................................................
|
|
4120
|
+
/**
|
|
4121
|
+
* Unregisters all conflict callbacks from the given route.
|
|
4122
|
+
* @param route - The route to clear conflict callbacks for
|
|
4123
|
+
*/
|
|
4124
|
+
unregisterAllConflictObservers(route) {
|
|
4125
|
+
this._conflictCallbacks.delete(route.flat);
|
|
4126
|
+
}
|
|
4127
|
+
// ...........................................................................
|
|
3838
4128
|
/**
|
|
3839
4129
|
* Get a controller for a specific table
|
|
3840
4130
|
* @param tableKey - The key of the table to get the controller for
|
|
@@ -3867,6 +4157,57 @@ class Db {
|
|
|
3867
4157
|
return controllers;
|
|
3868
4158
|
}
|
|
3869
4159
|
// ...........................................................................
|
|
4160
|
+
/**
|
|
4161
|
+
* Detects whether the InsertHistory for a table has diverged into
|
|
4162
|
+
* multiple branches (multiple "tips" — leaf nodes in the DAG).
|
|
4163
|
+
*
|
|
4164
|
+
* A tip is a timeId that is NOT referenced as `previous` by any other
|
|
4165
|
+
* InsertHistory row. Two or more tips indicate a DAG fork (conflict).
|
|
4166
|
+
* @param table - The table name (without "InsertHistory" suffix)
|
|
4167
|
+
* @returns A Conflict if a DAG branch is detected, or null otherwise
|
|
4168
|
+
*/
|
|
4169
|
+
async detectDagBranch(table) {
|
|
4170
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
4171
|
+
const hasTable = await this.core.hasTable(insertHistoryTable);
|
|
4172
|
+
if (!hasTable) return null;
|
|
4173
|
+
const dump = await this.core.dumpTable(insertHistoryTable);
|
|
4174
|
+
const rows = dump[insertHistoryTable]._data;
|
|
4175
|
+
if (rows.length < 2) return null;
|
|
4176
|
+
const referencedAsParent = /* @__PURE__ */ new Set();
|
|
4177
|
+
for (const row of rows) {
|
|
4178
|
+
if (row.previous) {
|
|
4179
|
+
for (const p of row.previous) {
|
|
4180
|
+
referencedAsParent.add(p);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
const tips = rows.filter((row) => !referencedAsParent.has(row.timeId));
|
|
4185
|
+
if (tips.length > 1) {
|
|
4186
|
+
return {
|
|
4187
|
+
table,
|
|
4188
|
+
type: "dagBranch",
|
|
4189
|
+
detectedAt: Date.now(),
|
|
4190
|
+
branches: tips.map((t) => t.timeId)
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
return null;
|
|
4194
|
+
}
|
|
4195
|
+
// ...........................................................................
|
|
4196
|
+
/**
|
|
4197
|
+
* Fires conflict callbacks registered on the route derived from the table.
|
|
4198
|
+
* @param table - The table name
|
|
4199
|
+
* @param conflict - The detected conflict
|
|
4200
|
+
*/
|
|
4201
|
+
_notifyConflict(table, conflict) {
|
|
4202
|
+
const route = Route.fromFlat(`/${table}`);
|
|
4203
|
+
const callbacks = this._conflictCallbacks.get(route.flat);
|
|
4204
|
+
if (callbacks) {
|
|
4205
|
+
for (const cb of callbacks) {
|
|
4206
|
+
cb(conflict);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
// ...........................................................................
|
|
3870
4211
|
/**
|
|
3871
4212
|
* Adds an InsertHistory row to the InsertHistory table of a table
|
|
3872
4213
|
* @param table - The table the Insert was made on
|
|
@@ -3881,6 +4222,10 @@ class Db {
|
|
|
3881
4222
|
_type: "insertHistory"
|
|
3882
4223
|
}
|
|
3883
4224
|
});
|
|
4225
|
+
const conflict = await this.detectDagBranch(table);
|
|
4226
|
+
if (conflict) {
|
|
4227
|
+
this._notifyConflict(table, conflict);
|
|
4228
|
+
}
|
|
3884
4229
|
}
|
|
3885
4230
|
// ...........................................................................
|
|
3886
4231
|
/**
|