@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,6 +8,11 @@
8
8
  // that SET NodiiContext at the entry seam.
9
9
  // - @nodii/telemetry/outbox — Outbox emit + propagation-column adapter.
10
10
  // - @nodii/telemetry/drizzle — Drizzle audit_event schema fragments.
11
+ // - @nodii/telemetry/lambda — AWS Lambda lifecycle wrapper (C4 lock):
12
+ // cold-start init at module-load +
13
+ // force-flush-on-return so spans/metrics/
14
+ // logs survive the post-handler freeze.
15
+ // First consumer: s2s-auth-service.
11
16
  //
12
17
  // Not yet shipped (D44 § 6.5 lists them; no implementation exists yet —
13
18
  // they are tracked as follow-on requests, NOT advertised as vapor):
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,0EAA0E;AAC1E,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,4EAA4E;AAC5E,2EAA2E;AAC3E,4EAA4E;AAC5E,wEAAwE;AACxE,EAAE;AACF,wEAAwE;AACxE,oEAAoE;AACpE,uEAAuE;AACvE,8DAA8D;AAC9D,mDAAmD;AACnD,EAAE;AACF,oEAAoE;AACpE,gBAAgB;AAChB,EAAE;AACF,oEAAoE;AACpE,gEAAgE;AAChE,0DAA0D;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOnD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAS5E,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,8BAA8B,EAC9B,KAAK,EACL,iBAAiB,EACjB,cAAc,GACf,MAAM,YAAY,CAAC;AAYpB,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AA0BrD,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,0EAA0E;AAC1E,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,4EAA4E;AAC5E,2EAA2E;AAC3E,4EAA4E;AAC5E,wEAAwE;AACxE,0EAA0E;AAC1E,mEAAmE;AACnE,0EAA0E;AAC1E,wEAAwE;AACxE,oEAAoE;AACpE,EAAE;AACF,wEAAwE;AACxE,oEAAoE;AACpE,uEAAuE;AACvE,8DAA8D;AAC9D,mDAAmD;AACnD,EAAE;AACF,oEAAoE;AACpE,gBAAgB;AAChB,EAAE;AACF,oEAAoE;AACpE,gEAAgE;AAChE,0DAA0D;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOnD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAS5E,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,8BAA8B,EAC9B,KAAK,EACL,iBAAiB,EACjB,cAAc,GACf,MAAM,YAAY,CAAC;AAYpB,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AA0BrD,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,YAAY,CAAC"}
@@ -0,0 +1,77 @@
1
+ import type { InitTelemetryConfig, MetricFamily } from "../types.js";
2
+ /**
3
+ * A minimal structural AWS Lambda handler. We intentionally do NOT depend
4
+ * on `aws-lambda` types — the wrapper is event-shape agnostic so it works
5
+ * for an APIGW-proxied Hono app (`handle(app)` from `hono/aws-lambda`), a
6
+ * raw SQS/SNS/EventBridge handler, or any other `async (event, context)`
7
+ * function. `Event` / `Result` are inferred from the wrapped handler.
8
+ */
9
+ export type LambdaHandler<Event = unknown, Result = unknown> = (event: Event, context?: unknown) => Promise<Result> | Result;
10
+ /** Config for the cold-start counter tick (C4: `s2s.cold_start_total`). */
11
+ export interface ColdStartMetricConfig {
12
+ /**
13
+ * The vocabulary-registered counter name to increment once per cold
14
+ * boot (e.g. `"s2s.cold_start_total"`). MUST be registered by the
15
+ * consumer via `initTelemetry({ vocabularyRegistry })` — the name is
16
+ * service-specific, so the wrapper cannot hardcode it.
17
+ *
18
+ * If the name is NOT registered (vocabulary miss), the increment is a
19
+ * caught no-op + a one-line warn — a metric mis-config must NOT crash
20
+ * module-load (R1 / pillar 5).
21
+ */
22
+ name: string;
23
+ /** The metric family for the counter. Defaults to `"request_handler"`. */
24
+ family?: MetricFamily;
25
+ /** Static labels stamped on the cold-start tick. */
26
+ labels?: Record<string, string | number>;
27
+ }
28
+ /**
29
+ * `initLambdaTelemetry({...})` argument shape. Extends the base
30
+ * `initTelemetry` config (serviceName + env + otlp + vocabulary, …) with
31
+ * the Lambda-specific cold-start knob.
32
+ */
33
+ export interface InitLambdaTelemetryConfig extends InitTelemetryConfig {
34
+ /**
35
+ * Counter to increment once per cold boot. Omit to skip the cold-start
36
+ * tick entirely (some services track cold starts via a log line only).
37
+ */
38
+ coldStartMetric?: ColdStartMetricConfig;
39
+ /**
40
+ * Name of the per-invocation request span opened by `wrap`. Defaults to
41
+ * `"<serviceName>.lambda.invoke"`.
42
+ */
43
+ requestSpanName?: string;
44
+ }
45
+ /** The handle returned by `initLambdaTelemetry`. */
46
+ export interface LambdaTelemetry {
47
+ /**
48
+ * Wrap a Lambda handler so every invocation:
49
+ * 1. runs inside a request span (`requestSpanName`);
50
+ * 2. force-flushes the trace + log + metric providers BEFORE the
51
+ * handler's result returns (success AND error paths), so nothing
52
+ * is lost when Lambda freezes;
53
+ * 3. is RESILIENT — a flush failure is caught + logged and never
54
+ * propagates; the handler's own result / error is what returns.
55
+ */
56
+ wrap<Event, Result>(handler: LambdaHandler<Event, Result>): (event: Event, context?: unknown) => Promise<Result>;
57
+ /**
58
+ * Force-flush the active OTLP pipelines (trace + log + metric). No-op
59
+ * when no OTLP adapter is installed (e.g. `disableOtlp` / no endpoint).
60
+ * Exposed for callers that flush at custom lifecycle points.
61
+ */
62
+ forceFlush(): Promise<void>;
63
+ }
64
+ /**
65
+ * Boot telemetry for a Lambda at MODULE LOAD (per C4). Call this once at
66
+ * the top of the handler module — not per invocation. Returns a
67
+ * `LambdaTelemetry` handle whose `wrap` adds the per-invocation request
68
+ * span + force-flush-on-return.
69
+ *
70
+ * Idempotency: `initTelemetry` is itself idempotent (a second call
71
+ * warn-no-ops), so a doubly-imported / re-required handler module does
72
+ * NOT double-configure the SDK. The cold-start tick is likewise fired
73
+ * only on the call that actually performs init (the first one) — a second
74
+ * `initLambdaTelemetry` will not re-tick, matching "once per cold boot".
75
+ */
76
+ export declare function initLambdaTelemetry(config: InitLambdaTelemetryConfig): LambdaTelemetry;
77
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lambda/index.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,OAAO,IAAI,CAC7D,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,OAAO,KACd,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AAE9B,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC1C;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE;;;OAGG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,GACpC,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD;;;;OAIG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,yBAAyB,GAChC,eAAe,CAiDjB"}
@@ -0,0 +1,130 @@
1
+ // `@nodii/telemetry/lambda` — the Lambda lifecycle wrapper.
2
+ //
3
+ // Spec: C4 lock — `services/s2s-auth-service/features/telemetry-instrumentation.md`
4
+ // § "The `@nodii/telemetry/lambda` sub-package" + `nodii-libs/telemetry.md`
5
+ // § 10 (subpath exports). First consumer: s2s-auth-service (Lambda-shaped).
6
+ //
7
+ // AWS Lambda freezes the execution environment the instant a handler
8
+ // returns. The OTel batch processors buffer spans / logs / metrics and
9
+ // flush them on a timer — so on a Lambda that timer rarely fires before
10
+ // the freeze, and signals are LOST. Two lifecycle hooks fix this, both
11
+ // locked by C4:
12
+ //
13
+ // 1. Cold-start init at module-load — `initLambdaTelemetry({...})` is
14
+ // called once at the top of the handler module (NOT per-invocation).
15
+ // It boots the SDK (idempotent — a second call warn-no-ops, see
16
+ // `initTelemetry`) and ticks a cold-start counter exactly once per
17
+ // cold boot.
18
+ //
19
+ // 2. `forceFlush()` before the handler returns — `handle.wrap(fn)`
20
+ // runs the handler inside a request span and `await`s a force-flush
21
+ // of the trace + log + metric providers BEFORE returning (and on the
22
+ // error path), so nothing is lost when the environment freezes.
23
+ //
24
+ // RESILIENCE (telemetry-doctrine pillar 5 / R1 — dependency-down path):
25
+ // a force-flush FAILURE (collector down / OTLP timeout) MUST NOT break
26
+ // the Lambda. A flaky telemetry backend cannot be allowed to take down
27
+ // the function. `wrap` therefore catches + logs any flush rejection and
28
+ // STILL returns the handler's result (or re-throws the handler's own
29
+ // error). The only thing that propagates out of the wrapper is the
30
+ // handler's own outcome — never a telemetry-plumbing failure.
31
+ //
32
+ // Lives behind a subpath so non-Lambda services never pull this lifecycle
33
+ // glue into their bundle.
34
+ import { getTelemetryOrNull, initTelemetry } from "../init.js";
35
+ import { metrics } from "../metrics.js";
36
+ import { forceFlush as flushOtlpPipelines } from "../otlp-adapter.js";
37
+ import { tracer } from "../tracer.js";
38
+ /**
39
+ * Increment the cold-start counter exactly once, defensively. A
40
+ * vocabulary miss (name not registered) or any other metric error is
41
+ * caught + warn-logged — a cold-start metric mis-config must NOT crash
42
+ * the Lambda at module load (R1 / pillar 5).
43
+ */
44
+ function tickColdStart(cfg) {
45
+ try {
46
+ const counter = metrics.counter(cfg.name, {
47
+ family: cfg.family ?? "request_handler",
48
+ description: "Lambda cold-start counter (incremented once per cold boot)",
49
+ labelKeys: Object.keys(cfg.labels ?? {}),
50
+ });
51
+ counter.inc(cfg.labels ?? {});
52
+ }
53
+ catch (err) {
54
+ const cause = err instanceof Error ? err.message : String(err);
55
+ // eslint-disable-next-line no-console
56
+ console.warn(`[@nodii/telemetry/lambda] cold-start counter "${cfg.name}" could not be incremented (is it in the vocabularyRegistry?). Continuing — a metric mis-config must not crash module-load. Cause: ${cause}`);
57
+ }
58
+ }
59
+ /**
60
+ * Boot telemetry for a Lambda at MODULE LOAD (per C4). Call this once at
61
+ * the top of the handler module — not per invocation. Returns a
62
+ * `LambdaTelemetry` handle whose `wrap` adds the per-invocation request
63
+ * span + force-flush-on-return.
64
+ *
65
+ * Idempotency: `initTelemetry` is itself idempotent (a second call
66
+ * warn-no-ops), so a doubly-imported / re-required handler module does
67
+ * NOT double-configure the SDK. The cold-start tick is likewise fired
68
+ * only on the call that actually performs init (the first one) — a second
69
+ * `initLambdaTelemetry` will not re-tick, matching "once per cold boot".
70
+ */
71
+ export function initLambdaTelemetry(config) {
72
+ // Was the SDK already initialized before this call? If so, this is a
73
+ // redundant init (re-imported module / double-call) and we must NOT
74
+ // re-tick the cold-start counter — "once per cold boot".
75
+ const alreadyInitialized = getTelemetryOrNull() !== null;
76
+ // `initTelemetry` consumes only the base config keys; the extra
77
+ // Lambda-specific keys are ignored by it (excess-property tolerant at
78
+ // runtime). It is idempotent — a second call warn-no-ops.
79
+ initTelemetry(config);
80
+ if (!alreadyInitialized && config.coldStartMetric) {
81
+ tickColdStart(config.coldStartMetric);
82
+ }
83
+ const spanName = config.requestSpanName ?? `${config.serviceName}.lambda.invoke`;
84
+ // The handle is built first so `wrap` can route its flush through
85
+ // `handle.forceFlush` — that is the single flush seam (and the one a
86
+ // test spies on to fault-inject a flush failure).
87
+ const handle = {
88
+ async forceFlush() {
89
+ await flushOtlpPipelines();
90
+ },
91
+ wrap(handler) {
92
+ return async (event, context) => {
93
+ try {
94
+ // Run the handler inside a request span. `tracer.span` opens +
95
+ // closes the span and applies D27 status mapping.
96
+ const result = await tracer.span(spanName, () => Promise.resolve(handler(event, context)));
97
+ // Flush BEFORE returning — but a flush failure must NOT mask a
98
+ // successful handler result.
99
+ await safeFlush(handle);
100
+ return result;
101
+ }
102
+ catch (handlerErr) {
103
+ // The handler itself threw. Still flush (so the error span +
104
+ // partial signals aren't lost on freeze) — non-fatally — then
105
+ // re-throw the handler's OWN error (NOT a flush error).
106
+ await safeFlush(handle);
107
+ throw handlerErr;
108
+ }
109
+ };
110
+ },
111
+ };
112
+ return handle;
113
+ }
114
+ /**
115
+ * Force-flush via `handle.forceFlush` but NEVER throw. A flush failure
116
+ * (collector down / OTLP timeout) is caught + warn-logged; the Lambda
117
+ * result must not be lost to a flaky telemetry backend (pillar 5 / R1
118
+ * dependency-down path).
119
+ */
120
+ async function safeFlush(handle) {
121
+ try {
122
+ await handle.forceFlush();
123
+ }
124
+ catch (err) {
125
+ const cause = err instanceof Error ? err.message : String(err);
126
+ // eslint-disable-next-line no-console
127
+ console.warn(`[@nodii/telemetry/lambda] forceFlush() failed before return (telemetry backend down?). Swallowed — the handler result is returned regardless. Cause: ${cause}`);
128
+ }
129
+ }
130
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lambda/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import type { SqlExecutor } from "./tx.js";
2
+ /** Default table name for the generalized checkpoint cursor. */
3
+ export declare const DEFAULT_CHECKPOINT_TABLE = "worker_checkpoint";
4
+ /** The durable checkpoint a worker resumes from. */
5
+ export interface Checkpoint {
6
+ subscription: string;
7
+ /** Redis-Streams id of the last event committed, or null if none yet. */
8
+ lastStreamId: string | null;
9
+ /** Optional LWW entity_version HWM (per D402 "+ optional"); null when unused. */
10
+ highWaterEntityVersion: bigint | null;
11
+ /** Count of events committed against this subscription. */
12
+ consumedCount: bigint;
13
+ }
14
+ /** The cursor position to advance to inside the apply tx. */
15
+ export interface CheckpointAdvance {
16
+ subscription: string;
17
+ /** The Redis-Streams id of the event being committed in this apply tx. */
18
+ lastStreamId: string;
19
+ /**
20
+ * Optional LWW entity_version high-water-mark. When provided the cursor only
21
+ * advances `high_water_entity_version` MONOTONICALLY (GREATEST of old/new) so
22
+ * an out-of-order replay cannot regress it. Omit when the subscription
23
+ * doesn't track an entity-version HWM.
24
+ */
25
+ highWaterEntityVersion?: bigint | null;
26
+ /** How much to bump `consumed_count` by (default 1). */
27
+ incrementBy?: number;
28
+ }
29
+ /** Idempotent DDL to create the generalized checkpoint table. */
30
+ export declare function checkpointTableDDL(table?: string): string;
31
+ /**
32
+ * Durable HWM cursor store. ONE instance per subscription family (or share one
33
+ * across subscriptions — the `subscription` key disambiguates). The
34
+ * `advance(...)` write joins the caller's apply transaction via the injected
35
+ * `tx`; the store has NO pool of its own and cannot commit independently.
36
+ */
37
+ export declare class CheckpointStore {
38
+ readonly table: string;
39
+ private readonly upsertSql;
40
+ private readonly selectSql;
41
+ constructor(opts?: {
42
+ table?: string;
43
+ });
44
+ /**
45
+ * Read the durable checkpoint for a subscription, or `null` if none exists
46
+ * yet (cold start → the worker bootstraps from the snapshot per
47
+ * 09-replication § 7, then stitches to live from `lastStreamId`).
48
+ *
49
+ * `tx` may be a transaction handle OR a pooled connection — a read does not
50
+ * need to join the apply tx, but accepting the same seam keeps the API
51
+ * uniform and lets the caller read its OWN uncommitted advance inside a tx if
52
+ * it wants to.
53
+ */
54
+ get(subscription: string, tx: SqlExecutor): Promise<Checkpoint | null>;
55
+ /**
56
+ * Advance the durable cursor INSIDE the caller's apply transaction. MUST be
57
+ * called on the SAME `tx` the caller runs its projection upsert + dedupe
58
+ * record on, so cursor + projection + dedupe commit together-or-none. This is
59
+ * the atomic-in-apply-tx guarantee D402 pins.
60
+ *
61
+ * @param adv the new cursor position (the event id being committed).
62
+ * @param tx the caller's transaction handle (NOT a fresh pool conn).
63
+ */
64
+ advance(adv: CheckpointAdvance, tx: SqlExecutor): Promise<void>;
65
+ }
66
+ //# sourceMappingURL=checkpoint-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-store.d.ts","sourceRoot":"","sources":["../../src/worker/checkpoint-store.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,SAAS,CAAC;AAYnD,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAE5D,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iFAAiF;IACjF,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,SAA2B,GAAG,MAAM,CAS3E;AAcD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO;IAsBzC;;;;;;;;;OASG;IACG,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAY5E;;;;;;;;OAQG;IACG,OAAO,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CActE"}
@@ -0,0 +1,131 @@
1
+ // D402 — CheckpointStore: the durable high-water-mark (HWM) cursor primitive.
2
+ //
3
+ // D402 (LOCKED):
4
+ // "CheckpointStore (HWM cursor) — durable `last_stream_id` per subscription;
5
+ // advanced ATOMICALLY inside the apply transaction (cursor + projection
6
+ // upsert + dedupe commit together or none); snapshot→live 'stitch' per
7
+ // 09-replication-doctrine § 7."
8
+ //
9
+ // This GENERALIZES the two hand-rolled cursors that exist today (the task's
10
+ // instruction — "generalize, don't duplicate the table"):
11
+ // - replica-consumer's `replication_cursor` keyed by `source_stream`
12
+ // (ts/replica-consumer/src/sql-builders.ts CURSOR_UPSERT_SQL).
13
+ // - outbox-dispatcher's in-memory `HwmTracker` (ts/outbox-dispatcher/src/hwm.ts).
14
+ //
15
+ // THE LOAD-BEARING CORRECTNESS PROPERTY — atomic-in-apply-tx:
16
+ // `advance(...)` runs on a `tx` handle the CALLER owns. The store NEVER opens
17
+ // its own connection for the cursor write, so it physically cannot commit the
18
+ // cursor independently of the projection. The caller wraps projection upsert +
19
+ // `dedupe.record(...)` + `checkpoint.advance(...)` in ONE transaction; a
20
+ // failure anywhere rolls back ALL THREE (cursor does NOT advance past an
21
+ // un-applied event → no silent gap, no double-apply on retry).
22
+ //
23
+ // Table shape (generalized `replication_cursor`, keyed by `subscription`):
24
+ // subscription TEXT PRIMARY KEY -- the per-subscription key
25
+ // last_stream_id TEXT NOT NULL -- Redis-Streams id of last applied
26
+ // high_water_entity_version BIGINT -- optional LWW pointer (nullable)
27
+ // consumed_count BIGINT NOT NULL DEFAULT 0
28
+ // updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
29
+ /** Strict SQL identifier whitelist (mirrors replica-consumer's quoteIdent). */
30
+ const IDENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
31
+ function quoteIdent(name) {
32
+ if (!IDENT_PATTERN.test(name)) {
33
+ throw new Error(`invalid SQL identifier: ${name}`);
34
+ }
35
+ return `"${name}"`;
36
+ }
37
+ /** Default table name for the generalized checkpoint cursor. */
38
+ export const DEFAULT_CHECKPOINT_TABLE = "worker_checkpoint";
39
+ /** Idempotent DDL to create the generalized checkpoint table. */
40
+ export function checkpointTableDDL(table = DEFAULT_CHECKPOINT_TABLE) {
41
+ const t = quoteIdent(table);
42
+ return `CREATE TABLE IF NOT EXISTS ${t} (
43
+ subscription TEXT PRIMARY KEY,
44
+ last_stream_id TEXT,
45
+ high_water_entity_version BIGINT,
46
+ consumed_count BIGINT NOT NULL DEFAULT 0,
47
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
48
+ )`;
49
+ }
50
+ function toBigintOrNull(v) {
51
+ if (v === null || v === undefined)
52
+ return null;
53
+ return BigInt(v);
54
+ }
55
+ /**
56
+ * Durable HWM cursor store. ONE instance per subscription family (or share one
57
+ * across subscriptions — the `subscription` key disambiguates). The
58
+ * `advance(...)` write joins the caller's apply transaction via the injected
59
+ * `tx`; the store has NO pool of its own and cannot commit independently.
60
+ */
61
+ export class CheckpointStore {
62
+ table;
63
+ upsertSql;
64
+ selectSql;
65
+ constructor(opts = {}) {
66
+ this.table = opts.table ?? DEFAULT_CHECKPOINT_TABLE;
67
+ const t = quoteIdent(this.table);
68
+ // ON CONFLICT upsert: advance the cursor + bump consumed_count.
69
+ // `high_water_entity_version` advances MONOTONICALLY (GREATEST) so an
70
+ // out-of-order apply can't regress the LWW HWM; a NULL incoming value keeps
71
+ // the existing one (COALESCE on EXCLUDED).
72
+ this.upsertSql = `INSERT INTO ${t}
73
+ (subscription, last_stream_id, high_water_entity_version, consumed_count, updated_at)
74
+ VALUES ($1, $2, $3, $4, now())
75
+ ON CONFLICT (subscription) DO UPDATE SET
76
+ last_stream_id = EXCLUDED.last_stream_id,
77
+ high_water_entity_version = GREATEST(
78
+ COALESCE(${t}.high_water_entity_version, EXCLUDED.high_water_entity_version, 0),
79
+ COALESCE(EXCLUDED.high_water_entity_version, ${t}.high_water_entity_version, 0)
80
+ ),
81
+ consumed_count = ${t}.consumed_count + EXCLUDED.consumed_count,
82
+ updated_at = now()`;
83
+ this.selectSql = `SELECT subscription, last_stream_id, high_water_entity_version, consumed_count
84
+ FROM ${t} WHERE subscription = $1`;
85
+ }
86
+ /**
87
+ * Read the durable checkpoint for a subscription, or `null` if none exists
88
+ * yet (cold start → the worker bootstraps from the snapshot per
89
+ * 09-replication § 7, then stitches to live from `lastStreamId`).
90
+ *
91
+ * `tx` may be a transaction handle OR a pooled connection — a read does not
92
+ * need to join the apply tx, but accepting the same seam keeps the API
93
+ * uniform and lets the caller read its OWN uncommitted advance inside a tx if
94
+ * it wants to.
95
+ */
96
+ async get(subscription, tx) {
97
+ const res = await tx.query(this.selectSql, [subscription]);
98
+ const row = res.rows[0];
99
+ if (!row)
100
+ return null;
101
+ return {
102
+ subscription: row.subscription,
103
+ lastStreamId: row.last_stream_id,
104
+ highWaterEntityVersion: toBigintOrNull(row.high_water_entity_version),
105
+ consumedCount: BigInt(row.consumed_count),
106
+ };
107
+ }
108
+ /**
109
+ * Advance the durable cursor INSIDE the caller's apply transaction. MUST be
110
+ * called on the SAME `tx` the caller runs its projection upsert + dedupe
111
+ * record on, so cursor + projection + dedupe commit together-or-none. This is
112
+ * the atomic-in-apply-tx guarantee D402 pins.
113
+ *
114
+ * @param adv the new cursor position (the event id being committed).
115
+ * @param tx the caller's transaction handle (NOT a fresh pool conn).
116
+ */
117
+ async advance(adv, tx) {
118
+ const inc = adv.incrementBy ?? 1;
119
+ const hwev = adv.highWaterEntityVersion === undefined ||
120
+ adv.highWaterEntityVersion === null
121
+ ? null
122
+ : adv.highWaterEntityVersion.toString();
123
+ await tx.query(this.upsertSql, [
124
+ adv.subscription,
125
+ adv.lastStreamId,
126
+ hwev,
127
+ inc,
128
+ ]);
129
+ }
130
+ }
131
+ //# sourceMappingURL=checkpoint-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-store.js","sourceRoot":"","sources":["../../src/worker/checkpoint-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import type { SqlExecutor } from "./tx.js";
2
+ /**
3
+ * A poison event handed to a dead-letter sink after exhausting in-line retries.
4
+ * Carries enough to make it REPLAYABLE (mirrors replica-consumer's DLQ envelope
5
+ * columns) plus the failure cause.
6
+ */
7
+ export interface DeadLetterItem {
8
+ /** Source stream the event came off (the subscription / topic stream). */
9
+ sourceStream: string;
10
+ /** The outbox topic. */
11
+ topic: string;
12
+ /** The event's dedupe id (event_id). */
13
+ eventId: string;
14
+ /**
15
+ * The event payload — possibly containing PII. The redaction hook scrubs this
16
+ * BEFORE it reaches the sink (fail-closed). The sink NEVER sees raw PII.
17
+ */
18
+ payload: Record<string, unknown>;
19
+ /** Why it was dead-lettered (the terminal error message). */
20
+ errorMessage: string;
21
+ /** How many in-line retries were attempted before giving up. */
22
+ retryCount: number;
23
+ }
24
+ /**
25
+ * The injected dead-letter sink. The caller provides the impl; there is NO
26
+ * default. `deadLetter(item)` MUST be durable / loud — a dead item must never
27
+ * vanish silently.
28
+ */
29
+ export interface DeadLetterSink {
30
+ deadLetter(item: DeadLetterItem): Promise<void>;
31
+ }
32
+ /**
33
+ * A minimal logger seam for the loud sink (structurally satisfied by the
34
+ * telemetry `logger` or `console`). Only `error` is required.
35
+ */
36
+ export interface DeadLetterLogger {
37
+ error(msg: string, fields?: Record<string, unknown>): void;
38
+ }
39
+ export declare function withPiiRedaction(sink: DeadLetterSink, opts?: {
40
+ extraPaths?: readonly string[];
41
+ }): DeadLetterSink;
42
+ /**
43
+ * The FAIL-CLOSED-BY-DEFAULT wrapper every worker applies to its injected
44
+ * DeadLetterSink: redaction runs on every dead item WITHOUT the consumer having
45
+ * to remember to wrap (D15: the SAFE state must be the default). An
46
+ * already-wrapped sink is returned as-is (no double-wrap). Used internally by the
47
+ * worker constructors.
48
+ */
49
+ export declare function ensurePiiRedaction(sink: DeadLetterSink): DeadLetterSink;
50
+ /**
51
+ * Reference sink #1: accepts the dead item but logs it LOUDLY (error-level,
52
+ * with the failure cause + identity, never the raw payload — that's already
53
+ * redacted by `withPiiRedaction`). It does NOT persist. Useful as an explicit
54
+ * "I have NO durable DLQ yet but I refuse to silently drop" choice. NEVER
55
+ * auto-wired — the caller must construct + inject it.
56
+ */
57
+ export declare class LoudNoopDeadLetterSink implements DeadLetterSink {
58
+ private readonly logger;
59
+ constructor(logger: DeadLetterLogger);
60
+ deadLetter(item: DeadLetterItem): Promise<void>;
61
+ }
62
+ /**
63
+ * Reference sink #2: persists the dead item into a CONSUMER-OWNED quarantine
64
+ * table the caller opts into (passing its OWN transaction/pool executor + table
65
+ * name). This is the SUBSCRIBER's own DLQ — NOT a source-side `outbox_dlq`
66
+ * (which D401 forbids). The caller controls the executor, so the write joins
67
+ * whatever transaction the caller chooses (or runs standalone on a pool conn).
68
+ */
69
+ export declare class TableDeadLetterSink implements DeadLetterSink {
70
+ readonly table: string;
71
+ private readonly insertSql;
72
+ private readonly exec;
73
+ constructor(opts: {
74
+ executor: SqlExecutor;
75
+ table?: string;
76
+ });
77
+ /** Idempotent DDL for the consumer-owned quarantine table. */
78
+ static tableDDL(table?: string): string;
79
+ deadLetter(item: DeadLetterItem): Promise<void>;
80
+ }
81
+ //# sourceMappingURL=dead-letter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.d.ts","sourceRoot":"","sources":["../../src/worker/dead-letter.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAY3C;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAqBD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAC5C,cAAc,CA6BhB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAGvE;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAE/C,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAWtD;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,cAAc;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;gBAEvB,IAAI,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAS3D,8DAA8D;IAC9D,MAAM,CAAC,QAAQ,CAAC,KAAK,SAAuB,GAAG,MAAM;IAc/C,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAUtD"}