@saga-bus/transport-azure-servicebus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +194 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dean Foran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @saga-bus/transport-azure-servicebus
|
|
2
|
+
|
|
3
|
+
Azure Service Bus transport for saga-bus. Provides integration with Azure Service Bus topics and subscriptions with support for sessions, scheduled messages, and dead-letter queues.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @saga-bus/transport-azure-servicebus @azure/service-bus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { AzureServiceBusTransport } from "@saga-bus/transport-azure-servicebus";
|
|
17
|
+
import { createBus } from "@saga-bus/core";
|
|
18
|
+
|
|
19
|
+
const transport = new AzureServiceBusTransport({
|
|
20
|
+
connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
|
|
21
|
+
subscriptionName: "my-worker",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const bus = createBus({
|
|
25
|
+
transport,
|
|
26
|
+
store,
|
|
27
|
+
sagas: [OrderSaga],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await bus.start();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Azure AD Authentication
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
37
|
+
|
|
38
|
+
const transport = new AzureServiceBusTransport({
|
|
39
|
+
fullyQualifiedNamespace: "mybus.servicebus.windows.net",
|
|
40
|
+
credential: new DefaultAzureCredential(),
|
|
41
|
+
subscriptionName: "my-worker",
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Session-Based Ordering
|
|
46
|
+
|
|
47
|
+
Enable sessions for guaranteed message ordering per correlation ID:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const transport = new AzureServiceBusTransport({
|
|
51
|
+
connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
|
|
52
|
+
subscriptionName: "my-worker",
|
|
53
|
+
sessionEnabled: true, // Uses correlation ID as session ID
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Topic/Subscription Model** - Publish to topics, subscribe via subscriptions
|
|
60
|
+
- **Session Support** - Ordered message processing using sessions
|
|
61
|
+
- **Scheduled Messages** - Native delayed delivery via `scheduledEnqueueTimeUtc`
|
|
62
|
+
- **Dead-Letter Queue** - Automatic DLQ for invalid messages
|
|
63
|
+
- **Auto-Lock Renewal** - Prevents message loss during long processing
|
|
64
|
+
- **Azure AD Authentication** - Support for managed identities
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
|--------|------|---------|-------------|
|
|
70
|
+
| `connectionString` | `string` | - | Connection string for Service Bus |
|
|
71
|
+
| `fullyQualifiedNamespace` | `string` | - | Namespace for Azure AD auth |
|
|
72
|
+
| `credential` | `TokenCredential` | - | Azure AD credential |
|
|
73
|
+
| `subscriptionName` | `string` | - | Subscription name for receiving |
|
|
74
|
+
| `defaultTopic` | `string` | message type | Default topic for publishing |
|
|
75
|
+
| `sessionEnabled` | `boolean` | `false` | Enable session-based ordering |
|
|
76
|
+
| `maxConcurrentCalls` | `number` | `1` | Concurrent message handlers |
|
|
77
|
+
| `maxAutoLockRenewalDurationInMs` | `number` | `300000` | Lock renewal duration |
|
|
78
|
+
| `autoCompleteMessages` | `boolean` | `false` | Auto-complete on success |
|
|
79
|
+
| `receiveMode` | `"peekLock" \| "receiveAndDelete"` | `"peekLock"` | Message receive mode |
|
|
80
|
+
| `entityPrefix` | `string` | `""` | Prefix for topic/queue names |
|
|
81
|
+
|
|
82
|
+
## Delayed Messages
|
|
83
|
+
|
|
84
|
+
The transport uses Azure Service Bus native scheduled messages:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// In a saga handler
|
|
88
|
+
await ctx.schedule(
|
|
89
|
+
{ type: "PaymentTimeout", orderId },
|
|
90
|
+
60000 // 1 minute delay
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Error Handling
|
|
95
|
+
|
|
96
|
+
- **Invalid messages** are sent to the dead-letter queue
|
|
97
|
+
- **Handler errors** cause message abandonment for retry
|
|
98
|
+
- **Connection errors** are logged and the receiver continues
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
AzureServiceBusTransport: () => AzureServiceBusTransport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/AzureServiceBusTransport.ts
|
|
28
|
+
var import_service_bus = require("@azure/service-bus");
|
|
29
|
+
var import_crypto = require("crypto");
|
|
30
|
+
var AzureServiceBusTransport = class {
|
|
31
|
+
client = null;
|
|
32
|
+
options;
|
|
33
|
+
senders = /* @__PURE__ */ new Map();
|
|
34
|
+
receivers = [];
|
|
35
|
+
subscriptions = [];
|
|
36
|
+
started = false;
|
|
37
|
+
stopping = false;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
if (!options.connectionString && !options.fullyQualifiedNamespace) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Either connectionString or fullyQualifiedNamespace must be provided"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (options.fullyQualifiedNamespace && !options.credential) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"credential is required when using fullyQualifiedNamespace"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
this.options = {
|
|
50
|
+
maxConcurrentCalls: 1,
|
|
51
|
+
maxAutoLockRenewalDurationInMs: 3e5,
|
|
52
|
+
autoCompleteMessages: false,
|
|
53
|
+
receiveMode: "peekLock",
|
|
54
|
+
entityPrefix: "",
|
|
55
|
+
sessionEnabled: false,
|
|
56
|
+
...options
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async start() {
|
|
60
|
+
if (this.started) return;
|
|
61
|
+
if (this.options.connectionString) {
|
|
62
|
+
this.client = new import_service_bus.ServiceBusClient(this.options.connectionString);
|
|
63
|
+
} else if (this.options.fullyQualifiedNamespace && this.options.credential) {
|
|
64
|
+
this.client = new import_service_bus.ServiceBusClient(
|
|
65
|
+
this.options.fullyQualifiedNamespace,
|
|
66
|
+
this.options.credential
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error("Invalid configuration");
|
|
70
|
+
}
|
|
71
|
+
for (const sub of this.subscriptions) {
|
|
72
|
+
await this.startReceiver(sub);
|
|
73
|
+
}
|
|
74
|
+
this.started = true;
|
|
75
|
+
}
|
|
76
|
+
async stop() {
|
|
77
|
+
if (!this.started || this.stopping) return;
|
|
78
|
+
this.stopping = true;
|
|
79
|
+
for (const receiver of this.receivers) {
|
|
80
|
+
try {
|
|
81
|
+
await receiver.close();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("[AzureServiceBus] Error closing receiver:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.receivers.length = 0;
|
|
87
|
+
for (const sender of this.senders.values()) {
|
|
88
|
+
try {
|
|
89
|
+
await sender.close();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("[AzureServiceBus] Error closing sender:", error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.senders.clear();
|
|
95
|
+
if (this.client) {
|
|
96
|
+
await this.client.close();
|
|
97
|
+
this.client = null;
|
|
98
|
+
}
|
|
99
|
+
this.started = false;
|
|
100
|
+
this.stopping = false;
|
|
101
|
+
}
|
|
102
|
+
async subscribe(options, handler) {
|
|
103
|
+
const { endpoint, concurrency = 1, group } = options;
|
|
104
|
+
const topicName = `${this.options.entityPrefix}${endpoint}`;
|
|
105
|
+
const subscriptionName = group ?? this.options.subscriptionName ?? "default";
|
|
106
|
+
const registration = {
|
|
107
|
+
endpoint,
|
|
108
|
+
topicName,
|
|
109
|
+
subscriptionName,
|
|
110
|
+
handler,
|
|
111
|
+
concurrency
|
|
112
|
+
};
|
|
113
|
+
this.subscriptions.push(registration);
|
|
114
|
+
if (this.started && this.client) {
|
|
115
|
+
await this.startReceiver(registration);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async publish(message, options) {
|
|
119
|
+
if (!this.client) {
|
|
120
|
+
throw new Error("Transport not started");
|
|
121
|
+
}
|
|
122
|
+
const { endpoint, key, headers = {}, delayMs } = options;
|
|
123
|
+
const topicName = `${this.options.entityPrefix}${endpoint ?? this.options.defaultTopic ?? message.type}`;
|
|
124
|
+
let sender = this.senders.get(topicName);
|
|
125
|
+
if (!sender) {
|
|
126
|
+
sender = this.client.createSender(topicName);
|
|
127
|
+
this.senders.set(topicName, sender);
|
|
128
|
+
}
|
|
129
|
+
const envelope = {
|
|
130
|
+
id: (0, import_crypto.randomUUID)(),
|
|
131
|
+
type: message.type,
|
|
132
|
+
payload: message,
|
|
133
|
+
headers,
|
|
134
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
135
|
+
partitionKey: key
|
|
136
|
+
};
|
|
137
|
+
const sbMessage = {
|
|
138
|
+
body: envelope,
|
|
139
|
+
messageId: envelope.id,
|
|
140
|
+
contentType: "application/json",
|
|
141
|
+
applicationProperties: {
|
|
142
|
+
messageType: message.type,
|
|
143
|
+
...headers
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (this.options.sessionEnabled && key) {
|
|
147
|
+
sbMessage.sessionId = key;
|
|
148
|
+
} else if (key) {
|
|
149
|
+
sbMessage.partitionKey = key;
|
|
150
|
+
}
|
|
151
|
+
if (delayMs && delayMs > 0) {
|
|
152
|
+
sbMessage.scheduledEnqueueTimeUtc = new Date(Date.now() + delayMs);
|
|
153
|
+
}
|
|
154
|
+
await sender.sendMessages(sbMessage);
|
|
155
|
+
}
|
|
156
|
+
async startReceiver(registration) {
|
|
157
|
+
if (!this.client) return;
|
|
158
|
+
const { topicName, subscriptionName, handler, concurrency } = registration;
|
|
159
|
+
let receiver;
|
|
160
|
+
if (this.options.sessionEnabled) {
|
|
161
|
+
receiver = await this.client.acceptNextSession(topicName, subscriptionName, {
|
|
162
|
+
maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,
|
|
163
|
+
receiveMode: this.options.receiveMode
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
receiver = this.client.createReceiver(topicName, subscriptionName, {
|
|
167
|
+
maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,
|
|
168
|
+
receiveMode: this.options.receiveMode
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.receivers.push(receiver);
|
|
172
|
+
receiver.subscribe(
|
|
173
|
+
{
|
|
174
|
+
processMessage: async (message) => {
|
|
175
|
+
try {
|
|
176
|
+
const envelope = message.body;
|
|
177
|
+
if (!envelope || !envelope.type || !envelope.payload) {
|
|
178
|
+
console.error("[AzureServiceBus] Invalid message format:", message);
|
|
179
|
+
if (this.options.receiveMode === "peekLock") {
|
|
180
|
+
await receiver.deadLetterMessage(message, {
|
|
181
|
+
deadLetterReason: "InvalidMessageFormat",
|
|
182
|
+
deadLetterErrorDescription: "Message body is not a valid envelope"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await handler(envelope);
|
|
188
|
+
if (this.options.receiveMode === "peekLock" && !this.options.autoCompleteMessages) {
|
|
189
|
+
await receiver.completeMessage(message);
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("[AzureServiceBus] Message handler error:", error);
|
|
193
|
+
if (this.options.receiveMode === "peekLock") {
|
|
194
|
+
await receiver.abandonMessage(message);
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
processError: async (args) => {
|
|
200
|
+
console.error(
|
|
201
|
+
"[AzureServiceBus] Error from receiver:",
|
|
202
|
+
args.error,
|
|
203
|
+
"Source:",
|
|
204
|
+
args.errorSource
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
maxConcurrentCalls: concurrency,
|
|
210
|
+
autoCompleteMessages: this.options.autoCompleteMessages
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
216
|
+
0 && (module.exports = {
|
|
217
|
+
AzureServiceBusTransport
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/AzureServiceBusTransport.ts"],"sourcesContent":["export { AzureServiceBusTransport } from \"./AzureServiceBusTransport.js\";\nexport type { AzureServiceBusTransportOptions } from \"./types.js\";\n","import {\n ServiceBusClient,\n ServiceBusSender,\n ServiceBusReceiver,\n ServiceBusReceivedMessage,\n ServiceBusSessionReceiver,\n} from \"@azure/service-bus\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n MessageEnvelope,\n BaseMessage,\n} from \"@saga-bus/core\";\nimport type { AzureServiceBusTransportOptions, SubscriptionRegistration } from \"./types.js\";\nimport { randomUUID } from \"crypto\";\n\n/**\n * Azure Service Bus transport implementation for saga-bus.\n *\n * Supports both queue and topic/subscription patterns with optional\n * session-based message ordering.\n */\nexport class AzureServiceBusTransport implements Transport {\n private client: ServiceBusClient | null = null;\n private readonly options: AzureServiceBusTransportOptions;\n private readonly senders = new Map<string, ServiceBusSender>();\n private readonly receivers: Array<ServiceBusReceiver | ServiceBusSessionReceiver> = [];\n private readonly subscriptions: SubscriptionRegistration[] = [];\n private started = false;\n private stopping = false;\n\n constructor(options: AzureServiceBusTransportOptions) {\n if (!options.connectionString && !options.fullyQualifiedNamespace) {\n throw new Error(\n \"Either connectionString or fullyQualifiedNamespace must be provided\"\n );\n }\n\n if (options.fullyQualifiedNamespace && !options.credential) {\n throw new Error(\n \"credential is required when using fullyQualifiedNamespace\"\n );\n }\n\n this.options = {\n maxConcurrentCalls: 1,\n maxAutoLockRenewalDurationInMs: 300000,\n autoCompleteMessages: false,\n receiveMode: \"peekLock\",\n entityPrefix: \"\",\n sessionEnabled: false,\n ...options,\n };\n }\n\n async start(): Promise<void> {\n if (this.started) return;\n\n // Create the ServiceBusClient\n if (this.options.connectionString) {\n this.client = new ServiceBusClient(this.options.connectionString);\n } else if (this.options.fullyQualifiedNamespace && this.options.credential) {\n this.client = new ServiceBusClient(\n this.options.fullyQualifiedNamespace,\n this.options.credential\n );\n } else {\n throw new Error(\"Invalid configuration\");\n }\n\n // Start all registered subscriptions\n for (const sub of this.subscriptions) {\n await this.startReceiver(sub);\n }\n\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started || this.stopping) return;\n this.stopping = true;\n\n // Close all receivers\n for (const receiver of this.receivers) {\n try {\n await receiver.close();\n } catch (error) {\n console.error(\"[AzureServiceBus] Error closing receiver:\", error);\n }\n }\n this.receivers.length = 0;\n\n // Close all senders\n for (const sender of this.senders.values()) {\n try {\n await sender.close();\n } catch (error) {\n console.error(\"[AzureServiceBus] Error closing sender:\", error);\n }\n }\n this.senders.clear();\n\n // Close the client\n if (this.client) {\n await this.client.close();\n this.client = null;\n }\n\n this.started = false;\n this.stopping = false;\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const { endpoint, concurrency = 1, group } = options;\n\n // Build topic and subscription names\n const topicName = `${this.options.entityPrefix}${endpoint}`;\n const subscriptionName =\n group ?? this.options.subscriptionName ?? \"default\";\n\n const registration: SubscriptionRegistration = {\n endpoint,\n topicName,\n subscriptionName,\n handler: handler as (envelope: unknown) => Promise<void>,\n concurrency,\n };\n\n this.subscriptions.push(registration);\n\n // If already started, immediately start the receiver\n if (this.started && this.client) {\n await this.startReceiver(registration);\n }\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n if (!this.client) {\n throw new Error(\"Transport not started\");\n }\n\n const { endpoint, key, headers = {}, delayMs } = options;\n const topicName = `${this.options.entityPrefix}${\n endpoint ?? this.options.defaultTopic ?? message.type\n }`;\n\n // Get or create sender\n let sender = this.senders.get(topicName);\n if (!sender) {\n sender = this.client.createSender(topicName);\n this.senders.set(topicName, sender);\n }\n\n // Create message envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: headers as Record<string, string>,\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Build Service Bus message\n const sbMessage: {\n body: MessageEnvelope<TMessage>;\n messageId: string;\n contentType: string;\n sessionId?: string;\n partitionKey?: string;\n applicationProperties: Record<string, string>;\n scheduledEnqueueTimeUtc?: Date;\n } = {\n body: envelope,\n messageId: envelope.id,\n contentType: \"application/json\",\n applicationProperties: {\n messageType: message.type,\n ...headers,\n },\n };\n\n // Set session ID for ordered delivery if enabled\n if (this.options.sessionEnabled && key) {\n sbMessage.sessionId = key;\n } else if (key) {\n sbMessage.partitionKey = key;\n }\n\n // Handle delayed delivery using native scheduled messages\n if (delayMs && delayMs > 0) {\n sbMessage.scheduledEnqueueTimeUtc = new Date(Date.now() + delayMs);\n }\n\n await sender.sendMessages(sbMessage);\n }\n\n private async startReceiver(registration: SubscriptionRegistration): Promise<void> {\n if (!this.client) return;\n\n const { topicName, subscriptionName, handler, concurrency } = registration;\n\n let receiver: ServiceBusReceiver | ServiceBusSessionReceiver;\n\n if (this.options.sessionEnabled) {\n // Session-enabled receiver for ordered processing\n receiver = await this.client.acceptNextSession(topicName, subscriptionName, {\n maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,\n receiveMode: this.options.receiveMode,\n });\n } else {\n // Regular subscription receiver\n receiver = this.client.createReceiver(topicName, subscriptionName, {\n maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,\n receiveMode: this.options.receiveMode,\n });\n }\n\n this.receivers.push(receiver);\n\n // Start message processing\n receiver.subscribe(\n {\n processMessage: async (message: ServiceBusReceivedMessage) => {\n try {\n const envelope = message.body as MessageEnvelope;\n\n // Ensure envelope has required fields\n if (!envelope || !envelope.type || !envelope.payload) {\n console.error(\"[AzureServiceBus] Invalid message format:\", message);\n if (this.options.receiveMode === \"peekLock\") {\n await receiver.deadLetterMessage(message, {\n deadLetterReason: \"InvalidMessageFormat\",\n deadLetterErrorDescription: \"Message body is not a valid envelope\",\n });\n }\n return;\n }\n\n await handler(envelope);\n\n // Complete message if not auto-completed\n if (\n this.options.receiveMode === \"peekLock\" &&\n !this.options.autoCompleteMessages\n ) {\n await receiver.completeMessage(message);\n }\n } catch (error) {\n console.error(\"[AzureServiceBus] Message handler error:\", error);\n\n if (this.options.receiveMode === \"peekLock\") {\n // Abandon for retry\n await receiver.abandonMessage(message);\n }\n\n throw error;\n }\n },\n processError: async (args) => {\n console.error(\n \"[AzureServiceBus] Error from receiver:\",\n args.error,\n \"Source:\",\n args.errorSource\n );\n },\n },\n {\n maxConcurrentCalls: concurrency,\n autoCompleteMessages: this.options.autoCompleteMessages,\n }\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
|
|
2
|
+
import { TokenCredential } from '@azure/service-bus';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for Azure Service Bus transport.
|
|
6
|
+
*/
|
|
7
|
+
interface AzureServiceBusTransportOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Connection string for Azure Service Bus.
|
|
10
|
+
* Either connectionString OR (fullyQualifiedNamespace + credential) must be provided.
|
|
11
|
+
*/
|
|
12
|
+
connectionString?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Fully qualified namespace (e.g., "mybus.servicebus.windows.net").
|
|
15
|
+
* Use with credential for Azure AD authentication.
|
|
16
|
+
*/
|
|
17
|
+
fullyQualifiedNamespace?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Token credential for Azure AD authentication.
|
|
20
|
+
* Use with fullyQualifiedNamespace.
|
|
21
|
+
*/
|
|
22
|
+
credential?: TokenCredential;
|
|
23
|
+
/**
|
|
24
|
+
* Default topic for publishing messages.
|
|
25
|
+
* If not provided, message type is used as topic name.
|
|
26
|
+
*/
|
|
27
|
+
defaultTopic?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Subscription name for receiving messages.
|
|
30
|
+
* Required for subscribing to topics.
|
|
31
|
+
*/
|
|
32
|
+
subscriptionName?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to use sessions for ordered message processing.
|
|
35
|
+
* When enabled, correlation ID is used as session ID.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
sessionEnabled?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum number of concurrent message handlers.
|
|
41
|
+
* @default 1
|
|
42
|
+
*/
|
|
43
|
+
maxConcurrentCalls?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum duration for auto-lock renewal in milliseconds.
|
|
46
|
+
* @default 300000 (5 minutes)
|
|
47
|
+
*/
|
|
48
|
+
maxAutoLockRenewalDurationInMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to auto-complete messages after successful processing.
|
|
51
|
+
* @default false (manual acknowledgment)
|
|
52
|
+
*/
|
|
53
|
+
autoCompleteMessages?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Receive mode: "peekLock" or "receiveAndDelete".
|
|
56
|
+
* @default "peekLock"
|
|
57
|
+
*/
|
|
58
|
+
receiveMode?: "peekLock" | "receiveAndDelete";
|
|
59
|
+
/**
|
|
60
|
+
* Prefix for queue/topic names.
|
|
61
|
+
* @default ""
|
|
62
|
+
*/
|
|
63
|
+
entityPrefix?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Azure Service Bus transport implementation for saga-bus.
|
|
68
|
+
*
|
|
69
|
+
* Supports both queue and topic/subscription patterns with optional
|
|
70
|
+
* session-based message ordering.
|
|
71
|
+
*/
|
|
72
|
+
declare class AzureServiceBusTransport implements Transport {
|
|
73
|
+
private client;
|
|
74
|
+
private readonly options;
|
|
75
|
+
private readonly senders;
|
|
76
|
+
private readonly receivers;
|
|
77
|
+
private readonly subscriptions;
|
|
78
|
+
private started;
|
|
79
|
+
private stopping;
|
|
80
|
+
constructor(options: AzureServiceBusTransportOptions);
|
|
81
|
+
start(): Promise<void>;
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
|
|
84
|
+
publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
|
|
85
|
+
private startReceiver;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { AzureServiceBusTransport, type AzureServiceBusTransportOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
|
|
2
|
+
import { TokenCredential } from '@azure/service-bus';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for Azure Service Bus transport.
|
|
6
|
+
*/
|
|
7
|
+
interface AzureServiceBusTransportOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Connection string for Azure Service Bus.
|
|
10
|
+
* Either connectionString OR (fullyQualifiedNamespace + credential) must be provided.
|
|
11
|
+
*/
|
|
12
|
+
connectionString?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Fully qualified namespace (e.g., "mybus.servicebus.windows.net").
|
|
15
|
+
* Use with credential for Azure AD authentication.
|
|
16
|
+
*/
|
|
17
|
+
fullyQualifiedNamespace?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Token credential for Azure AD authentication.
|
|
20
|
+
* Use with fullyQualifiedNamespace.
|
|
21
|
+
*/
|
|
22
|
+
credential?: TokenCredential;
|
|
23
|
+
/**
|
|
24
|
+
* Default topic for publishing messages.
|
|
25
|
+
* If not provided, message type is used as topic name.
|
|
26
|
+
*/
|
|
27
|
+
defaultTopic?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Subscription name for receiving messages.
|
|
30
|
+
* Required for subscribing to topics.
|
|
31
|
+
*/
|
|
32
|
+
subscriptionName?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to use sessions for ordered message processing.
|
|
35
|
+
* When enabled, correlation ID is used as session ID.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
sessionEnabled?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum number of concurrent message handlers.
|
|
41
|
+
* @default 1
|
|
42
|
+
*/
|
|
43
|
+
maxConcurrentCalls?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum duration for auto-lock renewal in milliseconds.
|
|
46
|
+
* @default 300000 (5 minutes)
|
|
47
|
+
*/
|
|
48
|
+
maxAutoLockRenewalDurationInMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to auto-complete messages after successful processing.
|
|
51
|
+
* @default false (manual acknowledgment)
|
|
52
|
+
*/
|
|
53
|
+
autoCompleteMessages?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Receive mode: "peekLock" or "receiveAndDelete".
|
|
56
|
+
* @default "peekLock"
|
|
57
|
+
*/
|
|
58
|
+
receiveMode?: "peekLock" | "receiveAndDelete";
|
|
59
|
+
/**
|
|
60
|
+
* Prefix for queue/topic names.
|
|
61
|
+
* @default ""
|
|
62
|
+
*/
|
|
63
|
+
entityPrefix?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Azure Service Bus transport implementation for saga-bus.
|
|
68
|
+
*
|
|
69
|
+
* Supports both queue and topic/subscription patterns with optional
|
|
70
|
+
* session-based message ordering.
|
|
71
|
+
*/
|
|
72
|
+
declare class AzureServiceBusTransport implements Transport {
|
|
73
|
+
private client;
|
|
74
|
+
private readonly options;
|
|
75
|
+
private readonly senders;
|
|
76
|
+
private readonly receivers;
|
|
77
|
+
private readonly subscriptions;
|
|
78
|
+
private started;
|
|
79
|
+
private stopping;
|
|
80
|
+
constructor(options: AzureServiceBusTransportOptions);
|
|
81
|
+
start(): Promise<void>;
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
|
|
84
|
+
publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
|
|
85
|
+
private startReceiver;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { AzureServiceBusTransport, type AzureServiceBusTransportOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/AzureServiceBusTransport.ts
|
|
2
|
+
import {
|
|
3
|
+
ServiceBusClient
|
|
4
|
+
} from "@azure/service-bus";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
var AzureServiceBusTransport = class {
|
|
7
|
+
client = null;
|
|
8
|
+
options;
|
|
9
|
+
senders = /* @__PURE__ */ new Map();
|
|
10
|
+
receivers = [];
|
|
11
|
+
subscriptions = [];
|
|
12
|
+
started = false;
|
|
13
|
+
stopping = false;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
if (!options.connectionString && !options.fullyQualifiedNamespace) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Either connectionString or fullyQualifiedNamespace must be provided"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (options.fullyQualifiedNamespace && !options.credential) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"credential is required when using fullyQualifiedNamespace"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
this.options = {
|
|
26
|
+
maxConcurrentCalls: 1,
|
|
27
|
+
maxAutoLockRenewalDurationInMs: 3e5,
|
|
28
|
+
autoCompleteMessages: false,
|
|
29
|
+
receiveMode: "peekLock",
|
|
30
|
+
entityPrefix: "",
|
|
31
|
+
sessionEnabled: false,
|
|
32
|
+
...options
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.started) return;
|
|
37
|
+
if (this.options.connectionString) {
|
|
38
|
+
this.client = new ServiceBusClient(this.options.connectionString);
|
|
39
|
+
} else if (this.options.fullyQualifiedNamespace && this.options.credential) {
|
|
40
|
+
this.client = new ServiceBusClient(
|
|
41
|
+
this.options.fullyQualifiedNamespace,
|
|
42
|
+
this.options.credential
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error("Invalid configuration");
|
|
46
|
+
}
|
|
47
|
+
for (const sub of this.subscriptions) {
|
|
48
|
+
await this.startReceiver(sub);
|
|
49
|
+
}
|
|
50
|
+
this.started = true;
|
|
51
|
+
}
|
|
52
|
+
async stop() {
|
|
53
|
+
if (!this.started || this.stopping) return;
|
|
54
|
+
this.stopping = true;
|
|
55
|
+
for (const receiver of this.receivers) {
|
|
56
|
+
try {
|
|
57
|
+
await receiver.close();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("[AzureServiceBus] Error closing receiver:", error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.receivers.length = 0;
|
|
63
|
+
for (const sender of this.senders.values()) {
|
|
64
|
+
try {
|
|
65
|
+
await sender.close();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("[AzureServiceBus] Error closing sender:", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.senders.clear();
|
|
71
|
+
if (this.client) {
|
|
72
|
+
await this.client.close();
|
|
73
|
+
this.client = null;
|
|
74
|
+
}
|
|
75
|
+
this.started = false;
|
|
76
|
+
this.stopping = false;
|
|
77
|
+
}
|
|
78
|
+
async subscribe(options, handler) {
|
|
79
|
+
const { endpoint, concurrency = 1, group } = options;
|
|
80
|
+
const topicName = `${this.options.entityPrefix}${endpoint}`;
|
|
81
|
+
const subscriptionName = group ?? this.options.subscriptionName ?? "default";
|
|
82
|
+
const registration = {
|
|
83
|
+
endpoint,
|
|
84
|
+
topicName,
|
|
85
|
+
subscriptionName,
|
|
86
|
+
handler,
|
|
87
|
+
concurrency
|
|
88
|
+
};
|
|
89
|
+
this.subscriptions.push(registration);
|
|
90
|
+
if (this.started && this.client) {
|
|
91
|
+
await this.startReceiver(registration);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async publish(message, options) {
|
|
95
|
+
if (!this.client) {
|
|
96
|
+
throw new Error("Transport not started");
|
|
97
|
+
}
|
|
98
|
+
const { endpoint, key, headers = {}, delayMs } = options;
|
|
99
|
+
const topicName = `${this.options.entityPrefix}${endpoint ?? this.options.defaultTopic ?? message.type}`;
|
|
100
|
+
let sender = this.senders.get(topicName);
|
|
101
|
+
if (!sender) {
|
|
102
|
+
sender = this.client.createSender(topicName);
|
|
103
|
+
this.senders.set(topicName, sender);
|
|
104
|
+
}
|
|
105
|
+
const envelope = {
|
|
106
|
+
id: randomUUID(),
|
|
107
|
+
type: message.type,
|
|
108
|
+
payload: message,
|
|
109
|
+
headers,
|
|
110
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
111
|
+
partitionKey: key
|
|
112
|
+
};
|
|
113
|
+
const sbMessage = {
|
|
114
|
+
body: envelope,
|
|
115
|
+
messageId: envelope.id,
|
|
116
|
+
contentType: "application/json",
|
|
117
|
+
applicationProperties: {
|
|
118
|
+
messageType: message.type,
|
|
119
|
+
...headers
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (this.options.sessionEnabled && key) {
|
|
123
|
+
sbMessage.sessionId = key;
|
|
124
|
+
} else if (key) {
|
|
125
|
+
sbMessage.partitionKey = key;
|
|
126
|
+
}
|
|
127
|
+
if (delayMs && delayMs > 0) {
|
|
128
|
+
sbMessage.scheduledEnqueueTimeUtc = new Date(Date.now() + delayMs);
|
|
129
|
+
}
|
|
130
|
+
await sender.sendMessages(sbMessage);
|
|
131
|
+
}
|
|
132
|
+
async startReceiver(registration) {
|
|
133
|
+
if (!this.client) return;
|
|
134
|
+
const { topicName, subscriptionName, handler, concurrency } = registration;
|
|
135
|
+
let receiver;
|
|
136
|
+
if (this.options.sessionEnabled) {
|
|
137
|
+
receiver = await this.client.acceptNextSession(topicName, subscriptionName, {
|
|
138
|
+
maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,
|
|
139
|
+
receiveMode: this.options.receiveMode
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
receiver = this.client.createReceiver(topicName, subscriptionName, {
|
|
143
|
+
maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,
|
|
144
|
+
receiveMode: this.options.receiveMode
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
this.receivers.push(receiver);
|
|
148
|
+
receiver.subscribe(
|
|
149
|
+
{
|
|
150
|
+
processMessage: async (message) => {
|
|
151
|
+
try {
|
|
152
|
+
const envelope = message.body;
|
|
153
|
+
if (!envelope || !envelope.type || !envelope.payload) {
|
|
154
|
+
console.error("[AzureServiceBus] Invalid message format:", message);
|
|
155
|
+
if (this.options.receiveMode === "peekLock") {
|
|
156
|
+
await receiver.deadLetterMessage(message, {
|
|
157
|
+
deadLetterReason: "InvalidMessageFormat",
|
|
158
|
+
deadLetterErrorDescription: "Message body is not a valid envelope"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await handler(envelope);
|
|
164
|
+
if (this.options.receiveMode === "peekLock" && !this.options.autoCompleteMessages) {
|
|
165
|
+
await receiver.completeMessage(message);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error("[AzureServiceBus] Message handler error:", error);
|
|
169
|
+
if (this.options.receiveMode === "peekLock") {
|
|
170
|
+
await receiver.abandonMessage(message);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
processError: async (args) => {
|
|
176
|
+
console.error(
|
|
177
|
+
"[AzureServiceBus] Error from receiver:",
|
|
178
|
+
args.error,
|
|
179
|
+
"Source:",
|
|
180
|
+
args.errorSource
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
maxConcurrentCalls: concurrency,
|
|
186
|
+
autoCompleteMessages: this.options.autoCompleteMessages
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
export {
|
|
192
|
+
AzureServiceBusTransport
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/AzureServiceBusTransport.ts"],"sourcesContent":["import {\n ServiceBusClient,\n ServiceBusSender,\n ServiceBusReceiver,\n ServiceBusReceivedMessage,\n ServiceBusSessionReceiver,\n} from \"@azure/service-bus\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n MessageEnvelope,\n BaseMessage,\n} from \"@saga-bus/core\";\nimport type { AzureServiceBusTransportOptions, SubscriptionRegistration } from \"./types.js\";\nimport { randomUUID } from \"crypto\";\n\n/**\n * Azure Service Bus transport implementation for saga-bus.\n *\n * Supports both queue and topic/subscription patterns with optional\n * session-based message ordering.\n */\nexport class AzureServiceBusTransport implements Transport {\n private client: ServiceBusClient | null = null;\n private readonly options: AzureServiceBusTransportOptions;\n private readonly senders = new Map<string, ServiceBusSender>();\n private readonly receivers: Array<ServiceBusReceiver | ServiceBusSessionReceiver> = [];\n private readonly subscriptions: SubscriptionRegistration[] = [];\n private started = false;\n private stopping = false;\n\n constructor(options: AzureServiceBusTransportOptions) {\n if (!options.connectionString && !options.fullyQualifiedNamespace) {\n throw new Error(\n \"Either connectionString or fullyQualifiedNamespace must be provided\"\n );\n }\n\n if (options.fullyQualifiedNamespace && !options.credential) {\n throw new Error(\n \"credential is required when using fullyQualifiedNamespace\"\n );\n }\n\n this.options = {\n maxConcurrentCalls: 1,\n maxAutoLockRenewalDurationInMs: 300000,\n autoCompleteMessages: false,\n receiveMode: \"peekLock\",\n entityPrefix: \"\",\n sessionEnabled: false,\n ...options,\n };\n }\n\n async start(): Promise<void> {\n if (this.started) return;\n\n // Create the ServiceBusClient\n if (this.options.connectionString) {\n this.client = new ServiceBusClient(this.options.connectionString);\n } else if (this.options.fullyQualifiedNamespace && this.options.credential) {\n this.client = new ServiceBusClient(\n this.options.fullyQualifiedNamespace,\n this.options.credential\n );\n } else {\n throw new Error(\"Invalid configuration\");\n }\n\n // Start all registered subscriptions\n for (const sub of this.subscriptions) {\n await this.startReceiver(sub);\n }\n\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started || this.stopping) return;\n this.stopping = true;\n\n // Close all receivers\n for (const receiver of this.receivers) {\n try {\n await receiver.close();\n } catch (error) {\n console.error(\"[AzureServiceBus] Error closing receiver:\", error);\n }\n }\n this.receivers.length = 0;\n\n // Close all senders\n for (const sender of this.senders.values()) {\n try {\n await sender.close();\n } catch (error) {\n console.error(\"[AzureServiceBus] Error closing sender:\", error);\n }\n }\n this.senders.clear();\n\n // Close the client\n if (this.client) {\n await this.client.close();\n this.client = null;\n }\n\n this.started = false;\n this.stopping = false;\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n const { endpoint, concurrency = 1, group } = options;\n\n // Build topic and subscription names\n const topicName = `${this.options.entityPrefix}${endpoint}`;\n const subscriptionName =\n group ?? this.options.subscriptionName ?? \"default\";\n\n const registration: SubscriptionRegistration = {\n endpoint,\n topicName,\n subscriptionName,\n handler: handler as (envelope: unknown) => Promise<void>,\n concurrency,\n };\n\n this.subscriptions.push(registration);\n\n // If already started, immediately start the receiver\n if (this.started && this.client) {\n await this.startReceiver(registration);\n }\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n if (!this.client) {\n throw new Error(\"Transport not started\");\n }\n\n const { endpoint, key, headers = {}, delayMs } = options;\n const topicName = `${this.options.entityPrefix}${\n endpoint ?? this.options.defaultTopic ?? message.type\n }`;\n\n // Get or create sender\n let sender = this.senders.get(topicName);\n if (!sender) {\n sender = this.client.createSender(topicName);\n this.senders.set(topicName, sender);\n }\n\n // Create message envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: headers as Record<string, string>,\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Build Service Bus message\n const sbMessage: {\n body: MessageEnvelope<TMessage>;\n messageId: string;\n contentType: string;\n sessionId?: string;\n partitionKey?: string;\n applicationProperties: Record<string, string>;\n scheduledEnqueueTimeUtc?: Date;\n } = {\n body: envelope,\n messageId: envelope.id,\n contentType: \"application/json\",\n applicationProperties: {\n messageType: message.type,\n ...headers,\n },\n };\n\n // Set session ID for ordered delivery if enabled\n if (this.options.sessionEnabled && key) {\n sbMessage.sessionId = key;\n } else if (key) {\n sbMessage.partitionKey = key;\n }\n\n // Handle delayed delivery using native scheduled messages\n if (delayMs && delayMs > 0) {\n sbMessage.scheduledEnqueueTimeUtc = new Date(Date.now() + delayMs);\n }\n\n await sender.sendMessages(sbMessage);\n }\n\n private async startReceiver(registration: SubscriptionRegistration): Promise<void> {\n if (!this.client) return;\n\n const { topicName, subscriptionName, handler, concurrency } = registration;\n\n let receiver: ServiceBusReceiver | ServiceBusSessionReceiver;\n\n if (this.options.sessionEnabled) {\n // Session-enabled receiver for ordered processing\n receiver = await this.client.acceptNextSession(topicName, subscriptionName, {\n maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,\n receiveMode: this.options.receiveMode,\n });\n } else {\n // Regular subscription receiver\n receiver = this.client.createReceiver(topicName, subscriptionName, {\n maxAutoLockRenewalDurationInMs: this.options.maxAutoLockRenewalDurationInMs,\n receiveMode: this.options.receiveMode,\n });\n }\n\n this.receivers.push(receiver);\n\n // Start message processing\n receiver.subscribe(\n {\n processMessage: async (message: ServiceBusReceivedMessage) => {\n try {\n const envelope = message.body as MessageEnvelope;\n\n // Ensure envelope has required fields\n if (!envelope || !envelope.type || !envelope.payload) {\n console.error(\"[AzureServiceBus] Invalid message format:\", message);\n if (this.options.receiveMode === \"peekLock\") {\n await receiver.deadLetterMessage(message, {\n deadLetterReason: \"InvalidMessageFormat\",\n deadLetterErrorDescription: \"Message body is not a valid envelope\",\n });\n }\n return;\n }\n\n await handler(envelope);\n\n // Complete message if not auto-completed\n if (\n this.options.receiveMode === \"peekLock\" &&\n !this.options.autoCompleteMessages\n ) {\n await receiver.completeMessage(message);\n }\n } catch (error) {\n console.error(\"[AzureServiceBus] Message handler error:\", error);\n\n if (this.options.receiveMode === \"peekLock\") {\n // Abandon for retry\n await receiver.abandonMessage(message);\n }\n\n throw error;\n }\n },\n processError: async (args) => {\n console.error(\n \"[AzureServiceBus] Error from receiver:\",\n args.error,\n \"Source:\",\n args.errorSource\n );\n },\n },\n {\n maxConcurrentCalls: concurrency,\n autoCompleteMessages: this.options.autoCompleteMessages,\n }\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saga-bus/transport-azure-servicebus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Azure Service Bus transport for saga-bus",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/deanforan/saga-bus.git",
|
|
26
|
+
"directory": "packages/transport-azure-servicebus"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@saga-bus/core": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@azure/service-bus": "^7.9.5",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"tsup": "^8.5.0",
|
|
35
|
+
"typescript": "^5.9.2",
|
|
36
|
+
"vitest": "^3.2.4",
|
|
37
|
+
"@repo/eslint-config": "0.0.0",
|
|
38
|
+
"@repo/typescript-config": "0.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@azure/service-bus": ">=7.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"lint": "eslint src/",
|
|
47
|
+
"check-types": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
}
|
|
51
|
+
}
|