@nodii/telemetry 0.12.0 → 0.14.0

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.
Files changed (60) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +5 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/lambda/index.d.ts +77 -0
  5. package/dist/lambda/index.d.ts.map +1 -0
  6. package/dist/lambda/index.js +130 -0
  7. package/dist/lambda/index.js.map +1 -0
  8. package/dist/worker/checkpoint-store.d.ts +66 -0
  9. package/dist/worker/checkpoint-store.d.ts.map +1 -0
  10. package/dist/worker/checkpoint-store.js +131 -0
  11. package/dist/worker/checkpoint-store.js.map +1 -0
  12. package/dist/worker/dead-letter.d.ts +81 -0
  13. package/dist/worker/dead-letter.d.ts.map +1 -0
  14. package/dist/worker/dead-letter.js +165 -0
  15. package/dist/worker/dead-letter.js.map +1 -0
  16. package/dist/worker/dedupe-store.d.ts +46 -0
  17. package/dist/worker/dedupe-store.d.ts.map +1 -0
  18. package/dist/worker/dedupe-store.js +113 -0
  19. package/dist/worker/dedupe-store.js.map +1 -0
  20. package/dist/worker/index.d.ts +23 -0
  21. package/dist/worker/index.d.ts.map +1 -0
  22. package/dist/worker/index.js +46 -0
  23. package/dist/worker/index.js.map +1 -0
  24. package/dist/worker/observability-hooks.d.ts +99 -0
  25. package/dist/worker/observability-hooks.d.ts.map +1 -0
  26. package/dist/worker/observability-hooks.js +189 -0
  27. package/dist/worker/observability-hooks.js.map +1 -0
  28. package/dist/worker/outbox-dispatcher.d.ts +106 -0
  29. package/dist/worker/outbox-dispatcher.d.ts.map +1 -0
  30. package/dist/worker/outbox-dispatcher.js +285 -0
  31. package/dist/worker/outbox-dispatcher.js.map +1 -0
  32. package/dist/worker/replication-worker.d.ts +162 -0
  33. package/dist/worker/replication-worker.d.ts.map +1 -0
  34. package/dist/worker/replication-worker.js +513 -0
  35. package/dist/worker/replication-worker.js.map +1 -0
  36. package/dist/worker/signal-drain.d.ts +83 -0
  37. package/dist/worker/signal-drain.d.ts.map +1 -0
  38. package/dist/worker/signal-drain.js +131 -0
  39. package/dist/worker/signal-drain.js.map +1 -0
  40. package/dist/worker/single-active-lease.d.ts +180 -0
  41. package/dist/worker/single-active-lease.d.ts.map +1 -0
  42. package/dist/worker/single-active-lease.js +394 -0
  43. package/dist/worker/single-active-lease.js.map +1 -0
  44. package/dist/worker/streams-worker.d.ts +192 -0
  45. package/dist/worker/streams-worker.d.ts.map +1 -0
  46. package/dist/worker/streams-worker.js +488 -0
  47. package/dist/worker/streams-worker.js.map +1 -0
  48. package/dist/worker/sweeper-worker.d.ts +107 -0
  49. package/dist/worker/sweeper-worker.d.ts.map +1 -0
  50. package/dist/worker/sweeper-worker.js +245 -0
  51. package/dist/worker/sweeper-worker.js.map +1 -0
  52. package/dist/worker/tx.d.ts +39 -0
  53. package/dist/worker/tx.d.ts.map +1 -0
  54. package/dist/worker/tx.js +51 -0
  55. package/dist/worker/tx.js.map +1 -0
  56. package/dist/worker/worker-handle.d.ts +94 -0
  57. package/dist/worker/worker-handle.d.ts.map +1 -0
  58. package/dist/worker/worker-handle.js +15 -0
  59. package/dist/worker/worker-handle.js.map +1 -0
  60. package/package.json +10 -1
