@nodii/telemetry 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/worker/checkpoint-store.d.ts +66 -0
  2. package/dist/worker/checkpoint-store.d.ts.map +1 -0
  3. package/dist/worker/checkpoint-store.js +131 -0
  4. package/dist/worker/checkpoint-store.js.map +1 -0
  5. package/dist/worker/dead-letter.d.ts +81 -0
  6. package/dist/worker/dead-letter.d.ts.map +1 -0
  7. package/dist/worker/dead-letter.js +165 -0
  8. package/dist/worker/dead-letter.js.map +1 -0
  9. package/dist/worker/dedupe-store.d.ts +46 -0
  10. package/dist/worker/dedupe-store.d.ts.map +1 -0
  11. package/dist/worker/dedupe-store.js +113 -0
  12. package/dist/worker/dedupe-store.js.map +1 -0
  13. package/dist/worker/index.d.ts +23 -0
  14. package/dist/worker/index.d.ts.map +1 -0
  15. package/dist/worker/index.js +46 -0
  16. package/dist/worker/index.js.map +1 -0
  17. package/dist/worker/observability-hooks.d.ts +99 -0
  18. package/dist/worker/observability-hooks.d.ts.map +1 -0
  19. package/dist/worker/observability-hooks.js +189 -0
  20. package/dist/worker/observability-hooks.js.map +1 -0
  21. package/dist/worker/outbox-dispatcher.d.ts +106 -0
  22. package/dist/worker/outbox-dispatcher.d.ts.map +1 -0
  23. package/dist/worker/outbox-dispatcher.js +285 -0
  24. package/dist/worker/outbox-dispatcher.js.map +1 -0
  25. package/dist/worker/replication-worker.d.ts +170 -0
  26. package/dist/worker/replication-worker.d.ts.map +1 -0
  27. package/dist/worker/replication-worker.js +576 -0
  28. package/dist/worker/replication-worker.js.map +1 -0
  29. package/dist/worker/signal-drain.d.ts +83 -0
  30. package/dist/worker/signal-drain.d.ts.map +1 -0
  31. package/dist/worker/signal-drain.js +131 -0
  32. package/dist/worker/signal-drain.js.map +1 -0
  33. package/dist/worker/single-active-lease.d.ts +180 -0
  34. package/dist/worker/single-active-lease.d.ts.map +1 -0
  35. package/dist/worker/single-active-lease.js +394 -0
  36. package/dist/worker/single-active-lease.js.map +1 -0
  37. package/dist/worker/streams-worker.d.ts +256 -0
  38. package/dist/worker/streams-worker.d.ts.map +1 -0
  39. package/dist/worker/streams-worker.js +621 -0
  40. package/dist/worker/streams-worker.js.map +1 -0
  41. package/dist/worker/sweeper-worker.d.ts +107 -0
  42. package/dist/worker/sweeper-worker.d.ts.map +1 -0
  43. package/dist/worker/sweeper-worker.js +245 -0
  44. package/dist/worker/sweeper-worker.js.map +1 -0
  45. package/dist/worker/tx.d.ts +39 -0
  46. package/dist/worker/tx.d.ts.map +1 -0
  47. package/dist/worker/tx.js +51 -0
  48. package/dist/worker/tx.js.map +1 -0
  49. package/dist/worker/worker-handle.d.ts +94 -0
  50. package/dist/worker/worker-handle.d.ts.map +1 -0
  51. package/dist/worker/worker-handle.js +15 -0
  52. package/dist/worker/worker-handle.js.map +1 -0
  53. package/package.json +6 -1
