@babelqueue/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammet Şafak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # BabelQueue for Node.js
2
+
3
+ [![CI](https://github.com/BabelQueue/babelqueue-node/actions/workflows/ci.yml/badge.svg)](https://github.com/BabelQueue/babelqueue-node/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@babelqueue/core.svg)](https://www.npmjs.com/package/@babelqueue/core)
5
+ [![node](https://img.shields.io/node/v/@babelqueue/core.svg)](https://www.npmjs.com/package/@babelqueue/core)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+
8
+ > **Polyglot Queues, Simplified.** Read and write the canonical BabelQueue message
9
+ > envelope from Node.js — so your Node services exchange messages with Laravel,
10
+ > Symfony, Python, Go and .NET over one strict JSON format, on the broker you
11
+ > already run.
12
+
13
+ This is the framework-agnostic **Node/TypeScript core**: the wire-envelope codec,
14
+ contracts and dead-letter helpers — **zero runtime dependencies**, shipped as a
15
+ dual **ESM + CommonJS** package with bundled types. The full standard is documented
16
+ at **[babelqueue.com](https://babelqueue.com)**.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @babelqueue/core
22
+ ```
23
+
24
+ Requires Node `>=18`.
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { EnvelopeCodec } from "@babelqueue/core";
30
+
31
+ // Produce — build the canonical envelope and publish the JSON to your broker.
32
+ const env = EnvelopeCodec.make(
33
+ "urn:babel:orders:created",
34
+ { order_id: 1042 },
35
+ { queue: "orders" },
36
+ );
37
+ const body = EnvelopeCodec.encode(env); // compact UTF-8 JSON string
38
+ // await redis.rpush("queues:orders", body);
39
+ // / channel.sendToQueue("orders", Buffer.from(body));
40
+
41
+ // Consume — decode a message produced by ANY BabelQueue SDK.
42
+ const incoming = EnvelopeCodec.decode(body);
43
+ if (EnvelopeCodec.accepts(incoming)) {
44
+ // `incoming` is now narrowed to a fully-typed Envelope
45
+ switch (EnvelopeCodec.urn(incoming)) {
46
+ case "urn:babel:orders:created":
47
+ console.log(incoming.data.order_id, incoming.trace_id);
48
+ break;
49
+ }
50
+ }
51
+ ```
52
+
53
+ CommonJS works too:
54
+
55
+ ```js
56
+ const { EnvelopeCodec } = require("@babelqueue/core");
57
+ ```
58
+
59
+ The envelope is identical to every other SDK's:
60
+
61
+ ```json
62
+ {
63
+ "job": "urn:babel:orders:created",
64
+ "trace_id": "…",
65
+ "data": { "order_id": 1042 },
66
+ "meta": { "id": "…", "queue": "orders", "lang": "node", "schema_version": 1, "created_at": 1749132727000 },
67
+ "attempts": 0
68
+ }
69
+ ```
70
+
71
+ ### Typed messages (optional)
72
+
73
+ ```ts
74
+ import { EnvelopeCodec, type PolyglotMessage } from "@babelqueue/core";
75
+
76
+ class OrderCreated implements PolyglotMessage {
77
+ constructor(private readonly orderId: number) {}
78
+ getBabelUrn() {
79
+ return "urn:babel:orders:created";
80
+ }
81
+ toPayload() {
82
+ return { order_id: this.orderId };
83
+ }
84
+ }
85
+
86
+ const env = EnvelopeCodec.fromMessage(new OrderCreated(1042), "orders");
87
+ ```
88
+
89
+ Continue an existing trace by adding `getBabelTraceId(): string | null` (see
90
+ `HasTraceId`), or pass `{ traceId }` to `EnvelopeCodec.make`.
91
+
92
+ ### Dead-letter
93
+
94
+ ```ts
95
+ import { annotate, EnvelopeCodec } from "@babelqueue/core";
96
+
97
+ const dlq = annotate(env, "failed", "orders", { attempts: 3, error: "boom" });
98
+ // publish EnvelopeCodec.encode(dlq) to the "orders.dlq" queue
99
+ ```
100
+
101
+ `annotate` returns a copy — the original envelope is preserved unchanged inside
102
+ the dead-lettered message, so any-language consumers can still read it.
103
+
104
+ ## What this core is (and isn't)
105
+
106
+ It enforces the **contract**: the envelope shape, URN identity, trace propagation,
107
+ schema-version gating and the dead-letter block. It is intentionally **not** a
108
+ worker/runtime — broker wiring, acks and retry loops stay in your own code (or a
109
+ future thin adapter), exactly as with the other SDK cores.
110
+
111
+ `UnknownUrnStrategy` (`FAIL`, `DELETE`, `RELEASE`, `DEAD_LETTER`) is provided for
112
+ adapters to act on.
113
+
114
+ ## Conformance
115
+
116
+ This core passes the shared **cross-SDK conformance suite** (vendored under
117
+ [`test/conformance/`](test/conformance)) — the same fixtures every BabelQueue SDK
118
+ must satisfy, so a Node producer and, say, a Laravel consumer agree byte-for-byte.
119
+
120
+ ```bash
121
+ npm test
122
+ ```
123
+
124
+ ## License
125
+
126
+ [MIT](LICENSE) © Muhammet Şafak
package/dist/index.cjs ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BabelQueueError: () => BabelQueueError,
24
+ EnvelopeCodec: () => EnvelopeCodec,
25
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
26
+ SOURCE_LANG: () => SOURCE_LANG,
27
+ UnknownUrnError: () => UnknownUrnError,
28
+ UnknownUrnStrategy: () => UnknownUrnStrategy,
29
+ annotate: () => annotate,
30
+ deadLetter: () => deadLetter_exports
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/codec.ts
35
+ var import_node_crypto = require("crypto");
36
+
37
+ // src/errors.ts
38
+ var BabelQueueError = class extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "BabelQueueError";
42
+ }
43
+ };
44
+ var UnknownUrnError = class extends BabelQueueError {
45
+ constructor(urn) {
46
+ super(`No handler is mapped for the message URN "${urn}".`);
47
+ this.name = "UnknownUrnError";
48
+ }
49
+ };
50
+
51
+ // src/codec.ts
52
+ var SCHEMA_VERSION = 1;
53
+ var SOURCE_LANG = "node";
54
+ var EnvelopeCodec = {
55
+ SCHEMA_VERSION,
56
+ SOURCE_LANG,
57
+ /**
58
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
59
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
60
+ * Throws {@link BabelQueueError} when the URN is blank.
61
+ */
62
+ make(urn, data, options = {}) {
63
+ const resolvedUrn = (urn ?? "").trim();
64
+ if (resolvedUrn === "") {
65
+ throw new BabelQueueError(
66
+ "A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
67
+ );
68
+ }
69
+ const traceId = (options.traceId ?? "").trim() || (0, import_node_crypto.randomUUID)();
70
+ return {
71
+ job: resolvedUrn,
72
+ trace_id: traceId,
73
+ data: { ...data },
74
+ meta: {
75
+ id: (0, import_node_crypto.randomUUID)(),
76
+ queue: options.queue ?? "default",
77
+ lang: SOURCE_LANG,
78
+ schema_version: SCHEMA_VERSION,
79
+ created_at: Date.now()
80
+ },
81
+ attempts: 0
82
+ };
83
+ },
84
+ /**
85
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
86
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
87
+ * reused.
88
+ */
89
+ fromMessage(message, queue = "default") {
90
+ const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
91
+ return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
92
+ queue,
93
+ traceId
94
+ });
95
+ },
96
+ /**
97
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
98
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
99
+ * the other SDK cores.
100
+ */
101
+ encode(envelope) {
102
+ return JSON.stringify(envelope);
103
+ },
104
+ /**
105
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
106
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
107
+ * alias into `job`.
108
+ */
109
+ decode(raw) {
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(raw);
113
+ } catch {
114
+ return {};
115
+ }
116
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
117
+ return {};
118
+ }
119
+ const envelope = parsed;
120
+ if (!envelope.job && typeof envelope.urn === "string") {
121
+ envelope.job = envelope.urn;
122
+ }
123
+ return envelope;
124
+ },
125
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
126
+ urn(envelope) {
127
+ const value = envelope?.job ?? envelope?.urn ?? "";
128
+ return typeof value === "string" ? value.trim() : "";
129
+ },
130
+ /**
131
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
132
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
133
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
134
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
135
+ */
136
+ accepts(envelope) {
137
+ if (EnvelopeCodec.urn(envelope) === "") {
138
+ return false;
139
+ }
140
+ const meta = envelope.meta;
141
+ if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
142
+ return false;
143
+ }
144
+ const data = envelope.data;
145
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
146
+ return false;
147
+ }
148
+ const attempts = envelope.attempts;
149
+ if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
150
+ return false;
151
+ }
152
+ const traceId = envelope.trace_id;
153
+ if (typeof traceId !== "string" || traceId.trim() === "") {
154
+ return false;
155
+ }
156
+ return true;
157
+ }
158
+ };
159
+
160
+ // src/deadLetter.ts
161
+ var deadLetter_exports = {};
162
+ __export(deadLetter_exports, {
163
+ annotate: () => annotate
164
+ });
165
+ function annotate(envelope, reason, originalQueue, options = {}) {
166
+ const deadLetter = {
167
+ reason,
168
+ error: options.error ?? null,
169
+ exception: options.exception ?? null,
170
+ failed_at: Date.now(),
171
+ original_queue: originalQueue,
172
+ attempts: options.attempts ?? envelope.attempts ?? 0,
173
+ lang: SOURCE_LANG
174
+ };
175
+ return { ...envelope, dead_letter: deadLetter };
176
+ }
177
+
178
+ // src/routing.ts
179
+ var UnknownUrnStrategy = {
180
+ /** Surface an error; let the worker decide. */
181
+ FAIL: "fail",
182
+ /** Drop the message. */
183
+ DELETE: "delete",
184
+ /** Requeue for another consumer. */
185
+ RELEASE: "release",
186
+ /** Route to the dead-letter queue. */
187
+ DEAD_LETTER: "dead_letter"
188
+ };
189
+ // Annotate the CommonJS export names for ESM import in node:
190
+ 0 && (module.exports = {
191
+ BabelQueueError,
192
+ EnvelopeCodec,
193
+ SCHEMA_VERSION,
194
+ SOURCE_LANG,
195
+ UnknownUrnError,
196
+ UnknownUrnStrategy,
197
+ annotate,
198
+ deadLetter
199
+ });
200
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.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 } from \"./errors.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","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"],"mappings":"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","names":[]}
@@ -0,0 +1,175 @@
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
+ /** Options for {@link annotate}. */
129
+ interface AnnotateOptions {
130
+ /** Defaults to the envelope's current `attempts`. */
131
+ attempts?: number;
132
+ /** A human-readable error message (JSON `null` when omitted). */
133
+ error?: string | null;
134
+ /** The originating error type/class name (JSON `null` when omitted). */
135
+ exception?: string | null;
136
+ }
137
+ /**
138
+ * Return a copy of the envelope with a `dead_letter` block attached, recording
139
+ * why and where it failed. The original envelope is preserved unchanged inside
140
+ * the result, so any-language consumers can still read it.
141
+ */
142
+ declare function annotate(envelope: Envelope, reason: string, originalQueue: string, options?: AnnotateOptions): Envelope;
143
+
144
+ type deadLetter_AnnotateOptions = AnnotateOptions;
145
+ declare const deadLetter_annotate: typeof annotate;
146
+ declare namespace deadLetter {
147
+ export { type deadLetter_AnnotateOptions as AnnotateOptions, deadLetter_annotate as annotate };
148
+ }
149
+
150
+ /**
151
+ * What a consumer does with a message whose URN has no registered handler.
152
+ * Mirrors the constants in every other SDK core.
153
+ */
154
+ declare const UnknownUrnStrategy: {
155
+ /** Surface an error; let the worker decide. */
156
+ readonly FAIL: "fail";
157
+ /** Drop the message. */
158
+ readonly DELETE: "delete";
159
+ /** Requeue for another consumer. */
160
+ readonly RELEASE: "release";
161
+ /** Route to the dead-letter queue. */
162
+ readonly DEAD_LETTER: "dead_letter";
163
+ };
164
+ type UnknownUrnStrategy = (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];
165
+
166
+ /** Base error for all BabelQueue failures. */
167
+ declare class BabelQueueError extends Error {
168
+ constructor(message: string);
169
+ }
170
+ /** Raised when no handler is mapped for a message URN. */
171
+ declare class UnknownUrnError extends BabelQueueError {
172
+ constructor(urn: string);
173
+ }
174
+
175
+ export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type HasTraceId, type IncomingEnvelope, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter };
@@ -0,0 +1,175 @@
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
+ /** Options for {@link annotate}. */
129
+ interface AnnotateOptions {
130
+ /** Defaults to the envelope's current `attempts`. */
131
+ attempts?: number;
132
+ /** A human-readable error message (JSON `null` when omitted). */
133
+ error?: string | null;
134
+ /** The originating error type/class name (JSON `null` when omitted). */
135
+ exception?: string | null;
136
+ }
137
+ /**
138
+ * Return a copy of the envelope with a `dead_letter` block attached, recording
139
+ * why and where it failed. The original envelope is preserved unchanged inside
140
+ * the result, so any-language consumers can still read it.
141
+ */
142
+ declare function annotate(envelope: Envelope, reason: string, originalQueue: string, options?: AnnotateOptions): Envelope;
143
+
144
+ type deadLetter_AnnotateOptions = AnnotateOptions;
145
+ declare const deadLetter_annotate: typeof annotate;
146
+ declare namespace deadLetter {
147
+ export { type deadLetter_AnnotateOptions as AnnotateOptions, deadLetter_annotate as annotate };
148
+ }
149
+
150
+ /**
151
+ * What a consumer does with a message whose URN has no registered handler.
152
+ * Mirrors the constants in every other SDK core.
153
+ */
154
+ declare const UnknownUrnStrategy: {
155
+ /** Surface an error; let the worker decide. */
156
+ readonly FAIL: "fail";
157
+ /** Drop the message. */
158
+ readonly DELETE: "delete";
159
+ /** Requeue for another consumer. */
160
+ readonly RELEASE: "release";
161
+ /** Route to the dead-letter queue. */
162
+ readonly DEAD_LETTER: "dead_letter";
163
+ };
164
+ type UnknownUrnStrategy = (typeof UnknownUrnStrategy)[keyof typeof UnknownUrnStrategy];
165
+
166
+ /** Base error for all BabelQueue failures. */
167
+ declare class BabelQueueError extends Error {
168
+ constructor(message: string);
169
+ }
170
+ /** Raised when no handler is mapped for a message URN. */
171
+ declare class UnknownUrnError extends BabelQueueError {
172
+ constructor(urn: string);
173
+ }
174
+
175
+ export { type AnnotateOptions, BabelQueueError, type DeadLetter, type Envelope, EnvelopeCodec, type HasTraceId, type IncomingEnvelope, type MakeOptions, type Meta, type PolyglotMessage, SCHEMA_VERSION, SOURCE_LANG, UnknownUrnError, UnknownUrnStrategy, annotate, deadLetter };
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/codec.ts
8
+ import { randomUUID } from "crypto";
9
+
10
+ // src/errors.ts
11
+ var BabelQueueError = class extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "BabelQueueError";
15
+ }
16
+ };
17
+ var UnknownUrnError = class extends BabelQueueError {
18
+ constructor(urn) {
19
+ super(`No handler is mapped for the message URN "${urn}".`);
20
+ this.name = "UnknownUrnError";
21
+ }
22
+ };
23
+
24
+ // src/codec.ts
25
+ var SCHEMA_VERSION = 1;
26
+ var SOURCE_LANG = "node";
27
+ var EnvelopeCodec = {
28
+ SCHEMA_VERSION,
29
+ SOURCE_LANG,
30
+ /**
31
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
32
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
33
+ * Throws {@link BabelQueueError} when the URN is blank.
34
+ */
35
+ make(urn, data, options = {}) {
36
+ const resolvedUrn = (urn ?? "").trim();
37
+ if (resolvedUrn === "") {
38
+ throw new BabelQueueError(
39
+ "A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
40
+ );
41
+ }
42
+ const traceId = (options.traceId ?? "").trim() || randomUUID();
43
+ return {
44
+ job: resolvedUrn,
45
+ trace_id: traceId,
46
+ data: { ...data },
47
+ meta: {
48
+ id: randomUUID(),
49
+ queue: options.queue ?? "default",
50
+ lang: SOURCE_LANG,
51
+ schema_version: SCHEMA_VERSION,
52
+ created_at: Date.now()
53
+ },
54
+ attempts: 0
55
+ };
56
+ },
57
+ /**
58
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
59
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
60
+ * reused.
61
+ */
62
+ fromMessage(message, queue = "default") {
63
+ const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
64
+ return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
65
+ queue,
66
+ traceId
67
+ });
68
+ },
69
+ /**
70
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
71
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
72
+ * the other SDK cores.
73
+ */
74
+ encode(envelope) {
75
+ return JSON.stringify(envelope);
76
+ },
77
+ /**
78
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
79
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
80
+ * alias into `job`.
81
+ */
82
+ decode(raw) {
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ } catch {
87
+ return {};
88
+ }
89
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
90
+ return {};
91
+ }
92
+ const envelope = parsed;
93
+ if (!envelope.job && typeof envelope.urn === "string") {
94
+ envelope.job = envelope.urn;
95
+ }
96
+ return envelope;
97
+ },
98
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
99
+ urn(envelope) {
100
+ const value = envelope?.job ?? envelope?.urn ?? "";
101
+ return typeof value === "string" ? value.trim() : "";
102
+ },
103
+ /**
104
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
105
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
106
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
107
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
108
+ */
109
+ accepts(envelope) {
110
+ if (EnvelopeCodec.urn(envelope) === "") {
111
+ return false;
112
+ }
113
+ const meta = envelope.meta;
114
+ if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
115
+ return false;
116
+ }
117
+ const data = envelope.data;
118
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
119
+ return false;
120
+ }
121
+ const attempts = envelope.attempts;
122
+ if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
123
+ return false;
124
+ }
125
+ const traceId = envelope.trace_id;
126
+ if (typeof traceId !== "string" || traceId.trim() === "") {
127
+ return false;
128
+ }
129
+ return true;
130
+ }
131
+ };
132
+
133
+ // src/deadLetter.ts
134
+ var deadLetter_exports = {};
135
+ __export(deadLetter_exports, {
136
+ annotate: () => annotate
137
+ });
138
+ function annotate(envelope, reason, originalQueue, options = {}) {
139
+ const deadLetter = {
140
+ reason,
141
+ error: options.error ?? null,
142
+ exception: options.exception ?? null,
143
+ failed_at: Date.now(),
144
+ original_queue: originalQueue,
145
+ attempts: options.attempts ?? envelope.attempts ?? 0,
146
+ lang: SOURCE_LANG
147
+ };
148
+ return { ...envelope, dead_letter: deadLetter };
149
+ }
150
+
151
+ // src/routing.ts
152
+ var UnknownUrnStrategy = {
153
+ /** Surface an error; let the worker decide. */
154
+ FAIL: "fail",
155
+ /** Drop the message. */
156
+ DELETE: "delete",
157
+ /** Requeue for another consumer. */
158
+ RELEASE: "release",
159
+ /** Route to the dead-letter queue. */
160
+ DEAD_LETTER: "dead_letter"
161
+ };
162
+ export {
163
+ BabelQueueError,
164
+ EnvelopeCodec,
165
+ SCHEMA_VERSION,
166
+ SOURCE_LANG,
167
+ UnknownUrnError,
168
+ UnknownUrnStrategy,
169
+ annotate,
170
+ deadLetter_exports as deadLetter
171
+ };
172
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codec.ts","../src/errors.ts","../src/deadLetter.ts","../src/routing.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n","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"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@babelqueue/core",
3
+ "version": "0.1.0",
4
+ "description": "Polyglot Queues, Simplified — the Node/TypeScript core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.",
5
+ "keywords": [
6
+ "queue",
7
+ "polyglot",
8
+ "microservices",
9
+ "json",
10
+ "envelope",
11
+ "messaging",
12
+ "rabbitmq",
13
+ "redis"
14
+ ],
15
+ "homepage": "https://babelqueue.com",
16
+ "bugs": {
17
+ "url": "https://github.com/BabelQueue/babelqueue-node/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/BabelQueue/babelqueue-node.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ }
37
+ },
38
+ "main": "./dist/index.cjs",
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "sideEffects": false,
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts",
52
+ "prepublishOnly": "npm run build"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22",
56
+ "tsup": "^8",
57
+ "tsx": "^4",
58
+ "typescript": "^5.5"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }