@rljson/db 0.0.21 → 0.0.23

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,8 +1,14 @@
1
1
  import { Socket } from '@rljson/io';
2
- import { AckPayload, ClientId, ConflictCallback, InsertHistoryTimeId, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
2
+ import { AckPayload, ClientId, ConflictCallback, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
3
3
  import { Db } from '../db.ts';
4
4
  export type { ConnectorPayload } from '@rljson/rljson';
5
- export type ConnectorCallback = (ref: string) => Promise<any>;
5
+ /**
6
+ * Invoked for each deduplicated incoming ref. `predecessorRefs` carries the
7
+ * causal predecessor *content refs* (shared identity across clients) when
8
+ * `causalOrdering` is enabled, so the receiver can record correct ancestry in
9
+ * its own InsertHistory. Empty/undefined for roots or when causal ordering is off.
10
+ */
11
+ export type ConnectorCallback = (ref: string, predecessorRefs?: string[]) => Promise<any>;
6
12
  export declare class Connector {
7
13
  private readonly _db;
8
14
  private readonly _route;
@@ -11,6 +17,7 @@ export declare class Connector {
11
17
  private _callbacks;
12
18
  private _conflictCallbacks;
13
19
  private _missedRef;
20
+ private _missedPredecessorRefs;
14
21
  private _lastSentRef;
15
22
  private _isListening;
16
23
  private _sentRefsCurrent;
@@ -39,10 +46,12 @@ export declare class Connector {
39
46
  */
40
47
  sendWithAck(ref: string): Promise<AckPayload>;
41
48
  /**
42
- * Sets the causal predecessors for the next send.
43
- * @param predecessors - The InsertHistory timeIds of causal predecessors
49
+ * Sets the causal predecessors (content refs) attached to the next send.
50
+ * Normally auto-populated from the InsertHistoryRow; exposed for tests/manual
51
+ * control.
52
+ * @param predecessors - The predecessor content refs
44
53
  */
45
- setPredecessors(predecessors: InsertHistoryTimeId[]): void;
54
+ setPredecessors(predecessors: string[]): void;
46
55
  /**
47
56
  * Registers a callback for incoming refs on this route.
48
57
  *
@@ -64,6 +73,20 @@ export declare class Connector {
64
73
  * @param callback - Invoked with the detected Conflict
65
74
  */
66
75
  onConflict(callback: ConflictCallback): void;
76
+ /**
77
+ * Removes a ref from the received-dedup set so a later re-advertisement of
78
+ * the same ref (e.g. the server's bootstrap heartbeat, which only re-sends
79
+ * the *latest* ref) is delivered to listeners again instead of being
80
+ * silently deduplicated.
81
+ *
82
+ * A ref is added to the dedup set the instant it is *received* — before the
83
+ * listener callback (which materializes it) has a chance to succeed. If that
84
+ * apply step fails terminally, the ref is stuck in the dedup set forever and
85
+ * the heartbeat can never heal it. Consumers whose apply failed call this so
86
+ * recovery becomes eventually-consistent rather than permanent loss.
87
+ * @param ref - The ref to allow re-delivery for.
88
+ */
89
+ invalidateReceived(ref: string): void;
67
90
  /**
68
91
  * Returns the current sequence number.
69
92
  * Only meaningful when `causalOrdering` is enabled.
package/dist/db.js CHANGED
@@ -24,6 +24,7 @@ class Connector {
24
24
  _callbacks = [];
25
25
  _conflictCallbacks = [];
26
26
  _missedRef = null;
27
+ _missedPredecessorRefs = void 0;
27
28
  _lastSentRef = null;
28
29
  _isListening = false;
29
30
  // Two-generation dedup sets — bounded memory
@@ -37,6 +38,9 @@ class Connector {
37
38
  _clientId;
38
39
  _events;
39
40
  _seq = 0;
41
+ // Predecessor *content refs* (not timeIds) attached to the next send. Refs are
42
+ // the only identity shared across clients, so the receiver can map them to its
43
+ // own local ancestry. Auto-populated from the InsertHistoryRow on db inserts.
40
44
  _lastPredecessors = [];
41
45
  _peerSeqs = /* @__PURE__ */ new Map();
42
46
  // ...........................................................................
@@ -92,8 +96,10 @@ class Connector {
92
96
  }
93
97
  // ...........................................................................
94
98
  /**
95
- * Sets the causal predecessors for the next send.
96
- * @param predecessors - The InsertHistory timeIds of causal predecessors
99
+ * Sets the causal predecessors (content refs) attached to the next send.
100
+ * Normally auto-populated from the InsertHistoryRow; exposed for tests/manual
101
+ * control.
102
+ * @param predecessors - The predecessor content refs
97
103
  */
98
104
  setPredecessors(predecessors) {
99
105
  this._lastPredecessors = predecessors;
@@ -111,8 +117,12 @@ class Connector {
111
117
  this._callbacks.push(callback);
112
118
  if (this._missedRef !== null) {
113
119
  const ref = this._missedRef;
120
+ const predecessorRefs = this._missedPredecessorRefs;
114
121
  this._missedRef = null;
115
- Promise.resolve(callback(ref)).catch(console.error);
122
+ this._missedPredecessorRefs = void 0;
123
+ Promise.resolve(
124
+ predecessorRefs ? callback(ref, predecessorRefs) : callback(ref)
125
+ ).catch(console.error);
116
126
  }
117
127
  }
118
128
  // ...........................................................................
@@ -131,6 +141,24 @@ class Connector {
131
141
  this._conflictCallbacks.push(callback);
132
142
  }
133
143
  // ...........................................................................
144
+ /**
145
+ * Removes a ref from the received-dedup set so a later re-advertisement of
146
+ * the same ref (e.g. the server's bootstrap heartbeat, which only re-sends
147
+ * the *latest* ref) is delivered to listeners again instead of being
148
+ * silently deduplicated.
149
+ *
150
+ * A ref is added to the dedup set the instant it is *received* — before the
151
+ * listener callback (which materializes it) has a chance to succeed. If that
152
+ * apply step fails terminally, the ref is stuck in the dedup set forever and
153
+ * the heartbeat can never heal it. Consumers whose apply failed call this so
154
+ * recovery becomes eventually-consistent rather than permanent loss.
155
+ * @param ref - The ref to allow re-delivery for.
156
+ */
157
+ invalidateReceived(ref) {
158
+ this._receivedRefsCurrent.delete(ref);
159
+ this._receivedRefsPrevious.delete(ref);
160
+ }
161
+ // ...........................................................................
134
162
  /**
135
163
  * Returns the current sequence number.
136
164
  * Only meaningful when `causalOrdering` is enabled.
@@ -209,12 +237,17 @@ class Connector {
209
237
  this._receivedRefsCurrent = /* @__PURE__ */ new Set();
210
238
  }
211
239
  }
212
- _notifyCallbacks(ref) {
240
+ _notifyCallbacks(ref, predecessorRefs) {
213
241
  if (this._callbacks.length === 0) {
214
242
  this._missedRef = ref;
243
+ this._missedPredecessorRefs = predecessorRefs;
215
244
  return;
216
245
  }
217
- Promise.all(this._callbacks.map((cb) => cb(ref))).catch((err) => {
246
+ Promise.all(
247
+ this._callbacks.map(
248
+ (cb) => predecessorRefs ? cb(ref, predecessorRefs) : cb(ref)
249
+ )
250
+ ).catch((err) => {
218
251
  console.error(`Error notifying connector callbacks for ref ${ref}:`, err);
219
252
  });
220
253
  }
@@ -235,7 +268,7 @@ class Connector {
235
268
  this._peerSeqs.set(payload.c, payload.seq);
236
269
  }
237
270
  this._addReceivedRef(ref);
238
- this._notifyCallbacks(ref);
271
+ this._notifyCallbacks(ref, payload.p);
239
272
  if (this._syncConfig?.requireAck) {
240
273
  this._socket.emit(this._events.ackClient, { r: ref });
241
274
  }
@@ -273,19 +306,23 @@ class Connector {
273
306
  });
274
307
  }
275
308
  _registerDbObserver() {
276
- this._db.registerObserver(this._route, (ins) => {
277
- return new Promise((resolve) => {
278
- const ref = ins[this.route.root.tableKey + "Ref"];
279
- if (this._hasSentRef(ref)) {
280
- resolve();
281
- return;
282
- }
283
- if (this._syncConfig?.causalOrdering && ins.previous?.length) {
284
- this._lastPredecessors = [...ins.previous];
309
+ this._db.registerObserver(this._route, async (ins) => {
310
+ const tableKey = this.route.root.tableKey;
311
+ const ref = ins[tableKey + "Ref"];
312
+ if (this._hasSentRef(ref)) {
313
+ return;
314
+ }
315
+ if (this._syncConfig?.causalOrdering && ins.previous?.length) {
316
+ const refs = [];
317
+ for (const timeId2 of ins.previous) {
318
+ const predRef = await this._db.getRefOfTimeId(tableKey, timeId2);
319
+ if (predRef) {
320
+ refs.push(predRef);
321
+ }
285
322
  }
286
- this.send(ref);
287
- resolve();
288
- });
323
+ this._lastPredecessors = refs;
324
+ }
325
+ this.send(ref);
289
326
  });
290
327
  }
291
328
  get socket() {