@babelqueue/core 1.0.0 → 1.2.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/chunk-7FUZ3LYT.js +150 -0
- package/dist/chunk-7FUZ3LYT.js.map +1 -0
- package/dist/idempotency-DDHjGwF7.d.cts +178 -0
- package/dist/idempotency-DDHjGwF7.d.ts +178 -0
- package/dist/index.cjs +205 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -127
- package/dist/index.d.ts +91 -127
- package/dist/index.js +197 -130
- package/dist/index.js.map +1 -1
- package/dist/otel.cjs +245 -0
- package/dist/otel.cjs.map +1 -0
- package/dist/otel.d.cts +52 -0
- package/dist/otel.d.ts +52 -0
- package/dist/otel.js +105 -0
- package/dist/otel.js.map +1 -0
- package/package.json +22 -2
|
@@ -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 };
|