@babelqueue/core 1.1.0 → 1.3.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 CHANGED
@@ -1,129 +1,5 @@
1
- /**
2
- * A message that can be produced as a polyglot envelope. Implement it on your
3
- * own classes/objects so {@link EnvelopeCodec.fromMessage} can build the canonical
4
- * envelope without ever leaking a language-specific class name onto the wire.
5
- */
6
- interface PolyglotMessage {
7
- /** The stable URN that identifies this message across languages. */
8
- getBabelUrn(): string;
9
- /** The pure-JSON payload (no class instances). */
10
- toPayload(): Record<string, unknown>;
11
- }
12
- /**
13
- * Optionally implemented alongside {@link PolyglotMessage} to continue an existing
14
- * distributed trace instead of minting a fresh one.
15
- */
16
- interface HasTraceId {
17
- /** The trace id to reuse, or null/undefined to mint a new one. */
18
- getBabelTraceId(): string | null | undefined;
19
- }
20
-
21
- /** The wire envelope schema version this core implements (versioned independently of the package version). */
22
- declare const SCHEMA_VERSION = 1;
23
- /** Stamped into `meta.lang` for envelopes produced by this core. */
24
- declare const SOURCE_LANG = "node";
25
- /** Immutable per-message metadata. */
26
- interface Meta {
27
- id: string;
28
- queue: string;
29
- lang: string;
30
- schema_version: number;
31
- /** Unix milliseconds, UTC. */
32
- created_at: number;
33
- }
34
- /** The additive block appended to an envelope when a message is dead-lettered. */
35
- interface DeadLetter {
36
- reason: string;
37
- error: string | null;
38
- exception: string | null;
39
- /** Unix milliseconds, UTC. */
40
- failed_at: number;
41
- original_queue: string;
42
- attempts: number;
43
- lang: string;
44
- }
45
- /**
46
- * The canonical BabelQueue wire message: a strict, language-neutral JSON shape
47
- * that every SDK produces and consumes identically. The property order here is
48
- * significant — it matches the other cores so {@link EnvelopeCodec.encode} is
49
- * byte-for-byte identical across the insertion-order languages (PHP/Python).
50
- */
51
- interface Envelope {
52
- /** The message URN (never a class name). */
53
- job: string;
54
- /** Correlation id, preserved across every hop. */
55
- trace_id: string;
56
- /** The pure-JSON payload. */
57
- data: Record<string, unknown>;
58
- meta: Meta;
59
- /** Top-level transport retry counter. */
60
- attempts: number;
61
- /** Present only once the message has been dead-lettered. */
62
- dead_letter?: DeadLetter;
63
- }
64
- /**
65
- * A decoded, not-yet-validated envelope. Fields are loosely typed because they
66
- * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it
67
- * with {@link EnvelopeCodec.accepts} before trusting the contents.
68
- */
69
- interface IncomingEnvelope {
70
- job?: string;
71
- /** Inbound alias for `job`. */
72
- urn?: string;
73
- trace_id?: string;
74
- data?: unknown;
75
- meta?: unknown;
76
- attempts?: unknown;
77
- dead_letter?: unknown;
78
- }
79
- /** Options for {@link EnvelopeCodec.make}. */
80
- interface MakeOptions {
81
- /** Logical queue name recorded in `meta.queue` (default `"default"`). */
82
- queue?: string;
83
- /** Reuse an existing trace id (trace continuation) instead of minting one. */
84
- traceId?: string;
85
- }
86
- /**
87
- * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript
88
- * implementation of the wire format.
89
- */
90
- declare const EnvelopeCodec: {
91
- readonly SCHEMA_VERSION: 1;
92
- readonly SOURCE_LANG: "node";
93
- /**
94
- * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
95
- * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
96
- * Throws {@link BabelQueueError} when the URN is blank.
97
- */
98
- readonly make: (urn: string, data: Record<string, unknown>, options?: MakeOptions) => Envelope;
99
- /**
100
- * Build the envelope from a {@link PolyglotMessage}. If the message also
101
- * implements {@link HasTraceId} and returns a non-empty value, that trace id is
102
- * reused.
103
- */
104
- readonly fromMessage: (message: PolyglotMessage & Partial<HasTraceId>, queue?: string) => Envelope;
105
- /**
106
- * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
107
- * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
108
- * the other SDK cores.
109
- */
110
- readonly encode: (envelope: Envelope) => string;
111
- /**
112
- * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
113
- * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
114
- * alias into `job`.
115
- */
116
- readonly decode: (raw: string) => IncomingEnvelope;
117
- /** The message URN — canonical `job`, with `urn` accepted as an alias. */
118
- readonly urn: (envelope: IncomingEnvelope) => string;
119
- /**
120
- * Whether a consumer should accept this envelope. Rejects a missing URN, an
121
- * unsupported `meta.schema_version`, a non-object `data`, a non-integer
122
- * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
123
- * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
124
- */
125
- readonly accepts: (envelope: IncomingEnvelope) => envelope is Envelope;
126
- };
1
+ import { E as Envelope } from './idempotency-DDHjGwF7.js';
2
+ export { D as DeadLetter, a as EnvelopeCodec, H as Handler, b as HasTraceId, I as InMemoryStore, c as IncomingEnvelope, M as MakeOptions, d as Meta, P as PolyglotMessage, S as SCHEMA_VERSION, e as SOURCE_LANG, f as Store, W as Wrap } from './idempotency-DDHjGwF7.js';
127
3
 
128
4
  /** Options for {@link annotate}. */
