@rljson/db 0.0.14 → 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.
@@ -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,7 @@ export declare class Connector {
9
9
  private readonly _socket;
10
10
  private _origin;
11
11
  private _callbacks;
12
+ private _conflictCallbacks;
12
13
  private _isListening;
13
14
  private _sentRefsCurrent;
14
15
  private _sentRefsPrevious;
@@ -49,6 +50,18 @@ export declare class Connector {
49
50
  * @param callback - The callback to invoke with each deduplicated incoming ref
50
51
  */
51
52
  listen(callback: ConnectorCallback): void;
53
+ /**
54
+ * Registers a callback that fires when a DAG conflict is detected.
55
+ *
56
+ * A conflict occurs when the InsertHistory for this route's table
57
+ * has multiple "tips" (leaf nodes), indicating concurrent writes
58
+ * from different clients that have not yet been merged.
59
+ *
60
+ * Detection-only: the callback receives a `Conflict` object
61
+ * describing the branches. Resolution is left to upper layers.
62
+ * @param callback - Invoked with the detected Conflict
63
+ */
64
+ onConflict(callback: ConflictCallback): void;
52
65
  /**
53
66
  * Returns the current sequence number.
54
67
  * Only meaningful when `causalOrdering` is enabled.
@@ -83,6 +96,7 @@ export declare class Connector {
83
96
  * _processIncoming handles dedup so already-seen refs are filtered out.
84
97
  */
85
98
  private _registerBootstrapHandler;
99
+ private _registerConflictObserver;
86
100
  private _registerDbObserver;
87
101
  get socket(): Socket;
88
102
  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,7 @@ class Connector {
22
22
  }
23
23
  _origin;
24
24
  _callbacks = [];
25
+ _conflictCallbacks = [];
25
26
  _isListening = false;
26
27
  // Two-generation dedup sets — bounded memory
27
28
  _sentRefsCurrent = /* @__PURE__ */ new Set();
@@ -107,6 +108,21 @@ class Connector {
107
108
  this._callbacks.push(callback);
108
109
  }
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
+ // ...........................................................................
110
126
  /**
111
127
  * Returns the current sequence number.
112
128
  * Only meaningful when `causalOrdering` is enabled.
@@ -143,6 +159,7 @@ class Connector {
143
159
  this._registerSocketObserver();
144
160
  this._registerBootstrapHandler();
145
161
  this._registerDbObserver();
162
+ this._registerConflictObserver();
146
163
  if (this._syncConfig?.causalOrdering) {
147
164
  this._registerGapFillHandler();
148
165
  }
@@ -158,6 +175,7 @@ class Connector {
158
175
  this._socket.removeAllListeners(this._events.ack);
159
176
  }
160
177
  this._db.unregisterAllObservers(this._route);
178
+ this._db.unregisterAllConflictObservers(this._route);
161
179
  this._isListening = false;
162
180
  }
163
181
  // ...........................................................................
@@ -235,6 +253,13 @@ class Connector {
235
253
  this._processIncoming(p);
236
254
  });
237
255
  }
256
+ _registerConflictObserver() {
257
+ this._db.registerConflictObserver(this._route, (conflict) => {
258
+ for (const cb of this._conflictCallbacks) {
259
+ cb(conflict);
260
+ }
261
+ });
262
+ }
238
263
  _registerDbObserver() {
239
264
  this._db.registerObserver(this._route, (ins) => {
240
265
  return new Promise((resolve) => {
@@ -3092,6 +3117,7 @@ class Db {
3092
3117
  * Notification system to register callbacks on data changes
3093
3118
  */
3094
3119
  notify;
3120
+ _conflictCallbacks = /* @__PURE__ */ new Map();
3095
3121
  _cache = /* @__PURE__ */ new Map();
3096
3122
  // ...........................................................................
3097
3123
  /**
@@ -4061,6 +4087,44 @@ class Db {
4061
4087
  this.notify.unregisterAll(route);
4062
4088
  }
4063
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
+ // ...........................................................................
4064
4128
  /**
4065
4129
  * Get a controller for a specific table
4066
4130
  * @param tableKey - The key of the table to get the controller for
@@ -4093,6 +4157,57 @@ class Db {
4093
4157
  return controllers;
4094
4158
  }
4095
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
+ // ...........................................................................
4096
4211
  /**
4097
4212
  * Adds an InsertHistory row to the InsertHistory table of a table
4098
4213
  * @param table - The table the Insert was made on
@@ -4107,6 +4222,10 @@ class Db {
4107
4222
  _type: "insertHistory"
4108
4223
  }
4109
4224
  });
4225
+ const conflict = await this.detectDagBranch(table);
4226
+ if (conflict) {
4227
+ this._notifyConflict(table, conflict);
4228
+ }
4110
4229
  }
4111
4230
  // ...........................................................................
4112
4231
  /**