@babelqueue/azure-service-bus 1.0.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 ADDED
@@ -0,0 +1,71 @@
1
+ # @babelqueue/azure-service-bus
2
+
3
+ Azure Service Bus adapter for [BabelQueue](https://babelqueue.com) — a canonical-envelope
4
+ **publisher** and a URN-routed **consumer** over `@azure/service-bus`, on
5
+ `@babelqueue/core`. Implements
6
+ [§4 of the broker-bindings contract](https://babelqueue.com/docs/spec/1.x/broker-bindings#azure-service-bus),
7
+ so an ASB-based Node service speaks the same wire contract as the .NET, Java, Python and Go
8
+ SDKs.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm i @babelqueue/azure-service-bus @azure/service-bus
14
+ ```
15
+
16
+ `@azure/service-bus` is an optional peer — **you provide the sender/receiver** (a
17
+ `ServiceBusSender` / `ServiceBusReceiver` satisfies the adapter structurally).
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import { ServiceBusClient } from "@azure/service-bus";
23
+ import { AsbPublisher, AsbConsumer } from "@babelqueue/azure-service-bus";
24
+
25
+ const client = new ServiceBusClient(connectionString); // or (namespace, credential)
26
+
27
+ // produce
28
+ const id = await new AsbPublisher(client.createSender("orders"))
29
+ .publish("urn:babel:orders:created", { order_id: 1042 });
30
+
31
+ // consume (PeekLock)
32
+ const consumer = new AsbConsumer(
33
+ client.createReceiver("orders"),
34
+ {
35
+ "urn:babel:orders:created": async (env, message) => {
36
+ console.log(env.data.order_id, env.trace_id, env.attempts);
37
+ },
38
+ },
39
+ { onError: (err) => console.error(err) },
40
+ );
41
+ await consumer.run();
42
+ ```
43
+
44
+ Delayed delivery: `publish(urn, data, { delayMs: 300000 })` → native
45
+ `scheduledEnqueueTimeUtc`.
46
+
47
+ ## Contract mapping (§4)
48
+
49
+ | Envelope | Azure Service Bus |
50
+ | :--- | :--- |
51
+ | body | `body` (byte-identical across SDKs) |
52
+ | `job` (URN) | `subject` |
53
+ | `trace_id` | `correlationId` |
54
+ | `meta.id` | `messageId` |
55
+ | `meta.schema_version` | `applicationProperties["bq-schema-version"]` |
56
+ | `meta.lang` | `applicationProperties["bq-source-lang"]` |
57
+ | `meta.created_at` | `applicationProperties["bq-created-at"]` (ms) |
58
+ | `attempts` | `deliveryCount − 1` (broker-authoritative) |
59
+ | reserve / ack / retry | PeekLock → `completeMessage` / `abandonMessage` |
60
+
61
+ A throwing handler `abandonMessage`s, so the broker redelivers and increments
62
+ `deliveryCount` (at-least-once). The poll loop never stops on a bad message — observe via
63
+ `onError` / `onUnknownUrn`. The envelope is unchanged (`schema_version` stays `1`); Azure
64
+ Service Bus is purely additive.
65
+
66
+ The sender/receiver are injected, so the unit tests use fakes — no Azure, no broker. Dual
67
+ ESM + CJS.
68
+
69
+ ## License
70
+
71
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AsbConsumer: () => AsbConsumer,
24
+ AsbPublisher: () => AsbPublisher,
25
+ toServiceBusMessage: () => toServiceBusMessage
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_core = require("@babelqueue/core");
29
+ function toServiceBusMessage(envelope) {
30
+ const applicationProperties = {};
31
+ if (envelope.meta.schema_version != null) {
32
+ applicationProperties["bq-schema-version"] = envelope.meta.schema_version;
33
+ }
34
+ if (envelope.meta.lang) applicationProperties["bq-source-lang"] = envelope.meta.lang;
35
+ if (envelope.meta.created_at != null) {
36
+ applicationProperties["bq-created-at"] = envelope.meta.created_at;
37
+ }
38
+ const message = {
39
+ body: import_core.EnvelopeCodec.encode(envelope),
40
+ contentType: "application/json"
41
+ };
42
+ if (envelope.job) message.subject = envelope.job;
43
+ if (envelope.trace_id) message.correlationId = envelope.trace_id;
44
+ if (envelope.meta.id) message.messageId = envelope.meta.id;
45
+ if (Object.keys(applicationProperties).length > 0) {
46
+ message.applicationProperties = applicationProperties;
47
+ }
48
+ return message;
49
+ }
50
+ var AsbPublisher = class {
51
+ constructor(sender) {
52
+ this.sender = sender;
53
+ }
54
+ sender;
55
+ /**
56
+ * Build the canonical envelope for `(urn, data)`, send it with the native projection,
57
+ * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed
58
+ * delivery.
59
+ */
60
+ async publish(urn, data, options = {}) {
61
+ const envelope = import_core.EnvelopeCodec.make(urn, data, {
62
+ queue: this.sender.entityPath,
63
+ traceId: options.traceId
64
+ });
65
+ const message = toServiceBusMessage(envelope);
66
+ if (options.delayMs != null && options.delayMs > 0) {
67
+ message.applicationProperties = { ...message.applicationProperties, "bq-delay": options.delayMs };
68
+ await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));
69
+ } else {
70
+ await this.sender.sendMessages(message);
71
+ }
72
+ return envelope.meta.id;
73
+ }
74
+ };
75
+ var AsbConsumer = class {
76
+ constructor(receiver, handlers, options = {}) {
77
+ this.receiver = receiver;
78
+ this.handlers = handlers;
79
+ this.options = options;
80
+ }
81
+ receiver;
82
+ handlers;
83
+ options;
84
+ /** Receive one batch, route each message, settle each. Returns the batch size. */
85
+ async poll() {
86
+ const messages = await this.receiver.receiveMessages(
87
+ this.options.maxMessages ?? 10,
88
+ this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : void 0
89
+ );
90
+ for (const message of messages) {
91
+ await this.handle(message);
92
+ }
93
+ return messages.length;
94
+ }
95
+ /** Poll until `signal` aborts. */
96
+ async run(signal) {
97
+ while (signal?.aborted !== true) {
98
+ await this.poll();
99
+ }
100
+ }
101
+ async handle(message) {
102
+ const raw = typeof message.body === "string" ? message.body : String(message.body ?? "");
103
+ const envelope = import_core.EnvelopeCodec.decode(raw);
104
+ const deliveryCount = typeof message.deliveryCount === "number" ? message.deliveryCount : 0;
105
+ const native = deliveryCount - 1;
106
+ const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
107
+ if (native > current) {
108
+ envelope.attempts = native;
109
+ }
110
+ if (!import_core.EnvelopeCodec.accepts(envelope)) {
111
+ this.options.onError?.(
112
+ new import_core.BabelQueueError("Rejected a non-conformant BabelQueue envelope from Azure Service Bus."),
113
+ envelope,
114
+ message
115
+ );
116
+ await this.receiver.abandonMessage(message);
117
+ return;
118
+ }
119
+ const urn = import_core.EnvelopeCodec.urn(envelope);
120
+ const handler = this.handlers[urn];
121
+ if (!handler) {
122
+ if (this.options.onUnknownUrn) {
123
+ await this.options.onUnknownUrn(envelope, message);
124
+ await this.receiver.completeMessage(message);
125
+ } else {
126
+ this.options.onError?.(new import_core.UnknownUrnError(urn), envelope, message);
127
+ await this.receiver.abandonMessage(message);
128
+ }
129
+ return;
130
+ }
131
+ try {
132
+ await handler(envelope, message);
133
+ await this.receiver.completeMessage(message);
134
+ } catch (error) {
135
+ this.options.onError?.(error, envelope, message);
136
+ await this.receiver.abandonMessage(message);
137
+ }
138
+ }
139
+ };
140
+ // Annotate the CommonJS export names for ESM import in node:
141
+ 0 && (module.exports = {
142
+ AsbConsumer,
143
+ AsbPublisher,
144
+ toServiceBusMessage
145
+ });
146
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,131 @@
1
+ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
2
+
3
+ /**
4
+ * Azure Service Bus adapter for BabelQueue.
5
+ *
6
+ * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service
7
+ * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN
8
+ * identity, trace propagation) as the .NET, Java, Python and Go SDKs.
9
+ *
10
+ * import { ServiceBusClient } from "@azure/service-bus";
11
+ * import { AsbPublisher, AsbConsumer } from "@babelqueue/azure-service-bus";
12
+ *
13
+ * const client = new ServiceBusClient(connectionString);
14
+ * await new AsbPublisher(client.createSender("orders"))
15
+ * .publish("urn:babel:orders:created", { order_id: 1042 });
16
+ *
17
+ * const consumer = new AsbConsumer(client.createReceiver("orders"), {
18
+ * "urn:babel:orders:created": async (env) => { ... },
19
+ * });
20
+ * await consumer.run();
21
+ *
22
+ * This implements §4 of the broker-bindings contract: the canonical envelope is the
23
+ * message body, projected onto native Service Bus fields (`subject` = URN,
24
+ * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
25
+ * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely
26
+ * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is
27
+ * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the
28
+ * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.
29
+ */
30
+
31
+ /** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */
32
+ interface AsbMessage {
33
+ body: unknown;
34
+ subject?: string;
35
+ correlationId?: string;
36
+ messageId?: string;
37
+ contentType?: string;
38
+ applicationProperties?: {
39
+ [key: string]: number | boolean | string | Date | null;
40
+ };
41
+ scheduledEnqueueTimeUtc?: Date;
42
+ }
43
+ /** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */
44
+ interface AsbReceivedMessage {
45
+ body: unknown;
46
+ subject?: string;
47
+ correlationId?: string;
48
+ messageId?: string;
49
+ deliveryCount?: number;
50
+ applicationProperties?: {
51
+ [key: string]: unknown;
52
+ };
53
+ }
54
+ /**
55
+ * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from
56
+ * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.
57
+ */
58
+ interface AsbSender {
59
+ entityPath: string;
60
+ sendMessages(message: AsbMessage): Promise<void>;
61
+ scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;
62
+ }
63
+ /**
64
+ * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`
65
+ * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.
66
+ */
67
+ interface AsbReceiver {
68
+ receiveMessages(maxMessageCount: number, options?: {
69
+ maxWaitTimeInMs?: number;
70
+ }): Promise<AsbReceivedMessage[]>;
71
+ completeMessage(message: AsbReceivedMessage): Promise<void>;
72
+ abandonMessage(message: AsbReceivedMessage): Promise<void>;
73
+ }
74
+ /**
75
+ * Project the envelope's contract fields onto a native Service Bus message — `subject`
76
+ * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
77
+ * properties. The body stays authoritative.
78
+ */
79
+ declare function toServiceBusMessage(envelope: Envelope): AsbMessage;
80
+ /** Options for {@link AsbPublisher.publish}. */
81
+ interface PublishOptions {
82
+ /** Reuse an existing trace id (trace continuation). */
83
+ traceId?: string;
84
+ /** Schedule native delayed delivery this many milliseconds from now (`scheduledEnqueueTimeUtc`). */
85
+ delayMs?: number;
86
+ }
87
+ /** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */
88
+ declare class AsbPublisher {
89
+ private readonly sender;
90
+ constructor(sender: AsbSender);
91
+ /**
92
+ * Build the canonical envelope for `(urn, data)`, send it with the native projection,
93
+ * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed
94
+ * delivery.
95
+ */
96
+ publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
+ }
98
+ /** A URN handler. Receives the validated envelope and the raw Service Bus message. */
99
+ type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
100
+ /** A map of URN → handler. */
101
+ type BabelHandlers = Record<string, BabelHandler>;
102
+ /** Options for {@link AsbConsumer}. */
103
+ interface AsbConsumerOptions {
104
+ /** Called instead of erroring when a message's URN has no handler (then the message is completed). */
105
+ onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
106
+ /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */
107
+ onError?: (error: unknown, envelope: IncomingEnvelope, message: AsbReceivedMessage) => void;
108
+ /** Max messages per receive (default 10). */
109
+ maxMessages?: number;
110
+ /** Max time to wait for a batch, in ms. */
111
+ maxWaitTimeInMs?: number;
112
+ }
113
+ /**
114
+ * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes
115
+ * it to the handler for its URN, and completes it on success. A throwing handler abandons
116
+ * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);
117
+ * `attempts` is reconciled to `deliveryCount − 1` for the handler.
118
+ */
119
+ declare class AsbConsumer {
120
+ private readonly receiver;
121
+ private readonly handlers;
122
+ private readonly options;
123
+ constructor(receiver: AsbReceiver, handlers: BabelHandlers, options?: AsbConsumerOptions);
124
+ /** Receive one batch, route each message, settle each. Returns the batch size. */
125
+ poll(): Promise<number>;
126
+ /** Poll until `signal` aborts. */
127
+ run(signal?: AbortSignal): Promise<void>;
128
+ private handle;
129
+ }
130
+
131
+ export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, toServiceBusMessage };
@@ -0,0 +1,131 @@
1
+ import { Envelope, IncomingEnvelope } from '@babelqueue/core';
2
+
3
+ /**
4
+ * Azure Service Bus adapter for BabelQueue.
5
+ *
6
+ * A canonical-envelope **publisher** and a URN-routed **consumer** over Azure Service
7
+ * Bus, so an ASB-based Node service speaks the same contract (envelope shape, URN
8
+ * identity, trace propagation) as the .NET, Java, Python and Go SDKs.
9
+ *
10
+ * import { ServiceBusClient } from "@azure/service-bus";
11
+ * import { AsbPublisher, AsbConsumer } from "@babelqueue/azure-service-bus";
12
+ *
13
+ * const client = new ServiceBusClient(connectionString);
14
+ * await new AsbPublisher(client.createSender("orders"))
15
+ * .publish("urn:babel:orders:created", { order_id: 1042 });
16
+ *
17
+ * const consumer = new AsbConsumer(client.createReceiver("orders"), {
18
+ * "urn:babel:orders:created": async (env) => { ... },
19
+ * });
20
+ * await consumer.run();
21
+ *
22
+ * This implements §4 of the broker-bindings contract: the canonical envelope is the
23
+ * message body, projected onto native Service Bus fields (`subject` = URN,
24
+ * `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
25
+ * properties). The envelope is unchanged (`schema_version` stays 1); ASB is purely
26
+ * additive. Retry is broker-native — a failed handler `abandon`s the message, so it is
27
+ * redelivered and `deliveryCount` is incremented; the authoritative attempt count is the
28
+ * native `deliveryCount`, surfaced to handlers as `attempts = deliveryCount − 1`.
29
+ */
30
+
31
+ /** A message to send (structural subset of @azure/service-bus `ServiceBusMessage`). */
32
+ interface AsbMessage {
33
+ body: unknown;
34
+ subject?: string;
35
+ correlationId?: string;
36
+ messageId?: string;
37
+ contentType?: string;
38
+ applicationProperties?: {
39
+ [key: string]: number | boolean | string | Date | null;
40
+ };
41
+ scheduledEnqueueTimeUtc?: Date;
42
+ }
43
+ /** A received message (structural subset of @azure/service-bus `ServiceBusReceivedMessage`). */
44
+ interface AsbReceivedMessage {
45
+ body: unknown;
46
+ subject?: string;
47
+ correlationId?: string;
48
+ messageId?: string;
49
+ deliveryCount?: number;
50
+ applicationProperties?: {
51
+ [key: string]: unknown;
52
+ };
53
+ }
54
+ /**
55
+ * The subset of the Service Bus sender this adapter calls. A `ServiceBusSender` from
56
+ * `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.
57
+ */
58
+ interface AsbSender {
59
+ entityPath: string;
60
+ sendMessages(message: AsbMessage): Promise<void>;
61
+ scheduleMessages(message: AsbMessage, scheduledEnqueueTimeUtc: Date): Promise<bigint[]>;
62
+ }
63
+ /**
64
+ * The subset of the Service Bus receiver this adapter calls. A `ServiceBusReceiver`
65
+ * from `@azure/service-bus` satisfies it structurally; a fake satisfies it in tests.
66
+ */
67
+ interface AsbReceiver {
68
+ receiveMessages(maxMessageCount: number, options?: {
69
+ maxWaitTimeInMs?: number;
70
+ }): Promise<AsbReceivedMessage[]>;
71
+ completeMessage(message: AsbReceivedMessage): Promise<void>;
72
+ abandonMessage(message: AsbReceivedMessage): Promise<void>;
73
+ }
74
+ /**
75
+ * Project the envelope's contract fields onto a native Service Bus message — `subject`
76
+ * = URN, `correlationId` = trace_id, `messageId` = meta.id, plus the `bq-` application
77
+ * properties. The body stays authoritative.
78
+ */
79
+ declare function toServiceBusMessage(envelope: Envelope): AsbMessage;
80
+ /** Options for {@link AsbPublisher.publish}. */
81
+ interface PublishOptions {
82
+ /** Reuse an existing trace id (trace continuation). */
83
+ traceId?: string;
84
+ /** Schedule native delayed delivery this many milliseconds from now (`scheduledEnqueueTimeUtc`). */
85
+ delayMs?: number;
86
+ }
87
+ /** Sends canonical-envelope messages to one Service Bus entity with the §4 native projection. */
88
+ declare class AsbPublisher {
89
+ private readonly sender;
90
+ constructor(sender: AsbSender);
91
+ /**
92
+ * Build the canonical envelope for `(urn, data)`, send it with the native projection,
93
+ * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed
94
+ * delivery.
95
+ */
96
+ publish(urn: string, data: Record<string, unknown>, options?: PublishOptions): Promise<string>;
97
+ }
98
+ /** A URN handler. Receives the validated envelope and the raw Service Bus message. */
99
+ type BabelHandler = (envelope: Envelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
100
+ /** A map of URN → handler. */
101
+ type BabelHandlers = Record<string, BabelHandler>;
102
+ /** Options for {@link AsbConsumer}. */
103
+ interface AsbConsumerOptions {
104
+ /** Called instead of erroring when a message's URN has no handler (then the message is completed). */
105
+ onUnknownUrn?: (envelope: IncomingEnvelope, message: AsbReceivedMessage) => unknown | Promise<unknown>;
106
+ /** Called for a non-conformant envelope, an unmapped URN (no `onUnknownUrn`), or a throwing handler. The loop never stops. */
107
+ onError?: (error: unknown, envelope: IncomingEnvelope, message: AsbReceivedMessage) => void;
108
+ /** Max messages per receive (default 10). */
109
+ maxMessages?: number;
110
+ /** Max time to wait for a batch, in ms. */
111
+ maxWaitTimeInMs?: number;
112
+ }
113
+ /**
114
+ * Receives from a Service Bus entity (PeekLock), decodes + validates each message, routes
115
+ * it to the handler for its URN, and completes it on success. A throwing handler abandons
116
+ * the message — the broker redelivers it and increments `deliveryCount` (at-least-once);
117
+ * `attempts` is reconciled to `deliveryCount − 1` for the handler.
118
+ */
119
+ declare class AsbConsumer {
120
+ private readonly receiver;
121
+ private readonly handlers;
122
+ private readonly options;
123
+ constructor(receiver: AsbReceiver, handlers: BabelHandlers, options?: AsbConsumerOptions);
124
+ /** Receive one batch, route each message, settle each. Returns the batch size. */
125
+ poll(): Promise<number>;
126
+ /** Poll until `signal` aborts. */
127
+ run(signal?: AbortSignal): Promise<void>;
128
+ private handle;
129
+ }
130
+
131
+ export { AsbConsumer, type AsbConsumerOptions, type AsbMessage, AsbPublisher, type AsbReceivedMessage, type AsbReceiver, type AsbSender, type BabelHandler, type BabelHandlers, type PublishOptions, toServiceBusMessage };
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ // src/index.ts
2
+ import { BabelQueueError, EnvelopeCodec, UnknownUrnError } from "@babelqueue/core";
3
+ function toServiceBusMessage(envelope) {
4
+ const applicationProperties = {};
5
+ if (envelope.meta.schema_version != null) {
6
+ applicationProperties["bq-schema-version"] = envelope.meta.schema_version;
7
+ }
8
+ if (envelope.meta.lang) applicationProperties["bq-source-lang"] = envelope.meta.lang;
9
+ if (envelope.meta.created_at != null) {
10
+ applicationProperties["bq-created-at"] = envelope.meta.created_at;
11
+ }
12
+ const message = {
13
+ body: EnvelopeCodec.encode(envelope),
14
+ contentType: "application/json"
15
+ };
16
+ if (envelope.job) message.subject = envelope.job;
17
+ if (envelope.trace_id) message.correlationId = envelope.trace_id;
18
+ if (envelope.meta.id) message.messageId = envelope.meta.id;
19
+ if (Object.keys(applicationProperties).length > 0) {
20
+ message.applicationProperties = applicationProperties;
21
+ }
22
+ return message;
23
+ }
24
+ var AsbPublisher = class {
25
+ constructor(sender) {
26
+ this.sender = sender;
27
+ }
28
+ sender;
29
+ /**
30
+ * Build the canonical envelope for `(urn, data)`, send it with the native projection,
31
+ * and return the message id (`meta.id`). A positive `delayMs` schedules native delayed
32
+ * delivery.
33
+ */
34
+ async publish(urn, data, options = {}) {
35
+ const envelope = EnvelopeCodec.make(urn, data, {
36
+ queue: this.sender.entityPath,
37
+ traceId: options.traceId
38
+ });
39
+ const message = toServiceBusMessage(envelope);
40
+ if (options.delayMs != null && options.delayMs > 0) {
41
+ message.applicationProperties = { ...message.applicationProperties, "bq-delay": options.delayMs };
42
+ await this.sender.scheduleMessages(message, new Date(Date.now() + options.delayMs));
43
+ } else {
44
+ await this.sender.sendMessages(message);
45
+ }
46
+ return envelope.meta.id;
47
+ }
48
+ };
49
+ var AsbConsumer = class {
50
+ constructor(receiver, handlers, options = {}) {
51
+ this.receiver = receiver;
52
+ this.handlers = handlers;
53
+ this.options = options;
54
+ }
55
+ receiver;
56
+ handlers;
57
+ options;
58
+ /** Receive one batch, route each message, settle each. Returns the batch size. */
59
+ async poll() {
60
+ const messages = await this.receiver.receiveMessages(
61
+ this.options.maxMessages ?? 10,
62
+ this.options.maxWaitTimeInMs != null ? { maxWaitTimeInMs: this.options.maxWaitTimeInMs } : void 0
63
+ );
64
+ for (const message of messages) {
65
+ await this.handle(message);
66
+ }
67
+ return messages.length;
68
+ }
69
+ /** Poll until `signal` aborts. */
70
+ async run(signal) {
71
+ while (signal?.aborted !== true) {
72
+ await this.poll();
73
+ }
74
+ }
75
+ async handle(message) {
76
+ const raw = typeof message.body === "string" ? message.body : String(message.body ?? "");
77
+ const envelope = EnvelopeCodec.decode(raw);
78
+ const deliveryCount = typeof message.deliveryCount === "number" ? message.deliveryCount : 0;
79
+ const native = deliveryCount - 1;
80
+ const current = typeof envelope.attempts === "number" ? envelope.attempts : 0;
81
+ if (native > current) {
82
+ envelope.attempts = native;
83
+ }
84
+ if (!EnvelopeCodec.accepts(envelope)) {
85
+ this.options.onError?.(
86
+ new BabelQueueError("Rejected a non-conformant BabelQueue envelope from Azure Service Bus."),
87
+ envelope,
88
+ message
89
+ );
90
+ await this.receiver.abandonMessage(message);
91
+ return;
92
+ }
93
+ const urn = EnvelopeCodec.urn(envelope);
94
+ const handler = this.handlers[urn];
95
+ if (!handler) {
96
+ if (this.options.onUnknownUrn) {
97
+ await this.options.onUnknownUrn(envelope, message);
98
+ await this.receiver.completeMessage(message);
99
+ } else {
100
+ this.options.onError?.(new UnknownUrnError(urn), envelope, message);
101
+ await this.receiver.abandonMessage(message);
102
+ }
103
+ return;
104
+ }
105
+ try {
106
+ await handler(envelope, message);
107
+ await this.receiver.completeMessage(message);
108
+ } catch (error) {
109
+ this.options.onError?.(error, envelope, message);
110
+ await this.receiver.abandonMessage(message);
111
+ }
112
+ }
113
+ };
114
+ export {
115
+ AsbConsumer,
116
+ AsbPublisher,
117
+ toServiceBusMessage
118
+ };
119
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@babelqueue/azure-service-bus",
3
+ "version": "1.0.0",
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"],
6
+ "homepage": "https://babelqueue.com",
7
+ "bugs": {
8
+ "url": "https://github.com/BabelQueue/babelqueue-node-adapters/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/BabelQueue/babelqueue-node-adapters.git",
13
+ "directory": "packages/azure-service-bus"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Muhammet Şafak <info@muhammetsafak.com.tr>",
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": ["dist"],
34
+ "sideEffects": false,
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "node --import tsx --test test/*.test.ts"
42
+ },
43
+ "dependencies": {
44
+ "@babelqueue/core": "^1.0.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@azure/service-bus": "^7.0.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@azure/service-bus": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }