@babelqueue/bullmq 1.0.1 → 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 CHANGED
@@ -39,15 +39,54 @@ new Worker(
39
39
  );
40
40
  ```
41
41
 
42
- `processor` validates each envelope (`EnvelopeCodec.accepts`) and routes by URN; a
43
- non-conformant envelope throws (BullMQ retries/fails per its options), and an
44
- unmapped URN throws unless you pass `onUnknownUrn`.
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
48
69
  > services. For raw cross-language queues, other SDKs read/write plain broker
49
70
  > queues directly.
50
71
 
72
+ ## OpenTelemetry tracing (ADR-0028)
73
+
74
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
75
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
76
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
77
+ BullMQ's native `telemetry.metadata` job-options slot (so the envelope — `job.data` — is never touched). On consume, the `processor` surfaces a delivered job's headers to the handler's third argument (and `headersOf(job)` reads them back),
78
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
79
+
80
+ ```ts
81
+ import { trace } from "@opentelemetry/api";
82
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
83
+ import type { HeaderCarrier } from "@babelqueue/core";
84
+
85
+ const headers: HeaderCarrier = {};
86
+ await tracedPublish(trace.getTracer("orders"), urn, data,
87
+ () => adapterPublish(urn, data, { headers }), { headers });
88
+ ```
89
+
51
90
  ## License
52
91
 
53
92
  [MIT](../../LICENSE) © Muhammet Şafak · [babelqueue.com](https://babelqueue.com)
package/dist/index.cjs CHANGED
@@ -20,26 +20,109 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ decodeMetadata: () => decodeMetadata,
24
+ encodeMetadata: () => encodeMetadata,
25
+ headersOf: () => headersOf,
23
26
  processor: () => processor,
24
27
  publish: () => publish
25
28
  });
26
29
  module.exports = __toCommonJS(index_exports);
27
30
  var import_core = require("@babelqueue/core");
31
+ var import_bullmq = require("bullmq");
32
+
33
+ // src/headers.ts
34
+ function sanitizeHeaders(headers) {
35
+ const out = {};
36
+ if (!headers) return out;
37
+ for (const key of Object.keys(headers)) {
38
+ const value = headers[key];
39
+ if (key === "" || value == null || value === "") continue;
40
+ out[key] = value;
41
+ }
42
+ return out;
43
+ }
44
+ function encodeMetadata(headers) {
45
+ const clean = sanitizeHeaders(headers);
46
+ return Object.keys(clean).length === 0 ? void 0 : JSON.stringify(clean);
47
+ }
48
+ function decodeMetadata(metadata) {
49
+ if (!metadata) return {};
50
+ try {
51
+ const parsed = JSON.parse(metadata);
52
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
53
+ return sanitizeHeaders(parsed);
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ // src/index.ts
60
+ function headersOf(job) {
61
+ return decodeMetadata(job.opts?.telemetry?.metadata);
62
+ }
28
63
  async function publish(queue, urn, data, options = {}) {
29
64
  const envelope = import_core.EnvelopeCodec.make(urn, data, {
30
65
  queue: queue.name,
31
66
  traceId: options.traceId
32
67
  });
33
- await queue.add(urn, envelope, options.jobsOptions);
68
+ await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));
34
69
  return envelope.meta.id;
35
70
  }
71
+ function withHeaders(jobsOptions, headers) {
72
+ const metadata = encodeMetadata(headers);
73
+ if (metadata === void 0) return jobsOptions;
74
+ const telemetry = jobsOptions?.telemetry;
75
+ if (telemetry?.metadata != null) return jobsOptions;
76
+ return { ...jobsOptions, telemetry: { ...telemetry, metadata } };
77
+ }
78
+ function workerQueueOptions(job) {
79
+ const opts = job.queue?.opts;
80
+ return { connection: opts?.connection, ...opts?.prefix === void 0 ? {} : { prefix: opts.prefix } };
81
+ }
36
82
  function processor(handlers, options = {}) {
37
- return async (job) => {
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) => {
38
119
  const envelope = job.data;
39
120
  if (!import_core.EnvelopeCodec.accepts(envelope)) {
40
- throw new import_core.BabelQueueError(
121
+ const error = new import_core.BabelQueueError(
41
122
  `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? "?"}.`
42
123
  );
124
+ await deadLetterPoison(job, error);
125
+ throw new import_bullmq.UnrecoverableError(error.message);
43
126
  }
44
127
  const urn = import_core.EnvelopeCodec.urn(envelope);
45
128
  const handler = handlers[urn];
@@ -49,11 +132,21 @@ function processor(handlers, options = {}) {
49
132
  }
50
133
  throw new import_core.UnknownUrnError(urn);
51
134
  }
52
- return handler(envelope, job);
135
+ return handler(envelope, job, headersOf(job));
53
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
+ });
54
144
  }
55
145
  // Annotate the CommonJS export names for ESM import in node:
56
146
  0 && (module.exports = {
147
+ decodeMetadata,
148
+ encodeMetadata,
149
+ headersOf,
57
150
  processor,
58
151
  publish
59
152
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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 IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport type { Job, JobsOptions, Queue } from \"bullmq\";\n\n/** A URN handler. Receives the validated envelope and the raw BullMQ job. */\nexport type BabelHandler = (envelope: Envelope, job: Job) => 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\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 */\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, options.jobsOptions);\n return envelope.meta.id;\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);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,kBAMO;AAsBP,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,QAAQ,WAAW;AAClD,SAAO,SAAS,KAAK;AACvB;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,GAAG;AAAA,EAC9B;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,27 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
2
- import { Job, JobsOptions, Queue } from 'bullmq';
1
+ import { HeaderCarrier, Envelope, IncomingEnvelope } from '@babelqueue/core';
2
+ import { Job, Queue, JobsOptions } from 'bullmq';
3
+
4
+ /**
5
+ * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node
6
+ * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\Headers`. They carry an injected
7
+ * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.
8
+ *
9
+ * BullMQ has no AMQP-style header table; its native context-propagation slot is
10
+ * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is
11
+ * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.
12
+ */
13
+
14
+ /**
15
+ * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is
16
+ * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).
17
+ */
18
+ declare function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined;
19
+ /**
20
+ * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for
21
+ * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue
22
+ * producer) consumes with no headers and no error.
23
+ */
24
+ declare function decodeMetadata(metadata: string | undefined | null): HeaderCarrier;
3
25
 