@@ -0,0 +1,107 @@
1
+ import type { DeadLetterSink } from "./dead-letter.js";
2
+ import { ObservabilityHooks } from "./observability-hooks.js";
3
+ import { type SingleActiveLeaseOpts } from "./single-active-lease.js";
4
+ import type { SqlExecutor } from "./tx.js";
5
+ import type { WorkerHandle, WorkerHwm, WorkerStopOptions, WorkerStopResult } from "./worker-handle.js";
6
+ /**
7
+ * The context a sweeper job receives each tick. Carries the executor the job
8
+ * runs its delete + checkpoint advance on, the durable cursor it resumes from,
9
+ * and the fence guard it MUST call before any non-idempotent delete.
10
+ */
11
+ export interface SweeperJobContext {
12
+ /** A pooled executor (the sweep is independent of any apply transaction; the
13
+ * job MAY open its own tx via the caller-provided executor factory). */
14
+ exec: SqlExecutor;
15
+ /** The durable cursor the worker resumes from (null on cold start). */
16
+ lastStreamId: string | null;
17
+ /**
18
+ * THE side-effecting guard. The job MUST `await ctx.validateFence()` right
19
+ * before any non-idempotent delete: it re-reads the live lease and throws
20
+ * {@link FenceLostError} if the lease was lost/taken-over since the tick
21
+ * began — so a stale sweeper's late delete cannot land.
22
+ */
23
+ validateFence(): Promise<void>;
24
+ }
25
+ /** What a single sweep tick returns: how many rows it removed + the new cursor. */
26
+ export interface SweeperJobResult {
27
+ /** Rows removed this tick (surfaced as `nodii.worker.swept_total`). */
28
+ swept: number;
29
+ /**
30
+ * The new durable cursor position to advance the checkpoint to (e.g. the
31
+ * max consumed_at swept, or a synthetic monotonic marker). Omit / null to
32
+ * leave the cursor unchanged (a no-op sweep).
33
+ */
34
+ lastStreamId?: string | null;
35
+ }
36
+ /** The injected unit of sweep work (e.g. `dedupe.sweep(...)`, a retention DELETE). */
37
+ export interface SweeperJobDefinition {
38
+ /** Stable name for logs/metrics (the lease scope defaults to this). */
39
+ name: string;
40
+ /** Run exactly one sweep pass. MUST call `ctx.validateFence()` before delete. */
41
+ runOnce(ctx: SweeperJobContext): Promise<SweeperJobResult>;
42
+ }
43
+ export interface SweeperWorkerOpts {
44
+ /** The injected sweep job. */
45
+ job: SweeperJobDefinition;
46
+ /** Provides a fresh pooled executor per tick (e.g. `() => postgresJsExecutor(sql)`). */
47
+ executor: () => SqlExecutor | Promise<SqlExecutor>;
48
+ /** Lease config (service + scope + ttlMs). The renew loop runs inside it. */
49
+ lease: Omit<SingleActiveLeaseOpts, "onLeaseLost">;
50
+ /** The durable checkpoint store (resume cursor). */
51
+ checkpoint: import("./checkpoint-store.js").CheckpointStore;
52
+ /** The checkpoint subscription key (defaults to `sweeper:<job.name>`). */
53
+ subscription?: string;
54
+ /** Tick interval (ms) for the autonomous loop. Default 60_000. */
55
+ intervalMs?: number;
56
+ /** Injected dead-letter sink (NO default). A tick error is dead-lettered. */
57
+ deadLetter: DeadLetterSink;
58
+ /** Observability hooks (canonical metrics). Constructed if omitted. */
59
+ hooks?: ObservabilityHooks;
60
+ /** Worker label for metric attributes. Default "sweeper". */
61
+ workerLabel?: string;
62
+ /** Launch the interval loop on `start()`. Default true; tests pass false to
63
+ * drive `runOnce()` deterministically. */
64
+ autoStart?: boolean;
65
+ }
66
+ /**
67
+ * SweeperWorker — the reference {@link WorkerHandle}. Acquires a single-active
68
+ * lease, runs the injected sweep job on an interval, advances the checkpoint
69
+ * each tick (resumable), and drains bounded on stop.
70
+ */
71
+ export declare class SweeperWorker implements WorkerHandle {
72
+ private readonly job;
73
+ private readonly executorFactory;
74
+ private readonly lease;
75
+ private readonly checkpoint;
76
+ private readonly subscription;
77
+ private readonly intervalMs;
78
+ private readonly deadLetter;
79
+ private readonly hooks;
80
+ private readonly autoStart;
81
+ private _running;
82
+ private _stopped;
83
+ private _inFlight;
84
+ private _hwm;
85
+ private _lastAppliedAtMs;
86
+ private loopTimer;
87
+ constructor(opts: SweeperWorkerOpts);
88
+ get running(): boolean;
89
+ get leaseAcquired(): boolean;
90
+ get lag(): number | null;
91
+ get currentHwm(): WorkerHwm;
92
+ /** Acquire the lease + start the interval loop. Idempotent. */
93
+ start(): Promise<void>;
94
+ /** Read the durable checkpoint so a restart resumes rather than re-sweeps. */
95
+ private hydrateCursor;
96
+ /** One interval tick: acquire-if-needed → runOnce → swallow into dead-letter. */
97
+ private tick;
98
+ /**
99
+ * Run exactly ONE sweep pass. Acquires/validates the lease for the tick,
100
+ * invokes the injected job, advances the checkpoint, emits `swept_total`.
101
+ * Resolves the number of rows swept (0 when the lease is not held).
102
+ */
103
+ runOnce(): Promise<number>;
104
+ /** Bounded graceful stop: halt the loop, let an in-flight tick finish, release. */
105
+ stop(opts?: WorkerStopOptions): Promise<WorkerStopResult>;
106
+ }
107
+ //# sourceMappingURL=sweeper-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweeper-worker.d.ts","sourceRoot":"","sources":["../../src/worker/sweeper-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,245 @@
1
+ // D402 — SweeperWorker: a single-active, checkpoint-resumable INTERVAL job.
2
+ //
3
+ // D402 (LOCKED): "SweeperWorker — single-active, checkpoint-resumable interval
4
+ // job." It is the SIMPLEST of the 4 worker types and the WorkerHandle reference
5
+ // impl — every other worker type follows its lifecycle shape (acquire lease →
6
+ // loop → bounded stop) but adds stream/outbox/replication-specific apply logic.
7
+ //
8
+ // ── Composition (which primitives, how) ──────────────────────────────────────
9
+ // - SingleActiveLease — acquire on start; the ttl/2 renew loop runs inside
10
+ // the lease; `validateFence(token)` guards the sweep so a lease-lost sweeper
11
+ // cannot run a delete after a successor took over.
12
+ // - CheckpointStore — the sweep is checkpoint-RESUMABLE: each tick advances
13
+ // the cursor (the injected job returns the new `lastStreamId`/position +
14
+ // count) inside the job's own tx, so a restart resumes from the last commit
15
+ // rather than re-sweeping from zero.
16
+ // - SignalDrain — `stop({timeoutMs})` is registered so SIGTERM drains it.
17
+ // - ObservabilityHooks — emits `nodii.worker.swept_total` (+ lease/lag/hwm).
18
+ // - DeadLetterSink — INJECTED (no default); a tick error is dead-lettered.
19
+ //
20
+ // A >grace handler is NOT double-run: single-active means only the lease holder
21
+ // sweeps, and `validateFence` rejects a stale holder's late delete.
22
+ import { ensurePiiRedaction } from "./dead-letter.js";
23
+ import { ObservabilityHooks } from "./observability-hooks.js";
24
+ import { FenceCheckUnavailableError, FenceLostError, SingleActiveLease, } from "./single-active-lease.js";
25
+ const DEFAULT_INTERVAL_MS = 60_000;
26
+ const DEFAULT_STOP_TIMEOUT_MS = 30_000;
27
+ /**
28
+ * SweeperWorker — the reference {@link WorkerHandle}. Acquires a single-active
29
+ * lease, runs the injected sweep job on an interval, advances the checkpoint
30
+ * each tick (resumable), and drains bounded on stop.
31
+ */
32
+ export class SweeperWorker {
33
+ job;
34
+ executorFactory;
35
+ lease;
36
+ checkpoint;
37
+ subscription;
38
+ intervalMs;
39
+ deadLetter;
40
+ hooks;
41
+ autoStart;
42
+ _running = false;
43
+ _stopped = false;
44
+ _inFlight = false;
45
+ _hwm = { lastStreamId: null };
46
+ _lastAppliedAtMs = null;
47
+ loopTimer = null;
48
+ constructor(opts) {
49
+ this.job = opts.job;
50
+ this.executorFactory = opts.executor;
51
+ this.lease = new SingleActiveLease({
52
+ ...opts.lease,
53
+ // When the renew loop detects loss, mark the lost metric — the next tick
54
+ // sees `leaseAcquired===false` and idles; a stale delete is fence-rejected.
55
+ onLeaseLost: () => this.hooks.leaseLost(),
56
+ });
57
+ this.checkpoint = opts.checkpoint;
58
+ this.subscription = opts.subscription ?? `sweeper:${opts.job.name}`;
59
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
60
+ this.autoStart = opts.autoStart !== false;
61
+ this.deadLetter = ensurePiiRedaction(opts.deadLetter);
62
+ this.hooks =
63
+ opts.hooks ??
64
+ new ObservabilityHooks({
65
+ attributes: {
66
+ service: opts.lease.service,
67
+ scope: opts.lease.scope,
68
+ worker: opts.workerLabel ?? "sweeper",
69
+ },
70
+ });
71
+ }
72
+ get running() {
73
+ return this._running;
74
+ }
75
+ get leaseAcquired() {
76
+ return this.lease.leaseAcquired;
77
+ }
78
+ get lag() {
79
+ if (this._lastAppliedAtMs === null)
80
+ return null;
81
+ return Math.max(0, (Date.now() - this._lastAppliedAtMs) / 1000);
82
+ }
83
+ get currentHwm() {
84
+ return this._hwm;
85
+ }
86
+ /** Acquire the lease + start the interval loop. Idempotent. */
87
+ async start() {
88
+ if (this._running)
89
+ return;
90
+ this._stopped = false;
91
+ const acquired = await this.lease.acquire();
92
+ if (acquired)
93
+ this.hooks.leaseAcquired();
94
+ // Hydrate the resume cursor from the durable checkpoint (cold-start → null).
95
+ await this.hydrateCursor();
96
+ this._running = true;
97
+ // Background interval loop. `autoStart:false` lets a test drive `runOnce()`.
98
+ if (this.autoStart) {
99
+ this.loopTimer = setInterval(() => {
100
+ void this.tick();
101
+ }, this.intervalMs);
102
+ this.loopTimer.unref?.();
103
+ }
104
+ }
105
+ /** Read the durable checkpoint so a restart resumes rather than re-sweeps. */
106
+ async hydrateCursor() {
107
+ try {
108
+ const ex = await this.executorFactory();
109
+ const cp = await this.checkpoint.get(this.subscription, ex);
110
+ if (cp)
111
+ this._hwm = { lastStreamId: cp.lastStreamId };
112
+ }
113
+ catch {
114
+ // A cold-start read failure is non-fatal — the cursor stays null and the
115
+ // first tick simply starts from the beginning of the retention horizon.
116
+ }
117
+ }
118
+ /** One interval tick: acquire-if-needed → runOnce → swallow into dead-letter. */
119
+ async tick() {
120
+ if (this._stopped || this._inFlight)
121
+ return;
122
+ // BUG[5]: the ENTIRE tick body — INCLUDING the autonomous re-acquire — is
123
+ // guarded. A transient lease-redis blip at the re-acquire moment throws out
124
+ // of `lease.acquire()`; if that escaped, the `void this.tick()` interval call
125
+ // would leak an unhandled rejection (TS) / kill the loop task (Python). It is
126
+ // caught here so the loop SURVIVES to the next tick, with a loud error signal.
127
+ try {
128
+ // Re-acquire if we don't hold the lease (a successor may have released it).
129
+ if (!this.lease.leaseAcquired) {
130
+ const got = await this.lease.acquire();
131
+ if (got)
132
+ this.hooks.leaseAcquired();
133
+ else
134
+ return; // someone else is the single-active sweeper; idle.
135
+ }
136
+ await this.runOnce();
137
+ }
138
+ catch {
139
+ // Either the autonomous re-acquire threw (lease-redis blip) or the
140
+ // dead-letter sink itself failed inside runOnce. Emit a loud error/depth
141
+ // signal and CONTINUE the loop — a single bad tick (or a transient
142
+ // lease-redis outage) must never stop retention sweeps forever.
143
+ this.hooks.recordDepth(-1, { error: "tick_failed" });
144
+ }
145
+ }
146
+ /**
147
+ * Run exactly ONE sweep pass. Acquires/validates the lease for the tick,
148
+ * invokes the injected job, advances the checkpoint, emits `swept_total`.
149
+ * Resolves the number of rows swept (0 when the lease is not held).
150
+ */
151
+ async runOnce() {
152
+ if (!this.lease.leaseAcquired) {
153
+ const got = await this.lease.acquire();
154
+ if (got)
155
+ this.hooks.leaseAcquired();
156
+ else
157
+ return 0;
158
+ }
159
+ this._inFlight = true;
160
+ const token = this.lease.fencingToken;
161
+ try {
162
+ const ex = await this.executorFactory();
163
+ const ctx = {
164
+ exec: ex,
165
+ lastStreamId: this._hwm.lastStreamId,
166
+ validateFence: async () => {
167
+ if (token === null) {
168
+ throw new FenceLostError({
169
+ leaseKey: this.lease.key,
170
+ expectedToken: -1,
171
+ current: null,
172
+ });
173
+ }
174
+ await this.lease.validateFence(token);
175
+ },
176
+ };
177
+ const result = await this.job.runOnce(ctx);
178
+ // Advance the checkpoint (resumable) when the job moved the cursor.
179
+ if (result.lastStreamId !== undefined && result.lastStreamId !== null) {
180
+ await this.checkpoint.advance({
181
+ subscription: this.subscription,
182
+ lastStreamId: result.lastStreamId,
183
+ }, ex);
184
+ this._hwm = { lastStreamId: result.lastStreamId };
185
+ }
186
+ this._lastAppliedAtMs = Date.now();
187
+ if (result.swept > 0) {
188
+ this.hooks.recordSwept(result.swept, {
189
+ subscription: this.subscription,
190
+ });
191
+ }
192
+ this.hooks.recordLag(0);
193
+ return result.swept;
194
+ }
195
+ catch (err) {
196
+ // A FenceLostError means a successor took over mid-tick — DO NOT dead-letter
197
+ // (it's a benign lease handoff, not a poison job); just yield the tick.
198
+ if (err instanceof FenceLostError) {
199
+ this.hooks.leaseLost();
200
+ return 0;
201
+ }
202
+ // A transient fence-check redis failure means we could not verify the lease
203
+ // — skip this tick (the sweep runs again next interval). Do NOT dead-letter
204
+ // a transient infra blip (cross-runtime-identical).
205
+ if (err instanceof FenceCheckUnavailableError) {
206
+ this.hooks.recordDepth(-1, { error: "fence_check_unavailable" });
207
+ return 0;
208
+ }
209
+ // Any other terminal error → dead-letter (durable, never a silent drop).
210
+ await this.deadLetter.deadLetter({
211
+ sourceStream: this.subscription,
212
+ topic: this.job.name,
213
+ eventId: `sweep:${Date.now()}`,
214
+ payload: {},
215
+ errorMessage: err instanceof Error ? err.message : String(err),
216
+ retryCount: 0,
217
+ });
218
+ return 0;
219
+ }
220
+ finally {
221
+ this._inFlight = false;
222
+ }
223
+ }
224
+ /** Bounded graceful stop: halt the loop, let an in-flight tick finish, release. */
225
+ async stop(opts) {
226
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
227
+ const start = Date.now();
228
+ this._stopped = true;
229
+ this._running = false;
230
+ if (this.loopTimer !== null) {
231
+ clearInterval(this.loopTimer);
232
+ this.loopTimer = null;
233
+ }
234
+ // Wait (bounded) for an in-flight tick to flush.
235
+ while (this._inFlight && Date.now() - start < timeoutMs) {
236
+ await new Promise((r) => setTimeout(r, 10));
237
+ }
238
+ const drainedCleanly = !this._inFlight;
239
+ await this.lease.release();
240
+ const durationMs = Date.now() - start;
241
+ this.hooks.recordDrainDuration(durationMs);
242
+ return { drainedCleanly, durationMs };
243
+ }
244
+ }
245
+ //# sourceMappingURL=sweeper-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweeper-worker.js","sourceRoot":"","sources":["../../src/worker/sweeper-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /** A single result row as a plain object. */
2
+ export type SqlRow = Record<string, unknown>;
3
+ export interface SqlResult<R extends SqlRow = SqlRow> {
4
+ rows: R[];
5
+ /** Affected-row count for write statements (INSERT/UPDATE/DELETE). */
6
+ rowCount: number;
7
+ }
8
+ /**
9
+ * The minimal executor a store needs: run a parameterized statement on a
10
+ * specific connection / transaction handle and get rows + an affected count
11
+ * back. Structurally compatible with node-postgres `PoolClient.query(text,
12
+ * values)` (which returns `{ rows, rowCount }`). A `tx` passed to a store
13
+ * method MUST be the SAME handle the caller runs its projection upsert on, so
14
+ * everything commits together-or-none.
15
+ */
16
+ export interface SqlExecutor {
17
+ query<R extends SqlRow = SqlRow>(text: string, values?: ReadonlyArray<unknown>): Promise<SqlResult<R>>;
18
+ }
19
+ /**
20
+ * Adapt a postgres-js transaction/connection handle (the `sql` you receive
21
+ * inside `sql.begin(async (sql) => …)`) to {@link SqlExecutor}. postgres-js
22
+ * exposes `sql.unsafe(text, params)` which resolves an array-like of rows with
23
+ * a `.count`. The returned executor runs every statement on that exact handle,
24
+ * so a store write joins the caller's transaction.
25
+ *
26
+ * Usage (postgres-js):
27
+ * await sql.begin(async (tx) => {
28
+ * const ex = postgresJsExecutor(tx);
29
+ * await applyProjection(tx, row);
30
+ * await dedupe.record(eventId, topic, ex);
31
+ * await checkpoint.advance(cursor, ex);
32
+ * });
33
+ */
34
+ export declare function postgresJsExecutor(handle: {
35
+ unsafe: (text: string, params?: ReadonlyArray<unknown>) => Promise<unknown> & {
36
+ count?: number;
37
+ };
38
+ }): SqlExecutor;
39
+ //# sourceMappingURL=tx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/worker/tx.ts"],"names":[],"mappings":"AA0BA,6CAA6C;AAC7C,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7C,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAClD,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAC9B,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,MAAM,EAAE,CACN,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,KAC5B,OAAO,CAAC,OAAO,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C,GAAG,WAAW,CAgBd"}
@@ -0,0 +1,51 @@
1
+ // D402 worker substrate — the minimal SQL-executor seam shared by
2
+ // CheckpointStore + DedupeStore + DeadLetter's table sink.
3
+ //
4
+ // The CORRECTNESS PROPERTY D402 pins for the checkpoint + dedupe primitives is:
5
+ // "advanced ATOMICALLY inside the apply transaction (cursor write + projection
6
+ // upsert + dedupe commit together-or-none)."
7
+ //
8
+ // That is only achievable if the cursor write, the dedupe record, AND the
9
+ // caller's projection upsert all run on the SAME transaction handle, and the
10
+ // caller is the one who BEGIN/COMMITs. So every store method here takes a `tx`
11
+ // (an `SqlExecutor`) rather than owning a pool: the store NEVER opens its own
12
+ // connection for a write, so it can never commit independently of the
13
+ // projection. The caller does:
14
+ //
15
+ // await pool.transaction(async (tx) => { // pg / postgres-js tx
16
+ // await applyProjectionUpsert(tx, row); // caller's projection
17
+ // await dedupe.record(eventId, topic, tx); // dedupe row
18
+ // await checkpoint.advance({ ...cursor }, tx); // HWM cursor
19
+ // }); // ← one COMMIT, or rollback all
20
+ //
21
+ // `SqlExecutor` is structurally satisfied by BOTH a node-postgres `PoolClient`
22
+ // (the `query(text, params)` shape) AND a postgres-js tagged transaction handle
23
+ // (which is also callable as `sql.unsafe(text, params)`). We standardize on the
24
+ // node-postgres `query(text, values)` shape and provide a thin postgres-js
25
+ // adapter so a postgres-js caller can pass its tx through unchanged.
26
+ /**
27
+ * Adapt a postgres-js transaction/connection handle (the `sql` you receive
28
+ * inside `sql.begin(async (sql) => …)`) to {@link SqlExecutor}. postgres-js
29
+ * exposes `sql.unsafe(text, params)` which resolves an array-like of rows with
30
+ * a `.count`. The returned executor runs every statement on that exact handle,
31
+ * so a store write joins the caller's transaction.
32
+ *
33
+ * Usage (postgres-js):
34
+ * await sql.begin(async (tx) => {
35
+ * const ex = postgresJsExecutor(tx);
36
+ * await applyProjection(tx, row);
37
+ * await dedupe.record(eventId, topic, ex);
38
+ * await checkpoint.advance(cursor, ex);
39
+ * });
40
+ */
41
+ export function postgresJsExecutor(handle) {
42
+ return {
43
+ async query(text, values = []) {
44
+ const res = (await handle.unsafe(text, values));
45
+ const rows = Array.from(res);
46
+ const rowCount = typeof res.count === "number" ? res.count : (res.length ?? rows.length);
47
+ return { rows, rowCount };
48
+ },
49
+ };
50
+ }
51
+ //# sourceMappingURL=tx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tx.js","sourceRoot":"","sources":["../../src/worker/tx.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,2DAA2D;AAC3D,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,gDAAgD;AAChD,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,+EAA+E;AAC/E,8EAA8E;AAC9E,sEAAsE;AACtE,+BAA+B;AAC/B,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,mEAAmE;AACnE,oEAAoE;AACpE,uFAAuF;AACvF,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,gFAAgF;AAChF,2EAA2E;AAC3E,qEAAqE;AA0BrE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAKlC;IACC,OAAO;QACL,KAAK,CAAC,KAAK,CACT,IAAY,EACZ,SAAiC,EAAE;YAEnC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAG9B,CAAC;YACjB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAmB,CAAC,CAAC;YAC7C,MAAM,QAAQ,GACZ,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1E,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The high-water-mark a worker has durably advanced its CheckpointStore to.
3
+ *
4
+ * `lastStreamId` is the Redis-Streams id (`<ms>-<seq>`) of the most-recent
5
+ * event committed inside the apply transaction (the value
6
+ * {@link "./checkpoint-store".CheckpointStore} persists). `null` before the
7
+ * first commit. `highWaterEntityVersion` mirrors the optional LWW pointer the
8
+ * CheckpointStore may also track (per D402's "+ optional
9
+ * high_water_entity_version").
10
+ */
11
+ export interface WorkerHwm {
12
+ /** Redis-Streams id of the last event committed, or null pre-first-commit. */
13
+ lastStreamId: string | null;
14
+ /** Optional LWW entity_version high-water-mark, when the worker tracks one. */
15
+ highWaterEntityVersion?: bigint | null;
16
+ }
17
+ /**
18
+ * Options for {@link WorkerHandle.stop}. The drain is BOUNDED: `timeoutMs` caps
19
+ * how long graceful shutdown waits for in-flight work to flush + leases to
20
+ * release before the worker forces down. D402 spells the bound into the signal
21
+ * verbatim — `stop(timeoutMs)`.
22
+ */
23
+ export interface WorkerStopOptions {
24
+ /**
25
+ * Max wall-clock ms to wait for in-flight work to flush + leases to release.
26
+ * After this elapses the worker stops forcibly (in-flight work is abandoned;
27
+ * the lease TTL-expires). Defaults are worker-type specific.
28
+ */
29
+ timeoutMs?: number;
30
+ }
31
+ /**
32
+ * The result of a graceful {@link WorkerHandle.stop}. `drainedCleanly` is true
33
+ * when all in-flight work flushed AND every held lease released within the
34
+ * bound; false when the timeout forced the stop with work still in flight.
35
+ */
36
+ export interface WorkerStopResult {
37
+ drainedCleanly: boolean;
38
+ /** Wall-clock ms the drain actually took (≤ the requested bound + slack). */
39
+ durationMs: number;
40
+ }
41
+ /**
42
+ * The uniform handle every D402 worker type exposes. A service starts a worker
43
+ * with {@link start}, drives it (the loop is internal; {@link runOnce} is the
44
+ * single-tick form for tests / cron triggers), observes it via {@link running}
45
+ * / {@link leaseAcquired} / {@link lag} / {@link currentHwm}, and shuts it down
46
+ * with {@link stop} (bounded graceful drain — wired to {@link "./signal-drain"}).
47
+ */
48
+ export interface WorkerHandle {
49
+ /**
50
+ * Begin the worker loop (acquire the lease if single-active, then consume /
51
+ * sweep / drain continuously). Idempotent: calling `start` on an
52
+ * already-running handle is a no-op. Resolves once the loop is running (it
53
+ * does NOT block until the loop finishes — that is {@link stop}).
54
+ */
55
+ start(): Promise<void>;
56
+ /**
57
+ * Stop the worker gracefully, BOUNDED by `opts.timeoutMs`: flush in-flight
58
+ * work, advance + commit the checkpoint, release held leases, then resolve.
59
+ * If the bound elapses first, force the stop and resolve with
60
+ * `drainedCleanly: false`. SIGTERM/SIGINT handlers call this.
61
+ */
62
+ stop(opts?: WorkerStopOptions): Promise<WorkerStopResult>;
63
+ /**
64
+ * Run exactly ONE iteration of the worker's unit of work (consume-one-batch /
65
+ * sweep-once / drain-one-batch) and resolve. The single-active lease (if any)
66
+ * is acquired/validated for the tick. Used by tests and by cron-triggered
67
+ * sweepers that don't want a long-lived loop. Resolves the number of items
68
+ * processed in the tick (0 when idle / lease not held).
69
+ */
70
+ runOnce(): Promise<number>;
71
+ /** Is the worker loop currently running (between start and stop)? */
72
+ readonly running: boolean;
73
+ /**
74
+ * Does this worker currently hold its single-active lease? Always `true` for
75
+ * non-leased (e.g. all-replicas) workers; reflects the live lease state for
76
+ * `singleActive` workers. Mirrors
77
+ * {@link "./single-active-lease".SingleActiveLease.leaseAcquired}.
78
+ */
79
+ readonly leaseAcquired: boolean;
80
+ /**
81
+ * Current consumer lag in SECONDS — wall-clock between the source emission of
82
+ * the most-recently-applied event and its apply here. `0` when caught up,
83
+ * `null` when nothing has been applied yet (no basis to measure). This is the
84
+ * value surfaced as the `nodii.worker.lag` gauge by
85
+ * {@link "./observability-hooks".ObservabilityHooks}.
86
+ */
87
+ readonly lag: number | null;
88
+ /**
89
+ * The durable high-water-mark the worker has advanced its checkpoint to. The
90
+ * value the `nodii.worker.current_hwm` gauge reflects.
91
+ */
92
+ readonly currentHwm: WorkerHwm;
93
+ }
94
+ //# sourceMappingURL=worker-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-handle.d.ts","sourceRoot":"","sources":["../../src/worker/worker-handle.ts"],"names":[],"mappings":"AAcA;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+EAA+E;IAC/E,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAE1D;;;;;;OAMG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE3B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAEhC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;CAChC"}
@@ -0,0 +1,15 @@
1
+ // D402 — WorkerHandle: the uniform lifecycle interface every worker TYPE
2
+ // (ReplicationWorker / SweeperWorker / StreamsWorker / OutboxDispatcher — the
3
+ // next chunk) implements.
4
+ //
5
+ // D402 (LOCKED) pins the surface verbatim:
6
+ // "WorkerHandle — start / stop(timeoutMs) / runOnce / running / lag /
7
+ // currentHwm / leaseAcquired."
8
+ //
9
+ // This file is JUST the interface + the shared value types the 4 worker types
10
+ // and the other primitives agree on. It deliberately carries NO I/O of its own
11
+ // — a handle is a thin façade over the composed primitives (lease + checkpoint +
12
+ // dedupe + drain + hooks). Keeping it dependency-free means a worker type can
13
+ // implement it without pulling Redis/Postgres into the type module.
14
+ export {};
15
+ //# sourceMappingURL=worker-handle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-handle.js","sourceRoot":"","sources":["../../src/worker/worker-handle.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,8EAA8E;AAC9E,0BAA0B;AAC1B,EAAE;AACF,2CAA2C;AAC3C,wEAAwE;AACxE,kCAAkC;AAClC,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,iFAAiF;AACjF,8EAA8E;AAC9E,oEAAoE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/telemetry",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Substrate observability library for the Nodii microservice stack — OTel wrapper, log envelope, intent lifecycle, audit signal, agent registry, context propagation. Spec: planning hub docKey=telemetry.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,6 +26,10 @@
26
26
  "./lambda": {
27
27
  "types": "./dist/lambda/index.d.ts",
28
28
  "default": "./dist/lambda/index.js"
29
+ },
30
+ "./worker": {
31
+ "types": "./dist/worker/index.d.ts",
32
+ "default": "./dist/worker/index.js"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -86,6 +90,7 @@
86
90
  "@nodii/audit-chain": "0.10.0",
87
91
  "drizzle-orm": "^0.45.2",
88
92
  "hono": "^4.4.0",
93
+ "ioredis": "^5.4.0",
89
94
  "postgres": "^3.4.0",
90
95
  "typescript": "^5.9.3"
91
96
  },