graphddb 0.3.0 → 0.3.1
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.md +1 -0
- package/dist/chunk-PNIZS37E.js +916 -0
- package/dist/{chunk-QEOFIXTN.js → chunk-VST3WOK3.js} +235 -556
- package/dist/{chunk-6AIAHP3A.js → chunk-YIXXTGZ6.js} +898 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +477 -3
- package/dist/index.js +412 -17
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-xJRn5qkv.d.ts → types-DuJ08bgc.d.ts} +675 -64
- package/package.json +1 -1
- package/dist/chunk-QQNP43JL.js +0 -461
package/package.json
CHANGED
package/dist/chunk-QQNP43JL.js
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeCaptureRegistry,
|
|
3
|
-
resolveModelClass
|
|
4
|
-
} from "./chunk-6AIAHP3A.js";
|
|
5
|
-
|
|
6
|
-
// src/cdc/prng.ts
|
|
7
|
-
var SeededRandom = class {
|
|
8
|
-
state;
|
|
9
|
-
constructor(seed) {
|
|
10
|
-
this.state = seed >>> 0 || 2654435769;
|
|
11
|
-
}
|
|
12
|
-
/** Next float in [0, 1). */
|
|
13
|
-
next() {
|
|
14
|
-
this.state |= 0;
|
|
15
|
-
this.state = this.state + 1831565813 | 0;
|
|
16
|
-
let t = Math.imul(this.state ^ this.state >>> 15, 1 | this.state);
|
|
17
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
18
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
19
|
-
}
|
|
20
|
-
/** Integer in [min, max] inclusive. */
|
|
21
|
-
int(min, max) {
|
|
22
|
-
return min + Math.floor(this.next() * (max - min + 1));
|
|
23
|
-
}
|
|
24
|
-
/** Bernoulli trial: true with probability `p`. */
|
|
25
|
-
chance(p) {
|
|
26
|
-
if (p <= 0) return false;
|
|
27
|
-
if (p >= 1) return true;
|
|
28
|
-
return this.next() < p;
|
|
29
|
-
}
|
|
30
|
-
/** In-place Fisher–Yates shuffle, deterministic for the seed state. */
|
|
31
|
-
shuffle(items) {
|
|
32
|
-
for (let i = items.length - 1; i > 0; i--) {
|
|
33
|
-
const j = this.int(0, i);
|
|
34
|
-
const tmp = items[i];
|
|
35
|
-
items[i] = items[j];
|
|
36
|
-
items[j] = tmp;
|
|
37
|
-
}
|
|
38
|
-
return items;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
function hashString(value) {
|
|
42
|
-
let h = 2166136261;
|
|
43
|
-
for (let i = 0; i < value.length; i++) {
|
|
44
|
-
h ^= value.charCodeAt(i);
|
|
45
|
-
h = Math.imul(h, 16777619);
|
|
46
|
-
}
|
|
47
|
-
return h >>> 0;
|
|
48
|
-
}
|
|
49
|
-
function shardIdFor(pk, shardCount) {
|
|
50
|
-
const index = hashString(pk) % shardCount;
|
|
51
|
-
return `shard-${String(index).padStart(4, "0")}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/cdc/emulator.ts
|
|
55
|
-
function resolveToClass(model) {
|
|
56
|
-
try {
|
|
57
|
-
return resolveModelClass(model);
|
|
58
|
-
} catch {
|
|
59
|
-
return model;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
var EPOCH = Date.parse("2020-01-01T00:00:00.000Z");
|
|
63
|
-
var CdcEmulator = class {
|
|
64
|
-
mode;
|
|
65
|
-
clockMode;
|
|
66
|
-
batchSize;
|
|
67
|
-
maxRetries;
|
|
68
|
-
startingPosition;
|
|
69
|
-
shardCount;
|
|
70
|
-
seed;
|
|
71
|
-
rng;
|
|
72
|
-
faultSpec = {};
|
|
73
|
-
concurrentRecompute = null;
|
|
74
|
-
concurrentRecomputeHook = null;
|
|
75
|
-
/** Per-shard monotonic sequence counter. */
|
|
76
|
-
seqCounters = /* @__PURE__ */ new Map();
|
|
77
|
-
/** Per-shard delivery queue (queued/replay modes). */
|
|
78
|
-
queues = /* @__PURE__ */ new Map();
|
|
79
|
-
/** Per-shard last successfully checkpointed sequence number. */
|
|
80
|
-
checkpointMap = /* @__PURE__ */ new Map();
|
|
81
|
-
/**
|
|
82
|
-
* Per-shard set of acked sequence numbers (as ints) not yet folded into the
|
|
83
|
-
* checkpoint. The checkpoint advances over the contiguous prefix of acked
|
|
84
|
-
* sequences (ReportBatchItemFailures semantics): a sequence is checkpointed
|
|
85
|
-
* only once every sequence at or before it has been acked.
|
|
86
|
-
*/
|
|
87
|
-
ackedSeqs = /* @__PURE__ */ new Map();
|
|
88
|
-
/** Per-shard count of sequences ever assigned, to bound the prefix scan. */
|
|
89
|
-
maxSeqSeen = /* @__PURE__ */ new Map();
|
|
90
|
-
/** Recorded event log (record mode and always, for replay()). */
|
|
91
|
-
log = [];
|
|
92
|
-
/** Dead-letter buffer. */
|
|
93
|
-
dlq = [];
|
|
94
|
-
handler = null;
|
|
95
|
-
/** Virtual clock (ms since EPOCH). */
|
|
96
|
-
clockMs = 0;
|
|
97
|
-
/** Pending async work from inline-mode captures, awaited by callers. */
|
|
98
|
-
inflight = Promise.resolve();
|
|
99
|
-
detachCapture = null;
|
|
100
|
-
models;
|
|
101
|
-
constructor(opts = {}) {
|
|
102
|
-
this.mode = opts.mode ?? "inline";
|
|
103
|
-
this.clockMode = opts.clock ?? (this.mode === "queued" ? "virtual" : "real");
|
|
104
|
-
this.batchSize = opts.batchSize ?? 100;
|
|
105
|
-
this.maxRetries = opts.maxRetries ?? 3;
|
|
106
|
-
this.startingPosition = opts.startingPosition ?? "TRIM_HORIZON";
|
|
107
|
-
this.shardCount = opts.shardCount ?? 8;
|
|
108
|
-
this.seed = opts.seed ?? 1;
|
|
109
|
-
this.rng = new SeededRandom(this.seed);
|
|
110
|
-
this.models = (opts.models ?? []).map(resolveToClass);
|
|
111
|
-
for (const model of this.models) {
|
|
112
|
-
ChangeCaptureRegistry.enableStream(model);
|
|
113
|
-
}
|
|
114
|
-
this.detachCapture = ChangeCaptureRegistry.register((record) => {
|
|
115
|
-
this.onRawWrite(record);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
// ── seam → ChangeEvent mapping (spec §4, §5) ─────────────────────────────
|
|
119
|
-
onRawWrite(record) {
|
|
120
|
-
if (record.model && this.models.length > 0) {
|
|
121
|
-
const enabled = this.models.some((m) => m.name === record.model);
|
|
122
|
-
if (!enabled) return;
|
|
123
|
-
}
|
|
124
|
-
const event = this.toChangeEvent(record);
|
|
125
|
-
this.log.push(event);
|
|
126
|
-
if (this.mode === "record" || this.mode === "replay") {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
this.enqueue(event);
|
|
130
|
-
if (this.mode === "inline") {
|
|
131
|
-
this.inflight = this.inflight.then(() => this.pump());
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
toChangeEvent(record) {
|
|
135
|
-
const shardId = shardIdFor(record.keys.pk, this.shardCount);
|
|
136
|
-
const seq = (this.seqCounters.get(shardId) ?? 0) + 1;
|
|
137
|
-
this.seqCounters.set(shardId, seq);
|
|
138
|
-
this.maxSeqSeen.set(shardId, seq);
|
|
139
|
-
const eventName = this.deriveEventName(record);
|
|
140
|
-
const newImage = eventName === "REMOVE" ? void 0 : record.newItem ?? void 0;
|
|
141
|
-
const oldImage = record.oldItem ?? void 0;
|
|
142
|
-
return {
|
|
143
|
-
eventName,
|
|
144
|
-
table: record.table,
|
|
145
|
-
...record.model ? { model: record.model } : {},
|
|
146
|
-
keys: { pk: record.keys.pk, sk: record.keys.sk },
|
|
147
|
-
...oldImage ? { oldImage } : {},
|
|
148
|
-
...newImage ? { newImage } : {},
|
|
149
|
-
approximateCreationTime: this.nowIso(),
|
|
150
|
-
sequenceNumber: String(seq).padStart(20, "0"),
|
|
151
|
-
shardId
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/** Parse a zero-padded sequence string back to its integer value. */
|
|
155
|
-
seqToInt(seq) {
|
|
156
|
-
return Number.parseInt(seq, 10);
|
|
157
|
-
}
|
|
158
|
-
/** Render an integer sequence as the zero-padded string form. */
|
|
159
|
-
intToSeq(n) {
|
|
160
|
-
return String(n).padStart(20, "0");
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Advance a shard's checkpoint over the contiguous prefix of acked sequence
|
|
164
|
-
* numbers. Starting just past the current checkpoint, consume every
|
|
165
|
-
* consecutive acked sequence; stop at the first gap (an un-acked sequence
|
|
166
|
-
* still pending redelivery). Consumed sequences are removed from the acked
|
|
167
|
-
* set to bound its size.
|
|
168
|
-
*/
|
|
169
|
-
advanceCheckpoint(shardId) {
|
|
170
|
-
const acked = this.ackedSeqs.get(shardId);
|
|
171
|
-
if (!acked || acked.size === 0) return;
|
|
172
|
-
const current = this.checkpointMap.get(shardId);
|
|
173
|
-
let next = current === void 0 ? 0 : this.seqToInt(current);
|
|
174
|
-
const maxSeq = this.maxSeqSeen.get(shardId) ?? 0;
|
|
175
|
-
let advanced = false;
|
|
176
|
-
while (next + 1 <= maxSeq && acked.has(next + 1)) {
|
|
177
|
-
next += 1;
|
|
178
|
-
acked.delete(next);
|
|
179
|
-
advanced = true;
|
|
180
|
-
}
|
|
181
|
-
if (advanced) this.checkpointMap.set(shardId, this.intToSeq(next));
|
|
182
|
-
}
|
|
183
|
-
deriveEventName(record) {
|
|
184
|
-
if (record.op === "delete") return "REMOVE";
|
|
185
|
-
return record.oldItem ? "MODIFY" : "INSERT";
|
|
186
|
-
}
|
|
187
|
-
nowIso() {
|
|
188
|
-
if (this.clockMode === "virtual") {
|
|
189
|
-
return new Date(EPOCH + this.clockMs).toISOString();
|
|
190
|
-
}
|
|
191
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
192
|
-
}
|
|
193
|
-
// ── enqueue + delivery (spec §6) ─────────────────────────────────────────
|
|
194
|
-
enqueue(event) {
|
|
195
|
-
const queue = this.queues.get(event.shardId) ?? [];
|
|
196
|
-
let dueAt = this.clockMode === "virtual" ? this.clockMs : 0;
|
|
197
|
-
if (this.faultSpec.delay) {
|
|
198
|
-
const { min, max } = this.faultSpec.delay;
|
|
199
|
-
dueAt += this.rng.int(min, max);
|
|
200
|
-
}
|
|
201
|
-
queue.push({ event, dueAt, attempts: 0, droppedOnce: false });
|
|
202
|
-
this.queues.set(event.shardId, queue);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Deliver buffered, due records to the consumer, advancing per-shard
|
|
206
|
-
* checkpoints on success and redelivering failures. In `queued`/`replay`
|
|
207
|
-
* modes call this (or {@link advanceClock}) to drive delivery; `inline` calls
|
|
208
|
-
* it automatically after each capture.
|
|
209
|
-
*/
|
|
210
|
-
async pump(maxBatches = Number.POSITIVE_INFINITY) {
|
|
211
|
-
if (!this.handler) return;
|
|
212
|
-
let delivered = 0;
|
|
213
|
-
let madeProgress = true;
|
|
214
|
-
while (madeProgress && delivered < maxBatches) {
|
|
215
|
-
madeProgress = false;
|
|
216
|
-
for (const shardId of [...this.queues.keys()]) {
|
|
217
|
-
const batch = this.collectBatch(shardId);
|
|
218
|
-
if (batch.length === 0) continue;
|
|
219
|
-
await this.deliverBatch(shardId, batch);
|
|
220
|
-
delivered += 1;
|
|
221
|
-
madeProgress = true;
|
|
222
|
-
if (delivered >= maxBatches) break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Pull up to `batchSize` due records from a shard, preserving ascending
|
|
228
|
-
* sequence order (spec §6). When the `reorder` fault is on, the within-shard
|
|
229
|
-
* order is deliberately shuffled to exercise invariant detection.
|
|
230
|
-
*/
|
|
231
|
-
collectBatch(shardId) {
|
|
232
|
-
const queue = this.queues.get(shardId);
|
|
233
|
-
if (!queue || queue.length === 0) return [];
|
|
234
|
-
const ready = [];
|
|
235
|
-
const now = this.clockMode === "virtual" ? this.clockMs : Number.POSITIVE_INFINITY;
|
|
236
|
-
const remaining = [];
|
|
237
|
-
for (const pd of queue) {
|
|
238
|
-
if (ready.length < this.batchSize && pd.dueAt <= now) {
|
|
239
|
-
ready.push(pd);
|
|
240
|
-
} else {
|
|
241
|
-
remaining.push(pd);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
this.queues.set(shardId, remaining);
|
|
245
|
-
if (this.faultSpec.reorder && ready.length > 1) {
|
|
246
|
-
this.rng.shuffle(ready);
|
|
247
|
-
}
|
|
248
|
-
return ready;
|
|
249
|
-
}
|
|
250
|
-
async deliverBatch(shardId, pending) {
|
|
251
|
-
const delivered = [];
|
|
252
|
-
const droppedThisRound = [];
|
|
253
|
-
for (const pd of pending) {
|
|
254
|
-
if (!pd.droppedOnce && this.faultSpec.dropThenRedeliver && this.rng.chance(this.faultSpec.dropThenRedeliver)) {
|
|
255
|
-
pd.droppedOnce = true;
|
|
256
|
-
droppedThisRound.push(pd);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
delivered.push(pd);
|
|
260
|
-
if (this.faultSpec.duplicate && this.rng.chance(this.faultSpec.duplicate)) {
|
|
261
|
-
delivered.push(pd);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (droppedThisRound.length > 0) {
|
|
265
|
-
const queue = this.queues.get(shardId) ?? [];
|
|
266
|
-
queue.unshift(...droppedThisRound);
|
|
267
|
-
this.queues.set(shardId, queue);
|
|
268
|
-
}
|
|
269
|
-
if (delivered.length === 0) return;
|
|
270
|
-
const records = delivered.map((pd) => pd.event);
|
|
271
|
-
if (this.concurrentRecompute && this.concurrentRecomputeHook) {
|
|
272
|
-
for (const event of records) {
|
|
273
|
-
await this.concurrentRecomputeHook(this.concurrentRecompute, event);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
const batch = { records };
|
|
277
|
-
const result = await this.handler(batch) ?? {};
|
|
278
|
-
const failures = new Set(result.batchItemFailures ?? []);
|
|
279
|
-
if (this.faultSpec.partialBatchFailure) {
|
|
280
|
-
for (const pd of delivered) {
|
|
281
|
-
if (this.rng.chance(this.faultSpec.partialBatchFailure)) {
|
|
282
|
-
failures.add(pd.event.sequenceNumber);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const requeue = [];
|
|
287
|
-
const seen = /* @__PURE__ */ new Set();
|
|
288
|
-
const acked = this.ackedSeqs.get(shardId) ?? /* @__PURE__ */ new Set();
|
|
289
|
-
for (const pd of delivered) {
|
|
290
|
-
const seq = pd.event.sequenceNumber;
|
|
291
|
-
if (failures.has(seq)) {
|
|
292
|
-
pd.attempts += 1;
|
|
293
|
-
if (pd.attempts > this.maxRetries) {
|
|
294
|
-
this.dlq.push(pd.event);
|
|
295
|
-
} else if (!seen.has(seq)) {
|
|
296
|
-
requeue.push(pd);
|
|
297
|
-
seen.add(seq);
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
acked.add(this.seqToInt(seq));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
this.ackedSeqs.set(shardId, acked);
|
|
304
|
-
this.advanceCheckpoint(shardId);
|
|
305
|
-
if (requeue.length > 0) {
|
|
306
|
-
const queue = this.queues.get(shardId) ?? [];
|
|
307
|
-
requeue.sort(
|
|
308
|
-
(a, b) => a.event.sequenceNumber < b.event.sequenceNumber ? -1 : 1
|
|
309
|
-
);
|
|
310
|
-
queue.unshift(...requeue);
|
|
311
|
-
this.queues.set(shardId, queue);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// ── public API (spec §10) ────────────────────────────────────────────────
|
|
315
|
-
/** Register the consumer. Returns an unsubscribe handle. */
|
|
316
|
-
subscribe(handler) {
|
|
317
|
-
this.handler = handler;
|
|
318
|
-
if (this.startingPosition === "LATEST") {
|
|
319
|
-
this.queues.clear();
|
|
320
|
-
}
|
|
321
|
-
return () => {
|
|
322
|
-
if (this.handler === handler) this.handler = null;
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
/** Await any inline-mode async deliveries triggered by writes. */
|
|
326
|
-
async flush() {
|
|
327
|
-
await this.inflight;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Advance the virtual clock by `ms` and deliver records whose due-time has
|
|
331
|
-
* arrived (spec §7). Reproduces throttle T / sweep period / staleness S time
|
|
332
|
-
* behavior deterministically (AC4).
|
|
333
|
-
*/
|
|
334
|
-
async advanceClock(ms) {
|
|
335
|
-
if (this.clockMode !== "virtual") {
|
|
336
|
-
throw new Error('advanceClock requires clock: "virtual"');
|
|
337
|
-
}
|
|
338
|
-
this.clockMs += ms;
|
|
339
|
-
await this.pump();
|
|
340
|
-
}
|
|
341
|
-
/** Current virtual time (ms since the emulator epoch). */
|
|
342
|
-
now() {
|
|
343
|
-
return this.clockMs;
|
|
344
|
-
}
|
|
345
|
-
/** Configure fault injection (spec §8). Merges into the current spec. */
|
|
346
|
-
fault(spec) {
|
|
347
|
-
this.faultSpec = { ...this.faultSpec, ...spec };
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Force a recompute to race a mark for the given ref (spec §8, §7). The
|
|
351
|
-
* harness supplies the actual recompute via {@link onConcurrentRecompute};
|
|
352
|
-
* this records which ref to race and arms the hook.
|
|
353
|
-
*/
|
|
354
|
-
injectConcurrentRecompute(ref) {
|
|
355
|
-
this.concurrentRecompute = ref;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Register the callback the emulator invokes (before each record reaches the
|
|
359
|
-
* consumer) when a concurrent recompute has been injected. This is how the
|
|
360
|
-
* test harness wires its recompute into the delivery race.
|
|
361
|
-
*/
|
|
362
|
-
onConcurrentRecompute(hook) {
|
|
363
|
-
this.concurrentRecomputeHook = hook;
|
|
364
|
-
}
|
|
365
|
-
/** Snapshot the recorded event log (spec §10). */
|
|
366
|
-
record() {
|
|
367
|
-
return { seed: this.seed, events: this.log.map((e) => ({ ...e })) };
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Replay an event log through the delivery pipeline, optionally shuffled and
|
|
371
|
-
* duplicated (spec §10, AC5). Used to prove **replay equivalence**: starting
|
|
372
|
-
* from any global order plus duplicates, a consumer that respects the
|
|
373
|
-
* per-shard ordering contract (spec §6) reaches a final aggregate equal to a
|
|
374
|
-
* full recompute from source.
|
|
375
|
-
*
|
|
376
|
-
* The shuffle models cross-shard reordering and at-least-once chaos at the
|
|
377
|
-
* delivery boundary. It must NOT, however, break the within-shard ordering
|
|
378
|
-
* guarantee that real DynamoDB Streams provides and that incremental
|
|
379
|
-
* aggregation depends on — so after shuffling/duplicating, each shard's queue
|
|
380
|
-
* is re-sorted into ascending original `sequenceNumber` order before delivery.
|
|
381
|
-
* The original `sequenceNumber` (assigned at record time) is preserved so it
|
|
382
|
-
* remains the source-of-truth ordering the consumer can rely on.
|
|
383
|
-
*/
|
|
384
|
-
async replay(log, opts = {}) {
|
|
385
|
-
const replayRng = new SeededRandom(log.seed ?? this.seed);
|
|
386
|
-
let events = log.events.map((e) => ({ ...e }));
|
|
387
|
-
if (opts.shuffle) {
|
|
388
|
-
events = replayRng.shuffle(events);
|
|
389
|
-
}
|
|
390
|
-
const expanded = [];
|
|
391
|
-
for (const e of events) {
|
|
392
|
-
expanded.push(e);
|
|
393
|
-
if (opts.duplicate && replayRng.chance(opts.duplicate)) {
|
|
394
|
-
expanded.push({ ...e });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
this.queues.clear();
|
|
398
|
-
this.seqCounters.clear();
|
|
399
|
-
this.ackedSeqs.clear();
|
|
400
|
-
this.maxSeqSeen.clear();
|
|
401
|
-
this.checkpointMap.clear();
|
|
402
|
-
for (const e of expanded) {
|
|
403
|
-
const shardId = shardIdFor(e.keys.pk, this.shardCount);
|
|
404
|
-
const resequenced = { ...e, shardId };
|
|
405
|
-
const queue = this.queues.get(shardId) ?? [];
|
|
406
|
-
queue.push({ event: resequenced, dueAt: 0, attempts: 0, droppedOnce: false });
|
|
407
|
-
this.queues.set(shardId, queue);
|
|
408
|
-
const seqInt = this.seqToInt(resequenced.sequenceNumber);
|
|
409
|
-
if (seqInt > (this.maxSeqSeen.get(shardId) ?? 0)) {
|
|
410
|
-
this.maxSeqSeen.set(shardId, seqInt);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
for (const [shardId, queue] of this.queues) {
|
|
414
|
-
queue.sort(
|
|
415
|
-
(a, b) => a.event.sequenceNumber < b.event.sequenceNumber ? -1 : a.event.sequenceNumber > b.event.sequenceNumber ? 1 : 0
|
|
416
|
-
);
|
|
417
|
-
this.queues.set(shardId, queue);
|
|
418
|
-
}
|
|
419
|
-
await this.pump();
|
|
420
|
-
}
|
|
421
|
-
/** Per-shard checkpoints (last acked sequence number). */
|
|
422
|
-
checkpoints() {
|
|
423
|
-
return Object.fromEntries(this.checkpointMap);
|
|
424
|
-
}
|
|
425
|
-
/** Dead-lettered events (exceeded maxRetries). */
|
|
426
|
-
deadLetters() {
|
|
427
|
-
return this.dlq.map((e) => ({ ...e }));
|
|
428
|
-
}
|
|
429
|
-
/** Reset delivery state and re-seed (keeps the subscription + model opt-in). */
|
|
430
|
-
reset() {
|
|
431
|
-
this.seqCounters.clear();
|
|
432
|
-
this.queues.clear();
|
|
433
|
-
this.checkpointMap.clear();
|
|
434
|
-
this.ackedSeqs.clear();
|
|
435
|
-
this.maxSeqSeen.clear();
|
|
436
|
-
this.log = [];
|
|
437
|
-
this.dlq = [];
|
|
438
|
-
this.clockMs = 0;
|
|
439
|
-
this.inflight = Promise.resolve();
|
|
440
|
-
this.faultSpec = {};
|
|
441
|
-
this.concurrentRecompute = null;
|
|
442
|
-
this.rng = new SeededRandom(this.seed);
|
|
443
|
-
}
|
|
444
|
-
/** Tear down: detach from the core seam and disable model streaming. */
|
|
445
|
-
close() {
|
|
446
|
-
this.detachCapture?.();
|
|
447
|
-
this.detachCapture = null;
|
|
448
|
-
for (const model of this.models) {
|
|
449
|
-
ChangeCaptureRegistry.disableStream(model);
|
|
450
|
-
}
|
|
451
|
-
this.handler = null;
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
function createCdcEmulator(opts = {}) {
|
|
455
|
-
return new CdcEmulator(opts);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export {
|
|
459
|
-
CdcEmulator,
|
|
460
|
-
createCdcEmulator
|
|
461
|
-
};
|