@babelqueue/core 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/errors.ts
8
+ var BabelQueueError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "BabelQueueError";
12
+ }
13
+ };
14
+ var UnknownUrnError = class extends BabelQueueError {
15
+ constructor(urn) {
16
+ super(`No handler is mapped for the message URN "${urn}".`);
17
+ this.name = "UnknownUrnError";
18
+ }
19
+ };
20
+ var InvalidPayloadError = class extends BabelQueueError {
21
+ constructor(urn, violation) {
22
+ super(`Message data for "${urn}" does not match its URN schema: ${violation}.`);
23
+ this.urn = urn;
24
+ this.violation = violation;
25
+ this.name = "InvalidPayloadError";
26
+ }
27
+ urn;
28
+ violation;
29
+ };
30
+
31
+ // src/codec.ts
32
+ import { randomUUID } from "crypto";
33
+ var SCHEMA_VERSION = 1;
34
+ var SOURCE_LANG = "node";
35
+ var EnvelopeCodec = {
36
+ SCHEMA_VERSION,
37
+ SOURCE_LANG,
38
+ /**
39
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
40
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
41
+ * Throws {@link BabelQueueError} when the URN is blank.
42
+ */
43
+ make(urn, data, options = {}) {
44
+ const resolvedUrn = (urn ?? "").trim();
45
+ if (resolvedUrn === "") {
46
+ throw new BabelQueueError(
47
+ "A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
48
+ );
49
+ }
50
+ const traceId = (options.traceId ?? "").trim() || randomUUID();
51
+ return {
52
+ job: resolvedUrn,
53
+ trace_id: traceId,
54
+ data: { ...data },
55
+ meta: {
56
+ id: randomUUID(),
57
+ queue: options.queue ?? "default",
58
+ lang: SOURCE_LANG,
59
+ schema_version: SCHEMA_VERSION,
60
+ created_at: Date.now()
61
+ },
62
+ attempts: 0
63
+ };
64
+ },
65
+ /**
66
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
67
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
68
+ * reused.
69
+ */
70
+ fromMessage(message, queue = "default") {
71
+ const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
72
+ return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
73
+ queue,
74
+ traceId
75
+ });
76
+ },
77
+ /**
78
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
79
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
80
+ * the other SDK cores.
81
+ */
82
+ encode(envelope) {
83
+ return JSON.stringify(envelope);
84
+ },
85
+ /**
86
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
87
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
88
+ * alias into `job`.
89
+ */
90
+ decode(raw) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(raw);
94
+ } catch {
95
+ return {};
96
+ }
97
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
98
+ return {};
99
+ }
100
+ const envelope = parsed;
101
+ if (!envelope.job && typeof envelope.urn === "string") {
102
+ envelope.job = envelope.urn;
103
+ }
104
+ return envelope;
105
+ },
106
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
107
+ urn(envelope) {
108
+ const value = envelope?.job ?? envelope?.urn ?? "";
109
+ return typeof value === "string" ? value.trim() : "";
110
+ },
111
+ /**
112
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
113
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
114
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
115
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
116
+ */
117
+ accepts(envelope) {
118
+ if (EnvelopeCodec.urn(envelope) === "") {
119
+ return false;
120
+ }
121
+ const meta = envelope.meta;
122
+ if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
123
+ return false;
124
+ }
125
+ const data = envelope.data;
126
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
127
+ return false;
128
+ }
129
+ const attempts = envelope.attempts;
130
+ if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
131
+ return false;
132
+ }
133
+ const traceId = envelope.trace_id;
134
+ if (typeof traceId !== "string" || traceId.trim() === "") {
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+ };
140
+
141
+ export {
142
+ __export,
143
+ BabelQueueError,
144
+ UnknownUrnError,
145
+ InvalidPayloadError,
146
+ SCHEMA_VERSION,
147
+ SOURCE_LANG,
148
+ EnvelopeCodec
149
+ };
150
+ //# sourceMappingURL=chunk-7FUZ3LYT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/codec.ts"],"sourcesContent":["/** 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 { 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"],"mappings":"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","names":[]}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * A message that can be produced as a polyglot envelope. Implement it on your
3
+ * own classes/objects so {@link EnvelopeCodec.fromMessage} can build the canonical
4
+ * envelope without ever leaking a language-specific class name onto the wire.
5
+ */
6
+ interface PolyglotMessage {
7
+ /** The stable URN that identifies this message across languages. */
8
+ getBabelUrn(): string;
9
+ /** The pure-JSON payload (no class instances). */
10
+ toPayload(): Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Optionally implemented alongside {@link PolyglotMessage} to continue an existing
14
+ * distributed trace instead of minting a fresh one.
15
+ */
16
+ interface HasTraceId {
17
+ /** The trace id to reuse, or null/undefined to mint a new one. */
18
+ getBabelTraceId(): string | null | undefined;
19
+ }
20
+
21
+ /** The wire envelope schema version this core implements (versioned independently of the package version). */
22
+ declare const SCHEMA_VERSION = 1;
23
+ /** Stamped into `meta.lang` for envelopes produced by this core. */
24
+ declare const SOURCE_LANG = "node";
25
+ /** Immutable per-message metadata. */
26
+ interface Meta {
27
+ id: string;
28
+ queue: string;
29
+ lang: string;
30
+ schema_version: number;
31
+ /** Unix milliseconds, UTC. */
32
+ created_at: number;
33
+ }
34
+ /** The additive block appended to an envelope when a message is dead-lettered. */
35
+ interface DeadLetter {
36
+ reason: string;
37
+ error: string | null;
38
+ exception: string | null;
39
+ /** Unix milliseconds, UTC. */
40
+ failed_at: number;
41
+ original_queue: string;
42
+ attempts: number;
43
+ lang: string;
44
+ }
45
+ /**
46
+ * The canonical BabelQueue wire message: a strict, language-neutral JSON shape
47
+ * that every SDK produces and consumes identically. The property order here is
48
+ * significant — it matches the other cores so {@link EnvelopeCodec.encode} is
49
+ * byte-for-byte identical across the insertion-order languages (PHP/Python).
50
+ */
51
+ interface Envelope {
52
+ /** The message URN (never a class name). */
53
+ job: string;
54
+ /** Correlation id, preserved across every hop. */
55
+ trace_id: string;
56
+ /** The pure-JSON payload. */
57
+ data: Record<string, unknown>;
58
+ meta: Meta;
59
+ /** Top-level transport retry counter. */
60
+ attempts: number;
61
+ /** Present only once the message has been dead-lettered. */
62
+ dead_letter?: DeadLetter;
63
+ }
64
+ /**
65
+ * A decoded, not-yet-validated envelope. Fields are loosely typed because they
66
+ * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it
67
+ * with {@link EnvelopeCodec.accepts} before trusting the contents.
68
+ */
69
+ interface IncomingEnvelope {
70
+ job?: string;
71
+ /** Inbound alias for `job`. */
72
+ urn?: string;
73
+ trace_id?: string;
74
+ data?: unknown;
75
+ meta?: unknown;
76
+ attempts?: unknown;
77
+ dead_letter?: unknown;
78
+ }
79
+ /** Options for {@link EnvelopeCodec.make}. */
80
+ interface MakeOptions {
81
+ /** Logical queue name recorded in `meta.queue` (default `"default"`). */
82
+ queue?: string;
83
+ /** Reuse an existing trace id (trace continuation) instead of minting one. */
84
+ traceId?: string;
85
+ }
86
+ /**
87
+ * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript
88
+ * implementation of the wire format.
89
+ */
90
+ declare const EnvelopeCodec: {
91
+ readonly SCHEMA_VERSION: 1;
92
+ readonly SOURCE_LANG: "node";
93
+ /**
94
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
95
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
96
+ * Throws {@link BabelQueueError} when the URN is blank.
97
+ */
98
+ readonly make: (urn: string, data: Record<string, unknown>, options?: MakeOptions) => Envelope;
99
+ /**
100
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
101
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
102
+ * reused.
103
+ */
104
+ readonly fromMessage: (message: PolyglotMessage & Partial<HasTraceId>, queue?: string) => Envelope;
105
+ /**
106
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
107
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
108
+ * the other SDK cores.
109
+ */
110
+ readonly encode: (envelope: Envelope) => string;
111
+ /**
112
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
113
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
114
+ * alias into `job`.
115
+ */
116
+ readonly decode: (raw: string) => IncomingEnvelope;
117
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
118
+ readonly urn: (envelope: IncomingEnvelope) => string;
119
+ /**
120
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
121
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
122
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
123
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
124
+ */
125
+ readonly accepts: (envelope: IncomingEnvelope) => envelope is Envelope;
126
+ };
127
+
128
+ /**
129
+ * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.
130
+ *
131
+ * The Node mirror of the PHP `BabelQueue\Idempotency` and Go `idempotency` helpers.
132
+ * The core is codec-only (no dispatcher), so this wraps a user-provided handler that
133
+ * an adapter (NestJS, BullMQ, ...) drives:
134
+ *
135
+ * ```ts
136
+ * import { Wrap, InMemoryStore, type Handler } from "@babelqueue/core";
137
+ *
138
+ * const store = new InMemoryStore();
139
+ * const handler = Wrap(store, async (env) => { ... });
140
+ * ```
141
+ *
142
+ * A previously-seen id returns early (the adapter acks it); a throwing/rejecting
143
+ * handler leaves the id unmarked so a redelivery runs it again; a message with no
144
+ * usable `meta.id` runs unchanged. "Seen-set" post-success dedupe — not exactly-once,
145
+ * not in-flight concurrency locking (a transactional mode is a future direction).
146
+ */
147
+
148
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
149
+ type Handler = (env: Envelope) => void | Promise<void>;
150
+ /**
151
+ * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may
152
+ * be sync or async so a production store can be Redis- or DB-backed; the reference
153
+ * {@link InMemoryStore} is synchronous.
154
+ */
155
+ interface Store {
156
+ seen(messageId: string): boolean | Promise<boolean>;
157
+ remember(messageId: string): void | Promise<void>;
158
+ forget(messageId: string): void | Promise<void>;
159
+ }
160
+ /**
161
+ * Process-local {@link Store} backed by a Set. For tests / single-process consumers;
162
+ * not shared across workers and not persistent — use a Redis- or DB-backed store for
163
+ * production fleets.
164
+ */
165
+ declare class InMemoryStore implements Store {
166
+ private readonly entries;
167
+ seen(messageId: string): boolean;
168
+ remember(messageId: string): void;
169
+ forget(messageId: string): void;
170
+ }
171
+ /**
172
+ * Wraps `handler` so a message whose `meta.id` was already processed successfully is
173
+ * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it
174
+ * again (retry / dead-letter still apply); a message with no usable id runs unchanged.
175
+ */
176
+ declare function Wrap(store: Store, handler: Handler): Handler;
177
+
178
+ export { type DeadLetter as D, type Envelope as E, type Handler as H, InMemoryStore as I, type MakeOptions as M, type PolyglotMessage as P, SCHEMA_VERSION as S, Wrap as W, EnvelopeCodec as a, type HasTraceId as b, type IncomingEnvelope as c, type Meta as d, SOURCE_LANG as e, type Store as f };
@@ -0,0 +1,178 @@
1
+ /**
2
+ * A message that can be produced as a polyglot envelope. Implement it on your
3
+ * own classes/objects so {@link EnvelopeCodec.fromMessage} can build the canonical
4
+ * envelope without ever leaking a language-specific class name onto the wire.
5
+ */
6
+ interface PolyglotMessage {
7
+ /** The stable URN that identifies this message across languages. */
8
+ getBabelUrn(): string;
9
+ /** The pure-JSON payload (no class instances). */
10
+ toPayload(): Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Optionally implemented alongside {@link PolyglotMessage} to continue an existing
14
+ * distributed trace instead of minting a fresh one.
15
+ */
16
+ interface HasTraceId {
17
+ /** The trace id to reuse, or null/undefined to mint a new one. */
18
+ getBabelTraceId(): string | null | undefined;
19
+ }
20
+
21
+ /** The wire envelope schema version this core implements (versioned independently of the package version). */
22
+ declare const SCHEMA_VERSION = 1;
23
+ /** Stamped into `meta.lang` for envelopes produced by this core. */
24
+ declare const SOURCE_LANG = "node";
25
+ /** Immutable per-message metadata. */
26
+ interface Meta {
27
+ id: string;
28
+ queue: string;
29
+ lang: string;
30
+ schema_version: number;
31
+ /** Unix milliseconds, UTC. */
32
+ created_at: number;
33
+ }
34
+ /** The additive block appended to an envelope when a message is dead-lettered. */
35
+ interface DeadLetter {
36
+ reason: string;
37
+ error: string | null;
38
+ exception: string | null;
39
+ /** Unix milliseconds, UTC. */
40
+ failed_at: number;
41
+ original_queue: string;
42
+ attempts: number;
43
+ lang: string;
44
+ }
45
+ /**
46
+ * The canonical BabelQueue wire message: a strict, language-neutral JSON shape
47
+ * that every SDK produces and consumes identically. The property order here is
48
+ * significant — it matches the other cores so {@link EnvelopeCodec.encode} is
49
+ * byte-for-byte identical across the insertion-order languages (PHP/Python).
50
+ */
51
+ interface Envelope {
52
+ /** The message URN (never a class name). */
53
+ job: string;
54
+ /** Correlation id, preserved across every hop. */
55
+ trace_id: string;
56
+ /** The pure-JSON payload. */
57
+ data: Record<string, unknown>;
58
+ meta: Meta;
59
+ /** Top-level transport retry counter. */
60
+ attempts: number;
61
+ /** Present only once the message has been dead-lettered. */
62
+ dead_letter?: DeadLetter;
63
+ }
64
+ /**
65
+ * A decoded, not-yet-validated envelope. Fields are loosely typed because they
66
+ * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it
67
+ * with {@link EnvelopeCodec.accepts} before trusting the contents.
68
+ */
69
+ interface IncomingEnvelope {
70
+ job?: string;
71
+ /** Inbound alias for `job`. */
72
+ urn?: string;
73
+ trace_id?: string;
74
+ data?: unknown;
75
+ meta?: unknown;
76
+ attempts?: unknown;
77
+ dead_letter?: unknown;
78
+ }
79
+ /** Options for {@link EnvelopeCodec.make}. */
80
+ interface MakeOptions {
81
+ /** Logical queue name recorded in `meta.queue` (default `"default"`). */
82
+ queue?: string;
83
+ /** Reuse an existing trace id (trace continuation) instead of minting one. */
84
+ traceId?: string;
85
+ }
86
+ /**
87
+ * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript
88
+ * implementation of the wire format.
89
+ */
90
+ declare const EnvelopeCodec: {
91
+ readonly SCHEMA_VERSION: 1;
92
+ readonly SOURCE_LANG: "node";
93
+ /**
94
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
95
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
96
+ * Throws {@link BabelQueueError} when the URN is blank.
97
+ */
98
+ readonly make: (urn: string, data: Record<string, unknown>, options?: MakeOptions) => Envelope;
99
+ /**
100
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
101
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
102
+ * reused.
103
+ */
104
+ readonly fromMessage: (message: PolyglotMessage & Partial<HasTraceId>, queue?: string) => Envelope;
105
+ /**
106
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
107
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
108
+ * the other SDK cores.
109
+ */
110
+ readonly encode: (envelope: Envelope) => string;
111
+ /**
112
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
113
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
114
+ * alias into `job`.
115
+ */
116
+ readonly decode: (raw: string) => IncomingEnvelope;
117
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
118
+ readonly urn: (envelope: IncomingEnvelope) => string;
119
+ /**
120
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
121
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
122
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
123
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
124
+ */
125
+ readonly accepts: (envelope: IncomingEnvelope) => envelope is Envelope;
126
+ };
127
+
128
+ /**
129
+ * Optional idempotency helper (ADR-0022): dedupe a consume handler on `meta.id`.
130
+ *
131
+ * The Node mirror of the PHP `BabelQueue\Idempotency` and Go `idempotency` helpers.
132
+ * The core is codec-only (no dispatcher), so this wraps a user-provided handler that
133
+ * an adapter (NestJS, BullMQ, ...) drives:
134
+ *
135
+ * ```ts
136
+ * import { Wrap, InMemoryStore, type Handler } from "@babelqueue/core";
137
+ *
138
+ * const store = new InMemoryStore();
139
+ * const handler = Wrap(store, async (env) => { ... });
140
+ * ```
141
+ *
142
+ * A previously-seen id returns early (the adapter acks it); a throwing/rejecting
143
+ * handler leaves the id unmarked so a redelivery runs it again; a message with no
144
+ * usable `meta.id` runs unchanged. "Seen-set" post-success dedupe — not exactly-once,
145
+ * not in-flight concurrency locking (a transactional mode is a future direction).
146
+ */
147
+
148
+ /** A consume handler: receives a decoded envelope, may be sync or async. */
149
+ type Handler = (env: Envelope) => void | Promise<void>;
150
+ /**
151
+ * A pluggable record of message ids already processed, keyed on `meta.id`. Methods may
152
+ * be sync or async so a production store can be Redis- or DB-backed; the reference
153
+ * {@link InMemoryStore} is synchronous.
154
+ */
155
+ interface Store {
156
+ seen(messageId: string): boolean | Promise<boolean>;
157
+ remember(messageId: string): void | Promise<void>;
158
+ forget(messageId: string): void | Promise<void>;
159
+ }
160
+ /**
161
+ * Process-local {@link Store} backed by a Set. For tests / single-process consumers;
162
+ * not shared across workers and not persistent — use a Redis- or DB-backed store for
163
+ * production fleets.
164
+ */
165
+ declare class InMemoryStore implements Store {
166
+ private readonly entries;
167
+ seen(messageId: string): boolean;
168
+ remember(messageId: string): void;
169
+ forget(messageId: string): void;
170
+ }
171
+ /**
172
+ * Wraps `handler` so a message whose `meta.id` was already processed successfully is
173
+ * skipped. A thrown/rejected handler leaves the id unmarked, so a redelivery runs it
174
+ * again (retry / dead-letter still apply); a message with no usable id runs unchanged.
175
+ */
176
+ declare function Wrap(store: Store, handler: Handler): Handler;
177
+
178
+ export { type DeadLetter as D, type Envelope as E, type Handler as H, InMemoryStore as I, type MakeOptions as M, type PolyglotMessage as P, SCHEMA_VERSION as S, Wrap as W, EnvelopeCodec as a, type HasTraceId as b, type IncomingEnvelope as c, type Meta as d, SOURCE_LANG as e, type Store as f };
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