@babelqueue/pulsar 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,7 +48,8 @@ await consumer.run();
48
48
  ```
49
49
 
50
50
  Delayed delivery: `publish(urn, data, { delayMs: 300000 })` → native `deliverAfter`. The
51
- consumer routes purely on the `bq-job` property.
51
+ consumer routes on the `bq-job` property (§5.2); when it is absent or blank it falls back to the
52
+ body's URN. The body is always decoded and the handler always receives the body's envelope.
52
53
 
53
54
  ## Contract mapping (§5)
54
55
 
@@ -61,14 +62,30 @@ consumer routes purely on the `bq-job` property.
61
62
  | `meta.schema_version` | property `bq-schema-version` |
62
63
  | `meta.lang` | property `bq-source-lang` |
63
64
  | `meta.created_at` | publish time (mirror; body authoritative) |
64
- | `attempts` | property `bq-attempts` (authoritative), cross-checked against `getRedeliveryCount()` |
65
+ | `attempts` | property `bq-attempts` (authoritative; body is the fallback), floored by `getRedeliveryCount()` |
65
66
  | reserve / ack / retry | `acknowledge` / `negativeAcknowledge` |
66
67
 
67
68
  Pulsar properties are string→string, so `bq-attempts` carries the contract `attempts` and is
68
- **authoritative**. The consumer reconciles to `max(bq-attempts, getRedeliveryCount())`:
69
- `getRedeliveryCount()` is 0-based (0 on first delivery) so it maps directly with **no −1**,
70
- and the `max` never lowers a higher body count — so a republish-driven retry and a native
71
- redelivery both converge on the same number. A throwing handler `negativeAcknowledge`s, so
69
+ **authoritative**. It is valid only when it is plain decimal digits (`^[0-9]+$`, at most
70
+ 2147483647) — a sign, whitespace, an empty string or an out-of-range value is invalid and the
71
+ consumer falls back to the body's `attempts`. The result is reconciled to
72
+ `max(.., getRedeliveryCount())`: `getRedeliveryCount()` is 0-based (0 on first delivery) so it
73
+ maps directly with **no −1** (the body is only rewritten when it holds an integer `attempts` that really changes; a missing or non-integer `attempts` is still rejected) — so a republish-driven retry and a native redelivery both
74
+ converge on the same number.
75
+
76
+ **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads
77
+ `bq-schema-version`. Missing or blank: nothing changes ("blank" is the same set in every SDK: the empty string or only ASCII whitespace — space, `\t`, `\n`, `\v`, `\f`, `\r`; NBSP, U+0085, U+FEFF, U+3000 and the like are not blank). Any value other than the core's supported
78
+ `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message **undecoded** through the same
79
+ path as a non-conformant envelope — `onError` (an error naming the version, with an empty
80
+ envelope) and a `negativeAcknowledge`; the handler, `onUnknownUrn` and `onRoutingMismatch` are
81
+ not called.
82
+
83
+ If the `bq-job` property and the body's URN differ, routing follows the property and the
84
+ optional `onRoutingMismatch(propertyUrn, bodyUrn, envelope)` hook is called (default: no-op).
85
+ It is separate from `onError`, its failures (including a rejected promise) are swallowed, and
86
+ it never changes the ack flow. The hook runs only for an accepted envelope — after `accepts()` and
87
+ the `attempts` reconcile — so it sees the same envelope the handler gets. A body without a `job` is still rejected by the codec even when
88
+ `bq-job` is set. A throwing handler `negativeAcknowledge`s, so
72
89
  the broker redelivers (at-least-once). The poll loop never stops on a bad message — observe
73
90
  via `onError` / `onUnknownUrn`. The envelope is unchanged (`schema_version` stays `1`);
74
91
  Pulsar is purely additive.
@@ -76,6 +93,24 @@ Pulsar is purely additive.
76
93
  The producer/consumer are injected, so the unit tests use fakes — no Pulsar, no broker. Dual
77
94
  ESM + CJS.
78
95
 
96
+ ## OpenTelemetry tracing (ADR-0028)
97
+
98
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
99
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
100
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
101
+ native Pulsar message properties, beside the contract `bq-*` properties (the contract wins a key collision). On consume, the consumer surfaces a delivered message's headers to the handler's third argument (and a `headersOf(...)` extractor reads them back),
102
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
103
+
104
+ ```ts
105
+ import { trace } from "@opentelemetry/api";
106
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
107
+ import type { HeaderCarrier } from "@babelqueue/core";
108
+
109
+ const headers: HeaderCarrier = {};
110
+ await tracedPublish(trace.getTracer("orders"), urn, data,
111
+ () => adapterPublish(urn, data, { headers }), { headers });
112
+ ```
113
+
79
114
  ## License
80
115
 
81
116
  MIT
package/dist/index.cjs CHANGED
@@ -22,11 +22,33 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  PulsarConsumer: () => PulsarConsumer,
24
24
  PulsarPublisher: () => PulsarPublisher,
25
+ headersOf: () => headersOf,
25
26
  pulsarProperties: () => pulsarProperties,
26
27
  toPulsarMessage: () => toPulsarMessage
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
29
30
  var import_core = require("@babelqueue/core");