129
5
  interface AnnotateOptions {
@@ -182,56 +58,6 @@ declare class InvalidPayloadError extends BabelQueueError {
182
58
  constructor(urn: string, violation: string);
183
59
  }
184
60
 
185
- /**
186
- * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.
187
- *
188
- * The Node mirror of the PHP `BabelQueue\Idempotency` and Go `idempotency` helpers.
189
- * The core is codec-only (no dispatcher), so this wraps a user-provided handler that
190
- * an adapter (NestJS, BullMQ, ...) drives:
191
- *
192
- * ```ts
193
- * import { Wrap, InMemoryStore, type Handler } from "@babelqueue/core";
194
- *
195
- * const store = new InMemoryStore();
196
- * const handler = Wrap(store, async (env) => { ... });
197
- * ```
198
- *
199
- * A previously-seen id returns early (the adapter acks it); a throwing/rejecting
200
- * handler leaves the id unmarked so a redelivery runs it again; a message with no
201
- * usable `meta.id` runs unchanged. "Seen-set" post-success dedupe — not exactly-once,
202
- * not in-flight concurrency locking (a transactional mode is a future direction).
203
- */
204
-
205
- /** A consume handler: receives a decoded envelope, may be sync or async. */
206
- type Handler = (env: Envelope) => void | Promise<void>;
207
- /**
208
- * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may
209
- * be sync or async so a production store can be Redis- or DB-backed; the reference
210
- * {@link InMemoryStore} is synchronous.
211
- */
212
- interface Store {
213
- seen(messageId: string): boolean | Promise<boolean>;
214
- remember(messageId: string): void | Promise<void>;
215
- forget(messageId: string): void | Promise<void>;
216
- }
217
- /**
218
- * Process-local {@link Store} backed by a Set. For tests / single-process consumers;
219
- * not shared across workers and not persistent — use a Redis- or DB-backed store for
220
- * production fleets.
221
- */
222
- declare class InMemoryStore implements Store {
223
- private readonly entries;
224
- seen(messageId: string): boolean;
225
- remember(messageId: string): void;
226
- forget(messageId: string): void;
227
- }
228
- /**
229
- * Wraps `handler` so a message whose `meta.id` was already processed successfully is
230
- * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it
231
- * again (retry / dead-letter still apply); a message with no usable id runs unchanged.
232
- */
233
- declare function Wrap(store: Store, handler: Handler): Handler;
234
-
235
61
  /**
236
62
  * Optional per-URN payload schema validation (ADR-0024).
237
63
  *
@@ -310,4 +136,80 @@ declare namespace schema {
310
136
  export { schema_MapProvider as MapProvider, type schema_SchemaHandler as SchemaHandler, type schema_SchemaNode as SchemaNode, type schema_SchemaProvider as SchemaProvider, schema_check as check, schema_validate as validate, schema_validateSchema as validateSchema, schema_wrap as wrap };
311
137
  }
312
138
 
313
- export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type Handler, type HasTraceId, InMemoryStore, type IncomingEnvelope, InvalidPayloadError, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, type SchemaNode, type SchemaProvider, type Store, UnknownUrnError, UnknownUrnStrategy, Wrap, annotate, deadLetter, schema };
139
+ /**
140
+ * DLQ redrive tooling — safe replay off the dead-letter queue (ADR-0026).
141
+ *
142
+ * The Node mirror of the Go reference `babelqueue-go/redrive.go`. Because the Node core is
143
+ * codec-only (no runtime / no transport), the orchestration takes a small {@link RedriveIO}
144
+ * the caller implements over their transport — the same shape the optional `otel.publish`
145
+ * helper used. {@link resetForRedrive} is the pure, transport-free core of it.
146
+ *
147
+ * A redriven message is **reset for reprocessing**: its `dead_letter` block is removed and
148
+ * `attempts` reset to 0, while `job`, `trace_id`, `data` and `meta` are preserved verbatim, so
149
+ * the replay is still fully traceable (same `trace_id`). The wire envelope is untouched (GR-1).
150
+ *
151
+ * Replay safety here is `dryRun` + `select` + redrive-to-`toQueue` (a sandbox). The
152
+ * **Replay-Bypass** guard — a `bq-replay-bypass` transport header surfaced to handlers so a
153
+ * replay can skip external side-effects — is a documented phase-two follow-up that touches the
154
+ * runtime + every transport, like ADR-0025's `traceparent` follow-up.
155
+ */
156
+
157
+ /** A message reserved from a queue, plus a way to acknowledge (remove) it. */
158
+ interface RedriveMessage {
159
+ body: string;
160
+ ack(): Promise<void>;
161
+ }
162
+ /** The minimal transport surface {@link redrive} needs: reserve the next message, and publish. */
163
+ interface RedriveIO {
164
+ /** Reserve the next message from queue, or `null` when it is empty. */
165
+ pop(queue: string): Promise<RedriveMessage | null>;
166
+ /** Append an already-encoded body to queue. */
167
+ publish(queue: string, body: string): Promise<void>;
168
+ }
169
+ /** Options for {@link redrive}. */
170
+ interface RedriveOptions {
171
+ /** Override where messages are re-published; default is each message's `dead_letter.original_queue`. Set a sandbox queue to replay safely. */
172
+ toQueue?: string;
173
+ /** Cap how many messages are pulled from the DLQ (0 / omitted = all currently available). */
174
+ max?: number;
175
+ /** Inspect without redriving: every message is read, reported, and returned to the DLQ unchanged. */
176
+ dryRun?: boolean;
177
+ /** Pick which messages to redrive (e.g. by reason or URN). Unselected messages are returned unchanged. */
178
+ select?: (envelope: Envelope) => boolean;
179
+ }
180
+ /** What happened to one message during a {@link redrive} run. */
181
+ interface RedriveItem {
182
+ messageId: string;
183
+ traceId: string;
184
+ urn: string;
185
+ reason: string;
186
+ from: string;
187
+ /** Target queue (the plan, even on a dry run; "" when skipped or undecodable). */
188
+ to: string;
189
+ /** True only when actually re-published to `to`. */
190
+ redriven: boolean;
191
+ }
192
+ /** Summary of a {@link redrive} run. */
193
+ interface RedriveResult {
194
+ redriven: number;
195
+ skipped: number;
196
+ items: RedriveItem[];
197
+ }
198
+ /**
199
+ * Returns a copy of `envelope` reset for reprocessing: no `dead_letter` block and `attempts`
200
+ * at 0, with `job`, `trace_id`, `data` and `meta` preserved verbatim. Pure — the input is not
201
+ * mutated.
202
+ */
203
+ declare function resetForRedrive(envelope: Envelope): Envelope;
204
+ /**
205
+ * Moves dead-lettered messages off the `dlq` queue and re-publishes each — via {@link resetForRedrive} —
206
+ * to its `dead_letter.original_queue` or `opts.toQueue`.
207
+ *
208
+ * Messages are drained from the DLQ first and then processed, so restored messages (skipped,
209
+ * dry-run, or undecodable) are never re-encountered in the same run. A message is acknowledged
210
+ * only after its re-publish succeeds; an undecodable body is restored, not dropped. On a publish
211
+ * failure the message is restored to the DLQ and the error is re-thrown.
212
+ */
213
+ declare function redrive(io: RedriveIO, dlq: string, opts?: RedriveOptions): Promise<RedriveResult>;
214
+
215
+ export { type AnnotateOptions, BabelQueueError, Envelope, InvalidPayloadError, type RedriveIO, type RedriveItem, type RedriveMessage, type RedriveOptions, type RedriveResult, type SchemaNode, type SchemaProvider, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter, redrive, resetForRedrive, schema };
package/dist/index.js CHANGED
@@ -1,144 +1,12 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- // src/codec.ts
8
- import { randomUUID } from "crypto";
9
-
10
- // src/errors.ts
11
- var BabelQueueError = class extends Error {
12
- constructor(message) {
13
- super(message);
14
- this.name = "BabelQueueError";
15
- }
16
- };
17
- var UnknownUrnError = class extends BabelQueueError {
18
- constructor(urn) {
19
- super(`No handler is mapped for the message URN "${urn}".`);
20
- this.name = "UnknownUrnError";
21
- }
22
- };
23
- var InvalidPayloadError = class extends BabelQueueError {
24
- constructor(urn, violation2) {
25
- super(`Message data for "${urn}" does not match its URN schema: ${violation2}.`);
26
- this.urn = urn;
27
- this.violation = violation2;
28
- this.name = "InvalidPayloadError";
29
- }
30
- urn;
31
- violation;
32
- };
33
-
34
- // src/codec.ts
35
- var SCHEMA_VERSION = 1;
36
- var SOURCE_LANG = "node";
37
- var EnvelopeCodec = {
1
+ import {
2
+ BabelQueueError,
3
+ EnvelopeCodec,
4
+ InvalidPayloadError,
38
5
  SCHEMA_VERSION,
39
6
  SOURCE_LANG,
40
- /**
41
- * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
42
- * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
43
- * Throws {@link BabelQueueError} when the URN is blank.
44
- */
45
- make(urn, data, options = {}) {
46
- const resolvedUrn = (urn ?? "").trim();
47
- if (resolvedUrn === "") {
48
- throw new BabelQueueError(
49
- "A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
50
- );
51
- }
52
- const traceId = (options.traceId ?? "").trim() || randomUUID();
53
- return {
54
- job: resolvedUrn,
55
- trace_id: traceId,
56
- data: { ...data },
57
- meta: {
58
- id: randomUUID(),
59
- queue: options.queue ?? "default",
60
- lang: SOURCE_LANG,
61
- schema_version: SCHEMA_VERSION,
62
- created_at: Date.now()
63
- },
64
- attempts: 0
65
- };
66
- },
67
- /**
68
- * Build the envelope from a {@link PolyglotMessage}. If the message also
69
- * implements {@link HasTraceId} and returns a non-empty value, that trace id is
70
- * reused.
71
- */
72
- fromMessage(message, queue = "default") {
73
- const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
74
- return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
75
- queue,
76
- traceId
77
- });
78
- },
79
- /**
80
- * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
81
- * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
82
- * the other SDK cores.
83
- */
84
- encode(envelope) {
85
- return JSON.stringify(envelope);
86
- },
87
- /**
88
- * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
89
- * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
90
- * alias into `job`.
91
- */
92
- decode(raw) {
93
- let parsed;
94
- try {
95
- parsed = JSON.parse(raw);
96
- } catch {
97
- return {};
98
- }
99
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
100
- return {};
101
- }
102
- const envelope = parsed;
103
- if (!envelope.job && typeof envelope.urn === "string") {
104
- envelope.job = envelope.urn;
105
- }
106
- return envelope;
107
- },
108
- /** The message URN — canonical `job`, with `urn` accepted as an alias. */
109
- urn(envelope) {
110
- const value = envelope?.job ?? envelope?.urn ?? "";
111
- return typeof value === "string" ? value.trim() : "";
112
- },
113
- /**
114
- * Whether a consumer should accept this envelope. Rejects a missing URN, an
115
- * unsupported `meta.schema_version`, a non-object `data`, a non-integer
116
- * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
117
- * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
118
- */
119
- accepts(envelope) {
120
- if (EnvelopeCodec.urn(envelope) === "") {
121
- return false;
122
- }
123
- const meta = envelope.meta;
124
- if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
125
- return false;
126
- }
127
- const data = envelope.data;
128
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
129
- return false;
130
- }
131
- const attempts = envelope.attempts;
132
- if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
133
- return false;
134
- }
135
- const traceId = envelope.trace_id;
136
- if (typeof traceId !== "string" || traceId.trim() === "") {
137
- return false;
138
- }
139
- return true;
140
- }
141
- };
7
+ UnknownUrnError,
8
+ __export
9
+ } from "./chunk-7FUZ3LYT.js";
142
10
 
