@babelqueue/azure-service-bus 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,9 +63,36 @@ A throwing handler `abandonMessage`s, so the broker redelivers and increments
63
63
  `onError` / `onUnknownUrn`. The envelope is unchanged (`schema_version` stays `1`); Azure
64
64
  Service Bus is purely additive.
65
65
 
66
+ **Version gate (§4.7).** Before the body is decoded the consumer reads the `bq-schema-version`
67
+ application property. A missing or blank value (empty or only ASCII whitespace) changes nothing;
68
+ the string `"1"` and the integral number `1` decode; any other value or type (`"2"`, `"x"`, `"01"`,
69
+ `2`, `1.5` — strings are never trimmed) rejects the message **undecoded**: it reaches `onError`
70
+ (with an empty envelope) and is **dead-lettered explicitly** (`deadLetterMessage`, reason
71
+ `unsupported schema version`) — never abandoned, so it does not loop back to the same consumer. The
72
+ receiver must provide `deadLetterMessage` (a `ServiceBusReceiver` does); one without it degrades to
73
+ `abandonMessage`.
74
+
66
75
  The sender/receiver are injected, so the unit tests use fakes — no Azure, no broker. Dual
67
76
  ESM + CJS.
68
77
 
78
+ ## OpenTelemetry tracing (ADR-0028)
79
+
80
+ Cross-hop **span** linkage rides on the out-of-band `HeaderCarrier` from
81
+ [`@babelqueue/core@^1.4.0`](https://www.npmjs.com/package/@babelqueue/core). Pass the carrier produced by
82
+ `@babelqueue/core/otel`'s `publish` to this adapter's `publish({ headers })`; it is carried on
83
+ native `applicationProperties`, 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),
84
+ so the core's `otel` `wrapHandler` starts the consumer span as a true **child** of the producer span.
85
+
86
+ ```ts
87
+ import { trace } from "@opentelemetry/api";
88
+ import { publish as tracedPublish } from "@babelqueue/core/otel";
89
+ import type { HeaderCarrier } from "@babelqueue/core";
90
+
91
+ const headers: HeaderCarrier = {};
92
+ await tracedPublish(trace.getTracer("orders"), urn, data,
93
+ () => adapterPublish(urn, data, { headers }), { headers });
94
+ ```
95
+
69
96
  ## License
70
97
 
71
98
  MIT
package/dist/index.cjs CHANGED
@@ -22,12 +22,42 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AsbConsumer: () => AsbConsumer,
24
24
  AsbPublisher: () => AsbPublisher,
25
+ headersOf: () => headersOf,
25
26
  toServiceBusMessage: () => toServiceBusMessage
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
  var import_core = require("@babelqueue/core");
29
- function toServiceBusMessage(envelope) {
30
+
31
+ // src/headers.ts
32
+ function sanitizeHeaders(headers) {
33
+ const out = {};
34
+ if (!headers) return out;
35
+ for (const key of Object.keys(headers)) {
36
+ const value = headers[key];
37
+ if (key === "" || value == null || value === "") continue;
38
+ out[key] = value;
39
+ }
40
+ return out;
41
+ }
42
+
43
+ // src/index.ts
44
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
45
+ var UNSUPPORTED_SCHEMA_VERSION_REASON = "unsupported schema version";
46
+ var MAX_REPORTED_SCHEMA_VERSION_LENGTH = 64;
47
+ function unsupportedSchemaVersion(message) {
48
+ const raw = message.applicationProperties?.["bq-schema-version"];
49
+ if (raw == null) return void 0;
50
+ if (typeof raw === "string") {
51
+ if (BLANK_ASCII.test(raw)) return void 0;
52
+ return raw === String(import_core.SCHEMA_VERSION) ? void 0 : raw;
53
+ }
54
+ if (typeof raw === "number") return raw === import_core.SCHEMA_VERSION ? void 0 : String(raw);
55
+ if (typeof raw === "bigint") return raw === BigInt(import_core.SCHEMA_VERSION) ? void 0 : String(raw);
56
+ return `${typeof raw}:${String(raw)}`;
57
+ }
58
+ function toServiceBusMessage(envelope, extra) {
30
59
  const applicationProperties = {};
60
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
31
61
  if (envelope.meta.schema_version != null) {
32
62
  applicationProperties["bq-schema-version"] = envelope.meta.schema_version;
33
63
  }
@@ -47,6 +77,18 @@ function toServiceBusMessage(envelope) {
47
77
  }
48
78
  return message;
49
79
  }
80
+ function headersOf(message) {
81
+ const props = message.applicationProperties;
82
+ const out = {};
83
+ if (!props) return out;
84
+ for (const key of Object.keys(props)) {
85
+ const value = props[key];
86
+ if (value == null) continue;
87
+ const s = String(value);
88
+ if (s !== "") out[key] = s;
89
+ }
90
+ return out;
91
+ }
50
92
  var AsbPublisher = class {
51
93
  constructor(sender) {
52
94
  this.sender = sender;
@@ -62,7 +104,7 @@ var AsbPublisher = class {
62
104
  queue: this.sender.entityPath,
63
105
  traceId: options.traceId
64
106
  });
65
- const message = toServiceBusMessage(envelope);
107
+ const message = toServiceBusMessage(envelope, options.headers);
66
108
  if (options.delayMs != null && options.delayMs > 0) {
67
109
  message.applicationProperties = { ...message.applicationProperties, "bq-delay": options.delayMs };
68
110
  await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));
@@ -99,6 +141,28 @@ var AsbConsumer = class {
99
141
  }
100
142
  }