4
26
  /**
5
27
  * BullMQ adapter for BabelQueue.
@@ -20,8 +42,18 @@ import { Job, JobsOptions, Queue } from 'bullmq';
20
42
  * }), { connection });
21
43
  */
22
44
 
23
- /** A URN handler. Receives the validated envelope and the raw BullMQ job. */
24
- type BabelHandler = (envelope: Envelope, job: Job) => unknown | Promise<unknown>;
45
+ /**
46
+ * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native
47
+ * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without
48
+ * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child
49
+ * of the producer span (ADR-0028).
50
+ */
51
+ declare function headersOf(job: Job): HeaderCarrier;
52
+ /**
53
+ * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band
54
+ * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).
55
+ */
56
+ type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;
25
57
  /** A map of URN → handler. */
26
58
  type BabelHandlers = Record<string, BabelHandler>;
27
59
  /** Options for {@link publish}. */
@@ -30,26 +62,59 @@ interface PublishOptions {
30
62
  traceId?: string;
31
63
  /** BullMQ job options (delay, attempts, backoff, …). */
32
64
  jobsOptions?: JobsOptions;
65
+ /**
66
+ * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —
67
+ * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope
68
+ * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options
69
+ * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).
70
+ */
71
+ headers?: HeaderCarrier;
33
72
  }
34
73
  /**
35
74
  * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the
36
75
  * job name is the URN, the job data is the envelope). Returns the message id
37
76
  * (`meta.id`).
77
+ *
78
+ * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized
79
+ * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the
80
+ * envelope itself is never touched.
38
81
  */
39
82
  declare function publish(queue: Queue, urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
40
83
  /** Options for {@link processor}. */
41
84
  interface ProcessorOptions {
42
85
  /** Called instead of throwing when a job's URN has no handler. */
43
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;
44
105
  }
45
106
  /**
46
107
  * Build a BullMQ processor that decodes each job's envelope, validates it, and
47
- * routes it to the handler registered for its URN. A non-conformant envelope
48
- * throws (so BullMQ retries / fails the job per its options); an unmapped URN
49
- * throws unless `onUnknownUrn` is given.
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.
50
113
  *
51
114
  * new Worker("orders", processor(handlers), { connection });
52
115
  */
53
- 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
+ };
54
119
 
