@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lambda/index.d.ts +77 -0
- package/dist/lambda/index.d.ts.map +1 -0
- package/dist/lambda/index.js +130 -0
- package/dist/lambda/index.js.map +1 -0
- package/dist/worker/checkpoint-store.d.ts +66 -0
- package/dist/worker/checkpoint-store.d.ts.map +1 -0
- package/dist/worker/checkpoint-store.js +131 -0
- package/dist/worker/checkpoint-store.js.map +1 -0
- package/dist/worker/dead-letter.d.ts +81 -0
- package/dist/worker/dead-letter.d.ts.map +1 -0
- package/dist/worker/dead-letter.js +165 -0
- package/dist/worker/dead-letter.js.map +1 -0
- package/dist/worker/dedupe-store.d.ts +46 -0
- package/dist/worker/dedupe-store.d.ts.map +1 -0
- package/dist/worker/dedupe-store.js +113 -0
- package/dist/worker/dedupe-store.js.map +1 -0
- package/dist/worker/index.d.ts +23 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +46 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/observability-hooks.d.ts +99 -0
- package/dist/worker/observability-hooks.d.ts.map +1 -0
- package/dist/worker/observability-hooks.js +189 -0
- package/dist/worker/observability-hooks.js.map +1 -0
- package/dist/worker/outbox-dispatcher.d.ts +106 -0
- package/dist/worker/outbox-dispatcher.d.ts.map +1 -0
- package/dist/worker/outbox-dispatcher.js +285 -0
- package/dist/worker/outbox-dispatcher.js.map +1 -0
- package/dist/worker/replication-worker.d.ts +162 -0
- package/dist/worker/replication-worker.d.ts.map +1 -0
- package/dist/worker/replication-worker.js +513 -0
- package/dist/worker/replication-worker.js.map +1 -0
- package/dist/worker/signal-drain.d.ts +83 -0
- package/dist/worker/signal-drain.d.ts.map +1 -0
- package/dist/worker/signal-drain.js +131 -0
- package/dist/worker/signal-drain.js.map +1 -0
- package/dist/worker/single-active-lease.d.ts +180 -0
- package/dist/worker/single-active-lease.d.ts.map +1 -0
- package/dist/worker/single-active-lease.js +394 -0
- package/dist/worker/single-active-lease.js.map +1 -0
- package/dist/worker/streams-worker.d.ts +192 -0
- package/dist/worker/streams-worker.d.ts.map +1 -0
- package/dist/worker/streams-worker.js +488 -0
- package/dist/worker/streams-worker.js.map +1 -0
- package/dist/worker/sweeper-worker.d.ts +107 -0
- package/dist/worker/sweeper-worker.d.ts.map +1 -0
- package/dist/worker/sweeper-worker.js +245 -0
- package/dist/worker/sweeper-worker.js.map +1 -0
- package/dist/worker/tx.d.ts +39 -0
- package/dist/worker/tx.d.ts.map +1 -0
- package/dist/worker/tx.js +51 -0
- package/dist/worker/tx.js.map +1 -0
- package/dist/worker/worker-handle.d.ts +94 -0
- package/dist/worker/worker-handle.d.ts.map +1 -0
- package/dist/worker/worker-handle.js +15 -0
- package/dist/worker/worker-handle.js.map +1 -0
- package/package.json +10 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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"}
|