@@ -0,0 +1,488 @@
1
+ // D402 — StreamsWorker: the generic (non-replication) Redis-Streams consumer —
2
+ // the canonical Pattern-2 begin/completion-event consumer.
3
+ //
4
+ // D402 (LOCKED): "StreamsWorker — generic (non-replication) stream consumer; the
5
+ // canonical Pattern-2 begin/completion-event consumer." It closes the gap D402
6
+ // names directly: "the Pattern-2 begin/completion-event consumer must be
7
+ // StreamsWorker (today hand-rolled per service via
8
+ // idempotency.wrapConsumerWorker)."
9
+ //
10
+ // ── Composition (which primitives, how) ──────────────────────────────────────
11
+ // - SingleActiveLease — single-active per (service, stream); ttl/2 renew loop
12
+ // runs inside it. `validateFence(token)` guards the apply side-effect: a
13
+ // lease-lost worker mid-apply has its stale write REJECTED (the key test).
14
+ // - DedupeStore — `seen(...)` skips an already-applied (event_id, topic)
15
+ // before any work; `record(...)` runs INSIDE the apply tx so the dedupe row
16
+ // commits together-or-none with the projection + the cursor advance.
17
+ // - CheckpointStore — `advance(...)` runs in the SAME apply tx → HWM cursor
18
+ // never advances past an un-applied event.
19
+ // - ObservabilityHooks — lease/lag/hwm/depth metrics, canonical names.
20
+ // - SignalDrain — `stop({timeoutMs})` is registerable; bounded drain.
21
+ // - DeadLetterSink — INJECTED (no default); a poison event after retries
22
+ // is dead-lettered + XACK'd so the PEL doesn't grow unbounded.
23
+ //
24
+ // ── The consume sequence (per 01-comms § 9.2 / 09-replication § 4) ───────────
25
+ // XREADGROUP > → for each (id, event):
26
+ // [1] dedupe.seen? → XACK + skip (already applied; redelivery is a no-op)
27
+ // [2] apply tx: projection upsert + dedupe.record + checkpoint.advance, with
28
+ // validateFence(token) called BEFORE the side-effect (the fence guard)
29
+ // [3] XACK on success; on error retry-then-dead-letter.
30
+ //
31
+ // The apply itself is the INJECTED handler — the caller owns the projection
32
+ // upsert (this lib doesn't know the consumer's schema). The handler receives the
33
+ // tx executor + the dedupe/checkpoint primitives pre-bound so it runs everything
34
+ // on ONE transaction.
35
+ import { ensurePiiRedaction } from "./dead-letter.js";
36
+ import { ObservabilityHooks } from "./observability-hooks.js";
37
+ import { FenceCheckUnavailableError, FenceLostError, SingleActiveLease, } from "./single-active-lease.js";
38
+ const DEFAULT_BATCH = 32;
39
+ const DEFAULT_BLOCK_MS = 2_000;
40
+ const DEFAULT_RETRY_MAX = 3;
41
+ const DEFAULT_STOP_TIMEOUT_MS = 30_000;
42
+ const DEFAULT_RECLAIM_MIN_IDLE_MS = 30_000;
43
+ /** Consumer-group name — `${service}:${stream}` (canonical per replica-consumer). */
44
+ export function streamsConsumerGroup(service, stream) {
45
+ return `${service}:${stream}`;
46
+ }
47
+ /** Parse an ioredis XREADGROUP response into flat (id, event) pairs. */
48
+ export function parseStreamEntries(raw) {
49
+ if (!Array.isArray(raw))
50
+ return [];
51
+ const out = [];
52
+ for (const streamEntry of raw) {
53
+ if (!Array.isArray(streamEntry) || streamEntry.length < 2)
54
+ continue;
55
+ const entries = streamEntry[1];
56
+ if (!Array.isArray(entries))
57
+ continue;
58
+ for (const e of entries) {
59
+ if (!Array.isArray(e) || e.length < 2)
60
+ continue;
61
+ const id = String(e[0]);
62
+ const fields = e[1];
63
+ if (!Array.isArray(fields))
64
+ continue;
65
+ let payload;
66
+ for (let i = 0; i < fields.length - 1; i += 2) {
67
+ if (String(fields[i]) === "event") {
68
+ payload = String(fields[i + 1]);
69
+ break;
70
+ }
71
+ }
72
+ if (payload === undefined)
73
+ continue;
74
+ try {
75
+ out.push({ id, event: JSON.parse(payload) });
76
+ }
77
+ catch {
78
+ // Malformed payload — skip (the entry stays in the PEL; an operator can
79
+ // inspect it). Never crash the parse loop on one bad entry.
80
+ }
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Parse an ioredis XAUTOCLAIM reply `[cursor, entries, deleted]` into flat
87
+ * (id, event) pairs. `entries` has the same `[[id, [f, v, ...]], ...]` shape as
88
+ * one stream's entries in an XREADGROUP reply, so we wrap it and reuse
89
+ * {@link parseStreamEntries}.
90
+ */
91
+ export function parseAutoClaimEntries(raw) {
92
+ if (!Array.isArray(raw) || raw.length < 2)
93
+ return [];
94
+ const entries = raw[1];
95
+ if (!Array.isArray(entries))
96
+ return [];
97
+ // Wrap as a single-stream XREADGROUP shape: [[<stream>, <entries>]].
98
+ return parseStreamEntries([["", entries]]);
99
+ }
100
+ /**
101
+ * StreamsWorker — the generic Pattern-2 single-active stream consumer. Composes
102
+ * lease + dedupe + checkpoint + hooks + drain to consume EXACTLY-ONCE.
103
+ */
104
+ export class StreamsWorker {
105
+ redis;
106
+ stream;
107
+ apply;
108
+ begin;
109
+ dedupe;
110
+ checkpoint;
111
+ lease;
112
+ subscription;
113
+ deadLetter;
114
+ retryMaxAttempts;
115
+ pollBatch;
116
+ blockMs;
117
+ reclaimMinIdleMs;
118
+ hooks;
119
+ autoStart;
120
+ group;
121
+ consumer;
122
+ retries = new Map();
123
+ _running = false;
124
+ _stopped = false;
125
+ _inFlight = 0;
126
+ _hwm = { lastStreamId: null };
127
+ _lastAppliedAtMs = null;
128
+ groupEnsured = false;
129
+ loop = Promise.resolve();
130
+ constructor(opts) {
131
+ this.redis = opts.redis;
132
+ this.stream = opts.stream;
133
+ this.apply = opts.apply;
134
+ this.begin = opts.begin;
135
+ this.dedupe = opts.dedupe;
136
+ this.checkpoint = opts.checkpoint;
137
+ this.lease = new SingleActiveLease({
138
+ ...opts.lease,
139
+ onLeaseLost: () => this.hooks.leaseLost(),
140
+ });
141
+ this.subscription = opts.subscription ?? `streams:${opts.stream}`;
142
+ this.deadLetter = ensurePiiRedaction(opts.deadLetter);
143
+ this.retryMaxAttempts = opts.retryMaxAttempts ?? DEFAULT_RETRY_MAX;
144
+ this.pollBatch = opts.pollBatch ?? DEFAULT_BATCH;
145
+ this.blockMs = opts.blockMs ?? DEFAULT_BLOCK_MS;
146
+ this.reclaimMinIdleMs =
147
+ opts.reclaimMinIdleMs ??
148
+ Math.max(opts.lease.ttlMs, DEFAULT_RECLAIM_MIN_IDLE_MS);
149
+ this.autoStart = opts.autoStart !== false;
150
+ this.hooks =
151
+ opts.hooks ??
152
+ new ObservabilityHooks({
153
+ attributes: {
154
+ service: opts.lease.service,
155
+ scope: opts.lease.scope,
156
+ worker: opts.workerLabel ?? "streams",
157
+ },
158
+ });
159
+ this.group = streamsConsumerGroup(opts.lease.service, opts.stream);
160
+ this.consumer = `${opts.lease.service}:${this.lease.ownerId}`;
161
+ }
162
+ get running() {
163
+ return this._running;
164
+ }
165
+ get leaseAcquired() {
166
+ return this.lease.leaseAcquired;
167
+ }
168
+ get lag() {
169
+ if (this._lastAppliedAtMs === null)
170
+ return null;
171
+ return Math.max(0, (Date.now() - this._lastAppliedAtMs) / 1000);
172
+ }
173
+ get currentHwm() {
174
+ return this._hwm;
175
+ }
176
+ /** Idempotently create the consumer group (swallow BUSYGROUP). */
177
+ async ensureGroup() {
178
+ if (this.groupEnsured)
179
+ return;
180
+ try {
181
+ await this.redis.xgroup("CREATE", this.stream, this.group, "$", "MKSTREAM");
182
+ }
183
+ catch (err) {
184
+ const msg = err instanceof Error ? err.message : String(err);
185
+ if (!/BUSYGROUP/i.test(msg))
186
+ throw err;
187
+ }
188
+ this.groupEnsured = true;
189
+ }
190
+ async start() {
191
+ if (this._running)
192
+ return;
193
+ this._stopped = false;
194
+ const acquired = await this.lease.acquire();
195
+ if (acquired)
196
+ this.hooks.leaseAcquired();
197
+ // BUG-1 fix: if ANY boot step AFTER a successful acquire fails (ensureGroup
198
+ // throws — redis down at boot / a non-BUSYGROUP error), RELEASE the lease
199
+ // before rethrowing. A boot failure must NOT orphan the lease (a held key
200
+ // blocks a restart until the TTL lapses).
201
+ try {
202
+ await this.ensureGroup();
203
+ await this.hydrateCursor();
204
+ }
205
+ catch (err) {
206
+ if (acquired)
207
+ await this.lease.release();
208
+ throw err;
209
+ }
210
+ this._running = true;
211
+ // Background consume loop. `autoStart:false` lets a test drive `runOnce()`.
212
+ if (this.autoStart)
213
+ this.loop = this.runLoop();
214
+ }
215
+ async hydrateCursor() {
216
+ try {
217
+ await this.begin(async (tx) => {
218
+ const cp = await this.checkpoint.get(this.subscription, tx);
219
+ if (cp)
220
+ this._hwm = { lastStreamId: cp.lastStreamId };
221
+ });
222
+ }
223
+ catch {
224
+ // Cold-start read failure — cursor stays null, consume starts from `>`.
225
+ }
226
+ }
227
+ async runLoop() {
228
+ while (!this._stopped) {
229
+ if (this.lease.lost)
230
+ break;
231
+ try {
232
+ const processed = await this.pollOnce();
233
+ if (processed === 0) {
234
+ // Yield so a synchronous test-double doesn't tight-loop.
235
+ await new Promise((r) => setTimeout(r, 0));
236
+ }
237
+ }
238
+ catch (err) {
239
+ // BUG-2 fix: a poll that throws BECAUSE we're shutting down (stop() set
240
+ // `_stopped` and may have torn down the redis client mid-poll) is a CLEAN
241
+ // cancel, NOT a redis failure — exit quietly (no error metric, no
242
+ // error-sleep). A genuine poll-level error (XREADGROUP rejected — Redis
243
+ // down) while still running is the Pillar-5 infra-failure case: log via
244
+ // metric + back off, NEVER crash the loop.
245
+ if (this._stopped)
246
+ break;
247
+ this.hooks.recordDepth(-1, { error: "poll_failed" });
248
+ void err;
249
+ await new Promise((r) => setTimeout(r, 200));
250
+ }
251
+ }
252
+ }
253
+ /**
254
+ * Poll one batch + apply each entry. Returns count processed.
255
+ *
256
+ * Two reads per poll: FIRST our OWN pending-entries-list (PEL) at id `0` —
257
+ * entries delivered to this consumer but NOT yet XACK'd (a prior apply failed
258
+ * + NACK'd them); they MUST be retried, and a plain `>` read never re-returns
259
+ * a PEL entry. THEN `>` for brand-new entries. Without the PEL re-read a
260
+ * poison event would be applied exactly once and never retried.
261
+ */
262
+ async pollOnce() {
263
+ if (!this.lease.leaseAcquired) {
264
+ const got = await this.lease.acquire();
265
+ if (got) {
266
+ this.hooks.leaseAcquired();
267
+ // Re-ensure the consumer group on every re-acquire: a Redis failover
268
+ // across the prior holder's death may have wiped the group, so a cached
269
+ // `groupEnsured` would let xreadgroup hit NOGROUP forever. Re-running is a
270
+ // harmless BUSYGROUP no-op when the group still exists.
271
+ this.groupEnsured = false;
272
+ // BUG fix (in-loop re-acquire): if ensureGroup throws AFTER this poll
273
+ // re-acquired the lease, RELEASE it before rethrowing — otherwise the
274
+ // renewal loop holds + renews the lease INDEFINITELY while the next poll
275
+ // skips this block (leaseAcquired is true) and hammers a never-created
276
+ // group (NOGROUP loop), deadlocking every other instance.
277
+ try {
278
+ await this.ensureGroup();
279
+ }
280
+ catch (err) {
281
+ await this.lease.release();
282
+ throw err;
283
+ }
284
+ }
285
+ else {
286
+ return 0;
287
+ }
288
+ }
289
+ let processed = 0;
290
+ // [reclaim] CRASH-RECOVERY reaper: XAUTOCLAIM transfers PENDING entries idle
291
+ // ≥ reclaimMinIdleMs from a DEAD predecessor (a crashed worker with a
292
+ // different consumer name) to US, the single-active successor. Without this,
293
+ // a crashed worker's un-ACK'd in-flight entries are PERMANENTLY ORPHANED (a
294
+ // plain "0" PEL re-read is per-consumer) → silent event loss → broken
295
+ // at-least-once. The fence guard inside the apply tx still rejects a stale
296
+ // write if a still-alive predecessor races us.
297
+ const claimedRaw = await this.redis.xautoclaim(this.stream, this.group, this.consumer, this.reclaimMinIdleMs, "0-0", "COUNT", this.pollBatch);
298
+ for (const p of parseAutoClaimEntries(claimedRaw)) {
299
+ if (this._stopped || this.lease.lost)
300
+ break;
301
+ await this.applyOne(p.id, p.event);
302
+ processed += 1;
303
+ }
304
+ // [retry] Re-read our own PEL (id `0`, non-blocking) for NACK'd entries.
305
+ const pendingRaw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "STREAMS", this.stream, "0");
306
+ for (const p of parseStreamEntries(pendingRaw)) {
307
+ if (this._stopped || this.lease.lost)
308
+ break;
309
+ await this.applyOne(p.id, p.event);
310
+ processed += 1;
311
+ }
312
+ // [new] Read brand-new entries (`>`, blocking up to blockMs).
313
+ const raw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "BLOCK", this.blockMs, "STREAMS", this.stream, ">");
314
+ // Sample backlog depth (cheap; best-effort).
315
+ try {
316
+ const depth = await this.redis.xlen(this.stream);
317
+ this.hooks.recordDepth(depth);
318
+ }
319
+ catch {
320
+ /* depth is advisory — never fail the poll on it */
321
+ }
322
+ for (const p of parseStreamEntries(raw)) {
323
+ if (this._stopped || this.lease.lost)
324
+ break;
325
+ await this.applyOne(p.id, p.event);
326
+ processed += 1;
327
+ }
328
+ return processed;
329
+ }
330
+ /**
331
+ * Apply one entry: dedupe-skip → apply-tx (projection + dedupe.record +
332
+ * checkpoint.advance, fence-guarded) → XACK. On error, retry-then-dead-letter.
333
+ */
334
+ async applyOne(streamId, event) {
335
+ // We dropped the lease mid-flight; the entry stays un-ACK'd in our PEL. The
336
+ // single-active successor (a new instance with a DIFFERENT consumer name)
337
+ // reclaims it via the [reclaim] XAUTOCLAIM reaper once idle — NOT via a plain
338
+ // "0" PEL re-read (that is per-consumer).
339
+ if (this.lease.lost)
340
+ return;
341
+ this._inFlight += 1;
342
+ const token = this.lease.fencingToken;
343
+ try {
344
+ // [1] Pre-tx dedupe probe (cheap). A redelivered already-applied event is
345
+ // XACK'd + skipped (exactly-once on the consumer side).
346
+ const alreadySeen = await this.begin((tx) => this.dedupe.seen(event.event_id, event.topic, tx));
347
+ if (alreadySeen) {
348
+ // The event already committed (durable dedupe row) — only the XACK is
349
+ // outstanding. A failing XACK here is a TRANSIENT ack-failure, NOT a
350
+ // poison event: swallow it (the entry stays un-acked in the PEL, re-acked
351
+ // next poll once redis recovers). NEVER route a committed event to
352
+ // retry/DLQ over an ack blip (matches Go's `_, _ = Ack(...)`).
353
+ await this.ackSwallow(streamId);
354
+ this.retries.delete(streamId);
355
+ return;
356
+ }
357
+ // [2] Apply tx — projection upsert + dedupe.record + checkpoint.advance,
358
+ // all on ONE tx (atomic). The fence guard runs BEFORE the projection.
359
+ await this.begin(async (tx) => {
360
+ const ctx = {
361
+ tx,
362
+ event,
363
+ streamId,
364
+ validateFence: async () => {
365
+ if (token === null) {
366
+ throw new FenceLostError({
367
+ leaseKey: this.lease.key,
368
+ expectedToken: -1,
369
+ current: null,
370
+ });
371
+ }
372
+ await this.lease.validateFence(token);
373
+ },
374
+ };
375
+ await this.apply(ctx);
376
+ await this.dedupe.record(event.event_id, event.topic, tx);
377
+ await this.checkpoint.advance({ subscription: this.subscription, lastStreamId: streamId }, tx);
378
+ });
379
+ // [3] Commit succeeded → XACK + advance local HWM. The XACK is POST-COMMIT
380
+ // and NOT part of the apply tx: a failing XACK here is a transient
381
+ // ack-failure, NOT a poison event — swallow it (the committed event is
382
+ // re-acked next poll; the dedupe row makes a redelivery a no-op). Do NOT
383
+ // route a successfully-committed event to retry/DLQ over an ack blip.
384
+ await this.ackSwallow(streamId);
385
+ this.retries.delete(streamId);
386
+ this._hwm = { lastStreamId: streamId };
387
+ this._lastAppliedAtMs = Date.now();
388
+ if (event.occurred_at_ms !== undefined) {
389
+ this.hooks.recordLag(Math.max(0, (Date.now() - event.occurred_at_ms) / 1000));
390
+ }
391
+ else {
392
+ this.hooks.recordLag(0);
393
+ }
394
+ }
395
+ catch (err) {
396
+ // A FenceLostError means the lease was lost/taken-over mid-apply — the
397
+ // stale write was REJECTED before it landed. Do NOT XACK (the entry stays
398
+ // in our PEL; the single-active successor reclaims it via the XAUTOCLAIM
399
+ // reaper once idle) and do NOT dead-letter (benign handoff).
400
+ if (err instanceof FenceLostError) {
401
+ this.hooks.leaseLost();
402
+ return;
403
+ }
404
+ // A FenceCheckUnavailableError means the fence-check READ failed (redis
405
+ // down) — TRANSIENT. Fail closed (the write was withheld), but do NOT XACK
406
+ // and do NOT dead-letter a non-poison event over a redis blip: leave it in
407
+ // the PEL to be reprocessed once redis recovers (cross-runtime-identical).
408
+ if (err instanceof FenceCheckUnavailableError) {
409
+ this.hooks.recordDepth(-1, { error: "fence_check_unavailable" });
410
+ return;
411
+ }
412
+ await this.retryOrDeadLetter(streamId, event, err);
413
+ }
414
+ finally {
415
+ this._inFlight -= 1;
416
+ }
417
+ }
418
+ /**
419
+ * XACK an already-committed entry, SWALLOWING a transient ack failure. The
420
+ * XACK is post-commit (or post-dedupe-skip) and NOT part of the apply tx — a
421
+ * failing XACK is a transient redis blip, never a poison event. On failure the
422
+ * entry simply stays un-acked in the PEL and is re-acked next poll once redis
423
+ * recovers (the durable dedupe row makes the redelivery a no-op). This is what
424
+ * Go does (`_, _ = Ack(...)`); a throw here must NEVER reach retry/DLQ.
425
+ */
426
+ async ackSwallow(streamId) {
427
+ try {
428
+ await this.redis.xack(this.stream, this.group, streamId);
429
+ }
430
+ catch {
431
+ this.hooks.recordDepth(-1, { error: "ack_failed" });
432
+ }
433
+ }
434
+ /** Increment the retry ledger; dead-letter (+XACK) once the budget is spent. */
435
+ async retryOrDeadLetter(streamId, event, err) {
436
+ const attempt = (this.retries.get(streamId) ?? 0) + 1;
437
+ this.retries.set(streamId, attempt);
438
+ if (attempt < this.retryMaxAttempts) {
439
+ // No XACK — the entry stays in the PEL and is redelivered next poll.
440
+ return;
441
+ }
442
+ try {
443
+ await this.deadLetter.deadLetter({
444
+ sourceStream: this.stream,
445
+ topic: event.topic,
446
+ eventId: event.event_id,
447
+ payload: event,
448
+ errorMessage: err instanceof Error ? err.message : String(err),
449
+ retryCount: attempt,
450
+ });
451
+ // ACK after the dead-letter is durable so the PEL doesn't grow unbounded.
452
+ await this.redis.xack(this.stream, this.group, streamId);
453
+ this.retries.delete(streamId);
454
+ }
455
+ catch {
456
+ // The dead-letter sink itself failed — leave the entry in the PEL (it will
457
+ // be redelivered) rather than losing it. Never XACK on a failed dead-letter.
458
+ }
459
+ }
460
+ /** Run exactly one poll batch (test seam / cron tick). */
461
+ async runOnce() {
462
+ return this.pollOnce();
463
+ }
464
+ async stop(opts) {
465
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
466
+ const start = Date.now();
467
+ this._stopped = true;
468
+ this._running = false;
469
+ try {
470
+ await Promise.race([
471
+ this.loop,
472
+ new Promise((r) => setTimeout(r, timeoutMs)),
473
+ ]);
474
+ }
475
+ catch {
476
+ /* swallow */
477
+ }
478
+ while (this._inFlight > 0 && Date.now() - start < timeoutMs) {
479
+ await new Promise((r) => setTimeout(r, 10));
480
+ }
481
+ const drainedCleanly = this._inFlight === 0;
482
+ await this.lease.release();
483
+ const durationMs = Date.now() - start;
484
+ this.hooks.recordDrainDuration(durationMs);
485
+ return { drainedCleanly, durationMs };
486
+ }
487
+ }
488
+ //# sourceMappingURL=streams-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams-worker.js","sourceRoot":"","sources":["../../src/worker/streams-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ import type { DeadLetterSink } from "./dead-letter.js";
2
+ import { ObservabilityHooks } from "./observability-hooks.js";
3
+ import { type SingleActiveLeaseOpts } from "./single-active-lease.js";
4
+ import type { SqlExecutor } from "./tx.js";
5
+ import type { WorkerHandle, WorkerHwm, WorkerStopOptions, WorkerStopResult } from "./worker-handle.js";
6
+ /**
7
+ * The context a sweeper job receives each tick. Carries the executor the job
8
+ * runs its delete + checkpoint advance on, the durable cursor it resumes from,
9
+ * and the fence guard it MUST call before any non-idempotent delete.
10
+ */
11
+ export interface SweeperJobContext {
12
+ /** A pooled executor (the sweep is independent of any apply transaction; the
13
+ * job MAY open its own tx via the caller-provided executor factory). */
14
+ exec: SqlExecutor;
15
+ /** The durable cursor the worker resumes from (null on cold start). */
16
+ lastStreamId: string | null;
17
+ /**
18
+ * THE side-effecting guard. The job MUST `await ctx.validateFence()` right
19
+ * before any non-idempotent delete: it re-reads the live lease and throws
20
+ * {@link FenceLostError} if the lease was lost/taken-over since the tick
21
+ * began — so a stale sweeper's late delete cannot land.
22
+ */
23
+ validateFence(): Promise<void>;
24
+ }
25
+ /** What a single sweep tick returns: how many rows it removed + the new cursor. */
26
+ export interface SweeperJobResult {
27
+ /** Rows removed this tick (surfaced as `nodii.worker.swept_total`). */
28
+ swept: number;
29
+ /**
30
+ * The new durable cursor position to advance the checkpoint to (e.g. the
31
+ * max consumed_at swept, or a synthetic monotonic marker). Omit / null to
32
+ * leave the cursor unchanged (a no-op sweep).
33
+ */
34
+ lastStreamId?: string | null;
35
+ }
36
+ /** The injected unit of sweep work (e.g. `dedupe.sweep(...)`, a retention DELETE). */
37
+ export interface SweeperJobDefinition {
38
+ /** Stable name for logs/metrics (the lease scope defaults to this). */
39
+ name: string;
40
+ /** Run exactly one sweep pass. MUST call `ctx.validateFence()` before delete. */
41
+ runOnce(ctx: SweeperJobContext): Promise<SweeperJobResult>;
42
+ }
43
+ export interface SweeperWorkerOpts {
44
+ /** The injected sweep job. */
45
+ job: SweeperJobDefinition;
46
+ /** Provides a fresh pooled executor per tick (e.g. `() => postgresJsExecutor(sql)`). */
47
+ executor: () => SqlExecutor | Promise<SqlExecutor>;
48
+ /** Lease config (service + scope + ttlMs). The renew loop runs inside it. */
49
+ lease: Omit<SingleActiveLeaseOpts, "onLeaseLost">;
50
+ /** The durable checkpoint store (resume cursor). */
51
+ checkpoint: import("./checkpoint-store.js").CheckpointStore;
52
+ /** The checkpoint subscription key (defaults to `sweeper:<job.name>`). */
53
+ subscription?: string;
54
+ /** Tick interval (ms) for the autonomous loop. Default 60_000. */
55
+ intervalMs?: number;
56
+ /** Injected dead-letter sink (NO default). A tick error is dead-lettered. */
57
+ deadLetter: DeadLetterSink;
58
+ /** Observability hooks (canonical metrics). Constructed if omitted. */
59
+ hooks?: ObservabilityHooks;
60
+ /** Worker label for metric attributes. Default "sweeper". */
61
+ workerLabel?: string;
62
+ /** Launch the interval loop on `start()`. Default true; tests pass false to
63
+ * drive `runOnce()` deterministically. */
64
+ autoStart?: boolean;
65
+ }
66
+ /**
67
+ * SweeperWorker — the reference {@link WorkerHandle}. Acquires a single-active
68
+ * lease, runs the injected sweep job on an interval, advances the checkpoint
69
+ * each tick (resumable), and drains bounded on stop.
70
+ */
71
+ export declare class SweeperWorker implements WorkerHandle {
72
+ private readonly job;
73
+ private readonly executorFactory;
74
+ private readonly lease;
75
+ private readonly checkpoint;
76
+ private readonly subscription;
77
+ private readonly intervalMs;
78
+ private readonly deadLetter;
79
+ private readonly hooks;
80
+ private readonly autoStart;
81
+ private _running;
82
+ private _stopped;
83
+ private _inFlight;
84
+ private _hwm;
85
+ private _lastAppliedAtMs;
86
+ private loopTimer;
87
+ constructor(opts: SweeperWorkerOpts);
88
+ get running(): boolean;
89
+ get leaseAcquired(): boolean;
90
+ get lag(): number | null;
91
+ get currentHwm(): WorkerHwm;
92
+ /** Acquire the lease + start the interval loop. Idempotent. */
93
+ start(): Promise<void>;
94
+ /** Read the durable checkpoint so a restart resumes rather than re-sweeps. */
95
+ private hydrateCursor;
96
+ /** One interval tick: acquire-if-needed → runOnce → swallow into dead-letter. */
97
+ private tick;
98
+ /**
99
+ * Run exactly ONE sweep pass. Acquires/validates the lease for the tick,
100
+ * invokes the injected job, advances the checkpoint, emits `swept_total`.
101
+ * Resolves the number of rows swept (0 when the lease is not held).
102
+ */
103
+ runOnce(): Promise<number>;
104
+ /** Bounded graceful stop: halt the loop, let an in-flight tick finish, release. */
105
+ stop(opts?: WorkerStopOptions): Promise<WorkerStopResult>;
106
+ }
107
+ //# sourceMappingURL=sweeper-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweeper-worker.d.ts","sourceRoot":"","sources":["../../src/worker/sweeper-worker.ts"],"names":[],"mappings":"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"}