55
- export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, processor, publish };
120
+ export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, decodeMetadata, encodeMetadata, headersOf, processor, publish };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,27 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
2
- import { Job, JobsOptions, Queue } from 'bullmq';
1
+ import { HeaderCarrier, Envelope, IncomingEnvelope } from '@babelqueue/core';
2
+ import { Job, Queue, JobsOptions } from 'bullmq';
3
+
4
+ /**
5
+ * Pure, dependency-free helpers for the out-of-band transport-header seam (ADR-0028) — the Node
6
+ * adapter mirror of Go's `sanitizeHeaders` and PHP's `Support\Headers`. They carry an injected
7
+ * {@link HeaderCarrier} (e.g. a W3C `traceparent`) beside the frozen envelope (GR-1), never inside it.
8
+ *
9
+ * BullMQ has no AMQP-style header table; its native context-propagation slot is
10
+ * `JobsOptions.telemetry.metadata` (a string, stored compactly as `tm` in Redis), so the carrier is
11
+ * serialized to JSON there — leaving `job.data` (the canonical envelope) byte-identical.
12
+ */
13
+
14
+ /**
15
+ * Serialize a header carrier for BullMQ's `telemetry.metadata` slot, or `undefined` when there is
16
+ * nothing to carry (so a header-less publish leaves `telemetry` unset and the job byte-identical).
17
+ */
18
+ declare function encodeMetadata(headers: HeaderCarrier | null | undefined): string | undefined;
19
+ /**
20
+ * Read a header carrier back from a BullMQ `telemetry.metadata` string. Returns an empty object for
21
+ * a missing or unparseable value, so a job produced without headers (or by a non-babelqueue
22
+ * producer) consumes with no headers and no error.
23
+ */
24
+ declare function decodeMetadata(metadata: string | undefined | null): HeaderCarrier;
3
25
 
4
26
  /**
5
27
  * BullMQ adapter for BabelQueue.
@@ -20,8 +42,18 @@ import { Job, JobsOptions, Queue } from 'bullmq';
20
42
  * }), { connection });
21
43
  */
22
44
 
23
- /** A URN handler. Receives the validated envelope and the raw BullMQ job. */
24
- type BabelHandler = (envelope: Envelope, job: Job) => unknown | Promise<unknown>;
45
+ /**
46
+ * The out-of-band {@link HeaderCarrier} carried by a job, read from BullMQ's native
47
+ * `telemetry.metadata` slot (`job.opts.telemetry.metadata`). Empty for a job published without
48
+ * headers. Pass it to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child
49
+ * of the producer span (ADR-0028).
50
+ */
51
+ declare function headersOf(job: Job): HeaderCarrier;
52
+ /**
53
+ * A URN handler. Receives the validated envelope, the raw BullMQ job, and the out-of-band
54
+ * {@link HeaderCarrier} carried in the job's `telemetry.metadata` (empty when there are none).
55
+ */
56
+ type BabelHandler = (envelope: Envelope, job: Job, headers: HeaderCarrier) => unknown | Promise<unknown>;
25
57
  /** A map of URN → handler. */
26
58
  type BabelHandlers = Record<string, BabelHandler>;
27
59
  /** Options for {@link publish}. */
@@ -30,26 +62,59 @@ interface PublishOptions {
30
62
  traceId?: string;
31
63
  /** BullMQ job options (delay, attempts, backoff, …). */
32
64
  jobsOptions?: JobsOptions;
65
+ /**
66
+ * Out-of-band transport headers carried in BullMQ's native `telemetry.metadata` slot (ADR-0028) —
67
+ * e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s `publish`. The canonical envelope
68
+ * (`job.data`) stays byte-identical (GR-1); an empty/omitted carrier leaves the job options
69
+ * unchanged. An explicit `jobsOptions.telemetry.metadata` wins (it is not clobbered).
70
+ */
71
+ headers?: HeaderCarrier;
33
72
  }
34
73
  /**
35
74
  * Build the canonical envelope for `(urn, data)` and add it as a BullMQ job (the
36
75
  * job name is the URN, the job data is the envelope). Returns the message id
37
76
  * (`meta.id`).
77
+ *
78
+ * When `options.headers` carries any out-of-band header (e.g. a `traceparent`) it is serialized
79
+ * into the job's `telemetry.metadata` so a consumer can read it back via {@link headersOf} — the
80
+ * envelope itself is never touched.
38
81
  */
39
82
  declare function publish(queue: Queue, urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
40
83
  /** Options for {@link processor}. */
41
84
  interface ProcessorOptions {
42
85
  /** Called instead of throwing when a job's URN has no handler. */
43
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;
44
105
  }
45
106
  /**
46
107
  * Build a BullMQ processor that decodes each job's envelope, validates it, and
47
- * routes it to the handler registered for its URN. A non-conformant envelope
48
- * throws (so BullMQ retries / fails the job per its options); an unmapped URN
49
- * throws unless `onUnknownUrn` is given.
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.
50
113
  *
51
114
  * new Worker("orders", processor(handlers), { connection });
52
115
  */
53
- 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
+ };
54
119
 