101
143
  async handle(message) {
144
+ const rejectedVersion = unsupportedSchemaVersion(message);
145
+ if (rejectedVersion !== void 0) {
146
+ const reported = rejectedVersion.length > MAX_REPORTED_SCHEMA_VERSION_LENGTH ? `${rejectedVersion.slice(0, MAX_REPORTED_SCHEMA_VERSION_LENGTH)}...` : rejectedVersion;
147
+ const description = `Rejected a message from Azure Service Bus with unsupported bq-schema-version "${reported}" (supported: ${import_core.SCHEMA_VERSION}).`;
148
+ const deadLetter = this.receiver.deadLetterMessage?.bind(this.receiver);
149
+ this.options.onError?.(
150
+ new import_core.BabelQueueError(
151
+ deadLetter ? description : `${description} The receiver has no deadLetterMessage; degraded to abandon.`
152
+ ),
153
+ {},
154
+ message
155
+ );
156
+ if (deadLetter) {
157
+ await deadLetter(message, {
158
+ deadLetterReason: UNSUPPORTED_SCHEMA_VERSION_REASON,
159
+ deadLetterErrorDescription: description
160
+ });
161
+ } else {
162
+ await this.receiver.abandonMessage(message);
163
+ }
164
+ return;
165
+ }
102
166
  const raw = typeof message.body === "string" ? message.body : String(message.body ?? "");
103
167
  const envelope = import_core.EnvelopeCodec.decode(raw);
104
168
  const deliveryCount = typeof message.deliveryCount === "number" ? message.deliveryCount : 0;
@@ -129,7 +193,7 @@ var AsbConsumer = class {
129
193
  return;
130
194
  }
131
195
  try {
132
- await handler(envelope, message);
196
+ await handler(envelope, message, headersOf(message));
133
197
  await this.receiver.completeMessage(message);
134
198
  } catch (error) {
135
199
  this.options.onError?.(error, envelope, message);
@@ -141,6 +205,7 @@ var AsbConsumer = class {
141
205
  0 && (module.exports = {
142
206
  AsbConsumer,
143
207
  AsbPublisher,
208
+ headersOf,
144
209
  toServiceBusMessage
145
210
  });
146
211
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Azure Service Bus adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service\n * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import { ServiceBusClient } from \"@azure/service-bus\";\n * import { AsbPublisher, AsbConsumer } from \"@babelqueue/azure-service-bus\";\n *\n * const client = new ServiceBusClient(connectionString);\n * await new AsbPublisher(client.createSender(\"orders\"))\n * .publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new AsbConsumer(client.createReceiver(\"orders\"), {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §4 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native Service Bus fields (`subject` = URN,\n * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely\n * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is\n * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the\n * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal Service Bus shapes (a structural subset of @azure/service-bus) -----\n\n/** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */\nexport interface AsbMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n contentType?: string;\n applicationProperties?: { [key: string]: number | boolean | string | Date | null };\n scheduledEnqueueTimeUtc?: Date;\n}\n\n/** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */\nexport interface AsbReceivedMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n deliveryCount?: number;\n applicationProperties?: { [key: string]: unknown };\n}\n\n/**\n * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from\n * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbSender {\n entityPath: string;\n sendMessages(message: AsbMessage): Promise<void>;\n scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;\n}\n\n/**\n * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`\n * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbReceiver {\n receiveMessages(maxMessageCount: number, options?: { maxWaitTimeInMs?: number }): Promise<AsbReceivedMessage[]>;\n completeMessage(message: AsbReceivedMessage): Promise<void>;\n abandonMessage(message: AsbReceivedMessage): Promise<void>;\n}\n\n// --- Native projection (contract §4.2–§4.3) ------------------------------------\n\n/**\n * Project the envelope's contract fields onto a native Service Bus message — `subject`\n * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties. The body stays authoritative.\n */\nexport function toServiceBusMessage(envelope: Envelope): AsbMessage {\n const applicationProperties: { [key: string]: number | boolean | string } = {};\n if (envelope.meta.schema_version != null) {\n applicationProperties[\"bq-schema-version\"] = envelope.meta.schema_version;\n }\n if (envelope.meta.lang) applicationProperties[\"bq-source-lang\"] = envelope.meta.lang;\n if (envelope.meta.created_at != null) {\n applicationProperties[\"bq-created-at\"] = envelope.meta.created_at;\n }\n\n const message: AsbMessage = {\n body: EnvelopeCodec.encode(envelope),\n contentType: \"application/json\",\n };\n if (envelope.job) message.subject = envelope.job;\n if (envelope.trace_id) message.correlationId = envelope.trace_id;\n if (envelope.meta.id) message.messageId = envelope.meta.id;\n if (Object.keys(applicationProperties).length > 0) {\n message.applicationProperties = applicationProperties;\n }\n return message;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link AsbPublisher.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 (`scheduledEnqueueTimeUtc`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */\nexport class AsbPublisher {\n constructor(private readonly sender: AsbSender) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the native projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery.\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: this.sender.entityPath,\n traceId: options.traceId,\n });\n const message = toServiceBusMessage(envelope);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.applicationProperties = { ...message.applicationProperties, \"bq-delay\": options.delayMs };\n await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));\n } else {\n await this.sender.sendMessages(message);\n }\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Service Bus message. */\nexport type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link AsbConsumer}. */\nexport interface AsbConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is completed). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => 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: AsbReceivedMessage) => void;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n /** Max time to wait for a batch, in ms. */\n maxWaitTimeInMs?: number;\n}\n\n/**\n * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes\n * it to the handler for its URN, and completes it on success. A throwing handler abandons\n * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);\n * `attempts` is reconciled to `deliveryCount − 1` for the handler.\n */\nexport class AsbConsumer {\n constructor(\n private readonly receiver: AsbReceiver,\n private readonly handlers: BabelHandlers,\n private readonly options: AsbConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, settle each. Returns the batch size. */\n async poll(): Promise<number> {\n const messages = await this.receiver.receiveMessages(\n this.options.maxMessages ?? 10,\n this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : undefined,\n );\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\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: AsbReceivedMessage): Promise<void> {\n const raw = typeof message.body === \"string\" ? message.body : String(message.body ?? \"\");\n const envelope = EnvelopeCodec.decode(raw);\n\n // attempts = max(current, deliveryCount − 1): deliveryCount (1-based) is the native\n // redelivery floor; the max never lowers a higher body count republished from another SDK.\n const deliveryCount = typeof message.deliveryCount === \"number\" ? message.deliveryCount : 0;\n const native = deliveryCount - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (native > current) {\n envelope.attempts = native;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Azure Service Bus.\"),\n envelope,\n message,\n );\n await this.receiver.abandonMessage(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.receiver.completeMessage(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.receiver.completeMessage(message);\n } catch (error) {\n // Abandon releases the lock — the broker redelivers and increments deliveryCount.\n this.options.onError?.(error, envelope, message);\n await this.receiver.abandonMessage(message);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Azure Service Bus adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service\n * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import { ServiceBusClient } from \"@azure/service-bus\";\n * import { AsbPublisher, AsbConsumer } from \"@babelqueue/azure-service-bus\";\n *\n * const client = new ServiceBusClient(connectionString);\n * await new AsbPublisher(client.createSender(\"orders\"))\n * .publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new AsbConsumer(client.createReceiver(\"orders\"), {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §4 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native Service Bus fields (`subject` = URN,\n * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely\n * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is\n * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the\n * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.\n *\n * **Version gate (§4.7).** Before the body is decoded the consumer reads the `bq-schema-version`\n * application property. A missing or blank value (empty or only ASCII whitespace) changes nothing;\n * the string `\"1\"` and the integral number `1` (what the producer writes) decode; any other value\n * or type (`\"2\"`, `\"x\"`, `\"01\"`, `2`, `1.5`, ...) rejects the message undecoded — `onError` (with an\n * empty envelope) and an explicit `deadLetterMessage` (reason `unsupported schema version`), never\n * `abandon`, so the message is not redelivered to the same consumer.\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 `applicationProperties` beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped. On\n * consume the inbound `applicationProperties` are surfaced as a `Record<string,string>` so the\n * 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 body is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal Service Bus shapes (a structural subset of @azure/service-bus) -----\n\n/** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */\nexport interface AsbMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n contentType?: string;\n applicationProperties?: { [key: string]: number | boolean | string | Date | null };\n scheduledEnqueueTimeUtc?: Date;\n}\n\n/** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */\nexport interface AsbReceivedMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n deliveryCount?: number;\n applicationProperties?: { [key: string]: unknown };\n}\n\n/**\n * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from\n * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbSender {\n entityPath: string;\n sendMessages(message: AsbMessage): Promise<void>;\n scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;\n}\n\n/**\n * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`\n * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbReceiver {\n receiveMessages(maxMessageCount: number, options?: { maxWaitTimeInMs?: number }): Promise<AsbReceivedMessage[]>;\n completeMessage(message: AsbReceivedMessage): Promise<void>;\n abandonMessage(message: AsbReceivedMessage): Promise<void>;\n /**\n * Move a message to the entity's dead-letter sub-queue. A `ServiceBusReceiver` from\n * `@azure/service-bus` provides it. Optional so existing structural implementations keep\n * compiling; a receiver without it cannot dead-letter, so an unsupported-schema-version\n * message degrades to `abandonMessage` (the broker's `MaxDeliveryCount` then dead-letters it).\n */\n deadLetterMessage?(\n message: AsbReceivedMessage,\n options?: { deadLetterReason?: string; deadLetterErrorDescription?: string },\n ): Promise<void>;\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/** Reason recorded on a message dead-lettered by the version gate. */\nconst UNSUPPORTED_SCHEMA_VERSION_REASON = \"unsupported schema version\";\n\n/** Longest rejected value echoed into reports; Service Bus caps the dead-letter description at 4096 chars. */\nconst MAX_REPORTED_SCHEMA_VERSION_LENGTH = 64;\n\n/**\n * The `bq-schema-version` application property rendered for reporting when it is present, non-blank\n * and not the supported version (compared against the core's `SCHEMA_VERSION`). The string must be\n * exactly `\"1\"` (`\"01\"`, `\" 1\"`, `\"1.0\"` are rejected, never trimmed); blank means the empty string\n * or only ASCII whitespace (the same set in every SDK, GR-5), while NBSP, U+0085, U+FEFF, U+3000\n * and the like are not blank. AMQP-typed numbers are accepted when they are the integer 1 (a JS\n * `number`, or the `bigint` an int64 may surface as); every other number or type is rejected.\n * `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(message: AsbReceivedMessage): string | undefined {\n const raw = message.applicationProperties?.[\"bq-schema-version\"];\n if (raw == null) return undefined;\n if (typeof raw === \"string\") {\n if (BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n }\n if (typeof raw === \"number\") return raw === SCHEMA_VERSION ? undefined : String(raw);\n if (typeof raw === \"bigint\") return raw === BigInt(SCHEMA_VERSION) ? undefined : String(raw);\n return `${typeof raw}:${String(raw)}`;\n}\n\n// --- Native projection (contract §4.2–§4.3) ------------------------------------\n\n/**\n * Project the envelope's contract fields onto a native Service Bus message — `subject`\n * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties. The body stays authoritative.\n *\n * When `extra` is given, those out-of-band headers (e.g. a W3C `traceparent`, ADR-0028) are merged\n * into `applicationProperties` **first**, then the contract `bq-*` properties are written so they\n * always win a key collision (merge-not-clobber); blank keys/values are dropped. GR-1: the wire\n * envelope body is never touched.\n */\nexport function toServiceBusMessage(envelope: Envelope, extra?: HeaderCarrier | null): AsbMessage {\n const applicationProperties: { [key: string]: number | boolean | string } = {};\n // Out-of-band riders go in first so the contract bq-* properties below win any key collision.\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n if (envelope.meta.schema_version != null) {\n applicationProperties[\"bq-schema-version\"] = envelope.meta.schema_version;\n }\n if (envelope.meta.lang) applicationProperties[\"bq-source-lang\"] = envelope.meta.lang;\n if (envelope.meta.created_at != null) {\n applicationProperties[\"bq-created-at\"] = envelope.meta.created_at;\n }\n\n const message: AsbMessage = {\n body: EnvelopeCodec.encode(envelope),\n contentType: \"application/json\",\n };\n if (envelope.job) message.subject = envelope.job;\n if (envelope.trace_id) message.correlationId = envelope.trace_id;\n if (envelope.meta.id) message.messageId = envelope.meta.id;\n if (Object.keys(applicationProperties).length > 0) {\n message.applicationProperties = applicationProperties;\n }\n return message;\n}\n\n/**\n * Read a received message's `applicationProperties` back into a flat {@link HeaderCarrier},\n * stringifying values defensively. Returns an empty object when there are none. Both the contract\n * `bq-*` properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel`\n * extract reads only the keys it knows.\n */\nexport function headersOf(message: AsbReceivedMessage): HeaderCarrier {\n const props = message.applicationProperties;\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) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link AsbPublisher.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 (`scheduledEnqueueTimeUtc`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native `applicationProperties` 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 Service Bus entity with the §4 native projection. */\nexport class AsbPublisher {\n constructor(private readonly sender: AsbSender) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the native projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery.\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: this.sender.entityPath,\n traceId: options.traceId,\n });\n const message = toServiceBusMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.applicationProperties = { ...message.applicationProperties, \"bq-delay\": options.delayMs };\n await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));\n } else {\n await this.sender.sendMessages(message);\n }\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Service Bus message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `applicationProperties` (empty when there are none).\n * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of\n * the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link AsbConsumer}. */\nexport interface AsbConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is completed). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => 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: AsbReceivedMessage) => void;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n /** Max time to wait for a batch, in ms. */\n maxWaitTimeInMs?: number;\n}\n\n/**\n * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes\n * it to the handler for its URN, and completes it on success. A throwing handler abandons\n * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);\n * `attempts` is reconciled to `deliveryCount − 1` for the handler.\n */\nexport class AsbConsumer {\n constructor(\n private readonly receiver: AsbReceiver,\n private readonly handlers: BabelHandlers,\n private readonly options: AsbConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, settle each. Returns the batch size. */\n async poll(): Promise<number> {\n const messages = await this.receiver.receiveMessages(\n this.options.maxMessages ?? 10,\n this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : undefined,\n );\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\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: AsbReceivedMessage): Promise<void> {\n // §4.7: gate on bq-schema-version before the body is decoded. The message is dead-lettered\n // explicitly (never abandoned) so it does not loop back to the same consumer.\n const rejectedVersion = unsupportedSchemaVersion(message);\n if (rejectedVersion !== undefined) {\n const reported =\n rejectedVersion.length > MAX_REPORTED_SCHEMA_VERSION_LENGTH\n ? `${rejectedVersion.slice(0, MAX_REPORTED_SCHEMA_VERSION_LENGTH)}...`\n : rejectedVersion;\n const description = `Rejected a message from Azure Service Bus with unsupported bq-schema-version \"${reported}\" (supported: ${SCHEMA_VERSION}).`;\n const deadLetter = this.receiver.deadLetterMessage?.bind(this.receiver);\n this.options.onError?.(\n new BabelQueueError(\n deadLetter ? description : `${description} The receiver has no deadLetterMessage; degraded to abandon.`,\n ),\n {},\n message,\n );\n if (deadLetter) {\n await deadLetter(message, {\n deadLetterReason: UNSUPPORTED_SCHEMA_VERSION_REASON,\n deadLetterErrorDescription: description,\n });\n } else {\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n const raw = typeof message.body === \"string\" ? message.body : String(message.body ?? \"\");\n const envelope = EnvelopeCodec.decode(raw);\n\n // attempts = max(current, deliveryCount − 1): deliveryCount (1-based) is the native\n // redelivery floor; the max never lowers a higher body count republished from another SDK.\n const deliveryCount = typeof message.deliveryCount === \"number\" ? message.deliveryCount : 0;\n const native = deliveryCount - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (native > current) {\n envelope.attempts = native;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Azure Service Bus.\"),\n envelope,\n message,\n );\n await this.receiver.abandonMessage(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.receiver.completeMessage(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.receiver.completeMessage(message);\n } catch (error) {\n // Abandon releases the lock — the broker redelivers and increments deliveryCount.\n this.options.onError?.(error, envelope, message);\n await this.receiver.abandonMessage(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"],"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
  * Azure Service Bus adapter for BabelQueue.
@@ -26,6 +26,20 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
26
26
  * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is
27
27
  * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the
28
28
  * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.
29
+ *
30
+ * **Version gate (§4.7).** Before the body is decoded the consumer reads the `bq-schema-version`
31
+ * application property. A missing or blank value (empty or only ASCII whitespace) changes nothing;
32
+ * the string `"1"` and the integral number `1` (what the producer writes) decode; any other value
33
+ * or type (`"2"`, `"x"`, `"01"`, `2`, `1.5`, ...) rejects the message undecoded — `onError` (with an
34
+ * empty envelope) and an explicit `deadLetterMessage` (reason `unsupported schema version`), never
35
+ * `abandon`, so the message is not redelivered to the same consumer.
36
+ *
37
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
38
+ * span linkage) rides as additional native `applicationProperties` beside the contract `bq-*`
39
+ * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped. On
40
+ * consume the inbound `applicationProperties` are surfaced as a `Record<string,string>` so the
41
+ * core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
42
+ * header-less publish is byte-identical. GR-1: the wire envelope body is never touched.
29
43
  */
30
44
 
31
45
  /** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */
@@ -70,19 +84,48 @@ interface AsbReceiver {
70
84
  }): Promise<AsbReceivedMessage[]>;
