@rljson/db 0.0.14 → 0.0.17
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 +40 -0
- package/README.public.md +51 -0
- package/dist/README.architecture.md +40 -0
- package/dist/README.public.md +51 -0
- package/dist/connector/connector.d.ts +16 -1
- package/dist/db.d.ts +36 -1
- package/dist/db.js +130 -0
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +2 -2
package/README.architecture.md
CHANGED
|
@@ -839,6 +839,46 @@ The Connector registers a listener on `events.bootstrap` in `_init()` via `_regi
|
|
|
839
839
|
|
|
840
840
|
The `tearDown()` method cleans up the bootstrap listener alongside all other socket listeners.
|
|
841
841
|
|
|
842
|
+
### Conflict Detection
|
|
843
|
+
|
|
844
|
+
The `Db` class integrates DAG branch conflict detection directly into the write path. After every call to `_writeInsertHistory()`, the system invokes `detectDagBranch(table)` to scan the InsertHistory for forks.
|
|
845
|
+
|
|
846
|
+
**Algorithm (`detectDagBranch`)**:
|
|
847
|
+
|
|
848
|
+
1. Load all rows from `{table}InsertHistory`.
|
|
849
|
+
2. Build a set of all timeIds that appear as someone's `previous`.
|
|
850
|
+
3. Tips = rows whose `timeId` is **not** in that set (no descendant).
|
|
851
|
+
4. If more than one tip exists → DAG branch conflict.
|
|
852
|
+
|
|
853
|
+
```text
|
|
854
|
+
┌─── tip A (17000…:AbCd)
|
|
855
|
+
root ──┤
|
|
856
|
+
└─── tip B (17000…:EfGh)
|
|
857
|
+
↑
|
|
858
|
+
DAG branch: two concurrent writes, no merge yet
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Observer pipeline:**
|
|
862
|
+
|
|
863
|
+
```text
|
|
864
|
+
_writeInsertHistory()
|
|
865
|
+
└──► detectDagBranch(table)
|
|
866
|
+
└──► if conflict found:
|
|
867
|
+
_notifyConflict(table, conflict)
|
|
868
|
+
└──► fire all callbacks in _conflictCallbacks[route]
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Connector integration**: `Connector.onConflict(callback)` calls `db.registerConflictObserver()` under the hood, so callbacks fire for conflicts on the route managed by that Connector. `tearDown()` calls `db.unregisterAllConflictObservers()` to clean up.
|
|
872
|
+
|
|
873
|
+
**Key properties:**
|
|
874
|
+
|
|
875
|
+
| Property | Value |
|
|
876
|
+
| ----------------- | ---------------------------------------------------------------------------- |
|
|
877
|
+
| Detection trigger | Every `_writeInsertHistory()` call |
|
|
878
|
+
| Scope | Per-table (each table's InsertHistory is independent) |
|
|
879
|
+
| Resolution | Not provided — detection and signaling only |
|
|
880
|
+
| Merge detection | A conflict **disappears** when a merge row references all tips as `previous` |
|
|
881
|
+
|
|
842
882
|
## Future Enhancements
|
|
843
883
|
|
|
844
884
|
### Planned Features
|
package/README.public.md
CHANGED
|
@@ -979,6 +979,57 @@ connector.tearDown();
|
|
|
979
979
|
// Removes all socket listeners and clears internal state
|
|
980
980
|
```
|
|
981
981
|
|
|
982
|
+
### Conflict Detection
|
|
983
|
+
|
|
984
|
+
The `Db` class detects DAG branch conflicts in the InsertHistory and notifies registered observers. A **DAG branch** occurs when two or more InsertHistory rows have no descendant — i.e., they are "tips" of the history graph — indicating concurrent writes from different clients that have not yet been merged.
|
|
985
|
+
|
|
986
|
+
#### Manual Detection
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
// Check whether a table's InsertHistory has diverged
|
|
990
|
+
const conflict = await db.detectDagBranch('cars');
|
|
991
|
+
if (conflict) {
|
|
992
|
+
console.log(conflict.table); // 'cars'
|
|
993
|
+
console.log(conflict.type); // 'dagBranch'
|
|
994
|
+
console.log(conflict.branches); // ['17000…:AbCd', '17000…:EfGh']
|
|
995
|
+
}
|
|
996
|
+
// Returns null when the history is linear (no conflict)
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### Automatic Detection via Observers
|
|
1000
|
+
|
|
1001
|
+
Conflict detection runs automatically after every `_writeInsertHistory()` call. Register callbacks on the `Db` to be notified immediately:
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
import type { Conflict, ConflictCallback } from '@rljson/db';
|
|
1005
|
+
|
|
1006
|
+
const onConflict: ConflictCallback = (conflict: Conflict) => {
|
|
1007
|
+
console.warn(`DAG branch in ${conflict.table}:`, conflict.branches);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// Register
|
|
1011
|
+
db.registerConflictObserver(route, onConflict);
|
|
1012
|
+
|
|
1013
|
+
// Unregister a specific callback
|
|
1014
|
+
db.unregisterConflictObserver(route, onConflict);
|
|
1015
|
+
|
|
1016
|
+
// Unregister all callbacks for a route
|
|
1017
|
+
db.unregisterAllConflictObservers(route);
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
#### Via Connector
|
|
1021
|
+
|
|
1022
|
+
The `Connector` provides a convenience method that wraps the Db observer API:
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
connector.onConflict((conflict) => {
|
|
1026
|
+
console.warn(`Conflict detected:`, conflict);
|
|
1027
|
+
});
|
|
1028
|
+
// Cleaned up automatically on connector.tearDown()
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
**Detection only — no resolution.** The system signals that a conflict exists; merging divergent branches is left to application code.
|
|
1032
|
+
|
|
982
1033
|
## Examples
|
|
983
1034
|
|
|
984
1035
|
See [src/example.ts](src/example.ts) for a complete working example demonstrating:
|
|
@@ -839,6 +839,46 @@ The Connector registers a listener on `events.bootstrap` in `_init()` via `_regi
|
|
|
839
839
|
|
|
840
840
|
The `tearDown()` method cleans up the bootstrap listener alongside all other socket listeners.
|
|
841
841
|
|
|
842
|
+
### Conflict Detection
|
|
843
|
+
|
|
844
|
+
The `Db` class integrates DAG branch conflict detection directly into the write path. After every call to `_writeInsertHistory()`, the system invokes `detectDagBranch(table)` to scan the InsertHistory for forks.
|
|
845
|
+
|
|
846
|
+
**Algorithm (`detectDagBranch`)**:
|
|
847
|
+
|
|
848
|
+
1. Load all rows from `{table}InsertHistory`.
|
|
849
|
+
2. Build a set of all timeIds that appear as someone's `previous`.
|
|
850
|
+
3. Tips = rows whose `timeId` is **not** in that set (no descendant).
|
|
851
|
+
4. If more than one tip exists → DAG branch conflict.
|
|
852
|
+
|
|
853
|
+
```text
|
|
854
|
+
┌─── tip A (17000…:AbCd)
|
|
855
|
+
root ──┤
|
|
856
|
+
└─── tip B (17000…:EfGh)
|
|
857
|
+
↑
|
|
858
|
+
DAG branch: two concurrent writes, no merge yet
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Observer pipeline:**
|
|
862
|
+
|
|
863
|
+
```text
|
|
864
|
+
_writeInsertHistory()
|
|
865
|
+
└──► detectDagBranch(table)
|
|
866
|
+
└──► if conflict found:
|
|
867
|
+
_notifyConflict(table, conflict)
|
|
868
|
+
└──► fire all callbacks in _conflictCallbacks[route]
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Connector integration**: `Connector.onConflict(callback)` calls `db.registerConflictObserver()` under the hood, so callbacks fire for conflicts on the route managed by that Connector. `tearDown()` calls `db.unregisterAllConflictObservers()` to clean up.
|
|
872
|
+
|
|
873
|
+
**Key properties:**
|
|
874
|
+
|
|
875
|
+
| Property | Value |
|
|
876
|
+
| ----------------- | ---------------------------------------------------------------------------- |
|
|
877
|
+
| Detection trigger | Every `_writeInsertHistory()` call |
|
|
878
|
+
| Scope | Per-table (each table's InsertHistory is independent) |
|
|
879
|
+
| Resolution | Not provided — detection and signaling only |
|
|
880
|
+
| Merge detection | A conflict **disappears** when a merge row references all tips as `previous` |
|
|
881
|
+
|
|
842
882
|
## Future Enhancements
|
|
843
883
|
|
|
844
884
|
### Planned Features
|
package/dist/README.public.md
CHANGED
|
@@ -979,6 +979,57 @@ connector.tearDown();
|
|
|
979
979
|
// Removes all socket listeners and clears internal state
|
|
980
980
|
```
|
|
981
981
|
|
|
982
|
+
### Conflict Detection
|
|
983
|
+
|
|
984
|
+
The `Db` class detects DAG branch conflicts in the InsertHistory and notifies registered observers. A **DAG branch** occurs when two or more InsertHistory rows have no descendant — i.e., they are "tips" of the history graph — indicating concurrent writes from different clients that have not yet been merged.
|
|
985
|
+
|
|
986
|
+
#### Manual Detection
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
// Check whether a table's InsertHistory has diverged
|
|
990
|
+
const conflict = await db.detectDagBranch('cars');
|
|
991
|
+
if (conflict) {
|
|
992
|
+
console.log(conflict.table); // 'cars'
|
|
993
|
+
console.log(conflict.type); // 'dagBranch'
|
|
994
|
+
console.log(conflict.branches); // ['17000…:AbCd', '17000…:EfGh']
|
|
995
|
+
}
|
|
996
|
+
// Returns null when the history is linear (no conflict)
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### Automatic Detection via Observers
|
|
1000
|
+
|
|
1001
|
+
Conflict detection runs automatically after every `_writeInsertHistory()` call. Register callbacks on the `Db` to be notified immediately:
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
import type { Conflict, ConflictCallback } from '@rljson/db';
|
|
1005
|
+
|
|
1006
|
+
const onConflict: ConflictCallback = (conflict: Conflict) => {
|
|
1007
|
+
console.warn(`DAG branch in ${conflict.table}:`, conflict.branches);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// Register
|
|
1011
|
+
db.registerConflictObserver(route, onConflict);
|
|
1012
|
+
|
|
1013
|
+
// Unregister a specific callback
|
|
1014
|
+
db.unregisterConflictObserver(route, onConflict);
|
|
1015
|
+
|
|
1016
|
+
// Unregister all callbacks for a route
|
|
1017
|
+
db.unregisterAllConflictObservers(route);
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
#### Via Connector
|
|
1021
|
+
|
|
1022
|
+
The `Connector` provides a convenience method that wraps the Db observer API:
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
connector.onConflict((conflict) => {
|
|
1026
|
+
console.warn(`Conflict detected:`, conflict);
|
|
1027
|
+
});
|
|
1028
|
+
// Cleaned up automatically on connector.tearDown()
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
**Detection only — no resolution.** The system signals that a conflict exists; merging divergent branches is left to application code.
|
|
1032
|
+
|
|
982
1033
|
## Examples
|
|
983
1034
|
|
|
984
1035
|
See [src/example.ts](src/example.ts) for a complete working example demonstrating:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Socket } from '@rljson/io';
|
|
2
|
-
import { AckPayload, ClientId, InsertHistoryTimeId, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
|
|
2
|
+
import { AckPayload, ClientId, ConflictCallback, InsertHistoryTimeId, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
|
|
3
3
|
import { Db } from '../db.ts';
|
|
4
4
|
export type { ConnectorPayload } from '@rljson/rljson';
|
|
5
5
|
export type ConnectorCallback = (ref: string) => Promise<any>;
|
|
@@ -9,6 +9,8 @@ export declare class Connector {
|
|
|
9
9
|
private readonly _socket;
|
|
10
10
|
private _origin;
|
|
11
11
|
private _callbacks;
|
|
12
|
+
private _conflictCallbacks;
|
|
13
|
+
private _missedRef;
|
|
12
14
|
private _isListening;
|
|
13
15
|
private _sentRefsCurrent;
|
|
14
16
|
private _sentRefsPrevious;
|
|
@@ -49,6 +51,18 @@ export declare class Connector {
|
|
|
49
51
|
* @param callback - The callback to invoke with each deduplicated incoming ref
|
|
50
52
|
*/
|
|
51
53
|
listen(callback: ConnectorCallback): void;
|
|
54
|
+
/**
|
|
55
|
+
* Registers a callback that fires when a DAG conflict is detected.
|
|
56
|
+
*
|
|
57
|
+
* A conflict occurs when the InsertHistory for this route's table
|
|
58
|
+
* has multiple "tips" (leaf nodes), indicating concurrent writes
|
|
59
|
+
* from different clients that have not yet been merged.
|
|
60
|
+
*
|
|
61
|
+
* Detection-only: the callback receives a `Conflict` object
|
|
62
|
+
* describing the branches. Resolution is left to upper layers.
|
|
63
|
+
* @param callback - Invoked with the detected Conflict
|
|
64
|
+
*/
|
|
65
|
+
onConflict(callback: ConflictCallback): void;
|
|
52
66
|
/**
|
|
53
67
|
* Returns the current sequence number.
|
|
54
68
|
* Only meaningful when `causalOrdering` is enabled.
|
|
@@ -83,6 +97,7 @@ export declare class Connector {
|
|
|
83
97
|
* _processIncoming handles dedup so already-seen refs are filtered out.
|
|
84
98
|
*/
|
|
85
99
|
private _registerBootstrapHandler;
|
|
100
|
+
private _registerConflictObserver;
|
|
86
101
|
private _registerDbObserver;
|
|
87
102
|
get socket(): Socket;
|
|
88
103
|
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, Tree } from '@rljson/rljson';
|
|
3
|
+
import { Conflict, ConflictCallback, 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';
|
|
@@ -43,6 +43,7 @@ export declare class Db {
|
|
|
43
43
|
* Notification system to register callbacks on data changes
|
|
44
44
|
*/
|
|
45
45
|
readonly notify: Notify;
|
|
46
|
+
private _conflictCallbacks;
|
|
46
47
|
private _cache;
|
|
47
48
|
/**
|
|
48
49
|
* Get data from a route with optional filtering
|
|
@@ -137,6 +138,24 @@ export declare class Db {
|
|
|
137
138
|
* Unregisters all observers from all routes
|
|
138
139
|
*/
|
|
139
140
|
unregisterAllObservers(route: Route): void;
|
|
141
|
+
/**
|
|
142
|
+
* Registers a callback to be called when a DAG conflict is detected
|
|
143
|
+
* on the given route.
|
|
144
|
+
* @param route - The route to register the conflict callback on
|
|
145
|
+
* @param callback - The callback to invoke with the Conflict
|
|
146
|
+
*/
|
|
147
|
+
registerConflictObserver(route: Route, callback: ConflictCallback): void;
|
|
148
|
+
/**
|
|
149
|
+
* Unregisters a specific conflict callback from the given route.
|
|
150
|
+
* @param route - The route to unregister the callback from
|
|
151
|
+
* @param callback - The callback to remove
|
|
152
|
+
*/
|
|
153
|
+
unregisterConflictObserver(route: Route, callback: ConflictCallback): void;
|
|
154
|
+
/**
|
|
155
|
+
* Unregisters all conflict callbacks from the given route.
|
|
156
|
+
* @param route - The route to clear conflict callbacks for
|
|
157
|
+
*/
|
|
158
|
+
unregisterAllConflictObservers(route: Route): void;
|
|
140
159
|
/**
|
|
141
160
|
* Get a controller for a specific table
|
|
142
161
|
* @param tableKey - The key of the table to get the controller for
|
|
@@ -146,6 +165,22 @@ export declare class Db {
|
|
|
146
165
|
*/
|
|
147
166
|
getController(tableKey: string, refs?: ControllerRefs): Promise<Controller<TableType, any, string>>;
|
|
148
167
|
indexedControllers(route: Route): Promise<Record<string, Controller<any, any, any>>>;
|
|
168
|
+
/**
|
|
169
|
+
* Detects whether the InsertHistory for a table has diverged into
|
|
170
|
+
* multiple branches (multiple "tips" — leaf nodes in the DAG).
|
|
171
|
+
*
|
|
172
|
+
* A tip is a timeId that is NOT referenced as `previous` by any other
|
|
173
|
+
* InsertHistory row. Two or more tips indicate a DAG fork (conflict).
|
|
174
|
+
* @param table - The table name (without "InsertHistory" suffix)
|
|
175
|
+
* @returns A Conflict if a DAG branch is detected, or null otherwise
|
|
176
|
+
*/
|
|
177
|
+
detectDagBranch(table: string): Promise<Conflict | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Fires conflict callbacks registered on the route derived from the table.
|
|
180
|
+
* @param table - The table name
|
|
181
|
+
* @param conflict - The detected conflict
|
|
182
|
+
*/
|
|
183
|
+
private _notifyConflict;
|
|
149
184
|
/**
|
|
150
185
|
* Adds an InsertHistory row to the InsertHistory table of a table
|
|
151
186
|
* @param table - The table the Insert was made on
|
package/dist/db.js
CHANGED
|
@@ -22,6 +22,8 @@ class Connector {
|
|
|
22
22
|
}
|
|
23
23
|
_origin;
|
|
24
24
|
_callbacks = [];
|
|
25
|
+
_conflictCallbacks = [];
|
|
26
|
+
_missedRef = null;
|
|
25
27
|
_isListening = false;
|
|
26
28
|
// Two-generation dedup sets — bounded memory
|
|
27
29
|
_sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
@@ -45,6 +47,7 @@ class Connector {
|
|
|
45
47
|
send(ref) {
|
|
46
48
|
if (this._hasSentRef(ref) || this._hasReceivedRef(ref)) return;
|
|
47
49
|
this._addSentRef(ref);
|
|
50
|
+
this._missedRef = null;
|
|
48
51
|
const payload = {
|
|
49
52
|
o: this._origin,
|
|
50
53
|
r: ref
|
|
@@ -105,6 +108,26 @@ class Connector {
|
|
|
105
108
|
*/
|
|
106
109
|
listen(callback) {
|
|
107
110
|
this._callbacks.push(callback);
|
|
111
|
+
if (this._missedRef !== null) {
|
|
112
|
+
const ref = this._missedRef;
|
|
113
|
+
this._missedRef = null;
|
|
114
|
+
Promise.resolve(callback(ref)).catch(console.error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ...........................................................................
|
|
118
|
+
/**
|
|
119
|
+
* Registers a callback that fires when a DAG conflict is detected.
|
|
120
|
+
*
|
|
121
|
+
* A conflict occurs when the InsertHistory for this route's table
|
|
122
|
+
* has multiple "tips" (leaf nodes), indicating concurrent writes
|
|
123
|
+
* from different clients that have not yet been merged.
|
|
124
|
+
*
|
|
125
|
+
* Detection-only: the callback receives a `Conflict` object
|
|
126
|
+
* describing the branches. Resolution is left to upper layers.
|
|
127
|
+
* @param callback - Invoked with the detected Conflict
|
|
128
|
+
*/
|
|
129
|
+
onConflict(callback) {
|
|
130
|
+
this._conflictCallbacks.push(callback);
|
|
108
131
|
}
|
|
109
132
|
// ...........................................................................
|
|
110
133
|
/**
|
|
@@ -143,6 +166,7 @@ class Connector {
|
|
|
143
166
|
this._registerSocketObserver();
|
|
144
167
|
this._registerBootstrapHandler();
|
|
145
168
|
this._registerDbObserver();
|
|
169
|
+
this._registerConflictObserver();
|
|
146
170
|
if (this._syncConfig?.causalOrdering) {
|
|
147
171
|
this._registerGapFillHandler();
|
|
148
172
|
}
|
|
@@ -158,6 +182,7 @@ class Connector {
|
|
|
158
182
|
this._socket.removeAllListeners(this._events.ack);
|
|
159
183
|
}
|
|
160
184
|
this._db.unregisterAllObservers(this._route);
|
|
185
|
+
this._db.unregisterAllConflictObservers(this._route);
|
|
161
186
|
this._isListening = false;
|
|
162
187
|
}
|
|
163
188
|
// ...........................................................................
|
|
@@ -184,6 +209,10 @@ class Connector {
|
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
_notifyCallbacks(ref) {
|
|
212
|
+
if (this._callbacks.length === 0) {
|
|
213
|
+
this._missedRef = ref;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
187
216
|
Promise.all(this._callbacks.map((cb) => cb(ref))).catch((err) => {
|
|
188
217
|
console.error(`Error notifying connector callbacks for ref ${ref}:`, err);
|
|
189
218
|
});
|
|
@@ -235,6 +264,13 @@ class Connector {
|
|
|
235
264
|
this._processIncoming(p);
|
|
236
265
|
});
|
|
237
266
|
}
|
|
267
|
+
_registerConflictObserver() {
|
|
268
|
+
this._db.registerConflictObserver(this._route, (conflict) => {
|
|
269
|
+
for (const cb of this._conflictCallbacks) {
|
|
270
|
+
cb(conflict);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
238
274
|
_registerDbObserver() {
|
|
239
275
|
this._db.registerObserver(this._route, (ins) => {
|
|
240
276
|
return new Promise((resolve) => {
|
|
@@ -3092,6 +3128,7 @@ class Db {
|
|
|
3092
3128
|
* Notification system to register callbacks on data changes
|
|
3093
3129
|
*/
|
|
3094
3130
|
notify;
|
|
3131
|
+
_conflictCallbacks = /* @__PURE__ */ new Map();
|
|
3095
3132
|
_cache = /* @__PURE__ */ new Map();
|
|
3096
3133
|
// ...........................................................................
|
|
3097
3134
|
/**
|
|
@@ -4061,6 +4098,44 @@ class Db {
|
|
|
4061
4098
|
this.notify.unregisterAll(route);
|
|
4062
4099
|
}
|
|
4063
4100
|
// ...........................................................................
|
|
4101
|
+
/**
|
|
4102
|
+
* Registers a callback to be called when a DAG conflict is detected
|
|
4103
|
+
* on the given route.
|
|
4104
|
+
* @param route - The route to register the conflict callback on
|
|
4105
|
+
* @param callback - The callback to invoke with the Conflict
|
|
4106
|
+
*/
|
|
4107
|
+
registerConflictObserver(route, callback) {
|
|
4108
|
+
const key = route.flat;
|
|
4109
|
+
this._conflictCallbacks.set(key, [
|
|
4110
|
+
...this._conflictCallbacks.get(key) || [],
|
|
4111
|
+
callback
|
|
4112
|
+
]);
|
|
4113
|
+
}
|
|
4114
|
+
// ...........................................................................
|
|
4115
|
+
/**
|
|
4116
|
+
* Unregisters a specific conflict callback from the given route.
|
|
4117
|
+
* @param route - The route to unregister the callback from
|
|
4118
|
+
* @param callback - The callback to remove
|
|
4119
|
+
*/
|
|
4120
|
+
unregisterConflictObserver(route, callback) {
|
|
4121
|
+
const key = route.flat;
|
|
4122
|
+
const callbacks = this._conflictCallbacks.get(key);
|
|
4123
|
+
if (callbacks) {
|
|
4124
|
+
this._conflictCallbacks.set(
|
|
4125
|
+
key,
|
|
4126
|
+
callbacks.filter((cb) => cb !== callback)
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
// ...........................................................................
|
|
4131
|
+
/**
|
|
4132
|
+
* Unregisters all conflict callbacks from the given route.
|
|
4133
|
+
* @param route - The route to clear conflict callbacks for
|
|
4134
|
+
*/
|
|
4135
|
+
unregisterAllConflictObservers(route) {
|
|
4136
|
+
this._conflictCallbacks.delete(route.flat);
|
|
4137
|
+
}
|
|
4138
|
+
// ...........................................................................
|
|
4064
4139
|
/**
|
|
4065
4140
|
* Get a controller for a specific table
|
|
4066
4141
|
* @param tableKey - The key of the table to get the controller for
|
|
@@ -4093,6 +4168,57 @@ class Db {
|
|
|
4093
4168
|
return controllers;
|
|
4094
4169
|
}
|
|
4095
4170
|
// ...........................................................................
|
|
4171
|
+
/**
|
|
4172
|
+
* Detects whether the InsertHistory for a table has diverged into
|
|
4173
|
+
* multiple branches (multiple "tips" — leaf nodes in the DAG).
|
|
4174
|
+
*
|
|
4175
|
+
* A tip is a timeId that is NOT referenced as `previous` by any other
|
|
4176
|
+
* InsertHistory row. Two or more tips indicate a DAG fork (conflict).
|
|
4177
|
+
* @param table - The table name (without "InsertHistory" suffix)
|
|
4178
|
+
* @returns A Conflict if a DAG branch is detected, or null otherwise
|
|
4179
|
+
*/
|
|
4180
|
+
async detectDagBranch(table) {
|
|
4181
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
4182
|
+
const hasTable = await this.core.hasTable(insertHistoryTable);
|
|
4183
|
+
if (!hasTable) return null;
|
|
4184
|
+
const dump = await this.core.dumpTable(insertHistoryTable);
|
|
4185
|
+
const rows = dump[insertHistoryTable]._data;
|
|
4186
|
+
if (rows.length < 2) return null;
|
|
4187
|
+
const referencedAsParent = /* @__PURE__ */ new Set();
|
|
4188
|
+
for (const row of rows) {
|
|
4189
|
+
if (row.previous) {
|
|
4190
|
+
for (const p of row.previous) {
|
|
4191
|
+
referencedAsParent.add(p);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
const tips = rows.filter((row) => !referencedAsParent.has(row.timeId));
|
|
4196
|
+
if (tips.length > 1) {
|
|
4197
|
+
return {
|
|
4198
|
+
table,
|
|
4199
|
+
type: "dagBranch",
|
|
4200
|
+
detectedAt: Date.now(),
|
|
4201
|
+
branches: tips.map((t) => t.timeId)
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
// ...........................................................................
|
|
4207
|
+
/**
|
|
4208
|
+
* Fires conflict callbacks registered on the route derived from the table.
|
|
4209
|
+
* @param table - The table name
|
|
4210
|
+
* @param conflict - The detected conflict
|
|
4211
|
+
*/
|
|
4212
|
+
_notifyConflict(table, conflict) {
|
|
4213
|
+
const route = Route.fromFlat(`/${table}`);
|
|
4214
|
+
const callbacks = this._conflictCallbacks.get(route.flat);
|
|
4215
|
+
if (callbacks) {
|
|
4216
|
+
for (const cb of callbacks) {
|
|
4217
|
+
cb(conflict);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
// ...........................................................................
|
|
4096
4222
|
/**
|
|
4097
4223
|
* Adds an InsertHistory row to the InsertHistory table of a table
|
|
4098
4224
|
* @param table - The table the Insert was made on
|
|
@@ -4107,6 +4233,10 @@ class Db {
|
|
|
4107
4233
|
_type: "insertHistory"
|
|
4108
4234
|
}
|
|
4109
4235
|
});
|
|
4236
|
+
const conflict = await this.detectDagBranch(table);
|
|
4237
|
+
if (conflict) {
|
|
4238
|
+
this._notifyConflict(table, conflict);
|
|
4239
|
+
}
|
|
4110
4240
|
}
|
|
4111
4241
|
// ...........................................................................
|
|
4112
4242
|
/**
|