143
11
  // src/deadLetter.ts
144
12
  var deadLetter_exports = {};
@@ -354,6 +222,79 @@ function violation(path, reason) {
354
222
  function join(path, key) {
355
223
  return path === "" ? key : `${path}.${key}`;
356
224
  }
225
+
226
+ // src/redrive.ts
227
+ function resetForRedrive(envelope) {
228
+ return {
229
+ job: envelope.job,
230
+ trace_id: envelope.trace_id,
231
+ data: envelope.data,
232
+ meta: envelope.meta,
233
+ attempts: 0
234
+ };
235
+ }
236
+ function sourceQueueOf(envelope) {
237
+ return envelope.dead_letter?.original_queue || envelope.meta.queue;
238
+ }
239
+ async function redrive(io, dlq, opts = {}) {
240
+ const max = opts.max ?? 0;
241
+ const batch = [];
242
+ while (max === 0 || batch.length < max) {
243
+ const message = await io.pop(dlq);
244
+ if (!message) {
245
+ break;
246
+ }
247
+ const decoded = EnvelopeCodec.decode(message.body);
248
+ batch.push({ message, envelope: EnvelopeCodec.accepts(decoded) ? decoded : null });
249
+ }
250
+ const result = { redriven: 0, skipped: 0, items: [] };
251
+ for (const { message, envelope } of batch) {
252
+ if (!envelope) {
253
+ await io.publish(dlq, message.body);
254
+ await message.ack();
255
+ result.skipped++;
256
+ result.items.push({ messageId: "", traceId: "", urn: "", reason: "", from: dlq, to: "", redriven: false });
257
+ continue;
258
+ }
259
+ const item = {
260
+ messageId: envelope.meta.id,
261
+ traceId: envelope.trace_id,
262
+ urn: EnvelopeCodec.urn(envelope),
263
+ reason: envelope.dead_letter?.reason ?? "",
264
+ from: dlq,
265
+ to: "",
266
+ redriven: false
267
+ };
268
+ if (opts.select && !opts.select(envelope)) {
269
+ await io.publish(dlq, message.body);
270
+ await message.ack();
271
+ result.skipped++;
272
+ result.items.push(item);
273
+ continue;
274
+ }
275
+ const target = opts.toQueue ?? sourceQueueOf(envelope);
276
+ item.to = target;
277
+ if (opts.dryRun) {
278
+ await io.publish(dlq, message.body);
279
+ await message.ack();
280
+ result.skipped++;
281
+ result.items.push(item);
282
+ continue;
283
+ }
284
+ try {
285
+ await io.publish(target, EnvelopeCodec.encode(resetForRedrive(envelope)));
286
+ } catch (err) {
287
+ await io.publish(dlq, message.body);
288
+ await message.ack();
289
+ throw err;
290
+ }
291
+ await message.ack();
292
+ item.redriven = true;
293
+ result.redriven++;
294
+ result.items.push(item);
295
+ }
296
+ return result;
297
+ }
357
298
  export {
358
299
  BabelQueueError,
359
300
  EnvelopeCodec,
@@ -366,6 +307,8 @@ export {
366
307
  Wrap,
367
308
  annotate,
368
309
  deadLetter_exports as deadLetter,
310
+ redrive,
311
+ resetForRedrive,
369
312
  schema_exports as schema
370
313
  };
371
314
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n\n/**\n * Raised when a message's `data` does not match the JSON Schema registered for its URN\n * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers\n * (and eventually dead-letters) a poison message.\n */\nexport class InvalidPayloadError extends BabelQueueError {\n constructor(\n readonly urn: string,\n readonly violation: string,\n ) {\n super(`Message data for \"${urn}\" does not match its URN schema: ${violation}.`);\n this.name = \"InvalidPayloadError\";\n }\n}\n","import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n","/**\n * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.\n *\n * The Node mirror of the PHP `BabelQueue\\Idempotency` and Go `idempotency` helpers.\n * The core is codec-only (no dispatcher), so this wraps a user-provided handler that\n * an adapter (NestJS, BullMQ, ...) drives:\n *\n * ```ts\n * import { Wrap, InMemoryStore, type Handler } from \"@babelqueue/core\";\n *\n * const store = new InMemoryStore();\n * const handler = Wrap(store, async (env) => { ... });\n * ```\n *\n * A previously-seen id returns early (the adapter acks it); a throwing/rejecting\n * handler leaves the id unmarked so a redelivery runs it again; a message with no\n * usable `meta.id` runs unchanged. \"Seen-set\" post-success dedupe — not exactly-once,\n * not in-flight concurrency locking (a transactional mode is a future direction).\n */\nimport type { Envelope } from \"./codec.js\";\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type Handler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may\n * be sync or async so a production store can be Redis- or DB-backed; the reference\n * {@link InMemoryStore} is synchronous.\n */\nexport interface Store {\n seen(messageId: string): boolean | Promise<boolean>;\n remember(messageId: string): void | Promise<void>;\n forget(messageId: string): void | Promise<void>;\n}\n\n/**\n * Process-local {@link Store} backed by a Set. For tests / single-process consumers;\n * not shared across workers and not persistent — use a Redis- or DB-backed store for\n * production fleets.\n */\nexport class InMemoryStore implements Store {\n private readonly entries = new Set<string>();\n\n seen(messageId: string): boolean {\n return this.entries.has(messageId);\n }\n\n remember(messageId: string): void {\n this.entries.add(messageId);\n }\n\n forget(messageId: string): void {\n this.entries.delete(messageId);\n }\n}\n\n/**\n * Wraps `handler` so a message whose `meta.id` was already processed successfully is\n * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it\n * again (retry / dead-letter still apply); a message with no usable id runs unchanged.\n */\nexport function Wrap(store: Store, handler: Handler): Handler {\n return async (env: Envelope): Promise<void> => {\n const id = env.meta.id;\n\n // No usable id → cannot dedupe; run the handler unchanged.\n if (!id) {\n await handler(env);\n return;\n }\n\n // Already processed on an earlier delivery: return so the adapter acks it.\n if (await store.seen(id)) {\n return;\n }\n\n // First success wins; a throw here leaves the id unmarked → retry/DLQ apply.\n await handler(env);\n await store.remember(id);\n };\n}\n","/**\n * Optional per-URN payload schema validation (ADR-0024).\n *\n * The Node mirror of the Go `schema` package and PHP `BabelQueue\\Schema`. A\n * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a\n * babelqueue-registry `registry.json` — and the message's `data` is validated against it.\n * It is opt-in: a URN with no registered schema is never validated.\n *\n * ```ts\n * import { schema } from \"@babelqueue/core\";\n *\n * const provider = schema.MapProvider.fromJson({ \"urn:babel:orders:created\": ORDERS_JSON });\n * schema.validate(provider, \"urn:babel:orders:created\", { order_id: 7 }); // throws on mismatch\n * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net\n * ```\n *\n * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node\n * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to\n * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)\n * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`\n * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,\n * `const`, `minLength`, `minimum`. Unknown keywords are ignored.\n */\nimport type { Envelope } from \"./codec.js\";\nimport { InvalidPayloadError } from \"./errors.js\";\n\n/** A parsed JSON Schema node. */\nexport type SchemaNode = Record<string, unknown>;\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type SchemaHandler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or\n * async so a production provider can be service- or cache-backed; the reference\n * {@link MapProvider} is synchronous.\n */\nexport interface SchemaProvider {\n schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;\n}\n\n/** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */\nexport class MapProvider implements SchemaProvider {\n private readonly schemas: Map<string, SchemaNode>;\n\n constructor(schemas: Record<string, SchemaNode>) {\n this.schemas = new Map(Object.entries(schemas));\n }\n\n /** Build a provider from URN -> raw JSON Schema strings, parsing each. */\n static fromJson(raw: Record<string, string>): MapProvider {\n const schemas: Record<string, SchemaNode> = {};\n for (const [urn, body] of Object.entries(raw)) {\n const decoded: unknown = JSON.parse(body);\n if (typeof decoded !== \"object\" || decoded === null || Array.isArray(decoded)) {\n throw new Error(`schema: invalid JSON schema for \"${urn}\"`);\n }\n schemas[urn] = decoded as SchemaNode;\n }\n return new MapProvider(schemas);\n }\n\n schemaFor(urn: string): SchemaNode | undefined {\n return this.schemas.get(urn);\n }\n}\n\n/**\n * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is\n * registered for the URN (opt-in). For producer-side branching.\n */\nexport async function check(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<string | null> {\n const schemaNode = await provider.schemaFor(urn);\n if (!schemaNode) {\n return null;\n }\n return validateSchema(schemaNode, data);\n}\n\n/**\n * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}\n * otherwise. The producer-side guard; call it before publishing.\n */\nexport async function validate(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const violation = await check(provider, urn, data);\n if (violation !== null) {\n throw new InvalidPayloadError(urn, violation);\n }\n}\n\n/**\n * Wrap a consume handler so each message's `data` is validated against its URN's schema\n * before the handler runs (consumer-side safety net). Invalid data throws\n * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the\n * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}\n * producer-side to keep invalid data out of the queue entirely.\n */\nexport function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler {\n return async (env: Envelope): Promise<void> => {\n await validate(provider, env.job, env.data);\n await handler(env);\n };\n}\n\n/** The first violation of `value` against a (subset) JSON Schema node, or null. */\nexport function validateSchema(schema: SchemaNode, value: unknown, path = \"\"): string | null {\n if (\"const\" in schema && !equal(value, schema.const)) {\n return violation(path, \"wrong_const\");\n }\n const enumValues = schema.enum;\n if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {\n return violation(path, \"not_in_enum\");\n }\n\n const type = typeof schema.type === \"string\" ? schema.type : \"\";\n switch (type) {\n case \"object\":\n return checkObject(schema, value, path);\n case \"array\":\n return checkArray(schema, value, path);\n case \"string\": {\n if (typeof value !== \"string\") {\n return violation(path, \"not_a_string\");\n }\n const minLength = schema.minLength;\n if (typeof minLength === \"number\" && value.length < minLength) {\n return violation(path, \"below_min_length\");\n }\n return null;\n }\n case \"integer\":\n if (!isInteger(value)) {\n return violation(path, \"not_an_integer\");\n }\n return checkMinimum(schema, value, path);\n case \"number\":\n if (typeof value !== \"number\") {\n return violation(path, \"not_a_number\");\n }\n return checkMinimum(schema, value, path);\n case \"boolean\":\n return typeof value === \"boolean\" ? null : violation(path, \"not_a_boolean\");\n case \"null\":\n return value === null ? null : violation(path, \"not_null\");\n default:\n return null;\n }\n}\n\nfunction checkObject(schema: SchemaNode, value: unknown, path: string): string | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return violation(path, \"not_an_object\");\n }\n const obj = value as Record<string, unknown>;\n\n const required = schema.required;\n if (Array.isArray(required)) {\n for (const key of required) {\n if (typeof key === \"string\" && !(key in obj)) {\n return violation(join(path, key), \"missing_required\");\n }\n }\n }\n\n const properties =\n typeof schema.properties === \"object\" && schema.properties !== null\n ? (schema.properties as Record<string, unknown>)\n : {};\n const additionalAllowed = schema.additionalProperties !== false;\n\n for (const [name, item] of Object.entries(obj)) {\n const propSchema = properties[name];\n if (typeof propSchema === \"object\" && propSchema !== null) {\n const found = validateSchema(propSchema as SchemaNode, item, join(path, name));\n if (found !== null) {\n return found;\n }\n continue;\n }\n if (!additionalAllowed) {\n return violation(join(path, name), \"additional_not_allowed\");\n }\n }\n\n return null;\n}\n\nfunction checkArray(schema: SchemaNode, value: unknown, path: string): string | null {\n if (!Array.isArray(value)) {\n return violation(path, \"not_an_array\");\n }\n const items = schema.items;\n if (typeof items !== \"object\" || items === null) {\n return null;\n }\n for (let i = 0; i < value.length; i++) {\n const found = validateSchema(items as SchemaNode, value[i], `${path}[${i}]`);\n if (found !== null) {\n return found;\n }\n }\n return null;\n}\n\nfunction checkMinimum(schema: SchemaNode, value: number, path: string): string | null {\n const minimum = schema.minimum;\n if (typeof minimum === \"number\" && value < minimum) {\n return violation(path, \"below_minimum\");\n }\n return null;\n}\n\n// JSON numbers are all `number` in JS; an integer is a whole number (and never a boolean).\nfunction isInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value);\n}\n\n// Structural equality for enum/const checks: JSON.stringify distinguishes a string \"1\" from\n// a number 1, matching the strict comparisons in the other SDK validators.\nfunction equal(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction violation(path: string, reason: string): string {\n return `${path === \"\" ? \"<root>\" : path}: ${reason}`;\n}\n\nfunction join(path: string, key: string): string {\n return path === \"\" ? key : `${path}.${key}`;\n}\n"],"mappings":"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","names":["violation","violation"]}
1
+ {"version":3,"sources":["../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts","../src/redrive.ts"],"sourcesContent":["import type { DeadLetter, Envelope } from \"./codec.js\";\nimport { SOURCE_LANG } from \"./codec.js\";\n\n/** Options for {@link annotate}. */\nexport interface AnnotateOptions {\n /** Defaults to the envelope's current `attempts`. */\n attempts?: number;\n /** A human-readable error message (JSON `null` when omitted). */\n error?: string | null;\n /** The originating error type/class name (JSON `null` when omitted). */\n exception?: string | null;\n}\n\n/**\n * Return a copy of the envelope with a `dead_letter` block attached, recording\n * why and where it failed. The original envelope is preserved unchanged inside\n * the result, so any-language consumers can still read it.\n */\nexport function annotate(\n envelope: Envelope,\n reason: string,\n originalQueue: string,\n options: AnnotateOptions = {},\n): Envelope {\n const deadLetter: DeadLetter = {\n reason,\n error: options.error ?? null,\n exception: options.exception ?? null,\n failed_at: Date.now(),\n original_queue: originalQueue,\n attempts: options.attempts ?? envelope.attempts ?? 0,\n lang: SOURCE_LANG,\n };\n\n return { ...envelope, dead_letter: deadLetter };\n}\n","/**\n * What a consumer does with a message whose URN has no registered handler.\n * Mirrors the constants in every other SDK core.\n */\nexport const UnknownUrnStrategy = {\n /** Surface an error; let the worker decide. */\n FAIL: \"fail\",\n /** Drop the message. */\n DELETE: \"delete\",\n /** Requeue for another consumer. */\n RELEASE: \"release\",\n /** Route to the dead-letter queue. */\n DEAD_LETTER: \"dead_letter\",\n} as const;\n\nexport type UnknownUrnStrategy =\n (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];\n","/**\n * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.\n *\n * The Node mirror of the PHP `BabelQueue\\Idempotency` and Go `idempotency` helpers.\n * The core is codec-only (no dispatcher), so this wraps a user-provided handler that\n * an adapter (NestJS, BullMQ, ...) drives:\n *\n * ```ts\n * import { Wrap, InMemoryStore, type Handler } from \"@babelqueue/core\";\n *\n * const store = new InMemoryStore();\n * const handler = Wrap(store, async (env) => { ... });\n * ```\n *\n * A previously-seen id returns early (the adapter acks it); a throwing/rejecting\n * handler leaves the id unmarked so a redelivery runs it again; a message with no\n * usable `meta.id` runs unchanged. \"Seen-set\" post-success dedupe — not exactly-once,\n * not in-flight concurrency locking (a transactional mode is a future direction).\n */\nimport type { Envelope } from \"./codec.js\";\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type Handler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may\n * be sync or async so a production store can be Redis- or DB-backed; the reference\n * {@link InMemoryStore} is synchronous.\n */\nexport interface Store {\n seen(messageId: string): boolean | Promise<boolean>;\n remember(messageId: string): void | Promise<void>;\n forget(messageId: string): void | Promise<void>;\n}\n\n/**\n * Process-local {@link Store} backed by a Set. For tests / single-process consumers;\n * not shared across workers and not persistent — use a Redis- or DB-backed store for\n * production fleets.\n */\nexport class InMemoryStore implements Store {\n private readonly entries = new Set<string>();\n\n seen(messageId: string): boolean {\n return this.entries.has(messageId);\n }\n\n remember(messageId: string): void {\n this.entries.add(messageId);\n }\n\n forget(messageId: string): void {\n this.entries.delete(messageId);\n }\n}\n\n/**\n * Wraps `handler` so a message whose `meta.id` was already processed successfully is\n * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it\n * again (retry / dead-letter still apply); a message with no usable id runs unchanged.\n */\nexport function Wrap(store: Store, handler: Handler): Handler {\n return async (env: Envelope): Promise<void> => {\n const id = env.meta.id;\n\n // No usable id → cannot dedupe; run the handler unchanged.\n if (!id) {\n await handler(env);\n return;\n }\n\n // Already processed on an earlier delivery: return so the adapter acks it.\n if (await store.seen(id)) {\n return;\n }\n\n // First success wins; a throw here leaves the id unmarked → retry/DLQ apply.\n await handler(env);\n await store.remember(id);\n };\n}\n","/**\n * Optional per-URN payload schema validation (ADR-0024).\n *\n * The Node mirror of the Go `schema` package and PHP `BabelQueue\\Schema`. A\n * {@link SchemaProvider} supplies a JSON Schema for a message URN — typically built from a\n * babelqueue-registry `registry.json` — and the message's `data` is validated against it.\n * It is opt-in: a URN with no registered schema is never validated.\n *\n * ```ts\n * import { schema } from \"@babelqueue/core\";\n *\n * const provider = schema.MapProvider.fromJson({ \"urn:babel:orders:created\": ORDERS_JSON });\n * schema.validate(provider, \"urn:babel:orders:created\", { order_id: 7 }); // throws on mismatch\n * const handler = schema.wrap(provider, async (env) => { ... }); // consumer safety net\n * ```\n *\n * The core stays dependency-free and I/O-free, so it carries no file-based provider: a Node\n * app or adapter reads its `registry.json` (with `node:fs`, etc.) and passes the schemas to\n * {@link MapProvider.fromJson}. The validator is a small subset of JSON Schema (draft-07)\n * whose verdicts match the Go, PHP and Python validators and babelqueue-registry's `compat`\n * linter: `type`, `required`, `properties`, `additionalProperties`, `items`, `enum`,\n * `const`, `minLength`, `minimum`. Unknown keywords are ignored.\n */\nimport type { Envelope } from \"./codec.js\";\nimport { InvalidPayloadError } from \"./errors.js\";\n\n/** A parsed JSON Schema node. */\nexport type SchemaNode = Record<string, unknown>;\n\n/** A consume handler: receives a decoded envelope, may be sync or async. */\nexport type SchemaHandler = (env: Envelope) => void | Promise<void>;\n\n/**\n * A source of per-URN `data` schemas, keyed on the message URN. `schemaFor` may be sync or\n * async so a production provider can be service- or cache-backed; the reference\n * {@link MapProvider} is synchronous.\n */\nexport interface SchemaProvider {\n schemaFor(urn: string): SchemaNode | undefined | Promise<SchemaNode | undefined>;\n}\n\n/** In-memory {@link SchemaProvider}, for tests and for embedding schemas in code. */\nexport class MapProvider implements SchemaProvider {\n private readonly schemas: Map<string, SchemaNode>;\n\n constructor(schemas: Record<string, SchemaNode>) {\n this.schemas = new Map(Object.entries(schemas));\n }\n\n /** Build a provider from URN -> raw JSON Schema strings, parsing each. */\n static fromJson(raw: Record<string, string>): MapProvider {\n const schemas: Record<string, SchemaNode> = {};\n for (const [urn, body] of Object.entries(raw)) {\n const decoded: unknown = JSON.parse(body);\n if (typeof decoded !== \"object\" || decoded === null || Array.isArray(decoded)) {\n throw new Error(`schema: invalid JSON schema for \"${urn}\"`);\n }\n schemas[urn] = decoded as SchemaNode;\n }\n return new MapProvider(schemas);\n }\n\n schemaFor(urn: string): SchemaNode | undefined {\n return this.schemas.get(urn);\n }\n}\n\n/**\n * The first `data` violation for `(urn, data)`, or null when it is valid or when no schema is\n * registered for the URN (opt-in). For producer-side branching.\n */\nexport async function check(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<string | null> {\n const schemaNode = await provider.schemaFor(urn);\n if (!schemaNode) {\n return null;\n }\n return validateSchema(schemaNode, data);\n}\n\n/**\n * Validate `(urn, data)` against its registered schema, throwing {@link InvalidPayloadError}\n * otherwise. The producer-side guard; call it before publishing.\n */\nexport async function validate(\n provider: SchemaProvider,\n urn: string,\n data: Record<string, unknown>,\n): Promise<void> {\n const violation = await check(provider, urn, data);\n if (violation !== null) {\n throw new InvalidPayloadError(urn, violation);\n }\n}\n\n/**\n * Wrap a consume handler so each message's `data` is validated against its URN's schema\n * before the handler runs (consumer-side safety net). Invalid data throws\n * {@link InvalidPayloadError}, so the adapter redelivers (and eventually dead-letters) the\n * poison message; a URN with no schema runs the handler unchanged. Prefer {@link check}\n * producer-side to keep invalid data out of the queue entirely.\n */\nexport function wrap(provider: SchemaProvider, handler: SchemaHandler): SchemaHandler {\n return async (env: Envelope): Promise<void> => {\n await validate(provider, env.job, env.data);\n await handler(env);\n };\n}\n\n/** The first violation of `value` against a (subset) JSON Schema node, or null. */\nexport function validateSchema(schema: SchemaNode, value: unknown, path = \"\"): string | null {\n if (\"const\" in schema && !equal(value, schema.const)) {\n return violation(path, \"wrong_const\");\n }\n const enumValues = schema.enum;\n if (Array.isArray(enumValues) && !enumValues.some((item) => equal(value, item))) {\n return violation(path, \"not_in_enum\");\n }\n\n const type = typeof schema.type === \"string\" ? schema.type : \"\";\n switch (type) {\n case \"object\":\n return checkObject(schema, value, path);\n case \"array\":\n return checkArray(schema, value, path);\n case \"string\": {\n if (typeof value !== \"string\") {\n return violation(path, \"not_a_string\");\n }\n const minLength = schema.minLength;\n if (typeof minLength === \"number\" && value.length < minLength) {\n return violation(path, \"below_min_length\");\n }\n return null;\n }\n case \"integer\":\n if (!isInteger(value)) {\n return violation(path, \"not_an_integer\");\n }\n return checkMinimum(schema, value, path);\n case \"number\":\n if (typeof value !== \"number\") {\n return violation(path, \"not_a_number\");\n }\n return checkMinimum(schema, value, path);\n case \"boolean\":\n return typeof value === \"boolean\" ? null : violation(path, \"not_a_boolean\");\n case \"null\":\n return value === null ? null : violation(path, \"not_null\");\n default:\n return null;\n }\n}\n\nfunction checkObject(schema: SchemaNode, value: unknown, path: string): string | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return violation(path, \"not_an_object\");\n }\n const obj = value as Record<string, unknown>;\n\n const required = schema.required;\n if (Array.isArray(required)) {\n for (const key of required) {\n if (typeof key === \"string\" && !(key in obj)) {\n return violation(join(path, key), \"missing_required\");\n }\n }\n }\n\n const properties =\n typeof schema.properties === \"object\" && schema.properties !== null\n ? (schema.properties as Record<string, unknown>)\n : {};\n const additionalAllowed = schema.additionalProperties !== false;\n\n for (const [name, item] of Object.entries(obj)) {\n const propSchema = properties[name];\n if (typeof propSchema === \"object\" && propSchema !== null) {\n const found = validateSchema(propSchema as SchemaNode, item, join(path, name));\n if (found !== null) {\n return found;\n }\n continue;\n }\n if (!additionalAllowed) {\n return violation(join(path, name), \"additional_not_allowed\");\n }\n }\n\n return null;\n}\n\nfunction checkArray(schema: SchemaNode, value: unknown, path: string): string | null {\n if (!Array.isArray(value)) {\n return violation(path, \"not_an_array\");\n }\n const items = schema.items;\n if (typeof items !== \"object\" || items === null) {\n return null;\n }\n for (let i = 0; i < value.length; i++) {\n const found = validateSchema(items as SchemaNode, value[i], `${path}[${i}]`);\n if (found !== null) {\n return found;\n }\n }\n return null;\n}\n\nfunction checkMinimum(schema: SchemaNode, value: number, path: string): string | null {\n const minimum = schema.minimum;\n if (typeof minimum === \"number\" && value < minimum) {\n return violation(path, \"below_minimum\");\n }\n return null;\n}\n\n// JSON numbers are all `number` in JS; an integer is a whole number (and never a boolean).\nfunction isInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value);\n}\n\n// Structural equality for enum/const checks: JSON.stringify distinguishes a string \"1\" from\n// a number 1, matching the strict comparisons in the other SDK validators.\nfunction equal(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction violation(path: string, reason: string): string {\n return `${path === \"\" ? \"<root>\" : path}: ${reason}`;\n}\n\nfunction join(path: string, key: string): string {\n return path === \"\" ? key : `${path}.${key}`;\n}\n","/**\n * DLQ redrive tooling — safe replay off the dead-letter queue (ADR-0026).\n *\n * The Node mirror of the Go reference `babelqueue-go/redrive.go`. Because the Node core is\n * codec-only (no runtime / no transport), the orchestration takes a small {@link RedriveIO}\n * the caller implements over their transport — the same shape the optional `otel.publish`\n * helper used. {@link resetForRedrive} is the pure, transport-free core of it.\n *\n * A redriven message is **reset for reprocessing**: its `dead_letter` block is removed and\n * `attempts` reset to 0, while `job`, `trace_id`, `data` and `meta` are preserved verbatim, so\n * the replay is still fully traceable (same `trace_id`). The wire envelope is untouched (GR-1).\n *\n * Replay safety here is `dryRun` + `select` + redrive-to-`toQueue` (a sandbox). The\n * **Replay-Bypass** guard — a `bq-replay-bypass` transport header surfaced to handlers so a\n * replay can skip external side-effects — is a documented phase-two follow-up that touches the\n * runtime + every transport, like ADR-0025's `traceparent` follow-up.\n */\n\nimport { EnvelopeCodec, type Envelope } from \"./codec.js\";\n\n/** A message reserved from a queue, plus a way to acknowledge (remove) it. */\nexport interface RedriveMessage {\n body: string;\n ack(): Promise<void>;\n}\n\n/** The minimal transport surface {@link redrive} needs: reserve the next message, and publish. */\nexport interface RedriveIO {\n /** Reserve the next message from queue, or `null` when it is empty. */\n pop(queue: string): Promise<RedriveMessage | null>;\n /** Append an already-encoded body to queue. */\n publish(queue: string, body: string): Promise<void>;\n}\n\n/** Options for {@link redrive}. */\nexport interface RedriveOptions {\n /** Override where messages are re-published; default is each message's `dead_letter.original_queue`. Set a sandbox queue to replay safely. */\n toQueue?: string;\n /** Cap how many messages are pulled from the DLQ (0 / omitted = all currently available). */\n max?: number;\n /** Inspect without redriving: every message is read, reported, and returned to the DLQ unchanged. */\n dryRun?: boolean;\n /** Pick which messages to redrive (e.g. by reason or URN). Unselected messages are returned unchanged. */\n select?: (envelope: Envelope) => boolean;\n}\n\n/** What happened to one message during a {@link redrive} run. */\nexport interface RedriveItem {\n messageId: string;\n traceId: string;\n urn: string;\n reason: string;\n from: string;\n /** Target queue (the plan, even on a dry run; \"\" when skipped or undecodable). */\n to: string;\n /** True only when actually re-published to `to`. */\n redriven: boolean;\n}\n\n/** Summary of a {@link redrive} run. */\nexport interface RedriveResult {\n redriven: number;\n skipped: number;\n items: RedriveItem[];\n}\n\n/**\n * Returns a copy of `envelope` reset for reprocessing: no `dead_letter` block and `attempts`\n * at 0, with `job`, `trace_id`, `data` and `meta` preserved verbatim. Pure — the input is not\n * mutated.\n */\nexport function resetForRedrive(envelope: Envelope): Envelope {\n return {\n job: envelope.job,\n trace_id: envelope.trace_id,\n data: envelope.data,\n meta: envelope.meta,\n attempts: 0,\n };\n}\n\nfunction sourceQueueOf(envelope: Envelope): string {\n return envelope.dead_letter?.original_queue || envelope.meta.queue;\n}\n\n/**\n * Moves dead-lettered messages off the `dlq` queue and re-publishes each — via {@link resetForRedrive} —\n * to its `dead_letter.original_queue` or `opts.toQueue`.\n *\n * Messages are drained from the DLQ first and then processed, so restored messages (skipped,\n * dry-run, or undecodable) are never re-encountered in the same run. A message is acknowledged\n * only after its re-publish succeeds; an undecodable body is restored, not dropped. On a publish\n * failure the message is restored to the DLQ and the error is re-thrown.\n */\nexport async function redrive(\n io: RedriveIO,\n dlq: string,\n opts: RedriveOptions = {},\n): Promise<RedriveResult> {\n const max = opts.max ?? 0;\n\n interface Pending {\n message: RedriveMessage;\n envelope: Envelope | null;\n }\n const batch: Pending[] = [];\n while (max === 0 || batch.length < max) {\n const message = await io.pop(dlq);\n if (!message) {\n break;\n }\n const decoded = EnvelopeCodec.decode(message.body);\n batch.push({ message, envelope: EnvelopeCodec.accepts(decoded) ? decoded : null });\n }\n\n const result: RedriveResult = { redriven: 0, skipped: 0, items: [] };\n\n for (const { message, envelope } of batch) {\n if (!envelope) {\n await io.publish(dlq, message.body); // restore the undecodable body; never drop it\n await message.ack();\n result.skipped++;\n result.items.push({ messageId: \"\", traceId: \"\", urn: \"\", reason: \"\", from: dlq, to: \"\", redriven: false });\n continue;\n }\n\n const item: RedriveItem = {\n messageId: envelope.meta.id,\n traceId: envelope.trace_id,\n urn: EnvelopeCodec.urn(envelope),\n reason: envelope.dead_letter?.reason ?? \"\",\n from: dlq,\n to: \"\",\n redriven: false,\n };\n\n if (opts.select && !opts.select(envelope)) {\n await io.publish(dlq, message.body); // not selected: restore unchanged\n await message.ack();\n result.skipped++;\n result.items.push(item);\n continue;\n }\n\n const target = opts.toQueue ?? sourceQueueOf(envelope);\n item.to = target;\n\n if (opts.dryRun) {\n await io.publish(dlq, message.body); // report the plan; restore unchanged\n await message.ack();\n result.skipped++;\n result.items.push(item);\n continue;\n }\n\n try {\n await io.publish(target, EnvelopeCodec.encode(resetForRedrive(envelope)));\n } catch (err) {\n await io.publish(dlq, message.body); // restore on a publish failure\n await message.ack();\n throw err;\n }\n await message.ack();\n item.redriven = true;\n result.redriven++;\n result.items.push(item);\n }\n\n return result;\n}\n"],"mappings":"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","names":["violation"]}