71
85
  completeMessage(message: AsbReceivedMessage): Promise<void>;
72
86
  abandonMessage(message: AsbReceivedMessage): Promise<void>;
87
+ /**
88
+ * Move a message to the entity's dead-letter sub-queue. A `ServiceBusReceiver` from
89
+ * `@azure/service-bus` provides it. Optional so existing structural implementations keep
90
+ * compiling; a receiver without it cannot dead-letter, so an unsupported-schema-version
91
+ * message degrades to `abandonMessage` (the broker's `MaxDeliveryCount` then dead-letters it).
92
+ */
93
+ deadLetterMessage?(message: AsbReceivedMessage, options?: {
94
+ deadLetterReason?: string;
95
+ deadLetterErrorDescription?: string;
96
+ }): Promise<void>;
73
97
  }
74
98
  /**
75
99
  * Project the envelope's contract fields onto a native Service Bus message — `subject`
76
100
  * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
77
101
  * properties. The body stays authoritative.
102
+ *
103
+ * When `extra` is given, those out-of-band headers (e.g. a W3C `traceparent`, ADR-0028) are merged
104
+ * into `applicationProperties` **first**, then the contract `bq-*` properties are written so they
105
+ * always win a key collision (merge-not-clobber); blank keys/values are dropped. GR-1: the wire
106
+ * envelope body is never touched.
78
107
  */
