@babelqueue/pulsar 1.0.0 → 1.1.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
@@ -76,6 +76,24 @@ Pulsar is purely additive.
76
76
  The producer/consumer are injected, so the unit tests use fakes — no Pulsar, no broker. Dual
77
77
  ESM + CJS.
78
78
 
79
+ ## OpenTelemetry tracing (ADR-0028)
80
+
81
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
82
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
83
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
84
+ 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),
85
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
86
+
87
+ ```ts
88
+ import { trace } from "@opentelemetry/api";
89
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
90
+ import type { HeaderCarrier } from "@babelqueue/core";
91
+
92
+ const headers: HeaderCarrier = {};
93
+ await tracedPublish(trace.getTracer("orders"), urn, data,
94
+ () => adapterPublish(urn, data, { headers }), { headers });
95
+ ```
96
+
79
97
  ## License
80
98
 
81
99
  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;
@@ -136,7 +168,7 @@ var PulsarConsumer = class {
136
168
  return;
137
169
  }
138
170
  try {
139
- await handler(envelope, message);
171
+ await handler(envelope, message, headersOf(message));
140
172
  await this.consumer.acknowledge(message);
141
173
  } catch (error) {
142
174
  this.options.onError?.(error, envelope, message);
@@ -148,6 +180,7 @@ var PulsarConsumer = class {
148
180
  0 && (module.exports = {
149
181
  PulsarConsumer,
150
182
  PulsarPublisher,
183
+ headersOf,
151
184
  pulsarProperties,
152
185
  toPulsarMessage
153
186
  });
@@ -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; 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":[]}
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.
@@ -31,6 +31,13 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
31
31
  * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative
32
32
  * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,
33
33
  * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.
34
+ *
35
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
36
+ * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
37
+ * properties — the contract properties win a key collision (merge-not-clobber), and blanks are
38
+ * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so
39
+ * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
40
+ * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.
34
41
  */
35
42
 
36
43
  /** A message to send (structural subset of pulsar-client `ProducerMessage`). */
@@ -75,14 +82,32 @@ interface PulsarConsumerClient {
75
82
  declare function pulsarProperties(envelope: Envelope): {
76
83
  [key: string]: string;
77
84
  };
78
- /** Project the envelope onto a native Pulsar producer message (payload + properties). */
79
- declare function toPulsarMessage(envelope: Envelope): PulsarProducerMessage;
85
+ /**
86
+ * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`
87
+ * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the
88
+ * contract `bq-*` properties without clobbering them (the contract wins a key collision).
89
+ */
90
+ declare function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage;
91
+ /**
92
+ * Read a received message's native properties back into a flat {@link HeaderCarrier} via
93
+ * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`
94
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
95
+ * reads only the keys it knows.
96
+ */
97
+ declare function headersOf(message: PulsarReceivedMessage): HeaderCarrier;
80
98
  /** Options for {@link PulsarPublisher.publish}. */
81
99
  interface PublishOptions {
82
100
  /** Reuse an existing trace id (trace continuation). */
83
101
  traceId?: string;
84
102
  /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */
85
103
  delayMs?: number;
104
+ /**
105
+ * Out-of-band transport headers carried as native Pulsar message properties beside the contract
106
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
107
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
108
+ * message unchanged.
109
+ */
110
+ headers?: HeaderCarrier;
86
111
  }
87
112
  /** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */
88
113
  declare class PulsarPublisher {
@@ -95,8 +120,13 @@ declare class PulsarPublisher {
95
120
  */
96
121
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
122
  }
98
- /** A URN handler. Receives the validated envelope and the raw Pulsar message. */
99
- type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;
123
+ /**
124
+ * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band
125
+ * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass
126
+ * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the
127
+ * producer span (ADR-0028).
128
+ */
129
+ type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
130
  /** A map of URN → handler. */
101
131
  type BabelHandlers = Record<string, BabelHandler>;
102
132
  /** Options for {@link PulsarConsumer}. */
@@ -127,4 +157,4 @@ declare class PulsarConsumer {
127
157
  private handle;
128
158
  }
129
159
 
130
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, pulsarProperties, toPulsarMessage };
160
+ 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.
@@ -31,6 +31,13 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
31
31
  * redelivered (at-least-once) and `getRedeliveryCount()` is incremented; the authoritative
32
32
  * attempt count is the body's `bq-attempts`, reconciled to `max(bq-attempts,
33
33
  * getRedeliveryCount())` — no −1, because Pulsar's redelivery count is 0-based.
34
+ *
35
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
36
+ * span linkage) rides as additional native Pulsar **message properties** beside the contract `bq-*`
37
+ * properties — the contract properties win a key collision (merge-not-clobber), and blanks are
38
+ * dropped. On consume the inbound `getProperties()` are surfaced as a `Record<string,string>` so
39
+ * the core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
40
+ * header-less publish is byte-identical. GR-1: the wire envelope (the payload) is never touched.
34
41
  */
35
42
 
36
43
  /** A message to send (structural subset of pulsar-client `ProducerMessage`). */
@@ -75,14 +82,32 @@ interface PulsarConsumerClient {
75
82
  declare function pulsarProperties(envelope: Envelope): {
76
83
  [key: string]: string;
77
84
  };
78
- /** Project the envelope onto a native Pulsar producer message (payload + properties). */
79
- declare function toPulsarMessage(envelope: Envelope): PulsarProducerMessage;
85
+ /**
86
+ * Project the envelope onto a native Pulsar producer message (payload + properties). When `extra`
87
+ * is given, those out-of-band headers (e.g. a `traceparent`, ADR-0028) are merged beside the
88
+ * contract `bq-*` properties without clobbering them (the contract wins a key collision).
89
+ */
90
+ declare function toPulsarMessage(envelope: Envelope, extra?: HeaderCarrier | null): PulsarProducerMessage;
91
+ /**
92
+ * Read a received message's native properties back into a flat {@link HeaderCarrier} via
93
+ * `getProperties()`. Returns an empty object when there are none. Both the contract `bq-*`
94
+ * properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel` extract
95
+ * reads only the keys it knows.
96
+ */
97
+ declare function headersOf(message: PulsarReceivedMessage): HeaderCarrier;
80
98
  /** Options for {@link PulsarPublisher.publish}. */
81
99
  interface PublishOptions {
82
100
  /** Reuse an existing trace id (trace continuation). */
83
101
  traceId?: string;
84
102
  /** Schedule native delayed delivery this many milliseconds from now (`deliverAfter`). */
85
103
  delayMs?: number;
104
+ /**
105
+ * Out-of-band transport headers carried as native Pulsar message properties beside the contract
106
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
107
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
108
+ * message unchanged.
109
+ */
110
+ headers?: HeaderCarrier;
86
111
  }
87
112
  /** Sends canonical-envelope messages to one Pulsar topic with the §5 property projection. */
88
113
  declare class PulsarPublisher {
@@ -95,8 +120,13 @@ declare class PulsarPublisher {
95
120
  */
96
121
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
122
  }
98
- /** A URN handler. Receives the validated envelope and the raw Pulsar message. */
99
- type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage) => unknown | Promise<unknown>;
123
+ /**
124
+ * A URN handler. Receives the validated envelope, the raw Pulsar message, and the out-of-band
125
+ * {@link HeaderCarrier} read from the message's properties (empty when there are none). Pass
126
+ * `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of the
127
+ * producer span (ADR-0028).
128
+ */
129
+ type BabelHandler = (envelope: Envelope, message: PulsarReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
130
  /** A map of URN → handler. */
101
131
  type BabelHandlers = Record<string, BabelHandler>;
102
132
  /** Options for {@link PulsarConsumer}. */
@@ -127,4 +157,4 @@ declare class PulsarConsumer {
127
157
  private handle;
128
158
  }
129
159
 
130
- export { type BabelHandler, type BabelHandlers, type PublishOptions, PulsarConsumer, type PulsarConsumerClient, type PulsarConsumerOptions, type PulsarProducer, type PulsarProducerMessage, PulsarPublisher, type PulsarReceivedMessage, pulsarProperties, toPulsarMessage };
160
+ 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
2
  import { BabelQueueError, EnvelopeCodec, 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;
@@ -109,7 +140,7 @@ var PulsarConsumer = class {
109
140
  return;
110
141
  }
111
142
  try {
112
- await handler(envelope, message);
143
+ await handler(envelope, message, headersOf(message));
113
144
  await this.consumer.acknowledge(message);
114
145
  } catch (error) {
115
146
  this.options.onError?.(error, envelope, message);
@@ -120,6 +151,7 @@ var PulsarConsumer = class {
120
151
  export {
121
152
  PulsarConsumer,
122
153
  PulsarPublisher,
154
+ headersOf,
123
155
  pulsarProperties,
124
156
  toPulsarMessage
125
157
  };
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; 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":[]}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@babelqueue/pulsar",
3
- "version": "1.0.0",
3
+ "version": "1.1.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"
@@ -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"