@babelqueue/bullmq 1.1.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/README.md +24 -3
- package/dist/index.cjs +51 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +54 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,9 +39,30 @@ new Worker(
|
|
|
39
39
|
);
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`processor` validates each envelope (`EnvelopeCodec.accepts`) and routes by URN
|
|
43
|
-
non-conformant envelope
|
|
44
|
-
|
|
42
|
+
`processor` validates each envelope (`EnvelopeCodec.accepts`) and routes by URN. A
|
|
43
|
+
non-conformant envelope is **poison** (ADR-0037 decision 3): it fails with BullMQ's
|
|
44
|
+
`UnrecoverableError` — **no retry**, straight to BullMQ's failed set, which keeps the job data. An
|
|
45
|
+
unmapped URN, an exhausted handler failure and a throwing handler keep BullMQ's own retry and
|
|
46
|
+
failed-set behaviour; an unmapped URN throws unless you pass `onUnknownUrn`.
|
|
47
|
+
|
|
48
|
+
To also keep poison jobs in a BabelQueue dead-letter queue, pass `deadLetterQueue` (opt-in):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const handler = processor(handlers, {
|
|
52
|
+
deadLetterQueue: true, // a Queue instance, a queue name, or `true` for "<queue>.dlq"
|
|
53
|
+
onError: (error, job) => console.error("DLQ add failed", job.id, error),
|
|
54
|
+
});
|
|
55
|
+
new Worker("orders", handler, { connection });
|
|
56
|
+
// on shutdown: await handler.close(); // closes a Queue the adapter created (string / true)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The [ADR-0035](https://babelqueue.com) record (`dead_letter.reason: "poison"`: an object body is spliced
|
|
60
|
+
with a `dead_letter` block, anything else wrapped in `raw`) is added to that queue as a job, named like the
|
|
61
|
+
poison job, **before** the job fails. A `Queue` instance is used as given; a name or `true` creates a `Queue`
|
|
62
|
+
on the worker's connection and prefix (one per queue name, so a processor shared by several Workers sends each queue's records to its own `<queue>.dlq`). If adding the record fails, the failure goes to `onError` and the job
|
|
63
|
+
still fails as unrecoverable. The body fed to the record rule is the job data serialised as JSON, and
|
|
64
|
+
BullMQ has already parsed what it stored, so a value `JSON.parse` cannot carry (`1e400`, an integer beyond
|
|
65
|
+
2^53) is lost before the adapter sees it — a property of BullMQ.
|
|
45
66
|
|
|
46
67
|
> BullMQ stores jobs in its own Redis structures, so this gives the canonical
|
|
47
68
|
> envelope **shape + URN routing + trace propagation** to BullMQ-based Node
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(index_exports);
|
|
30
30
|
var import_core = require("@babelqueue/core");
|
|
31
|
+
var import_bullmq = require("bullmq");
|
|
31
32
|
|
|
32
33
|
// src/headers.ts
|
|
33
34
|
function sanitizeHeaders(headers) {
|
|
@@ -74,13 +75,54 @@ function withHeaders(jobsOptions, headers) {
|
|
|
74
75
|
if (telemetry?.metadata != null) return jobsOptions;
|
|
75
76
|
return { ...jobsOptions, telemetry: { ...telemetry, metadata } };
|
|
76
77
|
}
|
|
78
|
+
function workerQueueOptions(job) {
|
|
79
|
+
const opts = job.queue?.opts;
|
|
80
|
+
return { connection: opts?.connection, ...opts?.prefix === void 0 ? {} : { prefix: opts.prefix } };
|
|
81
|
+
}
|
|
77
82
|
function processor(handlers, options = {}) {
|
|
78
|
-
|
|
83
|
+
const created = /* @__PURE__ */ new Map();
|
|
84
|
+
const resolveDeadLetterQueue = (job) => {
|
|
85
|
+
const target = options.deadLetterQueue;
|
|
86
|
+
if (target === void 0) return void 0;
|
|
87
|
+
if (typeof target === "object") return target;
|
|
88
|
+
const name = target === true ? `${job.queueName}.dlq` : target;
|
|
89
|
+
const queueOptions = workerQueueOptions(job);
|
|
90
|
+
const key = `${queueOptions.prefix ?? ""}\0${name}`;
|
|
91
|
+
let queue = created.get(key);
|
|
92
|
+
if (queue === void 0) {
|
|
93
|
+
queue = new import_bullmq.Queue(name, queueOptions);
|
|
94
|
+
created.set(key, queue);
|
|
95
|
+
}
|
|
96
|
+
return queue;
|
|
97
|
+
};
|
|
98
|
+
const deadLetterPoison = async (job, error) => {
|
|
99
|
+
try {
|
|
100
|
+
const dlq = resolveDeadLetterQueue(job);
|
|
101
|
+
if (dlq === void 0) return;
|
|
102
|
+
const body = JSON.stringify(job.data) ?? "null";
|
|
103
|
+
const delivered = typeof job.attemptsMade === "number" ? job.attemptsMade : 0;
|
|
104
|
+
const deadLetter = (0, import_core.annotate)({ attempts: delivered }, "poison", options.queue ?? job.queueName, {
|
|
105
|
+
attempts: delivered,
|
|
106
|
+
error: error.message,
|
|
107
|
+
exception: error.name
|
|
108
|
+
}).dead_letter;
|
|
109
|
+
const record = (0, import_core.buildPoisonRecord)(Buffer.from(body, "utf8"), JSON.stringify(deadLetter));
|
|
110
|
+
await dlq.add(job.name, JSON.parse(record));
|
|
111
|
+
} catch (writeError) {
|
|
112
|
+
try {
|
|
113
|
+
options.onError?.(writeError, job);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const run = async (job) => {
|
|
79
119
|
const envelope = job.data;
|
|
80
120
|
if (!import_core.EnvelopeCodec.accepts(envelope)) {
|
|
81
|
-
|
|
121
|
+
const error = new import_core.BabelQueueError(
|
|
82
122
|
`Rejected a non-conformant BabelQueue envelope on job ${job.id ?? "?"}.`
|
|
83
123
|
);
|
|
124
|
+
await deadLetterPoison(job, error);
|
|
125
|
+
throw new import_bullmq.UnrecoverableError(error.message);
|
|
84
126
|
}
|
|
85
127
|
const urn = import_core.EnvelopeCodec.urn(envelope);
|
|
86
128
|
const handler = handlers[urn];
|
|
@@ -92,6 +134,13 @@ function processor(handlers, options = {}) {
|
|
|
92
134
|
}
|
|
93
135
|
return handler(envelope, job, headersOf(job));
|
|
94
136
|
};
|
|
137
|
+
return Object.assign(run, {
|
|
138
|
+
async close() {
|
|
139
|
+
const queues = [...created.values()];
|
|
140
|
+
created.clear();
|
|
141
|
+
await Promise.all(queues.map((queue) => queue.close()));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
95
144
|
}
|
|
96
145
|
// Annotate the CommonJS export names for ESM import in node:
|
|
97
146
|
0 && (module.exports = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * BullMQ adapter for BabelQueue.\n *\n * It makes your BullMQ jobs carry the canonical BabelQueue envelope and routes\n * them by URN, so a BullMQ-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the PHP/Laravel, Python, Go, Java and\n * .NET SDKs.\n *\n * import { Queue, Worker } from \"bullmq\";\n * import { publish, processor } from \"@babelqueue/bullmq\";\n *\n * const queue = new Queue(\"orders\", { connection });\n * await publish(queue, \"urn:babel:orders:created\", { order_id: 1042 });\n *\n * new Worker(\"orders\", processor({\n * \"urn:babel:orders:created\": async (env) => { ... },\n * }), { connection });\n */\n\nimport {\n BabelQueueError,\n EnvelopeCodec,\n UnknownUrnError,\n type Envelope,\n type HeaderCarrier,\n type IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport type { Job, JobsOptions, Queue } from \"bullmq\";\n\nimport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\nexport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\n/**\n * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native\n * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without\n * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child\n * of the producer span (ADR-0028).\n */\nexport function headersOf(job: Job): HeaderCarrier {\n return decodeMetadata(job.opts?.telemetry?.metadata);\n}\n\n/**\n * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band\n * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).\n */\nexport type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** BullMQ job options (delay, attempts, backoff, …). */\n jobsOptions?: JobsOptions;\n /**\n * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —\n * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope\n * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options\n * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).\n */\n headers?: HeaderCarrier;\n}\n\n/**\n * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the\n * job name is the URN, the job data is the envelope). Returns the message id\n * (`meta.id`).\n *\n * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized\n * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the\n * envelope itself is never touched.\n */\nexport async function publish(\n queue: Queue,\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queue.name,\n traceId: options.traceId,\n });\n await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));\n return envelope.meta.id;\n}\n\n/**\n * Merge an out-of-band header carrier into a job's `telemetry.metadata` without clobbering an\n * explicit one the caller already set. Returns the original options unchanged when there is nothing\n * to carry, so a header-less publish stays byte-identical.\n */\nfunction withHeaders(jobsOptions: JobsOptions | undefined, headers: HeaderCarrier | undefined): JobsOptions | undefined {\n const metadata = encodeMetadata(headers);\n if (metadata === undefined) return jobsOptions;\n const telemetry = jobsOptions?.telemetry;\n if (telemetry?.metadata != null) return jobsOptions; // an explicit metadata wins (merge-not-clobber)\n return { ...jobsOptions, telemetry: { ...telemetry, metadata } };\n}\n\n/** Options for {@link processor}. */\nexport interface ProcessorOptions {\n /** Called instead of throwing when a job's URN has no handler. */\n onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;\n}\n\n/**\n * Build a BullMQ processor that decodes each job's envelope, validates it, and\n * routes it to the handler registered for its URN. A non-conformant envelope\n * throws (so BullMQ retries / fails the job per its options); an unmapped URN\n * throws unless `onUnknownUrn` is given.\n *\n * new Worker(\"orders\", processor(handlers), { connection });\n */\nexport function processor(\n handlers: BabelHandlers,\n options: ProcessorOptions = {},\n): (job: Job) => Promise<unknown> {\n return async (job: Job): Promise<unknown> => {\n const envelope = job.data as IncomingEnvelope;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n throw new BabelQueueError(\n `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? \"?\"}.`,\n );\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = handlers[urn];\n if (!handler) {\n if (options.onUnknownUrn) {\n return options.onUnknownUrn(envelope, job);\n }\n throw new UnknownUrnError(urn);\n }\n\n return handler(envelope, job, headersOf(job));\n };\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They carry an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.\n *\n * BullMQ has no AMQP-style header table; its native context-propagation slot is\n * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is\n * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is\n * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).\n */\nexport function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined {\n const clean = sanitizeHeaders(headers);\n return Object.keys(clean).length === 0 ? undefined : JSON.stringify(clean);\n}\n\n/**\n * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for\n * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue\n * producer) consumes with no headers and no error.\n */\nexport function decodeMetadata(metadata: string | undefined | null): HeaderCarrier {\n if (!metadata) return {};\n try {\n const parsed = JSON.parse(metadata) as unknown;\n if (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) return {};\n return sanitizeHeaders(parsed as HeaderCarrier);\n } catch {\n return {};\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,kBAOO;;;ACVA,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAMO,SAAS,eAAe,SAA+D;AAC5F,QAAM,QAAQ,gBAAgB,OAAO;AACrC,SAAO,OAAO,KAAK,KAAK,EAAE,WAAW,IAAI,SAAY,KAAK,UAAU,KAAK;AAC3E;AAOO,SAAS,eAAe,UAAoD;AACjF,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,QAAQ;AAClC,QAAI,UAAU,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACnF,WAAO,gBAAgB,MAAuB;AAAA,EAChD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ADXO,SAAS,UAAU,KAAyB;AACjD,SAAO,eAAe,IAAI,MAAM,WAAW,QAAQ;AACrD;AAmCA,eAAsB,QACpB,OACA,KACA,MACA,UAA0B,CAAC,GACV;AACjB,QAAM,WAAW,0BAAc,KAAK,KAAK,MAAM;AAAA,IAC7C,OAAO,MAAM;AAAA,IACb,SAAS,QAAQ;AAAA,EACnB,CAAC;AACD,QAAM,MAAM,IAAI,KAAK,UAAU,YAAY,QAAQ,aAAa,QAAQ,OAAO,CAAC;AAChF,SAAO,SAAS,KAAK;AACvB;AAOA,SAAS,YAAY,aAAsC,SAA6D;AACtH,QAAM,WAAW,eAAe,OAAO;AACvC,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,YAAY,aAAa;AAC/B,MAAI,WAAW,YAAY,KAAM,QAAO;AACxC,SAAO,EAAE,GAAG,aAAa,WAAW,EAAE,GAAG,WAAW,SAAS,EAAE;AACjE;AAgBO,SAAS,UACd,UACA,UAA4B,CAAC,GACG;AAChC,SAAO,OAAO,QAA+B;AAC3C,UAAM,WAAW,IAAI;AAErB,QAAI,CAAC,0BAAc,QAAQ,QAAQ,GAAG;AACpC,YAAM,IAAI;AAAA,QACR,wDAAwD,IAAI,MAAM,GAAG;AAAA,MACvE;AAAA,IACF;AAEA,UAAM,MAAM,0BAAc,IAAI,QAAQ;AACtC,UAAM,UAAU,SAAS,GAAG;AAC5B,QAAI,CAAC,SAAS;AACZ,UAAI,QAAQ,cAAc;AACxB,eAAO,QAAQ,aAAa,UAAU,GAAG;AAAA,MAC3C;AACA,YAAM,IAAI,4BAAgB,GAAG;AAAA,IAC/B;AAEA,WAAO,QAAQ,UAAU,KAAK,UAAU,GAAG,CAAC;AAAA,EAC9C;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * BullMQ adapter for BabelQueue.\n *\n * It makes your BullMQ jobs carry the canonical BabelQueue envelope and routes\n * them by URN, so a BullMQ-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the PHP/Laravel, Python, Go, Java and\n * .NET SDKs.\n *\n * import { Queue, Worker } from \"bullmq\";\n * import { publish, processor } from \"@babelqueue/bullmq\";\n *\n * const queue = new Queue(\"orders\", { connection });\n * await publish(queue, \"urn:babel:orders:created\", { order_id: 1042 });\n *\n * new Worker(\"orders\", processor({\n * \"urn:babel:orders:created\": async (env) => { ... },\n * }), { connection });\n */\n\nimport {\n BabelQueueError,\n EnvelopeCodec,\n UnknownUrnError,\n annotate,\n buildPoisonRecord,\n type Envelope,\n type HeaderCarrier,\n type IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport { Queue, UnrecoverableError } from \"bullmq\";\nimport type { Job, JobsOptions, QueueOptions } from \"bullmq\";\n\nimport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\nexport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\n/**\n * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native\n * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without\n * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child\n * of the producer span (ADR-0028).\n */\nexport function headersOf(job: Job): HeaderCarrier {\n return decodeMetadata(job.opts?.telemetry?.metadata);\n}\n\n/**\n * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band\n * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).\n */\nexport type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** BullMQ job options (delay, attempts, backoff, …). */\n jobsOptions?: JobsOptions;\n /**\n * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —\n * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope\n * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options\n * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).\n */\n headers?: HeaderCarrier;\n}\n\n/**\n * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the\n * job name is the URN, the job data is the envelope). Returns the message id\n * (`meta.id`).\n *\n * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized\n * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the\n * envelope itself is never touched.\n */\nexport async function publish(\n queue: Queue,\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queue.name,\n traceId: options.traceId,\n });\n await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));\n return envelope.meta.id;\n}\n\n/**\n * Merge an out-of-band header carrier into a job's `telemetry.metadata` without clobbering an\n * explicit one the caller already set. Returns the original options unchanged when there is nothing\n * to carry, so a header-less publish stays byte-identical.\n */\nfunction withHeaders(jobsOptions: JobsOptions | undefined, headers: HeaderCarrier | undefined): JobsOptions | undefined {\n const metadata = encodeMetadata(headers);\n if (metadata === undefined) return jobsOptions;\n const telemetry = jobsOptions?.telemetry;\n if (telemetry?.metadata != null) return jobsOptions; // an explicit metadata wins (merge-not-clobber)\n return { ...jobsOptions, telemetry: { ...telemetry, metadata } };\n}\n\n/** Options for {@link processor}. */\nexport interface ProcessorOptions {\n /** Called instead of throwing when a job's URN has no handler. */\n onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;\n /**\n * A dead-letter queue for **poison** jobs (ADR-0037 decision 3; opt-in, absent = none): a job whose data does\n * not decode into a valid envelope. When set, the ADR-0035 record (`reason: \"poison\"`, built with the core's\n * `buildPoisonRecord`) is **also** added to it as a job before the job fails.\n *\n * - a BullMQ `Queue` instance is used as given;\n * - a `string` is a queue name: the adapter creates a `Queue` with that name on the worker's connection;\n * - `true` creates a `Queue` named `<queue>.dlq` (`<queue>` = the worker's queue name) on the worker's connection.\n *\n * A `Queue` the adapter creates is closed by the `close()` of the returned processor; a `Queue` instance you pass is yours to close.\n * The job data is JSON that BullMQ has already parsed, so a value `JSON.parse` cannot carry (`1e400`, an integer\n * beyond 2^53) is lost before the adapter sees it: a property of BullMQ, not of the record rule.\n */\n deadLetterQueue?: Queue | string | true;\n /** The `dead_letter.original_queue` of a poison record. Defaults to the worker's queue name (`job.queueName`). */\n queue?: string;\n /** Called when adding the poison record to {@link deadLetterQueue} fails. The job still fails as unrecoverable. */\n onError?: (error: unknown, job: Job) => void;\n}\n\n/** The worker's `Queue` connection options and prefix, read from the job's queue (BullMQ keeps them on `queue.opts`). */\nfunction workerQueueOptions(job: Job): QueueOptions {\n const opts = (job as unknown as { queue?: { opts?: Partial<QueueOptions> } }).queue?.opts;\n return { connection: opts?.connection as QueueOptions[\"connection\"], ...(opts?.prefix === undefined ? {} : { prefix: opts.prefix }) };\n}\n\n/**\n * Build a BullMQ processor that decodes each job's envelope, validates it, and\n * routes it to the handler registered for its URN. A **poison** job (data that is\n * not a valid envelope) fails with BullMQ's `UnrecoverableError` — no retry, straight\n * to the failed set (ADR-0037 decision 3), after the ADR-0035 record was added to the\n * optional `deadLetterQueue`; an unmapped URN throws unless `onUnknownUrn` is given\n * (BullMQ retries / fails the job per its options), as does a throwing handler.\n *\n * new Worker(\"orders\", processor(handlers), { connection });\n */\nexport function processor(\n handlers: BabelHandlers,\n options: ProcessorOptions = {},\n): ((job: Job) => Promise<unknown>) & { close(): Promise<void> } {\n // One adapter-created Queue per (prefix, name): a processor shared by several Workers must not send queue B's poison\n // records to queue A's `<queue>.dlq` (ADR-0037 decision 3: `<queue>` is the *worker's* queue name).\n const created = new Map<string, Queue>();\n\n const resolveDeadLetterQueue = (job: Job): Queue | undefined => {\n const target = options.deadLetterQueue;\n if (target === undefined) return undefined;\n if (typeof target === \"object\") return target;\n const name = target === true ? `${job.queueName}.dlq` : target;\n const queueOptions = workerQueueOptions(job);\n const key = `${queueOptions.prefix ?? \"\"}\\u0000${name}`;\n let queue = created.get(key);\n if (queue === undefined) {\n queue = new Queue(name, queueOptions);\n created.set(key, queue);\n }\n return queue;\n };\n\n /** Add the ADR-0035 record to the DLQ; a failure is reported and never replaces the unrecoverable failure. */\n const deadLetterPoison = async (job: Job, error: BabelQueueError): Promise<void> => {\n try {\n const dlq = resolveDeadLetterQueue(job);\n if (dlq === undefined) return;\n // The body is the job data serialised as JSON (BullMQ already parsed what it stored).\n const body = JSON.stringify(job.data) ?? \"null\";\n const delivered = typeof job.attemptsMade === \"number\" ? job.attemptsMade : 0; // informational\n const deadLetter = annotate({ attempts: delivered } as unknown as Envelope, \"poison\", options.queue ?? job.queueName, {\n attempts: delivered,\n error: error.message,\n exception: error.name,\n }).dead_letter;\n const record = buildPoisonRecord(Buffer.from(body, \"utf8\"), JSON.stringify(deadLetter));\n await dlq.add(job.name, JSON.parse(record) as Record<string, unknown>);\n } catch (writeError) {\n try {\n options.onError?.(writeError, job);\n } catch {\n // a throwing report must not replace the unrecoverable failure\n }\n }\n };\n\n const run = async (job: Job): Promise<unknown> => {\n const envelope = job.data as IncomingEnvelope;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n const error = new BabelQueueError(\n `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? \"?\"}.`,\n );\n await deadLetterPoison(job, error);\n // Never retried: BullMQ moves an UnrecoverableError straight to the failed set, which keeps the job data.\n throw new UnrecoverableError(error.message);\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = handlers[urn];\n if (!handler) {\n if (options.onUnknownUrn) {\n return options.onUnknownUrn(envelope, job);\n }\n throw new UnknownUrnError(urn);\n }\n\n return handler(envelope, job, headersOf(job));\n };\n\n // Closes the `Queue` the adapter created for a `string` / `true` deadLetterQueue (a no-op otherwise: a\n // `Queue` instance you passed is yours to close).\n return Object.assign(run, {\n async close(): Promise<void> {\n const queues = [...created.values()];\n created.clear();\n await Promise.all(queues.map((queue) => queue.close()));\n },\n });\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They carry an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.\n *\n * BullMQ has no AMQP-style header table; its native context-propagation slot is\n * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is\n * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is\n * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).\n */\nexport function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined {\n const clean = sanitizeHeaders(headers);\n return Object.keys(clean).length === 0 ? undefined : JSON.stringify(clean);\n}\n\n/**\n * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for\n * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue\n * producer) consumes with no headers and no error.\n */\nexport function decodeMetadata(metadata: string | undefined | null): HeaderCarrier {\n if (!metadata) return {};\n try {\n const parsed = JSON.parse(metadata) as unknown;\n if (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) return {};\n return sanitizeHeaders(parsed as HeaderCarrier);\n } catch {\n return {};\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HeaderCarrier, Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
-
import { Job,
|
|
2
|
+
import { Job, Queue, JobsOptions } from 'bullmq';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node
|
|
@@ -84,15 +84,37 @@ declare function publish(queue: Queue, urn: string, data: Record<string, unknown
|
|
|
84
84
|
interface ProcessorOptions {
|
|
85
85
|
/** Called instead of throwing when a job's URN has no handler. */
|
|
86
86
|
onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;
|
|
87
|
+
/**
|
|
88
|
+
* A dead-letter queue for **poison** jobs (ADR-0037 decision 3; opt-in, absent = none): a job whose data does
|
|
89
|
+
* not decode into a valid envelope. When set, the ADR-0035 record (`reason: "poison"`, built with the core's
|
|
90
|
+
* `buildPoisonRecord`) is **also** added to it as a job before the job fails.
|
|
91
|
+
*
|
|
92
|
+
* - a BullMQ `Queue` instance is used as given;
|
|
93
|
+
* - a `string` is a queue name: the adapter creates a `Queue` with that name on the worker's connection;
|
|
94
|
+
* - `true` creates a `Queue` named `<queue>.dlq` (`<queue>` = the worker's queue name) on the worker's connection.
|
|
95
|
+
*
|
|
96
|
+
* A `Queue` the adapter creates is closed by the `close()` of the returned processor; a `Queue` instance you pass is yours to close.
|
|
97
|
+
* The job data is JSON that BullMQ has already parsed, so a value `JSON.parse` cannot carry (`1e400`, an integer
|
|
98
|
+
* beyond 2^53) is lost before the adapter sees it: a property of BullMQ, not of the record rule.
|
|
99
|
+
*/
|
|
100
|
+
deadLetterQueue?: Queue | string | true;
|
|
101
|
+
/** The `dead_letter.original_queue` of a poison record. Defaults to the worker's queue name (`job.queueName`). */
|
|
102
|
+
queue?: string;
|
|
103
|
+
/** Called when adding the poison record to {@link deadLetterQueue} fails. The job still fails as unrecoverable. */
|
|
104
|
+
onError?: (error: unknown, job: Job) => void;
|
|
87
105
|
}
|
|
88
106
|
/**
|
|
89
107
|
* Build a BullMQ processor that decodes each job's envelope, validates it, and
|
|
90
|
-
* routes it to the handler registered for its URN. A
|
|
91
|
-
*
|
|
92
|
-
*
|
|
108
|
+
* routes it to the handler registered for its URN. A **poison** job (data that is
|
|
109
|
+
* not a valid envelope) fails with BullMQ's `UnrecoverableError` — no retry, straight
|
|
110
|
+
* to the failed set (ADR-0037 decision 3), after the ADR-0035 record was added to the
|
|
111
|
+
* optional `deadLetterQueue`; an unmapped URN throws unless `onUnknownUrn` is given
|
|
112
|
+
* (BullMQ retries / fails the job per its options), as does a throwing handler.
|
|
93
113
|
*
|
|
94
114
|
* new Worker("orders", processor(handlers), { connection });
|
|
95
115
|
*/
|
|
96
|
-
declare function processor(handlers: BabelHandlers, options?: ProcessorOptions): (job: Job) => Promise<unknown
|
|
116
|
+
declare function processor(handlers: BabelHandlers, options?: ProcessorOptions): ((job: Job) => Promise<unknown>) & {
|
|
117
|
+
close(): Promise<void>;
|
|
118
|
+
};
|
|
97
119
|
|
|
98
120
|
export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, decodeMetadata, encodeMetadata, headersOf, processor, publish };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HeaderCarrier, Envelope, IncomingEnvelope } from '@babelqueue/core';
|
|
2
|
-
import { Job,
|
|
2
|
+
import { Job, Queue, JobsOptions } from 'bullmq';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node
|
|
@@ -84,15 +84,37 @@ declare function publish(queue: Queue, urn: string, data: Record<string, unknown
|
|
|
84
84
|
interface ProcessorOptions {
|
|
85
85
|
/** Called instead of throwing when a job's URN has no handler. */
|
|
86
86
|
onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;
|
|
87
|
+
/**
|
|
88
|
+
* A dead-letter queue for **poison** jobs (ADR-0037 decision 3; opt-in, absent = none): a job whose data does
|
|
89
|
+
* not decode into a valid envelope. When set, the ADR-0035 record (`reason: "poison"`, built with the core's
|
|
90
|
+
* `buildPoisonRecord`) is **also** added to it as a job before the job fails.
|
|
91
|
+
*
|
|
92
|
+
* - a BullMQ `Queue` instance is used as given;
|
|
93
|
+
* - a `string` is a queue name: the adapter creates a `Queue` with that name on the worker's connection;
|
|
94
|
+
* - `true` creates a `Queue` named `<queue>.dlq` (`<queue>` = the worker's queue name) on the worker's connection.
|
|
95
|
+
*
|
|
96
|
+
* A `Queue` the adapter creates is closed by the `close()` of the returned processor; a `Queue` instance you pass is yours to close.
|
|
97
|
+
* The job data is JSON that BullMQ has already parsed, so a value `JSON.parse` cannot carry (`1e400`, an integer
|
|
98
|
+
* beyond 2^53) is lost before the adapter sees it: a property of BullMQ, not of the record rule.
|
|
99
|
+
*/
|
|
100
|
+
deadLetterQueue?: Queue | string | true;
|
|
101
|
+
/** The `dead_letter.original_queue` of a poison record. Defaults to the worker's queue name (`job.queueName`). */
|
|
102
|
+
queue?: string;
|
|
103
|
+
/** Called when adding the poison record to {@link deadLetterQueue} fails. The job still fails as unrecoverable. */
|
|
104
|
+
onError?: (error: unknown, job: Job) => void;
|
|
87
105
|
}
|
|
88
106
|
/**
|
|
89
107
|
* Build a BullMQ processor that decodes each job's envelope, validates it, and
|
|
90
|
-
* routes it to the handler registered for its URN. A
|
|
91
|
-
*
|
|
92
|
-
*
|
|
108
|
+
* routes it to the handler registered for its URN. A **poison** job (data that is
|
|
109
|
+
* not a valid envelope) fails with BullMQ's `UnrecoverableError` — no retry, straight
|
|
110
|
+
* to the failed set (ADR-0037 decision 3), after the ADR-0035 record was added to the
|
|
111
|
+
* optional `deadLetterQueue`; an unmapped URN throws unless `onUnknownUrn` is given
|
|
112
|
+
* (BullMQ retries / fails the job per its options), as does a throwing handler.
|
|
93
113
|
*
|
|
94
114
|
* new Worker("orders", processor(handlers), { connection });
|
|
95
115
|
*/
|
|
96
|
-
declare function processor(handlers: BabelHandlers, options?: ProcessorOptions): (job: Job) => Promise<unknown
|
|
116
|
+
declare function processor(handlers: BabelHandlers, options?: ProcessorOptions): ((job: Job) => Promise<unknown>) & {
|
|
117
|
+
close(): Promise<void>;
|
|
118
|
+
};
|
|
97
119
|
|
|
98
120
|
export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, decodeMetadata, encodeMetadata, headersOf, processor, publish };
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
BabelQueueError,
|
|
4
4
|
EnvelopeCodec,
|
|
5
|
-
UnknownUrnError
|
|
5
|
+
UnknownUrnError,
|
|
6
|
+
annotate,
|
|
7
|
+
buildPoisonRecord
|
|
6
8
|
} from "@babelqueue/core";
|
|
9
|
+
import { Queue, UnrecoverableError } from "bullmq";
|
|
7
10
|
|
|
8
11
|
// src/headers.ts
|
|
9
12
|
function sanitizeHeaders(headers) {
|
|
@@ -50,13 +53,54 @@ function withHeaders(jobsOptions, headers) {
|
|
|
50
53
|
if (telemetry?.metadata != null) return jobsOptions;
|
|
51
54
|
return { ...jobsOptions, telemetry: { ...telemetry, metadata } };
|
|
52
55
|
}
|
|
56
|
+
function workerQueueOptions(job) {
|
|
57
|
+
const opts = job.queue?.opts;
|
|
58
|
+
return { connection: opts?.connection, ...opts?.prefix === void 0 ? {} : { prefix: opts.prefix } };
|
|
59
|
+
}
|
|
53
60
|
function processor(handlers, options = {}) {
|
|
54
|
-
|
|
61
|
+
const created = /* @__PURE__ */ new Map();
|
|
62
|
+
const resolveDeadLetterQueue = (job) => {
|
|
63
|
+
const target = options.deadLetterQueue;
|
|
64
|
+
if (target === void 0) return void 0;
|
|
65
|
+
if (typeof target === "object") return target;
|
|
66
|
+
const name = target === true ? `${job.queueName}.dlq` : target;
|
|
67
|
+
const queueOptions = workerQueueOptions(job);
|
|
68
|
+
const key = `${queueOptions.prefix ?? ""}\0${name}`;
|
|
69
|
+
let queue = created.get(key);
|
|
70
|
+
if (queue === void 0) {
|
|
71
|
+
queue = new Queue(name, queueOptions);
|
|
72
|
+
created.set(key, queue);
|
|
73
|
+
}
|
|
74
|
+
return queue;
|
|
75
|
+
};
|
|
76
|
+
const deadLetterPoison = async (job, error) => {
|
|
77
|
+
try {
|
|
78
|
+
const dlq = resolveDeadLetterQueue(job);
|
|
79
|
+
if (dlq === void 0) return;
|
|
80
|
+
const body = JSON.stringify(job.data) ?? "null";
|
|
81
|
+
const delivered = typeof job.attemptsMade === "number" ? job.attemptsMade : 0;
|
|
82
|
+
const deadLetter = annotate({ attempts: delivered }, "poison", options.queue ?? job.queueName, {
|
|
83
|
+
attempts: delivered,
|
|
84
|
+
error: error.message,
|
|
85
|
+
exception: error.name
|
|
86
|
+
}).dead_letter;
|
|
87
|
+
const record = buildPoisonRecord(Buffer.from(body, "utf8"), JSON.stringify(deadLetter));
|
|
88
|
+
await dlq.add(job.name, JSON.parse(record));
|
|
89
|
+
} catch (writeError) {
|
|
90
|
+
try {
|
|
91
|
+
options.onError?.(writeError, job);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const run = async (job) => {
|
|
55
97
|
const envelope = job.data;
|
|
56
98
|
if (!EnvelopeCodec.accepts(envelope)) {
|
|
57
|
-
|
|
99
|
+
const error = new BabelQueueError(
|
|
58
100
|
`Rejected a non-conformant BabelQueue envelope on job ${job.id ?? "?"}.`
|
|
59
101
|
);
|
|
102
|
+
await deadLetterPoison(job, error);
|
|
103
|
+
throw new UnrecoverableError(error.message);
|
|
60
104
|
}
|
|
61
105
|
const urn = EnvelopeCodec.urn(envelope);
|
|
62
106
|
const handler = handlers[urn];
|
|
@@ -68,6 +112,13 @@ function processor(handlers, options = {}) {
|
|
|
68
112
|
}
|
|
69
113
|
return handler(envelope, job, headersOf(job));
|
|
70
114
|
};
|
|
115
|
+
return Object.assign(run, {
|
|
116
|
+
async close() {
|
|
117
|
+
const queues = [...created.values()];
|
|
118
|
+
created.clear();
|
|
119
|
+
await Promise.all(queues.map((queue) => queue.close()));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
71
122
|
}
|
|
72
123
|
export {
|
|
73
124
|
decodeMetadata,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * BullMQ adapter for BabelQueue.\n *\n * It makes your BullMQ jobs carry the canonical BabelQueue envelope and routes\n * them by URN, so a BullMQ-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the PHP/Laravel, Python, Go, Java and\n * .NET SDKs.\n *\n * import { Queue, Worker } from \"bullmq\";\n * import { publish, processor } from \"@babelqueue/bullmq\";\n *\n * const queue = new Queue(\"orders\", { connection });\n * await publish(queue, \"urn:babel:orders:created\", { order_id: 1042 });\n *\n * new Worker(\"orders\", processor({\n * \"urn:babel:orders:created\": async (env) => { ... },\n * }), { connection });\n */\n\nimport {\n BabelQueueError,\n EnvelopeCodec,\n UnknownUrnError,\n type Envelope,\n type HeaderCarrier,\n type IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport type { Job, JobsOptions, Queue } from \"bullmq\";\n\nimport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\nexport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\n/**\n * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native\n * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without\n * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child\n * of the producer span (ADR-0028).\n */\nexport function headersOf(job: Job): HeaderCarrier {\n return decodeMetadata(job.opts?.telemetry?.metadata);\n}\n\n/**\n * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band\n * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).\n */\nexport type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** BullMQ job options (delay, attempts, backoff, …). */\n jobsOptions?: JobsOptions;\n /**\n * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —\n * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope\n * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options\n * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).\n */\n headers?: HeaderCarrier;\n}\n\n/**\n * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the\n * job name is the URN, the job data is the envelope). Returns the message id\n * (`meta.id`).\n *\n * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized\n * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the\n * envelope itself is never touched.\n */\nexport async function publish(\n queue: Queue,\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queue.name,\n traceId: options.traceId,\n });\n await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));\n return envelope.meta.id;\n}\n\n/**\n * Merge an out-of-band header carrier into a job's `telemetry.metadata` without clobbering an\n * explicit one the caller already set. Returns the original options unchanged when there is nothing\n * to carry, so a header-less publish stays byte-identical.\n */\nfunction withHeaders(jobsOptions: JobsOptions | undefined, headers: HeaderCarrier | undefined): JobsOptions | undefined {\n const metadata = encodeMetadata(headers);\n if (metadata === undefined) return jobsOptions;\n const telemetry = jobsOptions?.telemetry;\n if (telemetry?.metadata != null) return jobsOptions; // an explicit metadata wins (merge-not-clobber)\n return { ...jobsOptions, telemetry: { ...telemetry, metadata } };\n}\n\n/** Options for {@link processor}. */\nexport interface ProcessorOptions {\n /** Called instead of throwing when a job's URN has no handler. */\n onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;\n}\n\n/**\n * Build a BullMQ processor that decodes each job's envelope, validates it, and\n * routes it to the handler registered for its URN. A non-conformant envelope\n * throws (so BullMQ retries / fails the job per its options); an unmapped URN\n * throws unless `onUnknownUrn` is given.\n *\n * new Worker(\"orders\", processor(handlers), { connection });\n */\nexport function processor(\n handlers: BabelHandlers,\n options: ProcessorOptions = {},\n): (job: Job) => Promise<unknown> {\n return async (job: Job): Promise<unknown> => {\n const envelope = job.data as IncomingEnvelope;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n throw new BabelQueueError(\n `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? \"?\"}.`,\n );\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = handlers[urn];\n if (!handler) {\n if (options.onUnknownUrn) {\n return options.onUnknownUrn(envelope, job);\n }\n throw new UnknownUrnError(urn);\n }\n\n return handler(envelope, job, headersOf(job));\n };\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They carry an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.\n *\n * BullMQ has no AMQP-style header table; its native context-propagation slot is\n * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is\n * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is\n * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).\n */\nexport function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined {\n const clean = sanitizeHeaders(headers);\n return Object.keys(clean).length === 0 ? undefined : JSON.stringify(clean);\n}\n\n/**\n * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for\n * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue\n * producer) consumes with no headers and no error.\n */\nexport function decodeMetadata(metadata: string | undefined | null): HeaderCarrier {\n if (!metadata) return {};\n try {\n const parsed = JSON.parse(metadata) as unknown;\n if (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) return {};\n return sanitizeHeaders(parsed as HeaderCarrier);\n } catch {\n return {};\n }\n}\n"],"mappings":";AAmBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;;;ACVA,SAAS,gBAAgB,SAA0D;AACxF,QAAM,MAAqB,CAAC;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,QAAQ,MAAM,SAAS,QAAQ,UAAU,GAAI;AACjD,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAMO,SAAS,eAAe,SAA+D;AAC5F,QAAM,QAAQ,gBAAgB,OAAO;AACrC,SAAO,OAAO,KAAK,KAAK,EAAE,WAAW,IAAI,SAAY,KAAK,UAAU,KAAK;AAC3E;AAOO,SAAS,eAAe,UAAoD;AACjF,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,QAAQ;AAClC,QAAI,UAAU,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACnF,WAAO,gBAAgB,MAAuB;AAAA,EAChD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ADXO,SAAS,UAAU,KAAyB;AACjD,SAAO,eAAe,IAAI,MAAM,WAAW,QAAQ;AACrD;AAmCA,eAAsB,QACpB,OACA,KACA,MACA,UAA0B,CAAC,GACV;AACjB,QAAM,WAAW,cAAc,KAAK,KAAK,MAAM;AAAA,IAC7C,OAAO,MAAM;AAAA,IACb,SAAS,QAAQ;AAAA,EACnB,CAAC;AACD,QAAM,MAAM,IAAI,KAAK,UAAU,YAAY,QAAQ,aAAa,QAAQ,OAAO,CAAC;AAChF,SAAO,SAAS,KAAK;AACvB;AAOA,SAAS,YAAY,aAAsC,SAA6D;AACtH,QAAM,WAAW,eAAe,OAAO;AACvC,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,YAAY,aAAa;AAC/B,MAAI,WAAW,YAAY,KAAM,QAAO;AACxC,SAAO,EAAE,GAAG,aAAa,WAAW,EAAE,GAAG,WAAW,SAAS,EAAE;AACjE;AAgBO,SAAS,UACd,UACA,UAA4B,CAAC,GACG;AAChC,SAAO,OAAO,QAA+B;AAC3C,UAAM,WAAW,IAAI;AAErB,QAAI,CAAC,cAAc,QAAQ,QAAQ,GAAG;AACpC,YAAM,IAAI;AAAA,QACR,wDAAwD,IAAI,MAAM,GAAG;AAAA,MACvE;AAAA,IACF;AAEA,UAAM,MAAM,cAAc,IAAI,QAAQ;AACtC,UAAM,UAAU,SAAS,GAAG;AAC5B,QAAI,CAAC,SAAS;AACZ,UAAI,QAAQ,cAAc;AACxB,eAAO,QAAQ,aAAa,UAAU,GAAG;AAAA,MAC3C;AACA,YAAM,IAAI,gBAAgB,GAAG;AAAA,IAC/B;AAEA,WAAO,QAAQ,UAAU,KAAK,UAAU,GAAG,CAAC;AAAA,EAC9C;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * BullMQ adapter for BabelQueue.\n *\n * It makes your BullMQ jobs carry the canonical BabelQueue envelope and routes\n * them by URN, so a BullMQ-based Node service speaks the same contract (envelope\n * shape, URN identity, trace propagation) as the PHP/Laravel, Python, Go, Java and\n * .NET SDKs.\n *\n * import { Queue, Worker } from \"bullmq\";\n * import { publish, processor } from \"@babelqueue/bullmq\";\n *\n * const queue = new Queue(\"orders\", { connection });\n * await publish(queue, \"urn:babel:orders:created\", { order_id: 1042 });\n *\n * new Worker(\"orders\", processor({\n * \"urn:babel:orders:created\": async (env) => { ... },\n * }), { connection });\n */\n\nimport {\n BabelQueueError,\n EnvelopeCodec,\n UnknownUrnError,\n annotate,\n buildPoisonRecord,\n type Envelope,\n type HeaderCarrier,\n type IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport { Queue, UnrecoverableError } from \"bullmq\";\nimport type { Job, JobsOptions, QueueOptions } from \"bullmq\";\n\nimport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\nexport { decodeMetadata, encodeMetadata } from \"./headers.js\";\n\n/**\n * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native\n * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without\n * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child\n * of the producer span (ADR-0028).\n */\nexport function headersOf(job: Job): HeaderCarrier {\n return decodeMetadata(job.opts?.telemetry?.metadata);\n}\n\n/**\n * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band\n * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).\n */\nexport type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** BullMQ job options (delay, attempts, backoff, …). */\n jobsOptions?: JobsOptions;\n /**\n * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —\n * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope\n * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options\n * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).\n */\n headers?: HeaderCarrier;\n}\n\n/**\n * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the\n * job name is the URN, the job data is the envelope). Returns the message id\n * (`meta.id`).\n *\n * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized\n * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the\n * envelope itself is never touched.\n */\nexport async function publish(\n queue: Queue,\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: queue.name,\n traceId: options.traceId,\n });\n await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));\n return envelope.meta.id;\n}\n\n/**\n * Merge an out-of-band header carrier into a job's `telemetry.metadata` without clobbering an\n * explicit one the caller already set. Returns the original options unchanged when there is nothing\n * to carry, so a header-less publish stays byte-identical.\n */\nfunction withHeaders(jobsOptions: JobsOptions | undefined, headers: HeaderCarrier | undefined): JobsOptions | undefined {\n const metadata = encodeMetadata(headers);\n if (metadata === undefined) return jobsOptions;\n const telemetry = jobsOptions?.telemetry;\n if (telemetry?.metadata != null) return jobsOptions; // an explicit metadata wins (merge-not-clobber)\n return { ...jobsOptions, telemetry: { ...telemetry, metadata } };\n}\n\n/** Options for {@link processor}. */\nexport interface ProcessorOptions {\n /** Called instead of throwing when a job's URN has no handler. */\n onUnknownUrn?: (envelope: IncomingEnvelope, job: Job) => unknown | Promise<unknown>;\n /**\n * A dead-letter queue for **poison** jobs (ADR-0037 decision 3; opt-in, absent = none): a job whose data does\n * not decode into a valid envelope. When set, the ADR-0035 record (`reason: \"poison\"`, built with the core's\n * `buildPoisonRecord`) is **also** added to it as a job before the job fails.\n *\n * - a BullMQ `Queue` instance is used as given;\n * - a `string` is a queue name: the adapter creates a `Queue` with that name on the worker's connection;\n * - `true` creates a `Queue` named `<queue>.dlq` (`<queue>` = the worker's queue name) on the worker's connection.\n *\n * A `Queue` the adapter creates is closed by the `close()` of the returned processor; a `Queue` instance you pass is yours to close.\n * The job data is JSON that BullMQ has already parsed, so a value `JSON.parse` cannot carry (`1e400`, an integer\n * beyond 2^53) is lost before the adapter sees it: a property of BullMQ, not of the record rule.\n */\n deadLetterQueue?: Queue | string | true;\n /** The `dead_letter.original_queue` of a poison record. Defaults to the worker's queue name (`job.queueName`). */\n queue?: string;\n /** Called when adding the poison record to {@link deadLetterQueue} fails. The job still fails as unrecoverable. */\n onError?: (error: unknown, job: Job) => void;\n}\n\n/** The worker's `Queue` connection options and prefix, read from the job's queue (BullMQ keeps them on `queue.opts`). */\nfunction workerQueueOptions(job: Job): QueueOptions {\n const opts = (job as unknown as { queue?: { opts?: Partial<QueueOptions> } }).queue?.opts;\n return { connection: opts?.connection as QueueOptions[\"connection\"], ...(opts?.prefix === undefined ? {} : { prefix: opts.prefix }) };\n}\n\n/**\n * Build a BullMQ processor that decodes each job's envelope, validates it, and\n * routes it to the handler registered for its URN. A **poison** job (data that is\n * not a valid envelope) fails with BullMQ's `UnrecoverableError` — no retry, straight\n * to the failed set (ADR-0037 decision 3), after the ADR-0035 record was added to the\n * optional `deadLetterQueue`; an unmapped URN throws unless `onUnknownUrn` is given\n * (BullMQ retries / fails the job per its options), as does a throwing handler.\n *\n * new Worker(\"orders\", processor(handlers), { connection });\n */\nexport function processor(\n handlers: BabelHandlers,\n options: ProcessorOptions = {},\n): ((job: Job) => Promise<unknown>) & { close(): Promise<void> } {\n // One adapter-created Queue per (prefix, name): a processor shared by several Workers must not send queue B's poison\n // records to queue A's `<queue>.dlq` (ADR-0037 decision 3: `<queue>` is the *worker's* queue name).\n const created = new Map<string, Queue>();\n\n const resolveDeadLetterQueue = (job: Job): Queue | undefined => {\n const target = options.deadLetterQueue;\n if (target === undefined) return undefined;\n if (typeof target === \"object\") return target;\n const name = target === true ? `${job.queueName}.dlq` : target;\n const queueOptions = workerQueueOptions(job);\n const key = `${queueOptions.prefix ?? \"\"}\\u0000${name}`;\n let queue = created.get(key);\n if (queue === undefined) {\n queue = new Queue(name, queueOptions);\n created.set(key, queue);\n }\n return queue;\n };\n\n /** Add the ADR-0035 record to the DLQ; a failure is reported and never replaces the unrecoverable failure. */\n const deadLetterPoison = async (job: Job, error: BabelQueueError): Promise<void> => {\n try {\n const dlq = resolveDeadLetterQueue(job);\n if (dlq === undefined) return;\n // The body is the job data serialised as JSON (BullMQ already parsed what it stored).\n const body = JSON.stringify(job.data) ?? \"null\";\n const delivered = typeof job.attemptsMade === \"number\" ? job.attemptsMade : 0; // informational\n const deadLetter = annotate({ attempts: delivered } as unknown as Envelope, \"poison\", options.queue ?? job.queueName, {\n attempts: delivered,\n error: error.message,\n exception: error.name,\n }).dead_letter;\n const record = buildPoisonRecord(Buffer.from(body, \"utf8\"), JSON.stringify(deadLetter));\n await dlq.add(job.name, JSON.parse(record) as Record<string, unknown>);\n } catch (writeError) {\n try {\n options.onError?.(writeError, job);\n } catch {\n // a throwing report must not replace the unrecoverable failure\n }\n }\n };\n\n const run = async (job: Job): Promise<unknown> => {\n const envelope = job.data as IncomingEnvelope;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n const error = new BabelQueueError(\n `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? \"?\"}.`,\n );\n await deadLetterPoison(job, error);\n // Never retried: BullMQ moves an UnrecoverableError straight to the failed set, which keeps the job data.\n throw new UnrecoverableError(error.message);\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = handlers[urn];\n if (!handler) {\n if (options.onUnknownUrn) {\n return options.onUnknownUrn(envelope, job);\n }\n throw new UnknownUrnError(urn);\n }\n\n return handler(envelope, job, headersOf(job));\n };\n\n // Closes the `Queue` the adapter created for a `string` / `true` deadLetterQueue (a no-op otherwise: a\n // `Queue` instance you passed is yours to close).\n return Object.assign(run, {\n async close(): Promise<void> {\n const queues = [...created.values()];\n created.clear();\n await Promise.all(queues.map((queue) => queue.close()));\n },\n });\n}\n","/**\n * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node\n * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\\Headers`. They carry an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.\n *\n * BullMQ has no AMQP-style header table; its native context-propagation slot is\n * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is\n * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.\n */\n\nimport type { HeaderCarrier } from \"@babelqueue/core\";\n\n/**\n * Copy `headers`, dropping blank keys and blank values. Returns an empty object when nothing\n * survives. A nullish input yields an empty object.\n */\nexport function sanitizeHeaders(headers: HeaderCarrier | null | undefined): HeaderCarrier {\n const out: HeaderCarrier = {};\n if (!headers) return out;\n for (const key of Object.keys(headers)) {\n const value = headers[key];\n if (key === \"\" || value == null || value === \"\") continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is\n * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).\n */\nexport function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined {\n const clean = sanitizeHeaders(headers);\n return Object.keys(clean).length === 0 ? undefined : JSON.stringify(clean);\n}\n\n/**\n * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for\n * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue\n * producer) consumes with no headers and no error.\n */\nexport function decodeMetadata(metadata: string | undefined | null): HeaderCarrier {\n if (!metadata) return {};\n try {\n const parsed = JSON.parse(metadata) as unknown;\n if (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) return {};\n return sanitizeHeaders(parsed as HeaderCarrier);\n } catch {\n return {};\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babelqueue/bullmq",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "BullMQ adapter for BabelQueue — canonical-envelope jobs and a URN-routed processor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"babelqueue",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "packages/bullmq"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
|
-
"author": "Muhammet Şafak <info@muhammetsafak.com
|
|
23
|
+
"author": "Muhammet Şafak <info@muhammetsafak.com>",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"test": "node --import tsx --test test/*.test.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@babelqueue/core": "^1.
|
|
53
|
+
"@babelqueue/core": "^1.13.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"bullmq": "^5.0.0"
|