79
- declare function toServiceBusMessage(envelope: Envelope): AsbMessage;
108
+ declare function toServiceBusMessage(envelope: Envelope, extra?: HeaderCarrier | null): AsbMessage;
109
+ /**
110
+ * Read a received message's `applicationProperties` back into a flat {@link HeaderCarrier},
111
+ * stringifying values defensively. Returns an empty object when there are none. Both the contract
112
+ * `bq-*` properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel`
113
+ * extract reads only the keys it knows.
114
+ */
115
+ declare function headersOf(message: AsbReceivedMessage): HeaderCarrier;
80
116
  /** Options for {@link AsbPublisher.publish}. */
81
117
  interface PublishOptions {
82
118
  /** Reuse an existing trace id (trace continuation). */
83
119
  traceId?: string;
84
120
  /** Schedule native delayed delivery this many milliseconds from now (`scheduledEnqueueTimeUtc`). */
85
121
  delayMs?: number;
122
+ /**
123
+ * Out-of-band transport headers carried as native `applicationProperties` beside the contract
124
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
125
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
126
+ * message unchanged.
127
+ */
128
+ headers?: HeaderCarrier;
86
129
  }
87
130
  /** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */
88
131
  declare class AsbPublisher {
@@ -95,8 +138,13 @@ declare class AsbPublisher {
95
138
  */
96
139
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
140
  }
98
- /** A URN handler. Receives the validated envelope and the raw Service Bus message. */
99
- type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
141
+ /**
142
+ * A URN handler. Receives the validated envelope, the raw Service Bus message, and the out-of-band
143
+ * {@link HeaderCarrier} read from the message's `applicationProperties` (empty when there are none).
144
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
145
+ * the producer span (ADR-0028).
146
+ */
147
+ type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
148
  /** A map of URN → handler. */
101
149
  type BabelHandlers = Record<string, BabelHandler>;
102
150
  /** Options for {@link AsbConsumer}. */
@@ -128,4 +176,4 @@ declare class AsbConsumer {
128
176
  private handle;
129
177
  }
130
178
 
131
- export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, toServiceBusMessage };
179
+ export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, headersOf, toServiceBusMessage };
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
  * Azure Service Bus adapter for BabelQueue.
@@ -26,6 +26,20 @@ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
26
26
  * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is
27
27
  * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the
28
28
  * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.
29
+ *
30
+ * **Version gate (§4.7).** Before the body is decoded the consumer reads the `bq-schema-version`
31
+ * application property. A missing or blank value (empty or only ASCII whitespace) changes nothing;
32
+ * the string `"1"` and the integral number `1` (what the producer writes) decode; any other value
33
+ * or type (`"2"`, `"x"`, `"01"`, `2`, `1.5`, ...) rejects the message undecoded — `onError` (with an
34
+ * empty envelope) and an explicit `deadLetterMessage` (reason `unsupported schema version`), never
35
+ * `abandon`, so the message is not redelivered to the same consumer.
36
+ *
37
+ * **Out-of-band headers (ADR-0028).** A `headers` carrier (e.g. a W3C `traceparent` for cross-hop
38
+ * span linkage) rides as additional native `applicationProperties` beside the contract `bq-*`
39
+ * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped. On
40
+ * consume the inbound `applicationProperties` are surfaced as a `Record<string,string>` so the
41
+ * core's `otel` extract sees the `traceparent` and links the consumer span as a child. A
42
+ * header-less publish is byte-identical. GR-1: the wire envelope body is never touched.
29
43
  */
30
44
 
31
45
  /** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */
@@ -70,19 +84,48 @@ interface AsbReceiver {
70
84
  }): Promise<AsbReceivedMessage[]>;
71
85
  completeMessage(message: AsbReceivedMessage): Promise<void>;
72
86
  abandonMessage(message: AsbReceivedMessage): Promise<void>;
87
+ /**
88
+ * Move a message to the entity's dead-letter sub-queue. A `ServiceBusReceiver` from
89
+ * `@azure/service-bus` provides it. Optional so existing structural implementations keep
90
+ * compiling; a receiver without it cannot dead-letter, so an unsupported-schema-version
91
+ * message degrades to `abandonMessage` (the broker's `MaxDeliveryCount` then dead-letters it).
92
+ */
93
+ deadLetterMessage?(message: AsbReceivedMessage, options?: {
94
+ deadLetterReason?: string;
95
+ deadLetterErrorDescription?: string;
96
+ }): Promise<void>;
73
97
  }
74
98
  /**
75
99
  * Project the envelope's contract fields onto a native Service Bus message — `subject`
76
100
  * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
77
101
  * properties. The body stays authoritative.
102
+ *
103
+ * When `extra` is given, those out-of-band headers (e.g. a W3C `traceparent`, ADR-0028) are merged
104
+ * into `applicationProperties` **first**, then the contract `bq-*` properties are written so they
105
+ * always win a key collision (merge-not-clobber); blank keys/values are dropped. GR-1: the wire
106
+ * envelope body is never touched.
78
107
  */
79
- declare function toServiceBusMessage(envelope: Envelope): AsbMessage;
108
+ declare function toServiceBusMessage(envelope: Envelope, extra?: HeaderCarrier | null): AsbMessage;
109
+ /**
110
+ * Read a received message's `applicationProperties` back into a flat {@link HeaderCarrier},
111
+ * stringifying values defensively. Returns an empty object when there are none. Both the contract
112
+ * `bq-*` properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel`
113
+ * extract reads only the keys it knows.
114
+ */
115
+ declare function headersOf(message: AsbReceivedMessage): HeaderCarrier;
80
116
  /** Options for {@link AsbPublisher.publish}. */
81
117
  interface PublishOptions {
82
118
  /** Reuse an existing trace id (trace continuation). */
83
119
  traceId?: string;
84
120
  /** Schedule native delayed delivery this many milliseconds from now (`scheduledEnqueueTimeUtc`). */
85
121
  delayMs?: number;
122
+ /**
123
+ * Out-of-band transport headers carried as native `applicationProperties` beside the contract
124
+ * `bq-*` properties (ADR-0028) — e.g. a W3C `traceparent` written by `@babelqueue/core/otel`'s
125
+ * `publish`. The contract properties win a key collision; an empty/omitted carrier leaves the
126
+ * message unchanged.
127
+ */
128
+ headers?: HeaderCarrier;
86
129
  }
87
130
  /** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */
