@nodii/telemetry 0.13.0 → 0.14.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.
Files changed (53) hide show
  1. package/dist/worker/checkpoint-store.d.ts +66 -0
  2. package/dist/worker/checkpoint-store.d.ts.map +1 -0
  3. package/dist/worker/checkpoint-store.js +131 -0
  4. package/dist/worker/checkpoint-store.js.map +1 -0
  5. package/dist/worker/dead-letter.d.ts +81 -0
  6. package/dist/worker/dead-letter.d.ts.map +1 -0
  7. package/dist/worker/dead-letter.js +165 -0
  8. package/dist/worker/dead-letter.js.map +1 -0
  9. package/dist/worker/dedupe-store.d.ts +46 -0
  10. package/dist/worker/dedupe-store.d.ts.map +1 -0
  11. package/dist/worker/dedupe-store.js +113 -0
  12. package/dist/worker/dedupe-store.js.map +1 -0
  13. package/dist/worker/index.d.ts +23 -0
  14. package/dist/worker/index.d.ts.map +1 -0
  15. package/dist/worker/index.js +46 -0
  16. package/dist/worker/index.js.map +1 -0
  17. package/dist/worker/observability-hooks.d.ts +99 -0
  18. package/dist/worker/observability-hooks.d.ts.map +1 -0
  19. package/dist/worker/observability-hooks.js +189 -0
  20. package/dist/worker/observability-hooks.js.map +1 -0
  21. package/dist/worker/outbox-dispatcher.d.ts +106 -0
  22. package/dist/worker/outbox-dispatcher.d.ts.map +1 -0
  23. package/dist/worker/outbox-dispatcher.js +285 -0
  24. package/dist/worker/outbox-dispatcher.js.map +1 -0
  25. package/dist/worker/replication-worker.d.ts +170 -0
  26. package/dist/worker/replication-worker.d.ts.map +1 -0
  27. package/dist/worker/replication-worker.js +576 -0
  28. package/dist/worker/replication-worker.js.map +1 -0
  29. package/dist/worker/signal-drain.d.ts +83 -0
  30. package/dist/worker/signal-drain.d.ts.map +1 -0
  31. package/dist/worker/signal-drain.js +131 -0
  32. package/dist/worker/signal-drain.js.map +1 -0
  33. package/dist/worker/single-active-lease.d.ts +180 -0
  34. package/dist/worker/single-active-lease.d.ts.map +1 -0
  35. package/dist/worker/single-active-lease.js +394 -0
  36. package/dist/worker/single-active-lease.js.map +1 -0
  37. package/dist/worker/streams-worker.d.ts +256 -0
  38. package/dist/worker/streams-worker.d.ts.map +1 -0
  39. package/dist/worker/streams-worker.js +621 -0
  40. package/dist/worker/streams-worker.js.map +1 -0
  41. package/dist/worker/sweeper-worker.d.ts +107 -0
  42. package/dist/worker/sweeper-worker.d.ts.map +1 -0
  43. package/dist/worker/sweeper-worker.js +245 -0
  44. package/dist/worker/sweeper-worker.js.map +1 -0
  45. package/dist/worker/tx.d.ts +39 -0
  46. package/dist/worker/tx.d.ts.map +1 -0
  47. package/dist/worker/tx.js +51 -0
  48. package/dist/worker/tx.js.map +1 -0
  49. package/dist/worker/worker-handle.d.ts +94 -0
  50. package/dist/worker/worker-handle.d.ts.map +1 -0
  51. package/dist/worker/worker-handle.js +15 -0
  52. package/dist/worker/worker-handle.js.map +1 -0
  53. package/package.json +6 -1
