@babelqueue/core 1.2.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.cjs CHANGED
@@ -31,6 +31,8 @@ __export(index_exports, {
31
31
  Wrap: () => Wrap,
32
32
  annotate: () => annotate,
33
33
  deadLetter: () => deadLetter_exports,
34
+ redrive: () => redrive,
35
+ resetForRedrive: () => resetForRedrive,
34
36
  schema: () => schema_exports
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
@@ -385,6 +387,79 @@ function violation(path, reason) {
385
387
  function join(path, key) {
386
388
  return path === "" ? key : `${path}.${key}`;
387
389
  }
390
+
391
+ // src/redrive.ts
392
+ function resetForRedrive(envelope) {
393
+ return {
394
+ job: envelope.job,
395
+ trace_id: envelope.trace_id,
396
+ data: envelope.data,
397
+ meta: envelope.meta,
398
+ attempts: 0
399
+ };
400
+ }
401
+ function sourceQueueOf(envelope) {
402
+ return envelope.dead_letter?.original_queue || envelope.meta.queue;
403
+ }
404
+ async function redrive(io, dlq, opts = {}) {
405
+ const max = opts.max ?? 0;
406
+ const batch = [];
407
+ while (max === 0 || batch.length < max) {
408
+ const message = await io.pop(dlq);
409
+ if (!message) {
410
+ break;
411
+ }
412
+ const decoded = EnvelopeCodec.decode(message.body);
413
+ batch.push({ message, envelope: EnvelopeCodec.accepts(decoded) ? decoded : null });
414
+ }
415
+ const result = { redriven: 0, skipped: 0, items: [] };
416
+ for (const { message, envelope } of batch) {
417
+ if (!envelope) {
418
+ await io.publish(dlq, message.body);
419
+ await message.ack();
420
+ result.skipped++;
421
+ result.items.push({ messageId: "", traceId: "", urn: "", reason: "", from: dlq, to: "", redriven: false });
422
+ continue;
423
+ }
424
+ const item = {
425
+ messageId: envelope.meta.id,
426
+ traceId: envelope.trace_id,
427
+ urn: EnvelopeCodec.urn(envelope),
428
+ reason: envelope.dead_letter?.reason ?? "",
429
+ from: dlq,
430
+ to: "",
431
+ redriven: false
432
+ };
433
+ if (opts.select && !opts.select(envelope)) {
434
+ await io.publish(dlq, message.body);
435
+ await message.ack();
436
+ result.skipped++;
437
+ result.items.push(item);
438
+ continue;
439
+ }
440
+ const target = opts.toQueue ?? sourceQueueOf(envelope);
441
+ item.to = target;
442
+ if (opts.dryRun) {
443
+ await io.publish(dlq, message.body);
444
+ await message.ack();
445
+ result.skipped++;
446
+ result.items.push(item);
447
+ continue;
448
+ }
449
+ try {
450
+ await io.publish(target, EnvelopeCodec.encode(resetForRedrive(envelope)));
451
+ } catch (err) {
452
+ await io.publish(dlq, message.body);
453
+ await message.ack();
454
+ throw err;
455
+ }
456
+ await message.ack();
457
+ item.redriven = true;
458
+ result.redriven++;
459
+ result.items.push(item);
460
+ }
461
+ return result;
462
+ }
388
463
  // Annotate the CommonJS export names for ESM import in node:
389
464
  0 && (module.exports = {
390
465
  BabelQueueError,
@@ -398,6 +473,8 @@ function join(path, key) {
398
473
  Wrap,
399
474
  annotate,
400
475
  deadLetter,
476
+ redrive,
477
+ resetForRedrive,
401
478
  schema
402
479
  });
403
480
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts"],"sourcesContent":["/**\n * BabelQueue — Polyglot Queues, Simplified.\n *\n * The framework-agnostic Node/TypeScript core: the canonical wire-envelope codec,\n * contracts and dead-letter helpers. Zero runtime dependencies.\n *\n * ```ts\n * import { EnvelopeCodec } from \"@babelqueue/core\";\n *\n * const env = EnvelopeCodec.make(\"urn:babel:orders:created\", { order_id: 1042 });\n * const body = EnvelopeCodec.encode(env); // publish body to Redis / RabbitMQ / ...\n * ```\n *\n * Full spec: https://babelqueue.com\n */\n\nexport { EnvelopeCodec, SCHEMA_VERSION, SOURCE_LANG } from \"./codec.js\";\nexport type {\n DeadLetter,\n Envelope,\n IncomingEnvelope,\n MakeOptions,\n Meta,\n} from \"./codec.js\";\n\nexport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\n\nexport { annotate } from \"./deadLetter.js\";\nexport type { AnnotateOptions } from \"./deadLetter.js\";\nexport * as deadLetter from \"./deadLetter.js\";\n\nexport { UnknownUrnStrategy } from \"./routing.js\";\n\nexport { BabelQueueError, UnknownUrnError, InvalidPayloadError } from \"./errors.js\";\n\nexport { Wrap, InMemoryStore } from \"./idempotency.js\";\nexport type { Handler, Store } from \"./idempotency.js\";\n\nexport * as schema from \"./schema.js\";\nexport type { SchemaProvider, SchemaNode } from \"./schema.js\";\n","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/index.ts","../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.ts","../src/redrive.ts"],"sourcesContent":["/**\n * BabelQueue — Polyglot Queues, Simplified.\n *\n * The framework-agnostic Node/TypeScript core: the canonical wire-envelope codec,\n * contracts and dead-letter helpers. Zero runtime dependencies.\n *\n * ```ts\n * import { EnvelopeCodec } from \"@babelqueue/core\";\n *\n * const env = EnvelopeCodec.make(\"urn:babel:orders:created\", { order_id: 1042 });\n * const body = EnvelopeCodec.encode(env); // publish body to Redis / RabbitMQ / ...\n * ```\n *\n * Full spec: https://babelqueue.com\n */\n\nexport { EnvelopeCodec, SCHEMA_VERSION, SOURCE_LANG } from \"./codec.js\";\nexport type {\n DeadLetter,\n Envelope,\n IncomingEnvelope,\n MakeOptions,\n Meta,\n} from \"./codec.js\";\n\nexport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\n\nexport { annotate } from \"./deadLetter.js\";\nexport type { AnnotateOptions } from \"./deadLetter.js\";\nexport * as deadLetter from \"./deadLetter.js\";\n\nexport { UnknownUrnStrategy } from \"./routing.js\";\n\nexport { BabelQueueError, UnknownUrnError, InvalidPayloadError } from \"./errors.js\";\n\nexport { Wrap, InMemoryStore } from \"./idempotency.js\";\nexport type { Handler, Store } from \"./idempotency.js\";\n\nexport * as schema from \"./schema.js\";\nexport type { SchemaProvider, SchemaNode } from \"./schema.js\";\n\nexport { redrive, resetForRedrive } from \"./redrive.js\";\nexport type {\n RedriveIO,\n RedriveItem,\n RedriveMessage,\n RedriveOptions,\n RedriveResult,\n} from \"./redrive.js\";\n","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","/**\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","violation"]}
package/dist/index.d.cts CHANGED
@@ -136,4 +136,80 @@ declare namespace schema {
136
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 };
137
137
  }
138
138
 
139
- export { type AnnotateOptions, BabelQueueError, Envelope, InvalidPayloadError, type SchemaNode, type SchemaProvider, UnknownUrnError, UnknownUrnStrategy, 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.d.ts CHANGED
@@ -136,4 +136,80 @@ declare namespace schema {
136
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 };
137
137
  }
138
138
 
139
- export { type AnnotateOptions, BabelQueueError, Envelope, InvalidPayloadError, type SchemaNode, type SchemaProvider, UnknownUrnError, UnknownUrnStrategy, 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
@@ -222,6 +222,79 @@ function violation(path, reason) {
222
222
  function join(path, key) {
223
223
  return path === "" ? key : `${path}.${key}`;
224
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
+ }
225
298
  export {
226
299
  BabelQueueError,
227
300
  EnvelopeCodec,
@@ -234,6 +307,8 @@ export {
234
307
  Wrap,
235
308
  annotate,
236
309
  deadLetter_exports as deadLetter,
310
+ redrive,
311
+ resetForRedrive,
237
312
  schema_exports as schema
238
313
  };
239
314
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/deadLetter.ts","../src/routing.ts","../src/idempotency.ts","../src/schema.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"],"mappings":"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","names":["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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Polyglot Queues, Simplified — the Node/TypeScript core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.",
5
5
  "keywords": [
6
6
  "queue",
@@ -59,7 +59,7 @@
59
59
  "build": "tsup",
60
60
  "typecheck": "tsc --noEmit",
61
61
  "lint": "eslint src test",
62
- "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts test/overhead.test.ts test/idempotency.test.ts test/schema.test.ts test/otel.test.ts",
62
+ "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts test/overhead.test.ts test/idempotency.test.ts test/schema.test.ts test/otel.test.ts test/redrive.test.ts",
63
63
  "coverage": "c8 --check-coverage --lines 90 --functions 90 --branches 85 --reporter=text npm test",
64
64
  "prepublishOnly": "npm run build"
65
65
  },