55
- export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, processor, publish };
120
+ export { type BabelHandler, type BabelHandlers, type ProcessorOptions, type PublishOptions, decodeMetadata, encodeMetadata, headersOf, processor, publish };
package/dist/index.js CHANGED
@@ -2,23 +2,105 @@
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";
10
+
11
+ // src/headers.ts
12
+ function sanitizeHeaders(headers) {
13
+ const out = {};
14
+ if (!headers) return out;
15
+ for (const key of Object.keys(headers)) {
16
+ const value = headers[key];
17
+ if (key === "" || value == null || value === "") continue;
18
+ out[key] = value;
19
+ }
20
+ return out;
21
+ }
22
+ function encodeMetadata(headers) {
23
+ const clean = sanitizeHeaders(headers);
24
+ return Object.keys(clean).length === 0 ? void 0 : JSON.stringify(clean);
25
+ }
26
+ function decodeMetadata(metadata) {
27
+ if (!metadata) return {};
28
+ try {
29
+ const parsed = JSON.parse(metadata);
30
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
31
+ return sanitizeHeaders(parsed);
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ // src/index.ts
38
+ function headersOf(job) {
39
+ return decodeMetadata(job.opts?.telemetry?.metadata);
40
+ }
7
41
  async function publish(queue, urn, data, options = {}) {
8
42
  const envelope = EnvelopeCodec.make(urn, data, {
9
43
  queue: queue.name,
10
44
  traceId: options.traceId
11
45
  });
12
- await queue.add(urn, envelope, options.jobsOptions);
46
+ await queue.add(urn, envelope, withHeaders(options.jobsOptions, options.headers));
13
47
  return envelope.meta.id;
14
48
  }
49
+ function withHeaders(jobsOptions, headers) {
50
+ const metadata = encodeMetadata(headers);
51
+ if (metadata === void 0) return jobsOptions;
52
+ const telemetry = jobsOptions?.telemetry;
53
+ if (telemetry?.metadata != null) return jobsOptions;
54
+ return { ...jobsOptions, telemetry: { ...telemetry, metadata } };
55
+ }
56
+ function workerQueueOptions(job) {
57
+ const opts = job.queue?.opts;
58
+ return { connection: opts?.connection, ...opts?.prefix === void 0 ? {} : { prefix: opts.prefix } };
59
+ }
15
60
  function processor(handlers, options = {}) {
16
- return async (job) => {
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) => {
17
97
  const envelope = job.data;
18
98
  if (!EnvelopeCodec.accepts(envelope)) {
19
- throw new BabelQueueError(
99
+ const error = new BabelQueueError(
20
100
  `Rejected a non-conformant BabelQueue envelope on job ${job.id ?? "?"}.`
21
101
  );
102
+ await deadLetterPoison(job, error);
103
+ throw new UnrecoverableError(error.message);
22
104
  }
23
105
  const urn = EnvelopeCodec.urn(envelope);
24
106
  const handler = handlers[urn];
@@ -28,10 +110,20 @@ function processor(handlers, options = {}) {
28
110
  }
29
111
  throw new UnknownUrnError(urn);
30
112
  }
31
- return handler(envelope, job);
113
+ return handler(envelope, job, headersOf(job));
32
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
+ });
33
122
  }
34
123
  export {
124
+ decodeMetadata,
125
+ encodeMetadata,
126
+ headersOf,
35
127
  processor,
36
128
  publish
37
129
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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 IncomingEnvelope,\n} from \"@babelqueue/core\";\nimport type { Job, JobsOptions, Queue } from \"bullmq\";\n\n/** A URN handler. Receives the validated envelope and the raw BullMQ job. */\nexport type BabelHandler = (envelope: Envelope, job: Job) => 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\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 */\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, options.jobsOptions);\n return envelope.meta.id;\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);\n };\n}\n"],"mappings":";AAmBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAsBP,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,QAAQ,WAAW;AAClD,SAAO,SAAS,KAAK;AACvB;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,GAAG;AAAA,EAC9B;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":";AAmBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,OAAO,0BAA0B;;;ACbnC,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;;;ADRO,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;AA2BA,SAAS,mBAAmB,KAAwB;AAClD,QAAM,OAAQ,IAAgE,OAAO;AACrF,SAAO,EAAE,YAAY,MAAM,YAA0C,GAAI,MAAM,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,EAAG;AACtI;AAYO,SAAS,UACd,UACA,UAA4B,CAAC,GACkC;AAG/D,QAAM,UAAU,oBAAI,IAAmB;AAEvC,QAAM,yBAAyB,CAAC,QAAgC;AAC9D,UAAM,SAAS,QAAQ;AACvB,QAAI,WAAW,OAAW,QAAO;AACjC,QAAI,OAAO,WAAW,SAAU,QAAO;AACvC,UAAM,OAAO,WAAW,OAAO,GAAG,IAAI,SAAS,SAAS;AACxD,UAAM,eAAe,mBAAmB,GAAG;AAC3C,UAAM,MAAM,GAAG,aAAa,UAAU,EAAE,KAAS,IAAI;AACrD,QAAI,QAAQ,QAAQ,IAAI,GAAG;AAC3B,QAAI,UAAU,QAAW;AACvB,cAAQ,IAAI,MAAM,MAAM,YAAY;AACpC,cAAQ,IAAI,KAAK,KAAK;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AAGA,QAAM,mBAAmB,OAAO,KAAU,UAA0C;AAClF,QAAI;AACF,YAAM,MAAM,uBAAuB,GAAG;AACtC,UAAI,QAAQ,OAAW;AAEvB,YAAM,OAAO,KAAK,UAAU,IAAI,IAAI,KAAK;AACzC,YAAM,YAAY,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAC5E,YAAM,aAAa,SAAS,EAAE,UAAU,UAAU,GAA0B,UAAU,QAAQ,SAAS,IAAI,WAAW;AAAA,QACpH,UAAU;AAAA,QACV,OAAO,MAAM;AAAA,QACb,WAAW,MAAM;AAAA,MACnB,CAAC,EAAE;AACH,YAAM,SAAS,kBAAkB,OAAO,KAAK,MAAM,MAAM,GAAG,KAAK,UAAU,UAAU,CAAC;AACtF,YAAM,IAAI,IAAI,IAAI,MAAM,KAAK,MAAM,MAAM,CAA4B;AAAA,IACvE,SAAS,YAAY;AACnB,UAAI;AACF,gBAAQ,UAAU,YAAY,GAAG;AAAA,MACnC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,OAAO,QAA+B;AAChD,UAAM,WAAW,IAAI;AAErB,QAAI,CAAC,cAAc,QAAQ,QAAQ,GAAG;AACpC,YAAM,QAAQ,IAAI;AAAA,QAChB,wDAAwD,IAAI,MAAM,GAAG;AAAA,MACvE;AACA,YAAM,iBAAiB,KAAK,KAAK;AAEjC,YAAM,IAAI,mBAAmB,MAAM,OAAO;AAAA,IAC5C;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;AAIA,SAAO,OAAO,OAAO,KAAK;AAAA,IACxB,MAAM,QAAuB;AAC3B,YAAM,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC;AACnC,cAAQ,MAAM;AACd,YAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,IACxD;AAAA,EACF,CAAC;AACH;","names":[]}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@babelqueue/bullmq",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "BullMQ adapter for BabelQueue — canonical-envelope jobs and a URN-routed processor.",
5
- "keywords": ["babelqueue", "bullmq", "queue", "polyglot", "redis", "messaging"],
5
+ "keywords": [
6
+ "babelqueue",
7
+ "bullmq",
8
+ "queue",
9
+ "polyglot",
10
+ "redis",
11
+ "messaging"
12
+ ],
6
13
  "homepage": "https://babelqueue.com",
7
14
  "bugs": {
8
15
  "url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
@@ -13,7 +20,7 @@
13
20
  "directory": "packages/bullmq"
14
21
  },
15
22
  "license": "MIT",
16
- "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
23
+ "author": "Muhammet Şafak <info@muhammetsafak.com>",
17
24
  "type": "module",
18
25
  "exports": {
19
26
  ".": {
@@ -30,7 +37,9 @@
30
37
  "main": "./dist/index.cjs",
31
38
  "module": "./dist/index.js",
32
39
  "types": "./dist/index.d.ts",
33
- "files": ["dist"],
40
+ "files": [
41
+ "dist"
42
+ ],
34
43
  "sideEffects": false,
35
44
  "engines": {
36
45
  "node": ">=18"
@@ -41,7 +50,7 @@
41
50
  "test": "node --import tsx --test test/*.test.ts"
42
51
  },
43
52
  "dependencies": {
44
- "@babelqueue/core": "^1.0.0"
53
+ "@babelqueue/core": "^1.13.0"
45
54
  },
46
55
  "peerDependencies": {
47
56
  "bullmq": "^5.0.0"