@@ -0,0 +1,621 @@
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
+ /**
44
+ * Wraps a throw that originated from the INJECTED apply handler (the consumer's
45
+ * projection upsert) so the worker can tell a POISON failure (the handler itself
46
+ * rejected on a well-formed event) apart from a TRANSIENT infra failure (the tx
47
+ * open / commit, the dedupe-store probe, or `dedupe.record` / `checkpoint.advance`
48
+ * threw because postgres is unreachable). ONLY a poison failure may travel the
49
+ * retry→dead-letter path; an untagged throw inside the apply path is infra and is
50
+ * left in the PEL to be retried once the store recovers (symmetric with
51
+ * {@link FenceCheckUnavailableError} on the redis-down fence path, and with the
52
+ * OutboxDispatcher, which survives a `begin()` throw without dead-lettering).
53
+ */
54
+ export class PoisonApplyError extends Error {
55
+ /** The original error the apply handler threw. */
56
+ cause;
57
+ constructor(cause) {
58
+ super(cause instanceof Error ? cause.message : String(cause));
59
+ this.name = "PoisonApplyError";
60
+ this.cause = cause;
61
+ }
62
+ }
63
+ /** Unwrap a {@link PoisonApplyError} to the handler's original error. */
64
+ function unwrapPoison(err) {
65
+ return err.cause;
66
+ }
67
+ /** Consumer-group name — `${service}:${stream}` (canonical per replica-consumer). */
68
+ export function streamsConsumerGroup(service, stream) {
69
+ return `${service}:${stream}`;
70
+ }
71
+ /**
72
+ * Parse an ioredis XREADGROUP response into PARSED `(id, event)` pairs AND the
73
+ * structurally-unparseable entries (malformed JSON in the `event` field). An
74
+ * entry with NO `event` field is skipped (not malformed — nothing to apply); an
75
+ * entry WITH an `event` field that fails JSON.parse is MALFORMED (poison) so the
76
+ * worker can dead-letter + XACK it rather than orphan it in the PEL forever.
77
+ */
78
+ export function parseStreamEntriesPartitioned(raw) {
79
+ const entries = [];
80
+ const malformed = [];
81
+ if (!Array.isArray(raw))
82
+ return { entries, malformed };
83
+ for (const streamEntry of raw) {
84
+ if (!Array.isArray(streamEntry) || streamEntry.length < 2)
85
+ continue;
86
+ const streamEntries = streamEntry[1];
87
+ if (!Array.isArray(streamEntries))
88
+ continue;
89
+ for (const e of streamEntries) {
90
+ if (!Array.isArray(e) || e.length < 2)
91
+ continue;
92
+ const id = String(e[0]);
93
+ const fields = e[1];
94
+ if (!Array.isArray(fields))
95
+ continue;
96
+ let payload;
97
+ for (let i = 0; i < fields.length - 1; i += 2) {
98
+ if (String(fields[i]) === "event") {
99
+ payload = String(fields[i + 1]);
100
+ break;
101
+ }
102
+ }
103
+ if (payload === undefined)
104
+ continue;
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(payload);
108
+ }
109
+ catch {
110
+ // Malformed `event` JSON — a POISON entry that can never be applied.
111
+ // Surface it so the worker dead-letters + XACKs it (never a silent drop /
112
+ // an orphan that re-cycles through XAUTOCLAIM on every reclaim).
113
+ malformed.push({ id, rawPayload: payload });
114
+ continue;
115
+ }
116
+ // Valid JSON but NOT an object (a bare array / number / string) can never
117
+ // be an event dict → also poison (parity with Go's `map[string]any`
118
+ // unmarshal + Python's dict check).
119
+ if (typeof parsed !== "object" ||
120
+ parsed === null ||
121
+ Array.isArray(parsed)) {
122
+ malformed.push({ id, rawPayload: payload });
123
+ continue;
124
+ }
125
+ entries.push({ id, event: parsed });
126
+ }
127
+ }
128
+ return { entries, malformed };
129
+ }
130
+ /** Parse an ioredis XREADGROUP response into flat (id, event) pairs (the
131
+ * parsed entries only). Retained for API compatibility; the worker uses
132
+ * {@link parseStreamEntriesPartitioned} so it can also dead-letter malformed
133
+ * entries. */
134
+ export function parseStreamEntries(raw) {
135
+ return parseStreamEntriesPartitioned(raw).entries;
136
+ }
137
+ /**
138
+ * Parse an ioredis XAUTOCLAIM reply `[cursor, entries, deleted]` into PARSED
139
+ * (id, event) pairs AND the malformed entries. `entries` has the same
140
+ * `[[id, [f, v, ...]], ...]` shape as one stream's entries in an XREADGROUP
141
+ * reply, so we wrap it and reuse {@link parseStreamEntriesPartitioned}.
142
+ */
143
+ export function parseAutoClaimEntriesPartitioned(raw) {
144
+ if (!Array.isArray(raw) || raw.length < 2) {
145
+ return { entries: [], malformed: [] };
146
+ }
147
+ const claimed = raw[1];
148
+ if (!Array.isArray(claimed))
149
+ return { entries: [], malformed: [] };
150
+ // Wrap as a single-stream XREADGROUP shape: [[<stream>, <entries>]].
151
+ return parseStreamEntriesPartitioned([["", claimed]]);
152
+ }
153
+ /**
154
+ * Parse an ioredis XAUTOCLAIM reply `[cursor, entries, deleted]` into flat
155
+ * (id, event) pairs (parsed entries only). Retained for API compatibility.
156
+ */
157
+ export function parseAutoClaimEntries(raw) {
158
+ return parseAutoClaimEntriesPartitioned(raw).entries;
159
+ }
160
+ /**
161
+ * StreamsWorker — the generic Pattern-2 single-active stream consumer. Composes
162
+ * lease + dedupe + checkpoint + hooks + drain to consume EXACTLY-ONCE.
163
+ */
164
+ export class StreamsWorker {
165
+ redis;
166
+ stream;
167
+ apply;
168
+ begin;
169
+ dedupe;
170
+ checkpoint;
171
+ lease;
172
+ subscription;
173
+ deadLetter;
174
+ retryMaxAttempts;
175
+ pollBatch;
176
+ blockMs;
177
+ reclaimMinIdleMs;
178
+ hooks;
179
+ autoStart;
180
+ group;
181
+ consumer;
182
+ retries = new Map();
183
+ _running = false;
184
+ _stopped = false;
185
+ _inFlight = 0;
186
+ _hwm = { lastStreamId: null };
187
+ _lastAppliedAtMs = null;
188
+ groupEnsured = false;
189
+ loop = Promise.resolve();
190
+ constructor(opts) {
191
+ this.redis = opts.redis;
192
+ this.stream = opts.stream;
193
+ this.apply = opts.apply;
194
+ this.begin = opts.begin;
195
+ this.dedupe = opts.dedupe;
196
+ this.checkpoint = opts.checkpoint;
197
+ this.lease = new SingleActiveLease({
198
+ ...opts.lease,
199
+ onLeaseLost: () => this.hooks.leaseLost(),
200
+ });
201
+ this.subscription = opts.subscription ?? `streams:${opts.stream}`;
202
+ this.deadLetter = ensurePiiRedaction(opts.deadLetter);
203
+ this.retryMaxAttempts = opts.retryMaxAttempts ?? DEFAULT_RETRY_MAX;
204
+ this.pollBatch = opts.pollBatch ?? DEFAULT_BATCH;
205
+ this.blockMs = opts.blockMs ?? DEFAULT_BLOCK_MS;
206
+ this.reclaimMinIdleMs =
207
+ opts.reclaimMinIdleMs ??
208
+ Math.max(opts.lease.ttlMs, DEFAULT_RECLAIM_MIN_IDLE_MS);
209
+ this.autoStart = opts.autoStart !== false;
210
+ this.hooks =
211
+ opts.hooks ??
212
+ new ObservabilityHooks({
213
+ attributes: {
214
+ service: opts.lease.service,
215
+ scope: opts.lease.scope,
216
+ worker: opts.workerLabel ?? "streams",
217
+ },
218
+ });
219
+ this.group = streamsConsumerGroup(opts.lease.service, opts.stream);
220
+ this.consumer = `${opts.lease.service}:${this.lease.ownerId}`;
221
+ }
222
+ get running() {
223
+ return this._running;
224
+ }
225
+ get leaseAcquired() {
226
+ return this.lease.leaseAcquired;
227
+ }
228
+ get lag() {
229
+ if (this._lastAppliedAtMs === null)
230
+ return null;
231
+ return Math.max(0, (Date.now() - this._lastAppliedAtMs) / 1000);
232
+ }
233
+ get currentHwm() {
234
+ return this._hwm;
235
+ }
236
+ /** Idempotently create the consumer group (swallow BUSYGROUP). */
237
+ async ensureGroup() {
238
+ if (this.groupEnsured)
239
+ return;
240
+ try {
241
+ await this.redis.xgroup("CREATE", this.stream, this.group, "$", "MKSTREAM");
242
+ }
243
+ catch (err) {
244
+ const msg = err instanceof Error ? err.message : String(err);
245
+ if (!/BUSYGROUP/i.test(msg))
246
+ throw err;
247
+ }
248
+ this.groupEnsured = true;
249
+ }
250
+ async start() {
251
+ if (this._running)
252
+ return;
253
+ this._stopped = false;
254
+ const acquired = await this.lease.acquire();
255
+ if (acquired)
256
+ this.hooks.leaseAcquired();
257
+ // BUG-1 fix: if ANY boot step AFTER a successful acquire fails (ensureGroup
258
+ // throws — redis down at boot / a non-BUSYGROUP error), RELEASE the lease
259
+ // before rethrowing. A boot failure must NOT orphan the lease (a held key
260
+ // blocks a restart until the TTL lapses).
261
+ try {
262
+ await this.ensureGroup();
263
+ await this.hydrateCursor();
264
+ }
265
+ catch (err) {
266
+ if (acquired)
267
+ await this.lease.release();
268
+ throw err;
269
+ }
270
+ this._running = true;
271
+ // Background consume loop. `autoStart:false` lets a test drive `runOnce()`.
272
+ if (this.autoStart)
273
+ this.loop = this.runLoop();
274
+ }
275
+ async hydrateCursor() {
276
+ try {
277
+ await this.begin(async (tx) => {
278
+ const cp = await this.checkpoint.get(this.subscription, tx);
279
+ if (cp)
280
+ this._hwm = { lastStreamId: cp.lastStreamId };
281
+ });
282
+ }
283
+ catch {
284
+ // Cold-start read failure — cursor stays null, consume starts from `>`.
285
+ }
286
+ }
287
+ async runLoop() {
288
+ while (!this._stopped) {
289
+ if (this.lease.lost)
290
+ break;
291
+ try {
292
+ const processed = await this.pollOnce();
293
+ if (processed === 0) {
294
+ // Yield so a synchronous test-double doesn't tight-loop.
295
+ await new Promise((r) => setTimeout(r, 0));
296
+ }
297
+ }
298
+ catch (err) {
299
+ // BUG-2 fix: a poll that throws BECAUSE we're shutting down (stop() set
300
+ // `_stopped` and may have torn down the redis client mid-poll) is a CLEAN
301
+ // cancel, NOT a redis failure — exit quietly (no error metric, no
302
+ // error-sleep). A genuine poll-level error (XREADGROUP rejected — Redis
303
+ // down) while still running is the Pillar-5 infra-failure case: log via
304
+ // metric + back off, NEVER crash the loop.
305
+ if (this._stopped)
306
+ break;
307
+ this.hooks.recordDepth(-1, { error: "poll_failed" });
308
+ void err;
309
+ await new Promise((r) => setTimeout(r, 200));
310
+ }
311
+ }
312
+ }
313
+ /**
314
+ * Poll one batch + apply each entry. Returns count processed.
315
+ *
316
+ * Two reads per poll: FIRST our OWN pending-entries-list (PEL) at id `0` —
317
+ * entries delivered to this consumer but NOT yet XACK'd (a prior apply failed
318
+ * + NACK'd them); they MUST be retried, and a plain `>` read never re-returns
319
+ * a PEL entry. THEN `>` for brand-new entries. Without the PEL re-read a
320
+ * poison event would be applied exactly once and never retried.
321
+ */
322
+ async pollOnce() {
323
+ if (!this.lease.leaseAcquired) {
324
+ const got = await this.lease.acquire();
325
+ if (got) {
326
+ this.hooks.leaseAcquired();
327
+ // Re-ensure the consumer group on every re-acquire: a Redis failover
328
+ // across the prior holder's death may have wiped the group, so a cached
329
+ // `groupEnsured` would let xreadgroup hit NOGROUP forever. Re-running is a
330
+ // harmless BUSYGROUP no-op when the group still exists.
331
+ this.groupEnsured = false;
332
+ // BUG fix (in-loop re-acquire): if ensureGroup throws AFTER this poll
333
+ // re-acquired the lease, RELEASE it before rethrowing — otherwise the
334
+ // renewal loop holds + renews the lease INDEFINITELY while the next poll
335
+ // skips this block (leaseAcquired is true) and hammers a never-created
336
+ // group (NOGROUP loop), deadlocking every other instance.
337
+ try {
338
+ await this.ensureGroup();
339
+ }
340
+ catch (err) {
341
+ await this.lease.release();
342
+ throw err;
343
+ }
344
+ }
345
+ else {
346
+ return 0;
347
+ }
348
+ }
349
+ let processed = 0;
350
+ // [reclaim] CRASH-RECOVERY reaper: XAUTOCLAIM transfers PENDING entries idle
351
+ // ≥ reclaimMinIdleMs from a DEAD predecessor (a crashed worker with a
352
+ // different consumer name) to US, the single-active successor. Without this,
353
+ // a crashed worker's un-ACK'd in-flight entries are PERMANENTLY ORPHANED (a
354
+ // plain "0" PEL re-read is per-consumer) → silent event loss → broken
355
+ // at-least-once. The fence guard inside the apply tx still rejects a stale
356
+ // write if a still-alive predecessor races us.
357
+ const claimedRaw = await this.redis.xautoclaim(this.stream, this.group, this.consumer, this.reclaimMinIdleMs, "0-0", "COUNT", this.pollBatch);
358
+ const claimed = parseAutoClaimEntriesPartitioned(claimedRaw);
359
+ for (const p of claimed.entries) {
360
+ if (this._stopped || this.lease.lost)
361
+ break;
362
+ await this.applyOne(p.id, p.event);
363
+ processed += 1;
364
+ }
365
+ processed += await this.handleMalformedEntries(claimed.malformed);
366
+ // [retry] Re-read our own PEL (id `0`, non-blocking) for NACK'd entries.
367
+ const pendingRaw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "STREAMS", this.stream, "0");
368
+ const pending = parseStreamEntriesPartitioned(pendingRaw);
369
+ for (const p of pending.entries) {
370
+ if (this._stopped || this.lease.lost)
371
+ break;
372
+ await this.applyOne(p.id, p.event);
373
+ processed += 1;
374
+ }
375
+ processed += await this.handleMalformedEntries(pending.malformed);
376
+ // [new] Read brand-new entries (`>`, blocking up to blockMs).
377
+ const raw = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.pollBatch, "BLOCK", this.blockMs, "STREAMS", this.stream, ">");
378
+ // Sample backlog depth (cheap; best-effort).
379
+ try {
380
+ const depth = await this.redis.xlen(this.stream);
381
+ this.hooks.recordDepth(depth);
382
+ }
383
+ catch {
384
+ /* depth is advisory — never fail the poll on it */
385
+ }
386
+ const fresh = parseStreamEntriesPartitioned(raw);
387
+ for (const p of fresh.entries) {
388
+ if (this._stopped || this.lease.lost)
389
+ break;
390
+ await this.applyOne(p.id, p.event);
391
+ processed += 1;
392
+ }
393
+ processed += await this.handleMalformedEntries(fresh.malformed);
394
+ return processed;
395
+ }
396
+ /**
397
+ * Dead-letter + XACK every structurally-unparseable (malformed-JSON) entry. A
398
+ * malformed entry is a POISON message — it can NEVER be applied (there is no
399
+ * decodable event) — so leaving it in the PEL only re-cycles it through
400
+ * XAUTOCLAIM on every reclaim, forever. We route the raw bytes through the
401
+ * (PII-redacting) dead-letter sink with a parse-error message, then XACK it so
402
+ * it leaves the PEL. If the sink itself fails, we leave the entry in the PEL
403
+ * (recoverable; never silently dropped) — exactly like a failed poison DLQ.
404
+ * Returns the count handled (so the poll's processed count reflects them).
405
+ */
406
+ async handleMalformedEntries(malformed) {
407
+ let handled = 0;
408
+ for (const m of malformed) {
409
+ if (this._stopped || this.lease.lost)
410
+ break;
411
+ try {
412
+ await this.deadLetter.deadLetter({
413
+ sourceStream: this.stream,
414
+ topic: "",
415
+ eventId: "",
416
+ payload: { raw: m.rawPayload },
417
+ errorMessage: "malformed stream entry: event field is not valid JSON",
418
+ retryCount: 0,
419
+ });
420
+ // XACK so the poison entry leaves the PEL (no orphan / no re-cycle).
421
+ await this.redis.xack(this.stream, this.group, m.id);
422
+ this.retries.delete(m.id);
423
+ handled += 1;
424
+ }
425
+ catch {
426
+ // The dead-letter sink failed — leave the entry in the PEL (recoverable)
427
+ // rather than XACK + lose it. Never XACK on a failed dead-letter.
428
+ this.hooks.recordDepth(-1, { error: "malformed_dead_letter_failed" });
429
+ }
430
+ }
431
+ return handled;
432
+ }
433
+ /**
434
+ * Apply one entry: dedupe-skip → apply-tx (projection + dedupe.record +
435
+ * checkpoint.advance, fence-guarded) → XACK. On error, retry-then-dead-letter.
436
+ */
437
+ async applyOne(streamId, event) {
438
+ // We dropped the lease mid-flight; the entry stays un-ACK'd in our PEL. The
439
+ // single-active successor (a new instance with a DIFFERENT consumer name)
440
+ // reclaims it via the [reclaim] XAUTOCLAIM reaper once idle — NOT via a plain
441
+ // "0" PEL re-read (that is per-consumer).
442
+ if (this.lease.lost)
443
+ return;
444
+ this._inFlight += 1;
445
+ const token = this.lease.fencingToken;
446
+ try {
447
+ // [1] Pre-tx dedupe probe (cheap). A redelivered already-applied event is
448
+ // XACK'd + skipped (exactly-once on the consumer side).
449
+ const alreadySeen = await this.begin((tx) => this.dedupe.seen(event.event_id, event.topic, tx));
450
+ if (alreadySeen) {
451
+ // The event already committed (durable dedupe row) — only the XACK is
452
+ // outstanding. A failing XACK here is a TRANSIENT ack-failure, NOT a
453
+ // poison event: swallow it (the entry stays un-acked in the PEL, re-acked
454
+ // next poll once redis recovers). NEVER route a committed event to
455
+ // retry/DLQ over an ack blip (matches Go's `_, _ = Ack(...)`).
456
+ await this.ackSwallow(streamId);
457
+ this.retries.delete(streamId);
458
+ return;
459
+ }
460
+ // [2] Apply tx — projection upsert + dedupe.record + checkpoint.advance,
461
+ // all on ONE tx (atomic). The fence guard runs BEFORE the projection.
462
+ await this.begin(async (tx) => {
463
+ const ctx = {
464
+ tx,
465
+ event,
466
+ streamId,
467
+ validateFence: async () => {
468
+ if (token === null) {
469
+ throw new FenceLostError({
470
+ leaseKey: this.lease.key,
471
+ expectedToken: -1,
472
+ current: null,
473
+ });
474
+ }
475
+ await this.lease.validateFence(token);
476
+ },
477
+ };
478
+ // TAG the apply handler's throws as POISON so the catch can distinguish a
479
+ // bad-event handler rejection (→ retry/DLQ) from a TRANSIENT store-down
480
+ // throw of the surrounding tx machinery / dedupe-record / cursor-advance
481
+ // (→ leave in PEL, retry on recovery). A FenceLostError raised inside the
482
+ // handler's validateFence is re-thrown bare (NOT wrapped) so the outer
483
+ // FenceLostError branch still sees it.
484
+ try {
485
+ await this.apply(ctx);
486
+ }
487
+ catch (handlerErr) {
488
+ if (handlerErr instanceof FenceLostError ||
489
+ handlerErr instanceof FenceCheckUnavailableError) {
490
+ throw handlerErr;
491
+ }
492
+ throw new PoisonApplyError(handlerErr);
493
+ }
494
+ await this.dedupe.record(event.event_id, event.topic, tx);
495
+ await this.checkpoint.advance({ subscription: this.subscription, lastStreamId: streamId }, tx);
496
+ });
497
+ // [3] Commit succeeded → XACK + advance local HWM. The XACK is POST-COMMIT
498
+ // and NOT part of the apply tx: a failing XACK here is a transient
499
+ // ack-failure, NOT a poison event — swallow it (the committed event is
500
+ // re-acked next poll; the dedupe row makes a redelivery a no-op). Do NOT
501
+ // route a successfully-committed event to retry/DLQ over an ack blip.
502
+ await this.ackSwallow(streamId);
503
+ this.retries.delete(streamId);
504
+ this._hwm = { lastStreamId: streamId };
505
+ this._lastAppliedAtMs = Date.now();
506
+ if (event.occurred_at_ms !== undefined) {
507
+ this.hooks.recordLag(Math.max(0, (Date.now() - event.occurred_at_ms) / 1000));
508
+ }
509
+ else {
510
+ this.hooks.recordLag(0);
511
+ }
512
+ }
513
+ catch (err) {
514
+ // A FenceLostError means the lease was lost/taken-over mid-apply — the
515
+ // stale write was REJECTED before it landed. Do NOT XACK (the entry stays
516
+ // in our PEL; the single-active successor reclaims it via the XAUTOCLAIM
517
+ // reaper once idle) and do NOT dead-letter (benign handoff).
518
+ if (err instanceof FenceLostError) {
519
+ this.hooks.leaseLost();
520
+ return;
521
+ }
522
+ // A FenceCheckUnavailableError means the fence-check READ failed (redis
523
+ // down) — TRANSIENT. Fail closed (the write was withheld), but do NOT XACK
524
+ // and do NOT dead-letter a non-poison event over a redis blip: leave it in
525
+ // the PEL to be reprocessed once redis recovers (cross-runtime-identical).
526
+ if (err instanceof FenceCheckUnavailableError) {
527
+ this.hooks.recordDepth(-1, { error: "fence_check_unavailable" });
528
+ return;
529
+ }
530
+ // A POISON failure — the injected apply handler itself rejected on a
531
+ // well-formed event — is the ONLY thing that travels the retry→DLQ path.
532
+ if (err instanceof PoisonApplyError) {
533
+ await this.retryOrDeadLetter(streamId, event, unwrapPoison(err));
534
+ return;
535
+ }
536
+ // ANY OTHER throw inside the apply path is a TRANSIENT INFRA failure: the
537
+ // pre-tx dedupe-store probe, the tx open/commit, `dedupe.record`, or
538
+ // `checkpoint.advance` threw because postgres/the store is unreachable. A
539
+ // healthy event must NOT be dead-lettered over a store outage — treat it
540
+ // exactly like the redis-down fence path: do NOT bump the retry ledger, do
541
+ // NOT XACK, do NOT dead-letter; leave it in the PEL and retry on the next
542
+ // poll once the store recovers (symmetric with the OutboxDispatcher, which
543
+ // survives a `begin()` throw without dead-lettering).
544
+ this.hooks.recordDepth(-1, { error: "transient_infra" });
545
+ return;
546
+ }
547
+ finally {
548
+ this._inFlight -= 1;
549
+ }
550
+ }
551
+ /**
552
+ * XACK an already-committed entry, SWALLOWING a transient ack failure. The
553
+ * XACK is post-commit (or post-dedupe-skip) and NOT part of the apply tx — a
554
+ * failing XACK is a transient redis blip, never a poison event. On failure the
555
+ * entry simply stays un-acked in the PEL and is re-acked next poll once redis
556
+ * recovers (the durable dedupe row makes the redelivery a no-op). This is what
557
+ * Go does (`_, _ = Ack(...)`); a throw here must NEVER reach retry/DLQ.
558
+ */
559
+ async ackSwallow(streamId) {
560
+ try {
561
+ await this.redis.xack(this.stream, this.group, streamId);
562
+ }
563
+ catch {
564
+ this.hooks.recordDepth(-1, { error: "ack_failed" });
565
+ }
566
+ }
567
+ /** Increment the retry ledger; dead-letter (+XACK) once the budget is spent. */
568
+ async retryOrDeadLetter(streamId, event, err) {
569
+ const attempt = (this.retries.get(streamId) ?? 0) + 1;
570
+ this.retries.set(streamId, attempt);
571
+ if (attempt < this.retryMaxAttempts) {
572
+ // No XACK — the entry stays in the PEL and is redelivered next poll.
573
+ return;
574
+ }
575
+ try {
576
+ await this.deadLetter.deadLetter({
577
+ sourceStream: this.stream,
578
+ topic: event.topic,
579
+ eventId: event.event_id,
580
+ payload: event,
581
+ errorMessage: err instanceof Error ? err.message : String(err),
582
+ retryCount: attempt,
583
+ });
584
+ // ACK after the dead-letter is durable so the PEL doesn't grow unbounded.
585
+ await this.redis.xack(this.stream, this.group, streamId);
586
+ this.retries.delete(streamId);
587
+ }
588
+ catch {
589
+ // The dead-letter sink itself failed — leave the entry in the PEL (it will
590
+ // be redelivered) rather than losing it. Never XACK on a failed dead-letter.
591
+ }
592
+ }
593
+ /** Run exactly one poll batch (test seam / cron tick). */
594
+ async runOnce() {
595
+ return this.pollOnce();
596
+ }
597
+ async stop(opts) {
598
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
599
+ const start = Date.now();
600
+ this._stopped = true;
601
+ this._running = false;
602
+ try {
603
+ await Promise.race([
604
+ this.loop,
605
+ new Promise((r) => setTimeout(r, timeoutMs)),
606
+ ]);
607
+ }
608
+ catch {
609
+ /* swallow */
610
+ }
611
+ while (this._inFlight > 0 && Date.now() - start < timeoutMs) {
612
+ await new Promise((r) => setTimeout(r, 10));
613
+ }
614
+ const drainedCleanly = this._inFlight === 0;
615
+ await this.lease.release();
616
+ const durationMs = Date.now() - start;
617
+ this.hooks.recordDrainDuration(durationMs);
618
+ return { drainedCleanly, durationMs };
619
+ }
620
+ }
621
+ //# 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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"}