88
131
  declare class AsbPublisher {
@@ -95,8 +138,13 @@ declare class AsbPublisher {
95
138
  */
96
139
  publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
140
  }
98
- /** A URN handler. Receives the validated envelope and the raw Service Bus message. */
99
- type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
141
+ /**
142
+ * A URN handler. Receives the validated envelope, the raw Service Bus message, and the out-of-band
143
+ * {@link HeaderCarrier} read from the message's `applicationProperties` (empty when there are none).
144
+ * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of
145
+ * the producer span (ADR-0028).
146
+ */
147
+ type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;
100
148
  /** A map of URN → handler. */
101
149
  type BabelHandlers = Record<string, BabelHandler>;
102
150
  /** Options for {@link AsbConsumer}. */
@@ -128,4 +176,4 @@ declare class AsbConsumer {
128
176
  private handle;
129
177
  }
130
178
 
131
- export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, toServiceBusMessage };
179
+ export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, headersOf, toServiceBusMessage };
package/dist/index.js CHANGED
@@ -1,7 +1,36 @@
1
1
  // src/index.ts
2
- import { BabelQueueError, EnvelopeCodec, UnknownUrnError } from "@babelqueue/core";
3
- function toServiceBusMessage(envelope) {
2
+ import { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from "@babelqueue/core";
3
+
4
+ // src/headers.ts
5
+ function sanitizeHeaders(headers) {
6
+ const out = {};
7
+ if (!headers) return out;
8
+ for (const key of Object.keys(headers)) {
9
+ const value = headers[key];
10
+ if (key === "" || value == null || value === "") continue;
11
+ out[key] = value;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ // src/index.ts
17
+ var BLANK_ASCII = /^[ \t\n\v\f\r]*$/;
18
+ var UNSUPPORTED_SCHEMA_VERSION_REASON = "unsupported schema version";
19
+ var MAX_REPORTED_SCHEMA_VERSION_LENGTH = 64;
20
+ function unsupportedSchemaVersion(message) {
21
+ const raw = message.applicationProperties?.["bq-schema-version"];
22
+ if (raw == null) return void 0;
23
+ if (typeof raw === "string") {
24
+ if (BLANK_ASCII.test(raw)) return void 0;
25
+ return raw === String(SCHEMA_VERSION) ? void 0 : raw;
26
+ }
27
+ if (typeof raw === "number") return raw === SCHEMA_VERSION ? void 0 : String(raw);
28
+ if (typeof raw === "bigint") return raw === BigInt(SCHEMA_VERSION) ? void 0 : String(raw);
29
+ return `${typeof raw}:${String(raw)}`;
30
+ }
31
+ function toServiceBusMessage(envelope, extra) {
4
32
  const applicationProperties = {};
33
+ for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;
5
34
  if (envelope.meta.schema_version != null) {
6
35
  applicationProperties["bq-schema-version"] = envelope.meta.schema_version;
7
36
  }
@@ -21,6 +50,18 @@ function toServiceBusMessage(envelope) {
21
50
  }
22
51
  return message;
23
52
  }
53
+ function headersOf(message) {
54
+ const props = message.applicationProperties;
55
+ const out = {};
56
+ if (!props) return out;
57
+ for (const key of Object.keys(props)) {
58
+ const value = props[key];
59
+ if (value == null) continue;
60
+ const s = String(value);
61
+ if (s !== "") out[key] = s;
62
+ }
63
+ return out;
64
+ }
24
65
  var AsbPublisher = class {
25
66
  constructor(sender) {
26
67
  this.sender = sender;
@@ -36,7 +77,7 @@ var AsbPublisher = class {
36
77
  queue: this.sender.entityPath,
37
78
  traceId: options.traceId
38
79
  });
39
- const message = toServiceBusMessage(envelope);
80
+ const message = toServiceBusMessage(envelope, options.headers);
40
81
  if (options.delayMs != null && options.delayMs > 0) {
41
82
  message.applicationProperties = { ...message.applicationProperties, "bq-delay": options.delayMs };
42
83
  await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));
@@ -73,6 +114,28 @@ var AsbConsumer = class {
73
114
  }
74
115
  }
75
116
  async handle(message) {
117
+ const rejectedVersion = unsupportedSchemaVersion(message);
118
+ if (rejectedVersion !== void 0) {
119
+ const reported = rejectedVersion.length > MAX_REPORTED_SCHEMA_VERSION_LENGTH ? `${rejectedVersion.slice(0, MAX_REPORTED_SCHEMA_VERSION_LENGTH)}...` : rejectedVersion;
120
+ const description = `Rejected a message from Azure Service Bus with unsupported bq-schema-version "${reported}" (supported: ${SCHEMA_VERSION}).`;
121
+ const deadLetter = this.receiver.deadLetterMessage?.bind(this.receiver);
122
+ this.options.onError?.(
123
+ new BabelQueueError(
124
+ deadLetter ? description : `${description} The receiver has no deadLetterMessage; degraded to abandon.`
125
+ ),
126
+ {},
127
+ message
128
+ );
129
+ if (deadLetter) {
130
+ await deadLetter(message, {
131
+ deadLetterReason: UNSUPPORTED_SCHEMA_VERSION_REASON,
132
+ deadLetterErrorDescription: description
133
+ });
134
+ } else {
135
+ await this.receiver.abandonMessage(message);
136
+ }
137
+ return;
138
+ }
76
139
  const raw = typeof message.body === "string" ? message.body : String(message.body ?? "");
77
140
  const envelope = EnvelopeCodec.decode(raw);
78
141
  const deliveryCount = typeof message.deliveryCount === "number" ? message.deliveryCount : 0;
@@ -103,7 +166,7 @@ var AsbConsumer = class {
103
166
  return;
104
167
  }
105
168
  try {
106
- await handler(envelope, message);
169
+ await handler(envelope, message, headersOf(message));
107
170
  await this.receiver.completeMessage(message);
108
171
  } catch (error) {
109
172
  this.options.onError?.(error, envelope, message);
@@ -114,6 +177,7 @@ var AsbConsumer = class {
114
177
  export {
115
178
  AsbConsumer,
116
179
  AsbPublisher,
180
+ headersOf,
117
181
  toServiceBusMessage
118
182
  };
119
183
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Azure Service Bus adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service\n * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import { ServiceBusClient } from \"@azure/service-bus\";\n * import { AsbPublisher, AsbConsumer } from \"@babelqueue/azure-service-bus\";\n *\n * const client = new ServiceBusClient(connectionString);\n * await new AsbPublisher(client.createSender(\"orders\"))\n * .publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new AsbConsumer(client.createReceiver(\"orders\"), {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §4 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native Service Bus fields (`subject` = URN,\n * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely\n * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is\n * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the\n * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.\n */\n\nimport { BabelQueueError, EnvelopeCodec, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, IncomingEnvelope } from \"@babelqueue/core\";\n\n// --- Minimal Service Bus shapes (a structural subset of @azure/service-bus) -----\n\n/** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */\nexport interface AsbMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n contentType?: string;\n applicationProperties?: { [key: string]: number | boolean | string | Date | null };\n scheduledEnqueueTimeUtc?: Date;\n}\n\n/** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */\nexport interface AsbReceivedMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n deliveryCount?: number;\n applicationProperties?: { [key: string]: unknown };\n}\n\n/**\n * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from\n * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbSender {\n entityPath: string;\n sendMessages(message: AsbMessage): Promise<void>;\n scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;\n}\n\n/**\n * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`\n * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbReceiver {\n receiveMessages(maxMessageCount: number, options?: { maxWaitTimeInMs?: number }): Promise<AsbReceivedMessage[]>;\n completeMessage(message: AsbReceivedMessage): Promise<void>;\n abandonMessage(message: AsbReceivedMessage): Promise<void>;\n}\n\n// --- Native projection (contract §4.2–§4.3) ------------------------------------\n\n/**\n * Project the envelope's contract fields onto a native Service Bus message — `subject`\n * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties. The body stays authoritative.\n */\nexport function toServiceBusMessage(envelope: Envelope): AsbMessage {\n const applicationProperties: { [key: string]: number | boolean | string } = {};\n if (envelope.meta.schema_version != null) {\n applicationProperties[\"bq-schema-version\"] = envelope.meta.schema_version;\n }\n if (envelope.meta.lang) applicationProperties[\"bq-source-lang\"] = envelope.meta.lang;\n if (envelope.meta.created_at != null) {\n applicationProperties[\"bq-created-at\"] = envelope.meta.created_at;\n }\n\n const message: AsbMessage = {\n body: EnvelopeCodec.encode(envelope),\n contentType: \"application/json\",\n };\n if (envelope.job) message.subject = envelope.job;\n if (envelope.trace_id) message.correlationId = envelope.trace_id;\n if (envelope.meta.id) message.messageId = envelope.meta.id;\n if (Object.keys(applicationProperties).length > 0) {\n message.applicationProperties = applicationProperties;\n }\n return message;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link AsbPublisher.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 (`scheduledEnqueueTimeUtc`). */\n delayMs?: number;\n}\n\n/** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */\nexport class AsbPublisher {\n constructor(private readonly sender: AsbSender) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the native projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery.\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: this.sender.entityPath,\n traceId: options.traceId,\n });\n const message = toServiceBusMessage(envelope);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.applicationProperties = { ...message.applicationProperties, \"bq-delay\": options.delayMs };\n await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));\n } else {\n await this.sender.sendMessages(message);\n }\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/** A URN handler. Receives the validated envelope and the raw Service Bus message. */\nexport type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link AsbConsumer}. */\nexport interface AsbConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is completed). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => 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: AsbReceivedMessage) => void;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n /** Max time to wait for a batch, in ms. */\n maxWaitTimeInMs?: number;\n}\n\n/**\n * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes\n * it to the handler for its URN, and completes it on success. A throwing handler abandons\n * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);\n * `attempts` is reconciled to `deliveryCount − 1` for the handler.\n */\nexport class AsbConsumer {\n constructor(\n private readonly receiver: AsbReceiver,\n private readonly handlers: BabelHandlers,\n private readonly options: AsbConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, settle each. Returns the batch size. */\n async poll(): Promise<number> {\n const messages = await this.receiver.receiveMessages(\n this.options.maxMessages ?? 10,\n this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : undefined,\n );\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\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: AsbReceivedMessage): Promise<void> {\n const raw = typeof message.body === \"string\" ? message.body : String(message.body ?? \"\");\n const envelope = EnvelopeCodec.decode(raw);\n\n // attempts = max(current, deliveryCount − 1): deliveryCount (1-based) is the native\n // redelivery floor; the max never lowers a higher body count republished from another SDK.\n const deliveryCount = typeof message.deliveryCount === \"number\" ? message.deliveryCount : 0;\n const native = deliveryCount - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (native > current) {\n envelope.attempts = native;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Azure Service Bus.\"),\n envelope,\n message,\n );\n await this.receiver.abandonMessage(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.receiver.completeMessage(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n try {\n await handler(envelope, message);\n await this.receiver.completeMessage(message);\n } catch (error) {\n // Abandon releases the lock — the broker redelivers and increments deliveryCount.\n this.options.onError?.(error, envelope, message);\n await this.receiver.abandonMessage(message);\n }\n }\n}\n"],"mappings":";AA4BA,SAAS,iBAAiB,eAAe,uBAAuB;AAqDzD,SAAS,oBAAoB,UAAgC;AAClE,QAAM,wBAAsE,CAAC;AAC7E,MAAI,SAAS,KAAK,kBAAkB,MAAM;AACxC,0BAAsB,mBAAmB,IAAI,SAAS,KAAK;AAAA,EAC7D;AACA,MAAI,SAAS,KAAK,KAAM,uBAAsB,gBAAgB,IAAI,SAAS,KAAK;AAChF,MAAI,SAAS,KAAK,cAAc,MAAM;AACpC,0BAAsB,eAAe,IAAI,SAAS,KAAK;AAAA,EACzD;AAEA,QAAM,UAAsB;AAAA,IAC1B,MAAM,cAAc,OAAO,QAAQ;AAAA,IACnC,aAAa;AAAA,EACf;AACA,MAAI,SAAS,IAAK,SAAQ,UAAU,SAAS;AAC7C,MAAI,SAAS,SAAU,SAAQ,gBAAgB,SAAS;AACxD,MAAI,SAAS,KAAK,GAAI,SAAQ,YAAY,SAAS,KAAK;AACxD,MAAI,OAAO,KAAK,qBAAqB,EAAE,SAAS,GAAG;AACjD,YAAQ,wBAAwB;AAAA,EAClC;AACA,SAAO;AACT;AAaO,IAAM,eAAN,MAAmB;AAAA,EACxB,YAA6B,QAAmB;AAAnB;AAAA,EAAoB;AAAA,EAApB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7B,MAAM,QACJ,KACA,MACA,UAA0B,CAAC,GACV;AACjB,UAAM,WAAW,cAAc,KAAK,KAAK,MAAM;AAAA,MAC7C,OAAO,KAAK,OAAO;AAAA,MACnB,SAAS,QAAQ;AAAA,IACnB,CAAC;AACD,UAAM,UAAU,oBAAoB,QAAQ;AAE5C,QAAI,QAAQ,WAAW,QAAQ,QAAQ,UAAU,GAAG;AAClD,cAAQ,wBAAwB,EAAE,GAAG,QAAQ,uBAAuB,YAAY,QAAQ,QAAQ;AAChG,YAAM,KAAK,OAAO,iBAAiB,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,OAAO,CAAC;AAAA,IACpF,OAAO;AACL,YAAM,KAAK,OAAO,aAAa,OAAO;AAAA,IACxC;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;AA4BO,IAAM,cAAN,MAAkB;AAAA,EACvB,YACmB,UACA,UACA,UAA8B,CAAC,GAChD;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAHgB;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAInB,MAAM,OAAwB;AAC5B,UAAM,WAAW,MAAM,KAAK,SAAS;AAAA,MACnC,KAAK,QAAQ,eAAe;AAAA,MAC5B,KAAK,QAAQ,mBAAmB,OAAO,EAAE,iBAAiB,KAAK,QAAQ,gBAAgB,IAAI;AAAA,IAC7F;AACA,eAAW,WAAW,UAAU;AAC9B,YAAM,KAAK,OAAO,OAAO;AAAA,IAC3B;AACA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,MAAM,IAAI,QAAqC;AAC7C,WAAO,QAAQ,YAAY,MAAM;AAC/B,YAAM,KAAK,KAAK;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,MAAc,OAAO,SAA4C;AAC/D,UAAM,MAAM,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,OAAO,QAAQ,QAAQ,EAAE;AACvF,UAAM,WAAW,cAAc,OAAO,GAAG;AAIzC,UAAM,gBAAgB,OAAO,QAAQ,kBAAkB,WAAW,QAAQ,gBAAgB;AAC1F,UAAM,SAAS,gBAAgB;AAC/B,UAAM,UAAU,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AAC5E,QAAI,SAAS,SAAS;AACpB,eAAS,WAAW;AAAA,IACtB;AAEA,QAAI,CAAC,cAAc,QAAQ,QAAQ,GAAG;AACpC,WAAK,QAAQ;AAAA,QACX,IAAI,gBAAgB,uEAAuE;AAAA,QAC3F;AAAA,QACA;AAAA,MACF;AACA,YAAM,KAAK,SAAS,eAAe,OAAO;AAC1C;AAAA,IACF;AAEA,UAAM,MAAM,cAAc,IAAI,QAAQ;AACtC,UAAM,UAAU,KAAK,SAAS,GAAG;AACjC,QAAI,CAAC,SAAS;AACZ,UAAI,KAAK,QAAQ,cAAc;AAC7B,cAAM,KAAK,QAAQ,aAAa,UAAU,OAAO;AACjD,cAAM,KAAK,SAAS,gBAAgB,OAAO;AAAA,MAC7C,OAAO;AACL,aAAK,QAAQ,UAAU,IAAI,gBAAgB,GAAG,GAAG,UAAU,OAAO;AAClE,cAAM,KAAK,SAAS,eAAe,OAAO;AAAA,MAC5C;AACA;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,UAAU,OAAO;AAC/B,YAAM,KAAK,SAAS,gBAAgB,OAAO;AAAA,IAC7C,SAAS,OAAO;AAEd,WAAK,QAAQ,UAAU,OAAO,UAAU,OAAO;AAC/C,YAAM,KAAK,SAAS,eAAe,OAAO;AAAA,IAC5C;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/headers.ts"],"sourcesContent":["/**\n * Azure Service Bus adapter for BabelQueue.\n *\n * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service\n * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN\n * identity, trace propagation) as the .NET, Java, Python and Go SDKs.\n *\n * import { ServiceBusClient } from \"@azure/service-bus\";\n * import { AsbPublisher, AsbConsumer } from \"@babelqueue/azure-service-bus\";\n *\n * const client = new ServiceBusClient(connectionString);\n * await new AsbPublisher(client.createSender(\"orders\"))\n * .publish(\"urn:babel:orders:created\", { order_id: 1042 });\n *\n * const consumer = new AsbConsumer(client.createReceiver(\"orders\"), {\n * \"urn:babel:orders:created\": async (env) => { ... },\n * });\n * await consumer.run();\n *\n * This implements §4 of the broker-bindings contract: the canonical envelope is the\n * message body, projected onto native Service Bus fields (`subject` = URN,\n * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely\n * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is\n * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the\n * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.\n *\n * **Version gate (§4.7).** Before the body is decoded the consumer reads the `bq-schema-version`\n * application property. A missing or blank value (empty or only ASCII whitespace) changes nothing;\n * the string `\"1\"` and the integral number `1` (what the producer writes) decode; any other value\n * or type (`\"2\"`, `\"x\"`, `\"01\"`, `2`, `1.5`, ...) rejects the message undecoded — `onError` (with an\n * empty envelope) and an explicit `deadLetterMessage` (reason `unsupported schema version`), never\n * `abandon`, so the message is not redelivered to the same consumer.\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 `applicationProperties` beside the contract `bq-*`\n * properties — the contract properties win a key collision (merge-not-clobber), blanks dropped. On\n * consume the inbound `applicationProperties` are surfaced as a `Record<string,string>` so the\n * 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 body is never touched.\n */\n\nimport { BabelQueueError, EnvelopeCodec, SCHEMA_VERSION, UnknownUrnError } from \"@babelqueue/core\";\nimport type { Envelope, HeaderCarrier, IncomingEnvelope } from \"@babelqueue/core\";\n\nimport { sanitizeHeaders } from \"./headers.js\";\n\n// --- Minimal Service Bus shapes (a structural subset of @azure/service-bus) -----\n\n/** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */\nexport interface AsbMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n contentType?: string;\n applicationProperties?: { [key: string]: number | boolean | string | Date | null };\n scheduledEnqueueTimeUtc?: Date;\n}\n\n/** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */\nexport interface AsbReceivedMessage {\n body: unknown;\n subject?: string;\n correlationId?: string;\n messageId?: string;\n deliveryCount?: number;\n applicationProperties?: { [key: string]: unknown };\n}\n\n/**\n * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from\n * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbSender {\n entityPath: string;\n sendMessages(message: AsbMessage): Promise<void>;\n scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;\n}\n\n/**\n * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`\n * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.\n */\nexport interface AsbReceiver {\n receiveMessages(maxMessageCount: number, options?: { maxWaitTimeInMs?: number }): Promise<AsbReceivedMessage[]>;\n completeMessage(message: AsbReceivedMessage): Promise<void>;\n abandonMessage(message: AsbReceivedMessage): Promise<void>;\n /**\n * Move a message to the entity's dead-letter sub-queue. A `ServiceBusReceiver` from\n * `@azure/service-bus` provides it. Optional so existing structural implementations keep\n * compiling; a receiver without it cannot dead-letter, so an unsupported-schema-version\n * message degrades to `abandonMessage` (the broker's `MaxDeliveryCount` then dead-letters it).\n */\n deadLetterMessage?(\n message: AsbReceivedMessage,\n options?: { deadLetterReason?: string; deadLetterErrorDescription?: string },\n ): Promise<void>;\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/** Reason recorded on a message dead-lettered by the version gate. */\nconst UNSUPPORTED_SCHEMA_VERSION_REASON = \"unsupported schema version\";\n\n/** Longest rejected value echoed into reports; Service Bus caps the dead-letter description at 4096 chars. */\nconst MAX_REPORTED_SCHEMA_VERSION_LENGTH = 64;\n\n/**\n * The `bq-schema-version` application property rendered for reporting when it is present, non-blank\n * and not the supported version (compared against the core's `SCHEMA_VERSION`). The string must be\n * exactly `\"1\"` (`\"01\"`, `\" 1\"`, `\"1.0\"` are rejected, never trimmed); blank means the empty string\n * or only ASCII whitespace (the same set in every SDK, GR-5), while NBSP, U+0085, U+FEFF, U+3000\n * and the like are not blank. AMQP-typed numbers are accepted when they are the integer 1 (a JS\n * `number`, or the `bigint` an int64 may surface as); every other number or type is rejected.\n * `undefined` when the message may be decoded.\n */\nfunction unsupportedSchemaVersion(message: AsbReceivedMessage): string | undefined {\n const raw = message.applicationProperties?.[\"bq-schema-version\"];\n if (raw == null) return undefined;\n if (typeof raw === \"string\") {\n if (BLANK_ASCII.test(raw)) return undefined;\n return raw === String(SCHEMA_VERSION) ? undefined : raw;\n }\n if (typeof raw === \"number\") return raw === SCHEMA_VERSION ? undefined : String(raw);\n if (typeof raw === \"bigint\") return raw === BigInt(SCHEMA_VERSION) ? undefined : String(raw);\n return `${typeof raw}:${String(raw)}`;\n}\n\n// --- Native projection (contract §4.2–§4.3) ------------------------------------\n\n/**\n * Project the envelope's contract fields onto a native Service Bus message — `subject`\n * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application\n * properties. The body stays authoritative.\n *\n * When `extra` is given, those out-of-band headers (e.g. a W3C `traceparent`, ADR-0028) are merged\n * into `applicationProperties` **first**, then the contract `bq-*` properties are written so they\n * always win a key collision (merge-not-clobber); blank keys/values are dropped. GR-1: the wire\n * envelope body is never touched.\n */\nexport function toServiceBusMessage(envelope: Envelope, extra?: HeaderCarrier | null): AsbMessage {\n const applicationProperties: { [key: string]: number | boolean | string } = {};\n // Out-of-band riders go in first so the contract bq-* properties below win any key collision.\n for (const [key, value] of Object.entries(sanitizeHeaders(extra))) applicationProperties[key] = value;\n if (envelope.meta.schema_version != null) {\n applicationProperties[\"bq-schema-version\"] = envelope.meta.schema_version;\n }\n if (envelope.meta.lang) applicationProperties[\"bq-source-lang\"] = envelope.meta.lang;\n if (envelope.meta.created_at != null) {\n applicationProperties[\"bq-created-at\"] = envelope.meta.created_at;\n }\n\n const message: AsbMessage = {\n body: EnvelopeCodec.encode(envelope),\n contentType: \"application/json\",\n };\n if (envelope.job) message.subject = envelope.job;\n if (envelope.trace_id) message.correlationId = envelope.trace_id;\n if (envelope.meta.id) message.messageId = envelope.meta.id;\n if (Object.keys(applicationProperties).length > 0) {\n message.applicationProperties = applicationProperties;\n }\n return message;\n}\n\n/**\n * Read a received message's `applicationProperties` back into a flat {@link HeaderCarrier},\n * stringifying values defensively. Returns an empty object when there are none. Both the contract\n * `bq-*` properties and any out-of-band rider (e.g. `traceparent`) surface — the core's `otel`\n * extract reads only the keys it knows.\n */\nexport function headersOf(message: AsbReceivedMessage): HeaderCarrier {\n const props = message.applicationProperties;\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) continue;\n const s = String(value);\n if (s !== \"\") out[key] = s;\n }\n return out;\n}\n\n// --- Publisher -----------------------------------------------------------------\n\n/** Options for {@link AsbPublisher.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 (`scheduledEnqueueTimeUtc`). */\n delayMs?: number;\n /**\n * Out-of-band transport headers carried as native `applicationProperties` 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 Service Bus entity with the §4 native projection. */\nexport class AsbPublisher {\n constructor(private readonly sender: AsbSender) {}\n\n /**\n * Build the canonical envelope for `(urn, data)`, send it with the native projection,\n * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed\n * delivery.\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: this.sender.entityPath,\n traceId: options.traceId,\n });\n const message = toServiceBusMessage(envelope, options.headers);\n\n if (options.delayMs != null && options.delayMs > 0) {\n message.applicationProperties = { ...message.applicationProperties, \"bq-delay\": options.delayMs };\n await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));\n } else {\n await this.sender.sendMessages(message);\n }\n return envelope.meta.id;\n }\n}\n\n// --- Consumer ------------------------------------------------------------------\n\n/**\n * A URN handler. Receives the validated envelope, the raw Service Bus message, and the out-of-band\n * {@link HeaderCarrier} read from the message's `applicationProperties` (empty when there are none).\n * Pass `headers` to `@babelqueue/core/otel`'s `wrapHandler` to link the consumer span as a child of\n * the producer span (ADR-0028).\n */\nexport type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage, headers: HeaderCarrier) => unknown | Promise<unknown>;\n\n/** A map of URN → handler. */\nexport type BabelHandlers = Record<string, BabelHandler>;\n\n/** Options for {@link AsbConsumer}. */\nexport interface AsbConsumerOptions {\n /** Called instead of erroring when a message's URN has no handler (then the message is completed). */\n onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => 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: AsbReceivedMessage) => void;\n /** Max messages per receive (default 10). */\n maxMessages?: number;\n /** Max time to wait for a batch, in ms. */\n maxWaitTimeInMs?: number;\n}\n\n/**\n * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes\n * it to the handler for its URN, and completes it on success. A throwing handler abandons\n * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);\n * `attempts` is reconciled to `deliveryCount − 1` for the handler.\n */\nexport class AsbConsumer {\n constructor(\n private readonly receiver: AsbReceiver,\n private readonly handlers: BabelHandlers,\n private readonly options: AsbConsumerOptions = {},\n ) {}\n\n /** Receive one batch, route each message, settle each. Returns the batch size. */\n async poll(): Promise<number> {\n const messages = await this.receiver.receiveMessages(\n this.options.maxMessages ?? 10,\n this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : undefined,\n );\n for (const message of messages) {\n await this.handle(message);\n }\n return messages.length;\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: AsbReceivedMessage): Promise<void> {\n // §4.7: gate on bq-schema-version before the body is decoded. The message is dead-lettered\n // explicitly (never abandoned) so it does not loop back to the same consumer.\n const rejectedVersion = unsupportedSchemaVersion(message);\n if (rejectedVersion !== undefined) {\n const reported =\n rejectedVersion.length > MAX_REPORTED_SCHEMA_VERSION_LENGTH\n ? `${rejectedVersion.slice(0, MAX_REPORTED_SCHEMA_VERSION_LENGTH)}...`\n : rejectedVersion;\n const description = `Rejected a message from Azure Service Bus with unsupported bq-schema-version \"${reported}\" (supported: ${SCHEMA_VERSION}).`;\n const deadLetter = this.receiver.deadLetterMessage?.bind(this.receiver);\n this.options.onError?.(\n new BabelQueueError(\n deadLetter ? description : `${description} The receiver has no deadLetterMessage; degraded to abandon.`,\n ),\n {},\n message,\n );\n if (deadLetter) {\n await deadLetter(message, {\n deadLetterReason: UNSUPPORTED_SCHEMA_VERSION_REASON,\n deadLetterErrorDescription: description,\n });\n } else {\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n const raw = typeof message.body === \"string\" ? message.body : String(message.body ?? \"\");\n const envelope = EnvelopeCodec.decode(raw);\n\n // attempts = max(current, deliveryCount − 1): deliveryCount (1-based) is the native\n // redelivery floor; the max never lowers a higher body count republished from another SDK.\n const deliveryCount = typeof message.deliveryCount === \"number\" ? message.deliveryCount : 0;\n const native = deliveryCount - 1;\n const current = typeof envelope.attempts === \"number\" ? envelope.attempts : 0;\n if (native > current) {\n envelope.attempts = native;\n }\n\n if (!EnvelopeCodec.accepts(envelope)) {\n this.options.onError?.(\n new BabelQueueError(\"Rejected a non-conformant BabelQueue envelope from Azure Service Bus.\"),\n envelope,\n message,\n );\n await this.receiver.abandonMessage(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.receiver.completeMessage(message);\n } else {\n this.options.onError?.(new UnknownUrnError(urn), envelope, message);\n await this.receiver.abandonMessage(message);\n }\n return;\n }\n\n try {\n await handler(envelope, message, headersOf(message));\n await this.receiver.completeMessage(message);\n } catch (error) {\n // Abandon releases the lock — the broker redelivers and increments deliveryCount.\n this.options.onError?.(error, envelope, message);\n await this.receiver.abandonMessage(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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@babelqueue/azure-service-bus",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Azure Service Bus adapter for BabelQueue — canonical-envelope publisher and a URN-routed consumer.",
5
- "keywords": ["babelqueue", "azure", "servicebus", "asb", "queue", "polyglot", "messaging"],
5
+ "keywords": [
6
+ "babelqueue",
7
+ "azure",
8
+ "servicebus",
9
+ "asb",
10
+ "queue",
11
+ "polyglot",
12
+ "messaging"
13
+ ],
6
14
  "homepage": "https://babelqueue.com",
7
15
  "bugs": {
8
16
  "url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
@@ -13,7 +21,7 @@
13
21
  "directory": "packages/azure-service-bus"
14
22
  },
15
23
  "license": "MIT",
16
- "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
24
+ "author": "Muhammet Şafak <info@muhammetsafak.com>",
17
25
  "type": "module",
18
26
  "exports": {
19
27
  ".": {
@@ -30,7 +38,9 @@
30
38
  "main": "./dist/index.cjs",
31
39
  "module": "./dist/index.js",
32
40
  "types": "./dist/index.d.ts",
33
- "files": ["dist"],
41
+ "files": [
42
+ "dist"
43
+ ],
34
44
  "sideEffects": false,
35
45
  "engines": {
36
46
  "node": ">=18"
@@ -41,7 +51,7 @@
41
51
  "test": "node --import tsx --test test/*.test.ts"
42
52
  },
43
53
  "dependencies": {
44
- "@babelqueue/core": "^1.0.0"
54
+ "@babelqueue/core": "^1.4.0"
45
55
  },
46
56
  "peerDependencies": {
47
57
  "@azure/service-bus": "^7.0.0"