@rljson/db 0.0.15 → 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 +1 -0
- package/dist/db.js +11 -0
- package/dist/db.js.map +1 -1
- package/package.json +1 -1
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:
|
package/dist/db.js
CHANGED
|
@@ -23,6 +23,7 @@ class Connector {
|
|
|
23
23
|
_origin;
|
|
24
24
|
_callbacks = [];
|
|
25
25
|
_conflictCallbacks = [];
|
|
26
|
+
_missedRef = null;
|
|
26
27
|
_isListening = false;
|
|
27
28
|
// Two-generation dedup sets — bounded memory
|
|
28
29
|
_sentRefsCurrent = /* @__PURE__ */ new Set();
|
|
@@ -46,6 +47,7 @@ class Connector {
|
|
|
46
47
|
send(ref) {
|
|
47
48
|
if (this._hasSentRef(ref) || this._hasReceivedRef(ref)) return;
|
|
48
49
|
this._addSentRef(ref);
|
|
50
|
+
this._missedRef = null;
|
|
49
51
|
const payload = {
|
|
50
52
|
o: this._origin,
|
|
51
53
|
r: ref
|
|
@@ -106,6 +108,11 @@ class Connector {
|
|
|
106
108
|
*/
|
|
107
109
|
listen(callback) {
|
|
108
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
|
+
}
|
|
109
116
|
}
|
|
110
117
|
// ...........................................................................
|
|
111
118
|
/**
|
|
@@ -202,6 +209,10 @@ class Connector {
|
|
|
202
209
|
}
|
|
203
210
|
}
|
|
204
211
|
_notifyCallbacks(ref) {
|
|
212
|
+
if (this._callbacks.length === 0) {
|
|
213
|
+
this._missedRef = ref;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
205
216
|
Promise.all(this._callbacks.map((cb) => cb(ref))).catch((err) => {
|
|
206
217
|
console.error(`Error notifying connector callbacks for ref ${ref}:`, err);
|
|
207
218
|
});
|