@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,394 @@
1
+ // D402 — SingleActiveLease: the canonical single-active worker lease primitive
2
+ // of the @nodii/telemetry/worker substrate.
3
+ //
4
+ // This UNIFIES the two divergent hand-rolled leases that exist today:
5
+ // - ts/outbox-dispatcher/src/lease.ts (SET-NX-EX, renew @ ttl/2,
6
+ // key `lock:outbox:<service>`)
7
+ // - ts/replica-consumer/src/worker/lease.ts (renew @ ttl/3,
8
+ // key `nodii:replica-consumer:lease:<svc>:<stream>`)
9
+ // NEITHER carries a fencing token — that is the durability gap D402 closes.
10
+ //
11
+ // D402 (LOCKED) pins the lease terms:
12
+ // - Acquire : Redis `SET key owner NX EX ttl`; canonical key
13
+ // `nodii:lease:<service>:<scope>`.
14
+ // - Renew : Lua CAS at ttl/2 (renew only if WE still own it).
15
+ // - Release : Lua CAS-DEL (release only if WE still own it).
16
+ // - Fencing : the lease carries `(ownerId, fencingToken)`; the token is
17
+ // MONOTONIC and validated AT THE SIDE-EFFECTING MOMENT (not only
18
+ // at acquire/dequeue) so a lease-lost worker's stale write cannot
19
+ // land.
20
+ // - SIGTERM-safe heartbeat.
21
+ //
22
+ // ── Authored design decisions (D402 leaves the encoding / Lua / validate-API to
23
+ // the implementer; everything below is the chosen, justified shape) ─────────
24
+ //
25
+ // • Token generation — a Redis `INCR` on a sibling counter key
26
+ // `nodii:lease:<service>:<scope>:fence`, captured at acquire time. This is the
27
+ // ONLY way to obtain a token that is monotonic *across acquisitions and across
28
+ // processes* — a per-process counter is not globally ordered, so a stale
29
+ // holder on host A could mint a token that collides with the live holder on
30
+ // host B. The counter key is persistent (never expires) so the value survives
31
+ // lease gaps: a later acquirer ALWAYS receives a strictly greater token than
32
+ // any prior holder. Monotonicity needs only strictly-increasing (gaps from
33
+ // failed acquires are harmless), which INCR guarantees.
34
+ //
35
+ // • Value encoding — the lease key stores `"<ownerId>:<fencingToken>"`, NOT the
36
+ // bare ownerId. This lets every guarded operation (renew / release /
37
+ // validateFence) CAS on the *full* `(owner, token)` identity in a single
38
+ // atomic GET-equality check, which is exactly what detects a stale holder
39
+ // whose token no longer matches the key's current value.
40
+ //
41
+ // • Acquire atomicity — INCR-then-SET-NX runs inside one Lua script so the
42
+ // token capture and the lock take are atomic and no successor can interleave.
43
+ //
44
+ // • validateFence(token) — the side-effecting guard. It reads the lease key's
45
+ // CURRENT value LIVE from Redis (not a cached local copy — validating "at the
46
+ // side-effecting moment" means against the current truth) and throws
47
+ // `FenceLostError` unless the live value is exactly `"<ourOwnerId>:<token>"`.
48
+ // A worker calls it immediately before any non-idempotent write; if the lease
49
+ // was lost/expired/taken-over since the work was claimed, the write is
50
+ // rejected before it can land.
51
+ import { uuidv7 } from "../uuid.js";
52
+ import { WORKER_METRIC_NAMES } from "./observability-hooks.js";
53
+ /** Atomic acquire: capture the next monotonic fence + take the lock under NX.
54
+ * KEYS[1] = lease key, KEYS[2] = fence counter key.
55
+ * ARGV[1] = ownerId, ARGV[2] = ttl seconds.
56
+ * Returns the captured fence token (a number) on success, or nil if the lock
57
+ * is already held by someone else (no token is consumed in that case). */
58
+ const ACQUIRE_LUA = `
59
+ if redis.call("EXISTS", KEYS[1]) == 1 then
60
+ return false
61
+ end
62
+ local fence = redis.call("INCR", KEYS[2])
63
+ local val = ARGV[1] .. ":" .. tostring(fence)
64
+ redis.call("SET", KEYS[1], val, "EX", tonumber(ARGV[2]))
65
+ return fence
66
+ `;
67
+ /** CAS-renew: EXPIRE only if the key's current value is exactly our
68
+ * `owner:token`. ARGV[1] = `owner:token`, ARGV[2] = ttl seconds. */
69
+ const RENEW_LUA = `
70
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
71
+ return redis.call("EXPIRE", KEYS[1], ARGV[2])
72
+ end
73
+ return 0
74
+ `;
75
+ /** CAS-release: DEL only if the key's current value is exactly our
76
+ * `owner:token`. ARGV[1] = `owner:token`. */
77
+ const RELEASE_LUA = `
78
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
79
+ return redis.call("DEL", KEYS[1])
80
+ end
81
+ return 0
82
+ `;
83
+ /** Canonical lease key shape, pinned by D402. */
84
+ export function leaseKey(service, scope) {
85
+ return `nodii:lease:${service}:${scope}`;
86
+ }
87
+ /** Sibling monotonic fence-counter key for a `(service, scope)`. */
88
+ export function fenceKey(service, scope) {
89
+ return `${leaseKey(service, scope)}:fence`;
90
+ }
91
+ /** Encode the lease value as `<ownerId>:<fencingToken>`. */
92
+ export function encodeLeaseValue(ownerId, token) {
93
+ return `${ownerId}:${token}`;
94
+ }
95
+ /** Parse a `<ownerId>:<fencingToken>` lease value. Returns null if malformed.
96
+ * Owner ids are uuids (which contain `-` but no `:`), so the token is the
97
+ * segment after the LAST `:`. */
98
+ export function parseLeaseValue(value) {
99
+ if (value === null)
100
+ return null;
101
+ const idx = value.lastIndexOf(":");
102
+ if (idx <= 0 || idx === value.length - 1)
103
+ return null;
104
+ const ownerId = value.slice(0, idx);
105
+ const token = Number(value.slice(idx + 1));
106
+ if (!Number.isInteger(token))
107
+ return null;
108
+ return { ownerId, token };
109
+ }
110
+ /**
111
+ * Thrown by {@link SingleActiveLease.validateFence} when the lease no longer
112
+ * belongs to the caller under the token the work was claimed with — the lease
113
+ * expired, was released, or was taken over by a later acquirer (whose fence is
114
+ * strictly greater). A worker that catches this MUST NOT perform the guarded
115
+ * side effect; the write is stale and would corrupt single-active ordering.
116
+ */
117
+ export class FenceLostError extends Error {
118
+ leaseKey;
119
+ /** The token the work was claimed under (now stale). */
120
+ expectedToken;
121
+ /** The lease's current `(owner, token)`, or null if the key is gone. */
122
+ current;
123
+ constructor(args) {
124
+ const cur = args.current
125
+ ? `${args.current.ownerId}:${args.current.token}`
126
+ : "<none>";
127
+ super(`fence lost for ${args.leaseKey}: work claimed under token ${args.expectedToken} ` +
128
+ `but lease now holds ${cur} — stale write rejected`);
129
+ this.name = "FenceLostError";
130
+ this.leaseKey = args.leaseKey;
131
+ this.expectedToken = args.expectedToken;
132
+ this.current = args.current;
133
+ }
134
+ }
135
+ /**
136
+ * Thrown by {@link SingleActiveLease.validateFence} when the fence-check READ
137
+ * itself fails — Redis is down / unreachable / timed out, so ownership cannot be
138
+ * DETERMINED. DISTINCT from {@link FenceLostError}: a FenceLostError means the
139
+ * lease verifiably belongs to someone else now (a benign handoff); a
140
+ * FenceCheckUnavailableError means we could not verify ownership at all.
141
+ *
142
+ * Cross-runtime contract (D402): BOTH FAIL CLOSED — the guarded side-effect MUST
143
+ * NOT run. But the event is treated as a TRANSIENT infrastructure failure: the
144
+ * worker does NOT ack it and does NOT dead-letter it; the event stays in the
145
+ * consumer PEL and is reprocessed on the next poll after Redis recovers
146
+ * (at-least-once; side-effect fires exactly once on recovery). Identical
147
+ * behavior in TS, Python, and Go.
148
+ */
149
+ export class FenceCheckUnavailableError extends Error {
150
+ leaseKey;
151
+ expectedToken;
152
+ /** The underlying Redis read error. */
153
+ cause;
154
+ constructor(args) {
155
+ const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause);
156
+ super(`fence check unavailable for ${args.leaseKey} (token ${args.expectedToken}): ` +
157
+ `lease redis read failed: ${causeMsg} — write withheld, event retried`);
158
+ this.name = "FenceCheckUnavailableError";
159
+ this.leaseKey = args.leaseKey;
160
+ this.expectedToken = args.expectedToken;
161
+ this.cause = args.cause;
162
+ }
163
+ }
164
+ /**
165
+ * A single-active lease with a fencing token. One instance per `(service,
166
+ * scope)` may hold the lease at a time; the holder renews at ttl/2 and exposes
167
+ * {@link validateFence} as the side-effecting guard.
168
+ *
169
+ * Lifecycle:
170
+ * const lease = new SingleActiveLease(opts);
171
+ * if (!(await lease.acquire())) return; // someone else holds it; idle
172
+ * // ... do work; before each non-idempotent write:
173
+ * await lease.validateFence(lease.fencingToken!); // throws FenceLostError
174
+ * await lease.release(); // CAS-DEL on graceful stop
175
+ */
176
+ export class SingleActiveLease {
177
+ service;
178
+ scope;
179
+ ownerId;
180
+ key;
181
+ fenceCounterKey;
182
+ redis;
183
+ ttlMs;
184
+ ttlSeconds;
185
+ onLeaseLost;
186
+ telemetry;
187
+ _acquired = false;
188
+ _lost = false;
189
+ _fencingToken = null;
190
+ renewTimer = null;
191
+ constructor(opts) {
192
+ this.redis = opts.redis;
193
+ this.service = opts.service;
194
+ this.scope = opts.scope;
195
+ this.ownerId = opts.ownerId ?? uuidv7();
196
+ this.ttlMs = opts.ttlMs;
197
+ this.ttlSeconds = Math.max(1, Math.floor(opts.ttlMs / 1000));
198
+ this.onLeaseLost = opts.onLeaseLost;
199
+ this.telemetry = opts.telemetry;
200
+ this.key = leaseKey(opts.service, opts.scope);
201
+ this.fenceCounterKey = fenceKey(opts.service, opts.scope);
202
+ }
203
+ /** Did we currently hold the lease as of the last acquire/renew? */
204
+ get leaseAcquired() {
205
+ return this._acquired && !this._lost;
206
+ }
207
+ /** Did the renewal loop detect we lost the lease? */
208
+ get lost() {
209
+ return this._lost;
210
+ }
211
+ /** The fence token captured at acquire, or null if we never acquired. */
212
+ get fencingToken() {
213
+ return this._fencingToken;
214
+ }
215
+ /**
216
+ * Attempt to acquire. Resolves `true` if we took the lease (and starts the
217
+ * ttl/2 renewal loop), `false` if another instance already holds it. The
218
+ * fence token is captured atomically inside the acquire Lua.
219
+ */
220
+ async acquire() {
221
+ const result = await this.redis.eval(ACQUIRE_LUA, 2, this.key, this.fenceCounterKey, this.ownerId, String(this.ttlSeconds));
222
+ // Lua `false` → not acquired; a number → our captured fence.
223
+ if (result === false || result === null) {
224
+ this._acquired = false;
225
+ return false;
226
+ }
227
+ const token = Number(result);
228
+ if (!Number.isInteger(token)) {
229
+ this._acquired = false;
230
+ return false;
231
+ }
232
+ this._fencingToken = token;
233
+ this._acquired = true;
234
+ this._lost = false;
235
+ this.startRenewLoop();
236
+ // CANONICAL D402 name + ISOLATED emit (see `emit`): a vocab/metric throw must
237
+ // never influence lease state.
238
+ this.emit("counter", WORKER_METRIC_NAMES.leaseAcquired, {
239
+ service: this.service,
240
+ scope: this.scope,
241
+ });
242
+ return true;
243
+ }
244
+ /**
245
+ * Record a lease metric through the injected telemetry seam, ISOLATED so a
246
+ * throw (e.g. the lib's STRICT `recordMetric` raising `MetricVocabularyError`)
247
+ * can NEVER propagate into the lease's renew/CAS control flow and false-mark
248
+ * the lease lost. Mirrors Python's `_emit` try/except. Telemetry is best-effort
249
+ * observability; it must never break the lease.
250
+ */
251
+ emit(kind, name, attributes) {
252
+ const tel = this.telemetry;
253
+ if (tel === undefined)
254
+ return;
255
+ try {
256
+ tel.recordMetric({ kind, name, value: 1, attributes });
257
+ }
258
+ catch {
259
+ // Swallow — telemetry must never break the lease (R1: observability is
260
+ // best-effort; a metric vocab/backend error is not a lease failure).
261
+ }
262
+ }
263
+ /** Start the ttl/2 renewal loop (CAS on `owner:token`). */
264
+ startRenewLoop() {
265
+ const renewMs = Math.max(1_000, Math.floor(this.ttlMs / 2));
266
+ const encoded = encodeLeaseValue(this.ownerId, this._fencingToken ?? 0);
267
+ this.renewTimer = setInterval(() => {
268
+ void this.renewOnce(encoded);
269
+ }, renewMs);
270
+ this.renewTimer.unref?.();
271
+ }
272
+ /** One CAS renew. Marks the lease lost (and stops the loop) on failure. */
273
+ async renewOnce(encoded) {
274
+ try {
275
+ const r = (await this.redis.eval(RENEW_LUA, 1, this.key, encoded, String(this.ttlSeconds)));
276
+ if (Number(r) === 1) {
277
+ // CANONICAL renewed_total, ISOLATED — a metric throw here must NOT reach
278
+ // the outer catch (which would FALSE-mark the lease lost on a HEALTHY
279
+ // renew, tearing down the worker). That is exactly BUG[3].
280
+ this.emit("counter", WORKER_METRIC_NAMES.leaseRenewed, {
281
+ service: this.service,
282
+ scope: this.scope,
283
+ result: "held",
284
+ });
285
+ }
286
+ else {
287
+ this.markLost("lost");
288
+ }
289
+ }
290
+ catch {
291
+ // Only a GENUINE renew/CAS (redis) failure reaches here — the metric emit
292
+ // is isolated above, so this catch is never tripped by a telemetry throw.
293
+ this.markLost("error");
294
+ }
295
+ }
296
+ markLost(result) {
297
+ if (this._lost)
298
+ return;
299
+ this._lost = true;
300
+ this.stopRenewLoop();
301
+ // CANONICAL lost_total, ISOLATED.
302
+ this.emit("counter", WORKER_METRIC_NAMES.leaseLost, {
303
+ service: this.service,
304
+ scope: this.scope,
305
+ result,
306
+ });
307
+ this.onLeaseLost?.();
308
+ }
309
+ stopRenewLoop() {
310
+ if (this.renewTimer !== null) {
311
+ clearInterval(this.renewTimer);
312
+ this.renewTimer = null;
313
+ }
314
+ }
315
+ /**
316
+ * Renew once, on demand (e.g. a manual heartbeat tick). Returns `true` if we
317
+ * still hold the lease. A non-owner / stale call is a CAS no-op → `false`.
318
+ */
319
+ async renew() {
320
+ if (this._fencingToken === null)
321
+ return false;
322
+ const encoded = encodeLeaseValue(this.ownerId, this._fencingToken);
323
+ try {
324
+ const r = (await this.redis.eval(RENEW_LUA, 1, this.key, encoded, String(this.ttlSeconds)));
325
+ const held = Number(r) === 1;
326
+ if (!held)
327
+ this.markLost("lost");
328
+ return held;
329
+ }
330
+ catch {
331
+ this.markLost("error");
332
+ return false;
333
+ }
334
+ }
335
+ /**
336
+ * THE side-effecting guard. Reads the lease key's CURRENT value live from
337
+ * Redis and throws {@link FenceLostError} unless it is exactly
338
+ * `"<ourOwnerId>:<token>"`. Call immediately before any non-idempotent write
339
+ * to prove the lease is still ours under the token the work was claimed with.
340
+ *
341
+ * @param token the fence token the in-flight work was claimed under.
342
+ */
343
+ async validateFence(token) {
344
+ let raw;
345
+ try {
346
+ raw = await this.redis.get(this.key);
347
+ }
348
+ catch (cause) {
349
+ // A TRANSIENT Redis read failure (down / unreachable / timeout) means we
350
+ // cannot DETERMINE ownership. FAIL CLOSED (withhold the write) but classify
351
+ // it as transient-unavailable — NOT a verified fence-loss. The worker keeps
352
+ // the event in the PEL + retries on recovery (it does NOT dead-letter a
353
+ // non-poison event over a redis blip). Cross-runtime-identical to Go/Python.
354
+ throw new FenceCheckUnavailableError({
355
+ leaseKey: this.key,
356
+ expectedToken: token,
357
+ cause,
358
+ });
359
+ }
360
+ const current = parseLeaseValue(raw);
361
+ const ours = current !== null &&
362
+ current.ownerId === this.ownerId &&
363
+ current.token === token;
364
+ if (!ours) {
365
+ // The key is absent or holds a different (owner, token): a VERIFIED
366
+ // fence-loss / lease handoff.
367
+ throw new FenceLostError({
368
+ leaseKey: this.key,
369
+ expectedToken: token,
370
+ current,
371
+ });
372
+ }
373
+ }
374
+ /**
375
+ * Stop the renewal loop and CAS-release the lease (DEL only if we still hold
376
+ * `owner:token` — a stale owner's release never deletes a successor's lease).
377
+ * Best-effort: a release failure is swallowed since the key TTL-expires.
378
+ * SIGTERM-safe — call from a signal handler to hand off fast.
379
+ */
380
+ async release() {
381
+ this.stopRenewLoop();
382
+ this._acquired = false;
383
+ if (this._fencingToken === null)
384
+ return;
385
+ const encoded = encodeLeaseValue(this.ownerId, this._fencingToken);
386
+ try {
387
+ await this.redis.eval(RELEASE_LUA, 1, this.key, encoded);
388
+ }
389
+ catch {
390
+ /* swallow — key TTL-expires anyway */
391
+ }
392
+ }
393
+ }
394
+ //# sourceMappingURL=single-active-lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single-active-lease.js","sourceRoot":"","sources":["../../src/worker/single-active-lease.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,192 @@
1
+ import type { CheckpointStore } from "./checkpoint-store.js";
2
+ import type { DeadLetterSink } from "./dead-letter.js";
3
+ import type { DedupeStore } from "./dedupe-store.js";
4
+ import { ObservabilityHooks } from "./observability-hooks.js";
5
+ import { type SingleActiveLeaseOpts } from "./single-active-lease.js";
6
+ import type { SqlExecutor } from "./tx.js";
7
+ import type { WorkerHandle, WorkerHwm, WorkerStopOptions, WorkerStopResult } from "./worker-handle.js";
8
+ /**
9
+ * Minimal Redis-Streams client surface a StreamsWorker needs. Structurally
10
+ * compatible with `ioredis` (and replica-consumer's `RedisLike`). The consumer
11
+ * injects its own client; this lib has no hard Redis dependency in its source.
12
+ */
13
+ export interface StreamsRedisLike {
14
+ xgroup(...args: (string | number)[]): Promise<unknown>;
15
+ xreadgroup(...args: (string | number)[]): Promise<unknown>;
16
+ /**
17
+ * XAUTOCLAIM — transfers ownership of PENDING entries idle ≥ minIdleMs from any
18
+ * (possibly DEAD) consumer to the asking consumer, returning `[cursor, entries,
19
+ * deleted]`. This is the CRASH-RECOVERY reaper: a crashed worker's un-ACK'd
20
+ * entries (in its per-consumer PEL) are reclaimed by the single-active
21
+ * successor (a new instance with a different consumer name) — otherwise they
22
+ * are permanently orphaned and the event is lost (the "no-reaper" durability
23
+ * defect D402 eliminates). Structurally compatible with ioredis `xautoclaim`.
24
+ */
25
+ xautoclaim(...args: (string | number)[]): Promise<unknown>;
26
+ xack(stream: string, group: string, ...ids: string[]): Promise<number>;
27
+ xlen(stream: string): Promise<number>;
28
+ }
29
+ /**
30
+ * A consumed stream event handed to the apply handler. The worker parses the
31
+ * `event` field of each XREADGROUP entry as JSON; `event_id` + `topic` drive
32
+ * dedupe. The worker assigns `streamId` (the `<ms>-<seq>` id) for the cursor.
33
+ */
34
+ export interface StreamEvent {
35
+ /** The dedupe id — `(event_id, topic)` is the composite dedupe key (D198). */
36
+ event_id: string;
37
+ /** The outbox topic. */
38
+ topic: string;
39
+ /** Optional source emission ms for lag measurement. */
40
+ occurred_at_ms?: number;
41
+ /** The rest of the event payload (the projection input). */
42
+ [key: string]: unknown;
43
+ }
44
+ /**
45
+ * The apply context handed to the injected handler INSIDE the per-event apply
46
+ * tx. Everything here runs on ONE transaction so the projection upsert + dedupe
47
+ * record + cursor advance commit together-or-none.
48
+ */
49
+ export interface StreamApplyContext {
50
+ /** The apply transaction handle — run the projection upsert on THIS. */
51
+ tx: SqlExecutor;
52
+ /** The event being applied. */
53
+ event: StreamEvent;
54
+ /** The Redis-Streams id of this entry (the cursor position). */
55
+ streamId: string;
56
+ /**
57
+ * THE side-effecting guard — `await ctx.validateFence()` BEFORE any
58
+ * non-idempotent projection write. Re-reads the live lease and throws
59
+ * {@link FenceLostError} if the lease was lost/taken-over since dequeue.
60
+ */
61
+ validateFence(): Promise<void>;
62
+ }
63
+ /**
64
+ * The injected apply handler — the caller's projection upsert. It runs INSIDE
65
+ * the worker's apply tx; the worker handles dedupe.record + checkpoint.advance
66
+ * around it. The handler MUST call `ctx.validateFence()` before its write.
67
+ */
68
+ export type StreamApplyHandler = (ctx: StreamApplyContext) => Promise<void>;
69
+ export interface StreamsWorkerOpts {
70
+ redis: StreamsRedisLike;
71
+ /** The source stream key (and the consumer-group base). */
72
+ stream: string;
73
+ /** The injected apply handler (the projection upsert). */
74
+ apply: StreamApplyHandler;
75
+ /** Opens a fresh apply transaction; the worker runs dedupe/checkpoint/apply on it. */
76
+ begin: <T>(fn: (tx: SqlExecutor) => Promise<T>) => Promise<T>;
77
+ dedupe: DedupeStore;
78
+ checkpoint: CheckpointStore;
79
+ /** Lease config (service + scope=stream + ttlMs). */
80
+ lease: Omit<SingleActiveLeaseOpts, "onLeaseLost">;
81
+ /** Checkpoint subscription key. Default `streams:<stream>`. */
82
+ subscription?: string;
83
+ /** Injected dead-letter sink (NO default). */
84
+ deadLetter: DeadLetterSink;
85
+ /** Max in-line retries before dead-lettering a poison event. Default 3. */
86
+ retryMaxAttempts?: number;
87
+ /** XREADGROUP COUNT per poll. Default 32. */
88
+ pollBatch?: number;
89
+ /** XREADGROUP BLOCK ms. Default 2_000 (tests pass small). */
90
+ blockMs?: number;
91
+ /**
92
+ * XAUTOCLAIM crash-recovery threshold (ms): a PENDING entry idle at least this
93
+ * long is assumed to belong to a DEAD consumer and is reclaimed by the
94
+ * single-active successor. Default = max(lease ttlMs, 30_000) so a still-alive
95
+ * predecessor's normal in-flight latency never triggers a steal (the fence
96
+ * guard is the ultimate safety net for the rare race).
97
+ */
98
+ reclaimMinIdleMs?: number;
99
+ hooks?: ObservabilityHooks;
100
+ workerLabel?: string;
101
+ /** Launch the background consume loop on `start()`. Default true; tests pass
102
+ * false to drive `runOnce()` deterministically. */
103
+ autoStart?: boolean;
104
+ }
105
+ /** Consumer-group name — `${service}:${stream}` (canonical per replica-consumer). */
106
+ export declare function streamsConsumerGroup(service: string, stream: string): string;
107
+ /** Parse an ioredis XREADGROUP response into flat (id, event) pairs. */
108
+ export declare function parseStreamEntries(raw: unknown): Array<{
109
+ id: string;
110
+ event: StreamEvent;
111
+ }>;
112
+ /**
113
+ * Parse an ioredis XAUTOCLAIM reply `[cursor, entries, deleted]` into flat
114
+ * (id, event) pairs. `entries` has the same `[[id, [f, v, ...]], ...]` shape as
115
+ * one stream's entries in an XREADGROUP reply, so we wrap it and reuse
116
+ * {@link parseStreamEntries}.
117
+ */
118
+ export declare function parseAutoClaimEntries(raw: unknown): Array<{
119
+ id: string;
120
+ event: StreamEvent;
121
+ }>;
122
+ /**
123
+ * StreamsWorker — the generic Pattern-2 single-active stream consumer. Composes
124
+ * lease + dedupe + checkpoint + hooks + drain to consume EXACTLY-ONCE.
125
+ */
126
+ export declare class StreamsWorker implements WorkerHandle {
127
+ private readonly redis;
128
+ private readonly stream;
129
+ private readonly apply;
130
+ private readonly begin;
131
+ private readonly dedupe;
132
+ private readonly checkpoint;
133
+ private readonly lease;
134
+ private readonly subscription;
135
+ private readonly deadLetter;
136
+ private readonly retryMaxAttempts;
137
+ private readonly pollBatch;
138
+ private readonly blockMs;
139
+ private readonly reclaimMinIdleMs;
140
+ private readonly hooks;
141
+ private readonly autoStart;
142
+ private readonly group;
143
+ private readonly consumer;
144
+ private readonly retries;
145
+ private _running;
146
+ private _stopped;
147
+ private _inFlight;
148
+ private _hwm;
149
+ private _lastAppliedAtMs;
150
+ private groupEnsured;
151
+ private loop;
152
+ constructor(opts: StreamsWorkerOpts);
153
+ get running(): boolean;
154
+ get leaseAcquired(): boolean;
155
+ get lag(): number | null;
156
+ get currentHwm(): WorkerHwm;
157
+ /** Idempotently create the consumer group (swallow BUSYGROUP). */
158
+ private ensureGroup;
159
+ start(): Promise<void>;
160
+ private hydrateCursor;
161
+ private runLoop;
162
+ /**
163
+ * Poll one batch + apply each entry. Returns count processed.
164
+ *
165
+ * Two reads per poll: FIRST our OWN pending-entries-list (PEL) at id `0` —
166
+ * entries delivered to this consumer but NOT yet XACK'd (a prior apply failed
167
+ * + NACK'd them); they MUST be retried, and a plain `>` read never re-returns
168
+ * a PEL entry. THEN `>` for brand-new entries. Without the PEL re-read a
169
+ * poison event would be applied exactly once and never retried.
170
+ */
171
+ private pollOnce;
172
+ /**
173
+ * Apply one entry: dedupe-skip → apply-tx (projection + dedupe.record +
174
+ * checkpoint.advance, fence-guarded) → XACK. On error, retry-then-dead-letter.
175
+ */
176
+ private applyOne;
177
+ /**
178
+ * XACK an already-committed entry, SWALLOWING a transient ack failure. The
179
+ * XACK is post-commit (or post-dedupe-skip) and NOT part of the apply tx — a
180
+ * failing XACK is a transient redis blip, never a poison event. On failure the
181
+ * entry simply stays un-acked in the PEL and is re-acked next poll once redis
182
+ * recovers (the durable dedupe row makes the redelivery a no-op). This is what
183
+ * Go does (`_, _ = Ack(...)`); a throw here must NEVER reach retry/DLQ.
184
+ */
185
+ private ackSwallow;
186
+ /** Increment the retry ledger; dead-letter (+XACK) once the budget is spent. */
187
+ private retryOrDeadLetter;
188
+ /** Run exactly one poll batch (test seam / cron tick). */
189
+ runOnce(): Promise<number>;
190
+ stop(opts?: WorkerStopOptions): Promise<WorkerStopResult>;
191
+ }
192
+ //# sourceMappingURL=streams-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams-worker.d.ts","sourceRoot":"","sources":["../../src/worker/streams-worker.ts"],"names":[],"mappings":"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"}