@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.
@@ -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
@@ -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:
@@ -10,6 +10,7 @@ export declare class Connector {
10
10
  private _origin;
11
11
  private _callbacks;
12
12
  private _conflictCallbacks;
13
+ private _missedRef;
13
14
  private _isListening;
14
15
  private _sentRefsCurrent;
15
16
  private _sentRefsPrevious;
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
  });