@babelqueue/pulsar 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.
package/dist/index.cjs CHANGED
@@ -105,6 +105,18 @@ var PulsarPublisher = class {
105
105
  return envelope.meta.id;
106
106
  }
107
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
+ }
108
120
  function isReceiveTimeout(error) {
109
121
  const message = error instanceof Error ? error.message : String(error);
110
122
  return /timeout/i.test(message);
@@ -140,12 +152,33 @@ var PulsarConsumer = class {
140
152
  }
141
153
  }
142
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
+ }
143
168
  const envelope = import_core.EnvelopeCodec.decode(decodePayload(message.getData()));
144
169
  const redeliveryCount = typeof message.getRedeliveryCount === "function" ? message.getRedeliveryCount() : 0;
145
- const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
146
- if (redeliveryCount > current) {
147
- 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;
148
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;
149
182
  if (!import_core.EnvelopeCodec.accepts(envelope)) {
150
183
  this.options.onError?.(
151
184
  new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar."),
@@ -155,7 +188,9 @@ var PulsarConsumer = class {
155
188
  await this.consumer.negativeAcknowledge(message);
156
189
  return;
157
190
  }
158
- const urn = import_core.EnvelopeCodec.urn(envelope);
191
+ if (propertyUrn !== "" && bodyUrn !== "" && propertyUrn !== bodyUrn) {
192
+ this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);
193
+ }
159
194
  const handler = this.handlers[urn];
160
195
  if (!handler) {
161
196
  if (this.options.onUnknownUrn) {
@@ -175,7 +210,24 @@ var PulsarConsumer = class {
175
210
  await this.consumer.negativeAcknowledge(message);
176
211
  }
177
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
+ }
178
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
+ }
179
231
  // Annotate the CommonJS export names for ESM import in node:
180
232
  0 && (module.exports = {
181
233
  PulsarConsumer,
@@ -1 +1 @@
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; 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 * **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, 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\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, 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","/**\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":[]}
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
@@ -28,9 +28,18 @@ import { Envelope, HeaderCarrier, 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`.
34
43
  *
35
44
  * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
36
45
  * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
@@ -137,13 +146,21 @@ interface PulsarConsumerOptions {
137
146
  onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;
138
147
  /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */
139
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;
140
156
  }
141
157
  /**
142
158
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
143
159
  * handler for its URN, and acknowledges it on success. A throwing handler
144
160
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
145
- * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to
146
- * `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.
147
164
  */
148
165
  declare class PulsarConsumer {
149
166
  private readonly consumer;
@@ -155,6 +172,7 @@ declare class PulsarConsumer {
155
172
  /** Poll until `signal` aborts. */
156
173
  run(signal?: AbortSignal): Promise<void>;
157
174
  private handle;
175
+ private notifyRoutingMismatch;
158
176
  }
159
177
 
160
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
@@ -28,9 +28,18 @@ import { Envelope, HeaderCarrier, 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`.
34
43
  *
35
44
  * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
36
45
  * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
@@ -137,13 +146,21 @@ interface PulsarConsumerOptions {
137
146
  onError?: (error: unknown, envelope: IncomingEnvelope, message: PulsarReceivedMessage) => void;
138
147
  /** Per-receive timeout in ms (default 1000). A timeout yields an empty poll, not an error. */
139
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;
140
156
  }
141
157
  /**
142
158
  * Receives from a Pulsar subscription, decodes + validates each message, routes it to the
143
159
  * handler for its URN, and acknowledges it on success. A throwing handler
144
160
  * `negativeAcknowledge`s the message — the broker redelivers it and increments
145
- * `getRedeliveryCount()` (at-least-once); `attempts` is reconciled to
146
- * `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.
147
164
  */
148
165
  declare class PulsarConsumer {
149
166
  private readonly consumer;
@@ -155,6 +172,7 @@ declare class PulsarConsumer {
155
172
  /** Poll until `signal` aborts. */
156
173
  run(signal?: AbortSignal): Promise<void>;
157
174
  private handle;
175
+ private notifyRoutingMismatch;
158
176
  }
159
177
 
160
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,5 @@
1
1
  // src/index.ts
2
- import { BabelQueueError, EnvelopeCodec, UnknownUrnError } from "@babelqueue/core";
2
+ import { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from "@babelqueue/core";
3
3
 
4
4
  // src/headers.ts
5
5
  function sanitizeHeaders(headers) {
@@ -77,6 +77,18 @@ var PulsarPublisher = class {
77
77
  return envelope.meta.id;
78
78
  }
79
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
+ }
80
92
  function isReceiveTimeout(error) {
81
93
  const message = error instanceof Error ? error.message : String(error);
82
94
  return /timeout/i.test(message);
@@ -112,12 +124,33 @@ var PulsarConsumer = class {
112
124
  }
113
125
  }
114
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
+ }
115
140
  const envelope = EnvelopeCodec.decode(decodePayload(message.getData()));
116
141
  const redeliveryCount = typeof message.getRedeliveryCount === "function" ? message.getRedeliveryCount() : 0;
117
- const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
118
- if (redeliveryCount > current) {
119
- 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;
120
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;
121
154
  if (!EnvelopeCodec.accepts(envelope)) {
122
155
  this.options.onError?.(
123
156
  new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Apache Pulsar."),
@@ -127,7 +160,9 @@ var PulsarConsumer = class {
127
160
  await this.consumer.negativeAcknowledge(message);
128
161
  return;
129
162
  }
130
- const urn = EnvelopeCodec.urn(envelope);
163
+ if (propertyUrn !== "" && bodyUrn !== "" && propertyUrn !== bodyUrn) {
164
+ this.notifyRoutingMismatch(propertyUrn, bodyUrn, envelope);
165
+ }
131
166
  const handler = this.handlers[urn];
132
167
  if (!handler) {
133
168
  if (this.options.onUnknownUrn) {
@@ -147,7 +182,24 @@ var PulsarConsumer = class {
147
182
  await this.consumer.negativeAcknowledge(message);
148
183
  }
149
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
+ }
150
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
+ }
151
203
  export {
152
204
  PulsarConsumer,
153
205
  PulsarPublisher,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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; 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 * **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, 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\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, 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","/**\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":[]}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/pulsar",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Apache Pulsar adapter for BabelQueue — canonical-envelope publisher and a URN-routed consumer.",
5
5
  "keywords": [
6
6
  "babelqueue",
@@ -20,7 +20,7 @@
20
20
  "directory": "packages/pulsar"
21
21
  },
22
22
  "license": "MIT",
23
- "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
23
+ "author": "Muhammet Şafak <info@muhammetsafak.com>",
24
24
  "type": "module",
25
25
  "exports": {
26
26
  ".": {