31
+
32
+ // src/headers.ts
33
+ function sanitizeHeaders(headers) {
34
+ const out = {};
35
+ if (!headers) return out;
36
+ for (const key of Object.keys(headers)) {
37
+ const value = headers[key];
38
+ if (key === "" || value == null || value === "") continue;
39
+ out[key] = value;
40
+ }
41
+ return out;
42
+ }
43
+ function mergeInto(base, extra) {
44
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {
45
+ if (key in base) continue;
46
+ base[key] = value;
47
+ }
48
+ return base;
49
+ }
50
+
51
+ // src/index.ts
30
52
  function topicName(topic) {
31
53
  const slash = topic.lastIndexOf("/");
32
54
  return slash >= 0 ? topic.slice(slash + 1) : topic;
@@ -43,12 +65,22 @@ function pulsarProperties(envelope) {
43
65
  properties["bq-attempts"] = String(envelope.attempts ?? 0);
44
66
  return properties;
45
67
  }
46
- function toPulsarMessage(envelope) {
68
+ function toPulsarMessage(envelope, extra) {
47
69
  return {
48
70
  data: Buffer.from(import_core.EnvelopeCodec.encode(envelope), "utf8"),
49
- properties: pulsarProperties(envelope)
71
+ properties: mergeInto(pulsarProperties(envelope), extra)
50
72
  };
51
73
  }
74
+ function headersOf(message) {
75
+ const props = typeof message.getProperties === "function" ? message.getProperties() : void 0;
76
+ const out = {};
77
+ if (!props) return out;
78
+ for (const key of Object.keys(props)) {
79
+ const value = props[key];
80
+ if (value != null && value !== "") out[key] = value;
81
+ }
82
+ return out;
83
+ }
52
84
  var PulsarPublisher = class {
53
85
  constructor(producer) {
54
86
  this.producer = producer;
@@ -64,7 +96,7 @@ var PulsarPublisher = class {
64
96
  queue: topicName(this.producer.getTopic()),
65
97
  traceId: options.traceId
66
98
  });
67
- const message = toPulsarMessage(envelope);
99
+ const message = toPulsarMessage(envelope, options.headers);
68
100
  if (options.delayMs != null && options.delayMs > 0) {
69
101
  message.properties = { ...message.properties, "bq-delay": String(options.delayMs) };
70
102
  message.deliverAfter = options.delayMs;
@@ -73,6 +105,18 @@ var PulsarPublisher = class {
73
105
  return envelope.meta.id;
74
106
  }
75
107
  };
108
+ var MAX_ATTEMPTS = 2 ** 31 - 1;
109
+ function parseAttempts(raw) {
110
+ if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) return void 0;
111
+ const value = Number(raw);
112
+ return value <= MAX_ATTEMPTS ? value : void 0;
113
+ }
114
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
115
+ function unsupportedSchemaVersion(properties) {
116
+ const raw = properties["bq-schema-version"];
117
+ if (typeof raw !== "string" || BLANK_ASCII.test(raw)) return void 0;
118
+ return raw === String(import_core.SCHEMA_VERSION) ? void 0 : raw;
119
+ }
76
120
  function isReceiveTimeout(error) {
77
121
  const message = error instanceof Error ? error.message : String(error);
78
122
  return /timeout/i.test(message);
@@ -108,12 +152,33 @@ var PulsarConsumer = class {
108
152
  }
109
153
  }
110
154
  async handle(message) {
155
+ const properties = safeProperties(message);
156
+ const rejectedVersion = unsupportedSchemaVersion(properties);
157
+ if (rejectedVersion !== void 0) {
158
+ this.options.onError?.(
159
+ new import_core.BabelQueueError(
160
+ `Rejected a message from Apache Pulsar with unsupported bq-schema-version "${rejectedVersion}" (supported: ${import_core.SCHEMA_VERSION}).`
161
+ ),
162
+ {},
163
+ message
164
+ );
165
+ await this.consumer.negativeAcknowledge(message);
166
+ return;
167
+ }
111
168
  const envelope = import_core.EnvelopeCodec.decode(decodePayload(message.getData()));
112
169
  const redeliveryCount = typeof message.getRedeliveryCount === "function" ? message.getRedeliveryCount() : 0;
113
- const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
114
- if (redeliveryCount > current) {
115
- envelope.attempts = redeliveryCount;
170
+ const bodyAttempts = envelope.attempts;
171
+ let attempts = typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;
172
+ const fromProperty = parseAttempts(properties["bq-attempts"]);
173
+ if (fromProperty !== void 0) attempts = fromProperty;
174
+ if (redeliveryCount > attempts) attempts = redeliveryCount;
175
+ if (typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {
176
+ envelope.attempts = attempts;
116
177
  }
178
+ const bodyUrn = import_core.EnvelopeCodec.urn(envelope);
179
+ const rawJob = properties["bq-job"];
180
+ const propertyUrn = typeof rawJob === "string" ? rawJob.trim() : "";
181
+ const urn = propertyUrn !== "" ? propertyUrn : bodyUrn;
117
182
  if (!import_core.EnvelopeCodec.accepts(envelope)) {
118
183
  this.options.onError?.(
119
184
  new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar."),
@@ -123,7 +188,9 @@ var PulsarConsumer = class {
123
188
  await this.consumer.negativeAcknowledge(message);
124
189
  return;
125
190
  }
126
- const urn = import_core.EnvelopeCodec.urn(envelope);
191
+ if (propertyUrn !== "" && bodyUrn !== "" && propertyUrn !== bodyUrn) {
192
+ this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);
193
+ }
127
194
  const handler = this.handlers[urn];
128
195
  if (!handler) {
129
196
  if (this.options.onUnknownUrn) {
@@ -136,18 +203,36 @@ var PulsarConsumer = class {
136
203
  return;
137
204
  }
138
205
  try {
139
- await handler(envelope, message);
206
+ await handler(envelope, message, headersOf(message));
140
207
  await this.consumer.acknowledge(message);
141
208
  } catch (error) {
142
209
  this.options.onError?.(error, envelope, message);
143
210
  await this.consumer.negativeAcknowledge(message);
144
211
  }
145
212
  }
213
+ notifyRoutingMismatch(propertyUrn, bodyUrn, envelope) {
214
+ const hook = this.options.onRoutingMismatch;
215
+ if (hook === void 0) return;
216
+ try {
217
+ const result = hook(propertyUrn, bodyUrn, envelope);
218
+ Promise.resolve(result).catch(() => void 0);
219
+ } catch {
220
+ }
221
+ }
146
222
  };
223
+ function safeProperties(message) {
224
+ try {
225
+ const properties = typeof message.getProperties === "function" ? message.getProperties() : void 0;
226
+ return properties ?? {};
227
+ } catch {
228
+ return {};
229
+ }
230
+ }
147
231
  // Annotate the CommonJS export names for ESM import in node:
148
232
  0 && (module.exports = {
149
233
  PulsarConsumer,
150
234
  PulsarPublisher,
235
+ headersOf,
151
236
  pulsarProperties,
152
237
  toPulsarMessage
153
238
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative\n * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,\n * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/** Project the envelope onto a native Pulsar producer message (payload + properties). */\nexport function toPulsarMessage(envelope: Envelope): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: pulsarProperties(envelope),\n };\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Pulsar message. */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to\n * `max(bq-attempts, getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {}\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // attempts = max(current, getRedeliveryCount()): the redelivery count is 0-based, so it\n // maps directly with no −1; the max never lowers a higher body count republished from\n // another SDK (the Go/Python transports retry by re-sending with attempts+1).\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (redeliveryCount > current) {\n envelope.attempts = redeliveryCount;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\"),\n envelope,\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message);\n await this.consumer.acknowledge(message);\n } catch (error) {\n // Negative-ack releases the message — the broker redelivers and increments the count.\n this.options.onError?.(error, envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the\n * consumer routes on the `bq-job` property (the body's URN is used only when the property is\n * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the\n * body's value is used only when the property is absent or not a plain ASCII-decimal integer\n * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,\n * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.\n *\n * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the\n * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than\n * the core's supported `SCHEMA_VERSION` (`\"2\"`, `\"x\"`, `\"01\"`, ...) rejects the message undecoded\n * through the same path as a non-conformant envelope — `onError` (with an error naming the\n * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), and blanks are\n * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so\n * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A\n * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/**\n * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`\n * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the\n * contract `bq-*` properties without clobbering them (the contract wins a key collision).\n */\nexport function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: mergeInto(pulsarProperties(envelope), extra),\n };\n}\n\n/**\n * Read a received message's native properties back into a flat {@link HeaderCarrier} via\n * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: PulsarReceivedMessage): HeaderCarrier {\n const props = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native Pulsar message properties beside the contract\n * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s\n * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the\n * message unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band\n * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass\n * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the\n * producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n /**\n * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the\n * property wins and the message is routed on it). `envelope` is the body's envelope. Separate\n * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or\n * rejects with) is swallowed — it never changes how the message is handled or acknowledged.\n */\n onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;\n}\n\n/** `bq-attempts` is valid only as ASCII digits that fit a signed 32-bit int (matches Java/.NET, GR-5). */\nconst MAX_ATTEMPTS = 2 ** 31 - 1;\n\nfunction parseAttempts(raw: string | undefined): number | undefined {\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return undefined;\n const value = Number(raw);\n return value <= MAX_ATTEMPTS ? value : undefined;\n}\n\n/** Blank = empty or only ASCII whitespace (space, \\t, \\n, \\v, \\f, \\r) — not `String.prototype.trim()`'s wider set. */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The `bq-schema-version` value when it is present, non-blank and not the supported version\n * (compared against the core's `SCHEMA_VERSION`, exact string match: `\"01\"`, `\" 1\"`, `\"1.0\"` are\n * rejected). Blank means the empty string or only ASCII whitespace (the same set in every SDK, GR-5);\n * NBSP, U+0085, U+FEFF, U+3000 and the like are not blank. `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(properties: { [key: string]: string }): string | undefined {\n const raw: unknown = properties[\"bq-schema-version\"];\n if (typeof raw !== \"string\" || BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the\n * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to\n * `max(.., getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {}\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const properties = safeProperties(message);\n\n // §5.7 step 3: gate on bq-schema-version before the body is decoded.\n const rejectedVersion = unsupportedSchemaVersion(properties);\n if (rejectedVersion !== undefined) {\n this.options.onError?.(\n new BabelQueueError(\n `Rejected a message from Apache Pulsar with unsupported bq-schema-version \"${rejectedVersion}\" (supported: ${SCHEMA_VERSION}).`,\n ),\n {},\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // attempts: the authoritative bq-attempts property (the body's value when it is absent or\n // invalid), then max(.., getRedeliveryCount()): the redelivery count is 0-based, so it maps\n // directly with no −1; the max never lowers a higher count republished from another SDK\n // (the Go/Python transports retry by re-sending with attempts+1).\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n // A body without an integer attempts stays as sent, so accepts() below still rejects it\n // (the property can't repair a non-conformant body); the value is written only on a change.\n const bodyAttempts = envelope.attempts;\n let attempts = typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;\n const fromProperty = parseAttempts(properties[\"bq-attempts\"]);\n if (fromProperty !== undefined) attempts = fromProperty;\n if (redeliveryCount > attempts) attempts = redeliveryCount;\n if (typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {\n envelope.attempts = attempts;\n }\n\n // routing: the bq-job property wins; the body's URN is the fallback. A body with no URN is\n // still rejected below, whatever bq-job says.\n const bodyUrn = EnvelopeCodec.urn(envelope);\n const rawJob: unknown = properties[\"bq-job\"];\n const propertyUrn = typeof rawJob === \"string\" ? rawJob.trim() : \"\";\n const urn = propertyUrn !== \"\" ? propertyUrn : bodyUrn;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\"),\n envelope,\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n // Diagnostics only, and only for an accepted envelope (07a Java/.NET order): after accepts()\n // and the attempts reconcile, so the hook sees the same envelope the handler gets.\n if (propertyUrn !== \"\" && bodyUrn !== \"\" && propertyUrn !== bodyUrn) {\n this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);\n }\n\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message, headersOf(message));\n await this.consumer.acknowledge(message);\n } catch (error) {\n // Negative-ack releases the message — the broker redelivers and increments the count.\n this.options.onError?.(error, envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n }\n\n private notifyRoutingMismatch(propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope): void {\n const hook = this.options.onRoutingMismatch;\n if (hook === undefined) return;\n try {\n const result = hook(propertyUrn, bodyUrn, envelope);\n Promise.resolve(result).catch(() => undefined);\n } catch {\n // A diagnostic hook must never disturb consumption.\n }\n }\n}\n\nfunction safeProperties(message: PulsarReceivedMessage): { [key: string]: string } {\n try {\n const properties = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n return properties ?? {};\n } catch {\n return {};\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 fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\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 * Merge out-of-band `extra` headers onto an already-built `base` map **without clobbering** the\n * contract headers in `base` (they win a key collision) and dropping blanks. Returns `base`,\n * mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Apache Pulsar adapter for BabelQueue.
@@ -28,9 +28,25 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /
29
29
  * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);
30
30
  * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is
31
- * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative
32
- * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,
33
- * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.
31
+ * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the
32
+ * consumer routes on the `bq-job` property (the body's URN is used only when the property is
33
+ * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the
34
+ * body's value is used only when the property is absent or not a plain ASCII-decimal integer
35
+ * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,
36
+ * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.
37
+ *
38
+ * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the
39
+ * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than
40
+ * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded
41
+ * through the same path as a non-conformant envelope — `onError` (with an error naming the
42
+ * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.
43
+ *
44
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
45
+ * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
46
+ * properties — the contract properties win a key collision (merge-not-clobber), and blanks are
47
+ * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so
48
+ * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
49
+ * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.
34
50
  */
35
51
 
36
52
  /** A message to send (structural subset of pulsar-client `ProducerMessage`). */
@@ -75,14 +91,32 @@ interface PulsarConsumerClient {
75
91
  declare function pulsarProperties(envelope: Envelope): {
76
92
  [key: string]: string;
77
93
  };
78
- /** Project the envelope onto a native Pulsar producer message (payload + properties). */
79
- declare function toPulsarMessage(envelope: Envelope): PulsarProducerMessage;
94
+ /**
95
+ * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`
96
+ * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the
97
+ * contract `bq-*` properties without clobbering them (the contract wins a key collision).
98
+ */
99
+ declare function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage;
100
+ /**
101
+ * Read a received message's native properties back into a flat {@link HeaderCarrier} via
102
+ * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`
103
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
104
+ * reads only the keys it knows.
105
+ */
106
+ declare function headersOf(message: PulsarReceivedMessage): HeaderCarrier;
80
107
  /** Options for {@link PulsarPublisher.publish}. */
81
108
  interface PublishOptions {
82
109
  /** Reuse an existing trace id (trace continuation). */
83
110
  traceId?: string;
84
111
  /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */
85
112
  delayMs?: number;
113
+ /**
114
+ * Out-of-band transport headers carried as native Pulsar message properties beside the contract
115
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
116
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
117
+ * message unchanged.
118
+ */
119
+ headers?: HeaderCarrier;
86
120
  }
87
121
  /** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */
88
122
  declare class PulsarPublisher {
@@ -95,8 +129,13 @@ declare class PulsarPublisher {
95
129
  */
96
130
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
131
  }
98
- /** A URN handler. Receives the validated envelope and the raw Pulsar message. */
99
- type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;
132
+ /**
133
+ * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band
134
+ * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass
135
+ * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the
136
+ * producer span (ADR-0028).
137
+ */
138
+ type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
139
  /** A map of URN → handler. */
101
140
  type BabelHandlers = Record<string, BabelHandler>;
102
141
  /** Options for {@link PulsarConsumer}. */
@@ -107,13 +146,21 @@ interface PulsarConsumerOptions {
107
146
  onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;
108
147
  /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */
109
148
  receiveTimeoutMs?: number;
149
+ /**
150
+ * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the
151
+ * property wins and the message is routed on it). `envelope` is the body's envelope. Separate
152
+ * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or
153
+ * rejects with) is swallowed — it never changes how the message is handled or acknowledged.
154
+ */
155
+ onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;
110
156
  }
111
157
  /**
112
158
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
113
159
  * handler for its URN, and acknowledges it on success. A throwing handler
114
160
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
115
- * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to
116
- * `max(bq-attempts, getRedeliveryCount())` for the handler.
161
+ * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the
162
+ * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to
163
+ * `max(.., getRedeliveryCount())` for the handler.
117
164
  */
118
165
  declare class PulsarConsumer {
119
166
  private readonly consumer;
@@ -125,6 +172,7 @@ declare class PulsarConsumer {
125
172
  /** Poll until `signal` aborts. */
126
173
  run(signal?: AbortSignal): Promise<void>;
127
174
  private handle;
175
+ private notifyRoutingMismatch;
128
176
  }
129
177
 
130
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, pulsarProperties, toPulsarMessage };
178
+ export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, headersOf, pulsarProperties, toPulsarMessage };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Envelope, IncomingEnvelope } from '@babelqueue/core';
1
+ import { Envelope, HeaderCarrier, IncomingEnvelope } from '@babelqueue/core';
2
2
 
3
3
  /**
4
4
  * Apache Pulsar adapter for BabelQueue.
@@ -28,9 +28,25 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
28
28
  * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /
29
29
  * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);
30
30
  * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is
31
- * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative
32
- * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,
33
- * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.
31
+ * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the
32
+ * consumer routes on the `bq-job` property (the body's URN is used only when the property is
33
+ * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the
34
+ * body's value is used only when the property is absent or not a plain ASCII-decimal integer
35
+ * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,
36
+ * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.
37
+ *
38
+ * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the
39
+ * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than
40
+ * the core's supported `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) rejects the message undecoded
41
+ * through the same path as a non-conformant envelope — `onError` (with an error naming the
42
+ * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.
43
+ *
44
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
45
+ * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
46
+ * properties — the contract properties win a key collision (merge-not-clobber), and blanks are
47
+ * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so
48
+ * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
49
+ * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.
34
50
  */
35
51
 
36
52
  /** A message to send (structural subset of pulsar-client `ProducerMessage`). */
@@ -75,14 +91,32 @@ interface PulsarConsumerClient {
75
91
  declare function pulsarProperties(envelope: Envelope): {
76
92
  [key: string]: string;
77
93
  };
78
- /** Project the envelope onto a native Pulsar producer message (payload + properties). */
79
- declare function toPulsarMessage(envelope: Envelope): PulsarProducerMessage;
94
+ /**
95
+ * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`
96
+ * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the
97
+ * contract `bq-*` properties without clobbering them (the contract wins a key collision).
98
+ */
99
+ declare function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage;
100
+ /**
101
+ * Read a received message's native properties back into a flat {@link HeaderCarrier} via
102
+ * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`
103
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
104
+ * reads only the keys it knows.
105
+ */
106
+ declare function headersOf(message: PulsarReceivedMessage): HeaderCarrier;
80
107
  /** Options for {@link PulsarPublisher.publish}. */
81
108
  interface PublishOptions {
82
109
  /** Reuse an existing trace id (trace continuation). */
83
110
  traceId?: string;
84
111
  /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */
85
112
  delayMs?: number;
113
+ /**
114
+ * Out-of-band transport headers carried as native Pulsar message properties beside the contract
115
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
116
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
117
+ * message unchanged.
118
+ */
119
+ headers?: HeaderCarrier;
86
120
  }
87
121
  /** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */
88
122
  declare class PulsarPublisher {
@@ -95,8 +129,13 @@ declare class PulsarPublisher {
95
129
  */
96
130
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
131
  }
98
- /** A URN handler. Receives the validated envelope and the raw Pulsar message. */
99
- type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;
132
+ /**
133
+ * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band
134
+ * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass
135
+ * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the
136
+ * producer span (ADR-0028).
137
+ */
138
+ type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
139
  /** A map of URN → handler. */
101
140
  type BabelHandlers = Record<string, BabelHandler>;
102
141
  /** Options for {@link PulsarConsumer}. */
@@ -107,13 +146,21 @@ interface PulsarConsumerOptions {
107
146
  onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;
108
147
  /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */
109
148
  receiveTimeoutMs?: number;
149
+ /**
150
+ * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the
151
+ * property wins and the message is routed on it). `envelope` is the body's envelope. Separate
152
+ * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or
153
+ * rejects with) is swallowed — it never changes how the message is handled or acknowledged.
154
+ */
155
+ onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;
110
156
  }
111
157
  /**
112
158
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
113
159
  * handler for its URN, and acknowledges it on success. A throwing handler
114
160
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
115
- * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to
116
- * `max(bq-attempts, getRedeliveryCount())` for the handler.
161
+ * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the
162
+ * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to
163
+ * `max(.., getRedeliveryCount())` for the handler.
117
164
  */
118
165
  declare class PulsarConsumer {
119
166
  private readonly consumer;
@@ -125,6 +172,7 @@ declare class PulsarConsumer {
125
172
  /** Poll until `signal` aborts. */
126
173
  run(signal?: AbortSignal): Promise<void>;
127
174
  private handle;
175
+ private notifyRoutingMismatch;
128
176
  }
129
177
 
130
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, pulsarProperties, toPulsarMessage };
178
+ export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, headersOf, pulsarProperties, toPulsarMessage };
package/dist/index.js CHANGED
@@ -1,5 +1,26 @@
1
1
  // src/index.ts
2
- import { BabelQueueError, EnvelopeCodec, UnknownUrnError } from "@babelqueue/core";
2
+ import { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from "@babelqueue/core";
3
+
4
+ // src/headers.ts
5
+ function sanitizeHeaders(headers) {
6
+ const out = {};
7
+ if (!headers) return out;
8
+ for (const key of Object.keys(headers)) {
9
+ const value = headers[key];
10
+ if (key === "" || value == null || value === "") continue;
11
+ out[key] = value;
12
+ }
13
+ return out;
14
+ }
15
+ function mergeInto(base, extra) {
16
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {
17
+ if (key in base) continue;
18
+ base[key] = value;
19
+ }
20
+ return base;
21
+ }
22
+
23
+ // src/index.ts
3
24
  function topicName(topic) {
4
25
  const slash = topic.lastIndexOf("/");
5
26
  return slash >= 0 ? topic.slice(slash + 1) : topic;
@@ -16,12 +37,22 @@ function pulsarProperties(envelope) {
16
37
  properties["bq-attempts"] = String(envelope.attempts ?? 0);
17
38
  return properties;
18
39
  }
19
- function toPulsarMessage(envelope) {
40
+ function toPulsarMessage(envelope, extra) {
20
41
  return {
21
42
  data: Buffer.from(EnvelopeCodec.encode(envelope), "utf8"),
22
- properties: pulsarProperties(envelope)
43
+ properties: mergeInto(pulsarProperties(envelope), extra)
23
44
  };
24
45
  }
46
+ function headersOf(message) {
47
+ const props = typeof message.getProperties === "function" ? message.getProperties() : void 0;
48
+ const out = {};
49
+ if (!props) return out;
50
+ for (const key of Object.keys(props)) {
51
+ const value = props[key];
52
+ if (value != null && value !== "") out[key] = value;
53
+ }
54
+ return out;
55
+ }
25
56
  var PulsarPublisher = class {
26
57
  constructor(producer) {
27
58
  this.producer = producer;
@@ -37,7 +68,7 @@ var PulsarPublisher = class {
37
68
  queue: topicName(this.producer.getTopic()),
38
69
  traceId: options.traceId
39
70
  });
40
- const message = toPulsarMessage(envelope);
71
+ const message = toPulsarMessage(envelope, options.headers);
41
72
  if (options.delayMs != null && options.delayMs > 0) {
42
73
  message.properties = { ...message.properties, "bq-delay": String(options.delayMs) };
43
74
  message.deliverAfter = options.delayMs;
@@ -46,6 +77,18 @@ var PulsarPublisher = class {
46
77
  return envelope.meta.id;
47
78
  }
48
79
  };
80
+ var MAX_ATTEMPTS = 2 ** 31 - 1;
81
+ function parseAttempts(raw) {
82
+ if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) return void 0;
83
+ const value = Number(raw);
84
+ return value <= MAX_ATTEMPTS ? value : void 0;
85
+ }
86
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
87
+ function unsupportedSchemaVersion(properties) {
88
+ const raw = properties["bq-schema-version"];
89
+ if (typeof raw !== "string" || BLANK_ASCII.test(raw)) return void 0;
90
+ return raw === String(SCHEMA_VERSION) ? void 0 : raw;
91
+ }
49
92
  function isReceiveTimeout(error) {
50
93
  const message = error instanceof Error ? error.message : String(error);
51
94
  return /timeout/i.test(message);
@@ -81,12 +124,33 @@ var PulsarConsumer = class {
81
124
  }
82
125
  }
83
126
  async handle(message) {
127
+ const properties = safeProperties(message);
128
+ const rejectedVersion = unsupportedSchemaVersion(properties);
129
+ if (rejectedVersion !== void 0) {
130
+ this.options.onError?.(
131
+ new BabelQueueError(
132
+ `Rejected a message from Apache Pulsar with unsupported bq-schema-version "${rejectedVersion}" (supported: ${SCHEMA_VERSION}).`
133
+ ),
134
+ {},
135
+ message
136
+ );
137
+ await this.consumer.negativeAcknowledge(message);
138
+ return;
139
+ }
84
140
  const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));
85
141
  const redeliveryCount = typeof message.getRedeliveryCount === "function" ? message.getRedeliveryCount() : 0;
86
- const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
87
- if (redeliveryCount > current) {
88
- envelope.attempts = redeliveryCount;
142
+ const bodyAttempts = envelope.attempts;
143
+ let attempts = typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;
144
+ const fromProperty = parseAttempts(properties["bq-attempts"]);
145
+ if (fromProperty !== void 0) attempts = fromProperty;
146
+ if (redeliveryCount > attempts) attempts = redeliveryCount;
147
+ if (typeof bodyAttempts === "number" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {
148
+ envelope.attempts = attempts;
89
149
  }
150
+ const bodyUrn = EnvelopeCodec.urn(envelope);
151
+ const rawJob = properties["bq-job"];
152
+ const propertyUrn = typeof rawJob === "string" ? rawJob.trim() : "";
153
+ const urn = propertyUrn !== "" ? propertyUrn : bodyUrn;
90
154
  if (!EnvelopeCodec.accepts(envelope)) {
91
155
  this.options.onError?.(
92
156
  new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar."),
@@ -96,7 +160,9 @@ var PulsarConsumer = class {
96
160
  await this.consumer.negativeAcknowledge(message);
97
161
  return;
98
162
  }
99
- const urn = EnvelopeCodec.urn(envelope);
163
+ if (propertyUrn !== "" && bodyUrn !== "" && propertyUrn !== bodyUrn) {
164
+ this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);
165
+ }
100
166
  const handler = this.handlers[urn];
101
167
  if (!handler) {
102
168
  if (this.options.onUnknownUrn) {
@@ -109,17 +175,35 @@ var PulsarConsumer = class {
109
175
  return;
110
176
  }
111
177
  try {
112
- await handler(envelope, message);
178
+ await handler(envelope, message, headersOf(message));
113
179
  await this.consumer.acknowledge(message);
114
180
  } catch (error) {
115
181
  this.options.onError?.(error, envelope, message);
116
182
  await this.consumer.negativeAcknowledge(message);
117
183
  }
118
184
  }
185
+ notifyRoutingMismatch(propertyUrn, bodyUrn, envelope) {
186
+ const hook = this.options.onRoutingMismatch;
187
+ if (hook === void 0) return;
188
+ try {
189
+ const result = hook(propertyUrn, bodyUrn, envelope);
190
+ Promise.resolve(result).catch(() => void 0);
191
+ } catch {
192
+ }
193
+ }
119
194
  };
195
+ function safeProperties(message) {
196
+ try {
197
+ const properties = typeof message.getProperties === "function" ? message.getProperties() : void 0;
198
+ return properties ?? {};
199
+ } catch {
200
+ return {};
201
+ }
202
+ }
120
203
  export {
121
204
  PulsarConsumer,
122
205
  PulsarPublisher,
206
+ headersOf,
123
207
  pulsarProperties,
124
208
  toPulsarMessage
125
209
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative\n * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,\n * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/** Project the envelope onto a native Pulsar producer message (payload + properties). */\nexport function toPulsarMessage(envelope: Envelope): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: pulsarProperties(envelope),\n };\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Pulsar message. */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to\n * `max(bq-attempts, getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {}\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // attempts = max(current, getRedeliveryCount()): the redelivery count is 0-based, so it\n // maps directly with no −1; the max never lowers a higher body count republished from\n // another SDK (the Go/Python transports retry by re-sending with attempts+1).\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (redeliveryCount > current) {\n envelope.attempts = redeliveryCount;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\"),\n envelope,\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n const urn = EnvelopeCodec.urn(envelope);\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message);\n await this.consumer.acknowledge(message);\n } catch (error) {\n // Negative-ack releases the message — the broker redelivers and increments the count.\n this.options.onError?.(error, envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Apache Pulsar adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Apache Pulsar, so a\n * Pulsar-based Node service speaks the same contract (envelope shape, URN identity, trace\n * propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import Pulsar from \"pulsar-client\";\n * import { PulsarPublisher, PulsarConsumer } from \"@babelqueue/pulsar\";\n *\n * const client = new Pulsar.Client({ serviceUrl: \"pulsar://localhost:6650\" });\n *\n * const producer = await client.createProducer({ topic: \"orders\" });\n * await new PulsarPublisher(producer).publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const sub = await client.subscribe({\n * topic: \"orders\", subscription: \"babelqueue\", subscriptionType: \"Shared\",\n * });\n * const consumer = new PulsarConsumer(sub, {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §5 of the broker-bindings contract: the canonical envelope is the message\n * payload, projected onto native Pulsar message properties (string→string) — `bq-job` = URN,\n * `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus `bq-schema-version` /\n * `bq-source-lang` / `bq-attempts`. The envelope is unchanged (`schema_version` stays 1);\n * Pulsar is purely additive. A failed handler `negativeAcknowledge`s the message, so it is\n * redelivered (at-least-once) and `getRedeliveryCount()` is incremented. Per §5.2 / §5.7 the\n * consumer routes on the `bq-job` property (the body's URN is used only when the property is\n * absent or blank) and takes `attempts` from the authoritative `bq-attempts` property (the\n * body's value is used only when the property is absent or not a plain ASCII-decimal integer\n * that fits a 32-bit int), reconciled to `max(bq-attempts, getRedeliveryCount())` — no −1,\n * because Pulsar's redelivery count is 0-based. The handler still receives the body's envelope.\n *\n * **Version gate (§5.7 step 3).** Before the body is decoded the consumer reads the\n * `bq-schema-version` property. A missing or blank value (empty or only ASCII whitespace) changes nothing; any value other than\n * the core's supported `SCHEMA_VERSION` (`\"2\"`, `\"x\"`, `\"01\"`, ...) rejects the message undecoded\n * through the same path as a non-conformant envelope — `onError` (with an error naming the\n * version, and an empty envelope since nothing was decoded) and a `negativeAcknowledge`.\n *\n * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop\n * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), and blanks are\n * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so\n * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A\n * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { mergeInto } from \"./headers.js\";\n\n// --- Minimal Pulsar shapes (a structural subset of pulsar-client) --------------\n\n/** A message to send (structural subset of pulsar-client `ProducerMessage`). */\nexport interface PulsarProducerMessage {\n data: Buffer;\n properties?: { [key: string]: string };\n /** Relative delay in milliseconds before the message becomes available (native `deliverAfter`). */\n deliverAfter?: number;\n}\n\n/**\n * The subset of the Pulsar producer this adapter calls. A `Producer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarProducer {\n getTopic(): string;\n send(message: PulsarProducerMessage): Promise<unknown>;\n}\n\n/** A received message (structural subset of pulsar-client `Message`). */\nexport interface PulsarReceivedMessage {\n getData(): Buffer | string;\n getProperties(): { [key: string]: string };\n getRedeliveryCount(): number;\n}\n\n/**\n * The subset of the Pulsar consumer this adapter calls. A `Consumer` from `pulsar-client`\n * satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface PulsarConsumerClient {\n receive(timeoutMs?: number): Promise<PulsarReceivedMessage>;\n acknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n negativeAcknowledge(message: PulsarReceivedMessage): Promise<void> | void;\n}\n\n// --- Property projection (contract §5.2) ---------------------------------------\n\n/** Bare topic name (the part after the last `/`), used as `meta.queue`. */\nfunction topicName(topic: string): string {\n const slash = topic.lastIndexOf(\"/\");\n return slash >= 0 ? topic.slice(slash + 1) : topic;\n}\n\n/**\n * Project the envelope's contract fields onto native Pulsar message properties\n * (string→string): `bq-job` = URN, `bq-trace-id` = trace_id, `bq-message-id` = meta.id, plus\n * `bq-schema-version` / `bq-source-lang` / `bq-attempts`. The body stays authoritative.\n */\nexport function pulsarProperties(envelope: Envelope): { [key: string]: string } {\n const properties: { [key: string]: string } = {};\n if (envelope.job) properties[\"bq-job\"] = envelope.job;\n if (envelope.trace_id) properties[\"bq-trace-id\"] = envelope.trace_id;\n if (envelope.meta.id) properties[\"bq-message-id\"] = envelope.meta.id;\n if (envelope.meta.schema_version != null) {\n properties[\"bq-schema-version\"] = String(envelope.meta.schema_version);\n }\n if (envelope.meta.lang) properties[\"bq-source-lang\"] = envelope.meta.lang;\n properties[\"bq-attempts\"] = String(envelope.attempts ?? 0);\n return properties;\n}\n\n/**\n * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`\n * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the\n * contract `bq-*` properties without clobbering them (the contract wins a key collision).\n */\nexport function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage {\n return {\n data: Buffer.from(EnvelopeCodec.encode(envelope), \"utf8\"),\n properties: mergeInto(pulsarProperties(envelope), extra),\n };\n}\n\n/**\n * Read a received message's native properties back into a flat {@link HeaderCarrier} via\n * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`\n * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract\n * reads only the keys it knows.\n */\nexport function headersOf(message: PulsarReceivedMessage): HeaderCarrier {\n const props = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n const out: HeaderCarrier = {};\n if (!props) return out;\n for (const key of Object.keys(props)) {\n const value = props[key];\n if (value != null && value !== \"\") out[key] = value;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link PulsarPublisher.publish}. */\nexport interface PublishOptions {\n /** Reuse an existing trace id (trace continuation). */\n traceId?: string;\n /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native Pulsar message properties beside the contract\n * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s\n * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the\n * message unchanged.\n */\n headers?: HeaderCarrier;\n}\n\n/** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */\nexport class PulsarPublisher {\n constructor(private readonly producer: PulsarProducer) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the §5 property projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery via `deliverAfter` and mirrors `bq-delay`.\n */\n async publish(\n urn: string,\n data: Record<string, unknown>,\n options: PublishOptions = {},\n ): Promise<string> {\n const envelope = EnvelopeCodec.make(urn, data, {\n queue: topicName(this.producer.getTopic()),\n traceId: options.traceId,\n });\n const message = toPulsarMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.properties = { ...message.properties, \"bq-delay\": String(options.delayMs) };\n message.deliverAfter = options.delayMs;\n }\n\n await this.producer.send(message);\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band\n * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass\n * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the\n * producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link PulsarConsumer}. */\nexport interface PulsarConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is acknowledged). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;\n /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */\n onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;\n /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */\n receiveTimeoutMs?: number;\n /**\n * Optional diagnostic hook, called when the `bq-job` property and the body's URN differ (the\n * property wins and the message is routed on it). `envelope` is the body's envelope. Separate\n * from {@link PulsarConsumerOptions.onError}: it is not an error, and anything it throws (or\n * rejects with) is swallowed — it never changes how the message is handled or acknowledged.\n */\n onRoutingMismatch?: (propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope) => unknown;\n}\n\n/** `bq-attempts` is valid only as ASCII digits that fit a signed 32-bit int (matches Java/.NET, GR-5). */\nconst MAX_ATTEMPTS = 2 ** 31 - 1;\n\nfunction parseAttempts(raw: string | undefined): number | undefined {\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return undefined;\n const value = Number(raw);\n return value <= MAX_ATTEMPTS ? value : undefined;\n}\n\n/** Blank = empty or only ASCII whitespace (space, \\t, \\n, \\v, \\f, \\r) — not `String.prototype.trim()`'s wider set. */\nconst BLANK_ASCII = /^[ \\t\\n\\v\\f\\r]*$/;\n\n/**\n * The `bq-schema-version` value when it is present, non-blank and not the supported version\n * (compared against the core's `SCHEMA_VERSION`, exact string match: `\"01\"`, `\" 1\"`, `\"1.0\"` are\n * rejected). Blank means the empty string or only ASCII whitespace (the same set in every SDK, GR-5);\n * NBSP, U+0085, U+FEFF, U+3000 and the like are not blank. `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(properties: { [key: string]: string }): string | undefined {\n const raw: unknown = properties[\"bq-schema-version\"];\n if (typeof raw !== \"string\" || BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n}\n\nfunction isReceiveTimeout(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /timeout/i.test(message);\n}\n\nfunction decodePayload(data: Buffer | string): string {\n return typeof data === \"string\" ? data : data.toString(\"utf8\");\n}\n\n/**\n * Receives from a Pulsar subscription, decodes + validates each message, routes it to the\n * handler for its URN, and acknowledges it on success. A throwing handler\n * `negativeAcknowledge`s the message — the broker redelivers it and increments\n * `getRedeliveryCount()` (at-least-once). Routing uses the `bq-job` property (body URN as the\n * fallback); `attempts` is the `bq-attempts` property (body value as the fallback) reconciled to\n * `max(.., getRedeliveryCount())` for the handler.\n */\nexport class PulsarConsumer {\n constructor(\n private readonly consumer: PulsarConsumerClient,\n private readonly handlers: BabelHandlers,\n private readonly options: PulsarConsumerOptions = {},\n ) {}\n\n /** Receive one message (up to the receive timeout), route + settle it. Returns 1, or 0 on timeout. */\n async poll(): Promise<number> {\n let message: PulsarReceivedMessage;\n try {\n message = await this.consumer.receive(this.options.receiveTimeoutMs ?? 1000);\n } catch (error) {\n if (isReceiveTimeout(error)) return 0;\n throw error;\n }\n await this.handle(message);\n return 1;\n }\n\n /** Poll until `signal` aborts. */\n async run(signal?: AbortSignal): Promise<void> {\n while (signal?.aborted !== true) {\n await this.poll();\n }\n }\n\n private async handle(message: PulsarReceivedMessage): Promise<void> {\n const properties = safeProperties(message);\n\n // §5.7 step 3: gate on bq-schema-version before the body is decoded.\n const rejectedVersion = unsupportedSchemaVersion(properties);\n if (rejectedVersion !== undefined) {\n this.options.onError?.(\n new BabelQueueError(\n `Rejected a message from Apache Pulsar with unsupported bq-schema-version \"${rejectedVersion}\" (supported: ${SCHEMA_VERSION}).`,\n ),\n {},\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));\n\n // attempts: the authoritative bq-attempts property (the body's value when it is absent or\n // invalid), then max(.., getRedeliveryCount()): the redelivery count is 0-based, so it maps\n // directly with no −1; the max never lowers a higher count republished from another SDK\n // (the Go/Python transports retry by re-sending with attempts+1).\n const redeliveryCount = typeof message.getRedeliveryCount === \"function\" ? message.getRedeliveryCount() : 0;\n // A body without an integer attempts stays as sent, so accepts() below still rejects it\n // (the property can't repair a non-conformant body); the value is written only on a change.\n const bodyAttempts = envelope.attempts;\n let attempts = typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) ? bodyAttempts : 0;\n const fromProperty = parseAttempts(properties[\"bq-attempts\"]);\n if (fromProperty !== undefined) attempts = fromProperty;\n if (redeliveryCount > attempts) attempts = redeliveryCount;\n if (typeof bodyAttempts === \"number\" && Number.isInteger(bodyAttempts) && bodyAttempts !== attempts) {\n envelope.attempts = attempts;\n }\n\n // routing: the bq-job property wins; the body's URN is the fallback. A body with no URN is\n // still rejected below, whatever bq-job says.\n const bodyUrn = EnvelopeCodec.urn(envelope);\n const rawJob: unknown = properties[\"bq-job\"];\n const propertyUrn = typeof rawJob === \"string\" ? rawJob.trim() : \"\";\n const urn = propertyUrn !== \"\" ? propertyUrn : bodyUrn;\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Apache Pulsar.\"),\n envelope,\n message,\n );\n await this.consumer.negativeAcknowledge(message);\n return;\n }\n\n // Diagnostics only, and only for an accepted envelope (07a Java/.NET order): after accepts()\n // and the attempts reconcile, so the hook sees the same envelope the handler gets.\n if (propertyUrn !== \"\" && bodyUrn !== \"\" && propertyUrn !== bodyUrn) {\n this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);\n }\n\n const handler = this.handlers[urn];\n if (!handler) {\n if (this.options.onUnknownUrn) {\n await this.options.onUnknownUrn(envelope, message);\n await this.consumer.acknowledge(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n return;\n }\n\n try {\n await handler(envelope as Envelope, message, headersOf(message));\n await this.consumer.acknowledge(message);\n } catch (error) {\n // Negative-ack releases the message — the broker redelivers and increments the count.\n this.options.onError?.(error, envelope, message);\n await this.consumer.negativeAcknowledge(message);\n }\n }\n\n private notifyRoutingMismatch(propertyUrn: string, bodyUrn: string, envelope: IncomingEnvelope): void {\n const hook = this.options.onRoutingMismatch;\n if (hook === undefined) return;\n try {\n const result = hook(propertyUrn, bodyUrn, envelope);\n Promise.resolve(result).catch(() => undefined);\n } catch {\n // A diagnostic hook must never disturb consumption.\n }\n }\n}\n\nfunction safeProperties(message: PulsarReceivedMessage): { [key: string]: string } {\n try {\n const properties = typeof message.getProperties === \"function\" ? message.getProperties() : undefined;\n return properties ?? {};\n } catch {\n return {};\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 fold an injected\n * {@link HeaderCarrier} (e.g. a W3C `traceparent`) onto a transport's metadata channel **without\n * clobbering** the contract headers it already carries.\n *\n * Headers ride beside the frozen wire envelope (GR-1), never inside it.\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 * Merge out-of-band `extra` headers onto an already-built `base` map **without clobbering** the\n * contract headers in `base` (they win a key collision) and dropping blanks. Returns `base`,\n * mutated in place.\n */\nexport function mergeInto(base: { [key: string]: string }, extra: HeaderCarrier | null | undefined): { [key: string]: string } {\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) {\n if (key in base) continue; // the contract header already there wins\n base[key] = value;\n }\n return base;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@babelqueue/pulsar",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Apache Pulsar adapter for BabelQueue — canonical-envelope publisher and a URN-routed consumer.",
5
- "keywords": ["babelqueue", "pulsar", "apache-pulsar", "queue", "polyglot", "messaging"],
5
+ "keywords": [
6
+ "babelqueue",
7
+ "pulsar",
8
+ "apache-pulsar",
9
+ "queue",
10
+ "polyglot",
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/pulsar"
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.4.0"
45
54
  },
46
55
  "peerDependencies": {
47
56
  "pulsar-client": "^1.8.0"