@blokjs/trigger-pubsub 0.6.18 → 0.6.19
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/dist/PubSubTrigger.js +20 -1
- package/package.json +5 -4
- package/CHANGELOG.md +0 -22
- package/__tests__/integration/gcp-pubsub.real-emulator.test.ts +0 -235
- package/__tests__/integration/kafka-pubsub.real-kafka.test.ts +0 -269
- package/__tests__/integration/nats-pubsub.real-nats.test.ts +0 -138
- package/src/PubSubTrigger.test.ts +0 -151
- package/src/PubSubTrigger.ts +0 -402
- package/src/adapters/AWSSNSAdapter.ts +0 -322
- package/src/adapters/AzureServiceBusAdapter.ts +0 -263
- package/src/adapters/GCPPubSubAdapter.ts +0 -236
- package/src/adapters/KafkaPubSubAdapter.ts +0 -194
- package/src/adapters/NATSPubSubAdapter.ts +0 -326
- package/src/adapters/RedisStreamsPubSubAdapter.ts +0 -225
- package/src/adapters/factory.test.ts +0 -87
- package/src/adapters/factory.ts +0 -88
- package/src/adapters/new-adapters.test.ts +0 -108
- package/src/index.ts +0 -67
- package/template/.env.example +0 -8
- package/template/package.json +0 -44
- package/template/src/Nodes.ts +0 -10
- package/template/src/Workflows.ts +0 -8
- package/template/src/index.ts +0 -41
- package/template/src/runner/PubSubServer.ts +0 -39
- package/template/src/runner/types/Workflows.ts +0 -7
- package/template/src/workflows/messages/on-message.ts +0 -48
- package/template/tsconfig.json +0 -31
- package/template/vitest.config.ts +0 -39
- package/tsconfig.json +0 -32
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AWSSNSAdapter - AWS SNS/SQS adapter for PubSubTrigger
|
|
3
|
-
*
|
|
4
|
-
* Uses AWS SDK v3 for SNS/SQS connectivity.
|
|
5
|
-
* SNS topics deliver to SQS queues, which this adapter polls.
|
|
6
|
-
*
|
|
7
|
-
* Requires: npm install @aws-sdk/client-sns @aws-sdk/client-sqs
|
|
8
|
-
*
|
|
9
|
-
* Environment variables:
|
|
10
|
-
* - AWS_REGION: AWS region (default: us-east-1)
|
|
11
|
-
* - AWS_ACCESS_KEY_ID: AWS access key (optional if using IAM roles)
|
|
12
|
-
* - AWS_SECRET_ACCESS_KEY: AWS secret key (optional if using IAM roles)
|
|
13
|
-
* - SQS_WAIT_TIME_SECONDS: Long polling wait time (default: 20)
|
|
14
|
-
* - SQS_MAX_MESSAGES: Max messages per receive (default: 10)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { MessageAttributeValue, SQSClient } from "@aws-sdk/client-sqs";
|
|
18
|
-
import type { PubSubTriggerOpts } from "@blokjs/helper";
|
|
19
|
-
import { v4 as uuid } from "uuid";
|
|
20
|
-
import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Shape of an SNS notification when delivered to an SQS subscription.
|
|
24
|
-
* SNS-to-SQS subscriptions wrap the publisher's payload inside a JSON
|
|
25
|
-
* envelope; the receiver parses `msg.Body` as this envelope and reads
|
|
26
|
-
* `Message` (the actual payload) plus the SNS-side attributes.
|
|
27
|
-
*/
|
|
28
|
-
interface SNSNotificationEnvelope {
|
|
29
|
-
Type?: string;
|
|
30
|
-
MessageId?: string;
|
|
31
|
-
TopicArn?: string;
|
|
32
|
-
Message?: string;
|
|
33
|
-
Subject?: string;
|
|
34
|
-
Timestamp?: string;
|
|
35
|
-
MessageAttributes?: Record<string, { Type?: string; Value?: string }>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* AWS SNS/SQS configuration
|
|
40
|
-
*/
|
|
41
|
-
export interface AWSSNSConfig {
|
|
42
|
-
region: string;
|
|
43
|
-
waitTimeSeconds?: number;
|
|
44
|
-
maxNumberOfMessages?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* AWSSNSAdapter - AWS SNS implementation using SQS subscriptions
|
|
49
|
-
*/
|
|
50
|
-
export class AWSSNSAdapter implements PubSubAdapter {
|
|
51
|
-
readonly provider = "aws" as const;
|
|
52
|
-
|
|
53
|
-
private sqsClient: SQSClient | undefined;
|
|
54
|
-
private connected = false;
|
|
55
|
-
private config: AWSSNSConfig;
|
|
56
|
-
private pollingIntervals: Map<string, NodeJS.Timeout> = new Map();
|
|
57
|
-
private shouldStop = false;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Type-narrowing accessor for `this.sqsClient`. Field is undefined
|
|
61
|
-
* until `connect()` runs.
|
|
62
|
-
*/
|
|
63
|
-
private requireSqsClient(): SQSClient {
|
|
64
|
-
if (!this.sqsClient) {
|
|
65
|
-
throw new Error("[AWSSNSAdapter] SQS client is not initialised — call connect() first");
|
|
66
|
-
}
|
|
67
|
-
return this.sqsClient;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
constructor(config?: Partial<AWSSNSConfig>) {
|
|
71
|
-
this.config = {
|
|
72
|
-
region: config?.region || process.env.AWS_REGION || "us-east-1",
|
|
73
|
-
waitTimeSeconds: config?.waitTimeSeconds ?? Number.parseInt(process.env.SQS_WAIT_TIME_SECONDS || "20", 10),
|
|
74
|
-
maxNumberOfMessages: config?.maxNumberOfMessages ?? Number.parseInt(process.env.SQS_MAX_MESSAGES || "10", 10),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Connect to AWS
|
|
80
|
-
*/
|
|
81
|
-
async connect(): Promise<void> {
|
|
82
|
-
if (this.connected) return;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Dynamic import of AWS SDK
|
|
86
|
-
const { SQSClient } = await import("@aws-sdk/client-sqs");
|
|
87
|
-
|
|
88
|
-
this.sqsClient = new SQSClient({
|
|
89
|
-
region: this.config.region,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
this.connected = true;
|
|
93
|
-
this.shouldStop = false;
|
|
94
|
-
console.log(`[AWSSNSAdapter] Connected to AWS SNS/SQS: ${this.config.region}`);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`Failed to connect to AWS: ${(error as Error).message}. Make sure @aws-sdk/client-sqs is installed: npm install @aws-sdk/client-sqs`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Disconnect from AWS
|
|
104
|
-
*/
|
|
105
|
-
async disconnect(): Promise<void> {
|
|
106
|
-
if (!this.connected) return;
|
|
107
|
-
|
|
108
|
-
this.shouldStop = true;
|
|
109
|
-
|
|
110
|
-
// Clear all polling intervals
|
|
111
|
-
for (const [queueUrl, interval] of this.pollingIntervals) {
|
|
112
|
-
clearTimeout(interval);
|
|
113
|
-
}
|
|
114
|
-
this.pollingIntervals.clear();
|
|
115
|
-
|
|
116
|
-
this.connected = false;
|
|
117
|
-
console.log("[AWSSNSAdapter] Disconnected from AWS SNS/SQS");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Subscribe to an SNS topic via SQS queue
|
|
122
|
-
* Note: The SQS queue should be pre-configured as an SNS subscription
|
|
123
|
-
*/
|
|
124
|
-
async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
|
|
125
|
-
if (!this.connected) {
|
|
126
|
-
throw new Error("Not connected to AWS. Call connect() first.");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// In AWS, subscription is the SQS queue URL that's subscribed to the SNS topic.
|
|
130
|
-
if (!config.subscription) {
|
|
131
|
-
throw new Error("[AWSSNSAdapter] `subscription` is required — must be the SQS queue URL bound to the SNS topic.");
|
|
132
|
-
}
|
|
133
|
-
const queueUrl = config.subscription;
|
|
134
|
-
|
|
135
|
-
// Start polling the SQS queue
|
|
136
|
-
this.poll(queueUrl, config, handler);
|
|
137
|
-
|
|
138
|
-
console.log(`[AWSSNSAdapter] Subscribed to queue: ${queueUrl} (topic: ${config.topic})`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Poll SQS queue for messages (long polling)
|
|
143
|
-
*/
|
|
144
|
-
private async poll(
|
|
145
|
-
queueUrl: string,
|
|
146
|
-
config: PubSubTriggerOpts,
|
|
147
|
-
handler: (message: PubSubMessage) => Promise<void>,
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
if (this.shouldStop) return;
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const { ReceiveMessageCommand, DeleteMessageCommand } = await import("@aws-sdk/client-sqs");
|
|
153
|
-
|
|
154
|
-
const command = new ReceiveMessageCommand({
|
|
155
|
-
QueueUrl: queueUrl,
|
|
156
|
-
MaxNumberOfMessages: config.maxMessages || this.config.maxNumberOfMessages,
|
|
157
|
-
WaitTimeSeconds: this.config.waitTimeSeconds,
|
|
158
|
-
MessageAttributeNames: ["All"],
|
|
159
|
-
AttributeNames: ["All"],
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const response = await this.requireSqsClient().send(command);
|
|
163
|
-
|
|
164
|
-
if (response.Messages && response.Messages.length > 0) {
|
|
165
|
-
for (const msg of response.Messages) {
|
|
166
|
-
// Parse SNS message wrapper
|
|
167
|
-
let snsMessage: SNSNotificationEnvelope;
|
|
168
|
-
let body: unknown;
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
snsMessage = JSON.parse(msg.Body || "{}") as SNSNotificationEnvelope;
|
|
172
|
-
// SNS wraps the actual message in a "Message" field
|
|
173
|
-
if (snsMessage.Type === "Notification" && snsMessage.Message) {
|
|
174
|
-
try {
|
|
175
|
-
body = JSON.parse(snsMessage.Message);
|
|
176
|
-
} catch {
|
|
177
|
-
body = snsMessage.Message;
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
body = snsMessage;
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
body = msg.Body;
|
|
184
|
-
snsMessage = {};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Extract attributes from both SQS and SNS
|
|
188
|
-
const attributes: Record<string, string> = {};
|
|
189
|
-
|
|
190
|
-
// SQS message attributes
|
|
191
|
-
if (msg.MessageAttributes) {
|
|
192
|
-
for (const [key, attr] of Object.entries(msg.MessageAttributes)) {
|
|
193
|
-
const sqsAttr = attr as MessageAttributeValue;
|
|
194
|
-
attributes[key] = sqsAttr.StringValue || "";
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// SNS message attributes (if present)
|
|
199
|
-
if (snsMessage.MessageAttributes) {
|
|
200
|
-
for (const [key, attr] of Object.entries(snsMessage.MessageAttributes)) {
|
|
201
|
-
attributes[`sns_${key}`] = attr.Value || "";
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Create pub/sub message
|
|
206
|
-
const pubsubMessage: PubSubMessage = {
|
|
207
|
-
id: snsMessage.MessageId || msg.MessageId || uuid(),
|
|
208
|
-
body,
|
|
209
|
-
attributes,
|
|
210
|
-
raw: msg,
|
|
211
|
-
topic: snsMessage.TopicArn || config.topic,
|
|
212
|
-
subscription: queueUrl,
|
|
213
|
-
publishTime: snsMessage.Timestamp ? new Date(snsMessage.Timestamp) : new Date(),
|
|
214
|
-
ack: async () => {
|
|
215
|
-
const deleteCommand = new DeleteMessageCommand({
|
|
216
|
-
QueueUrl: queueUrl,
|
|
217
|
-
ReceiptHandle: msg.ReceiptHandle,
|
|
218
|
-
});
|
|
219
|
-
await this.requireSqsClient().send(deleteCommand);
|
|
220
|
-
},
|
|
221
|
-
nack: async () => {
|
|
222
|
-
// Let the visibility timeout expire to return the message
|
|
223
|
-
// Or change visibility to 0 for immediate retry
|
|
224
|
-
const { ChangeMessageVisibilityCommand } = await import("@aws-sdk/client-sqs");
|
|
225
|
-
const changeCommand = new ChangeMessageVisibilityCommand({
|
|
226
|
-
QueueUrl: queueUrl,
|
|
227
|
-
ReceiptHandle: msg.ReceiptHandle,
|
|
228
|
-
VisibilityTimeout: 0,
|
|
229
|
-
});
|
|
230
|
-
await this.requireSqsClient().send(changeCommand);
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Process message
|
|
235
|
-
try {
|
|
236
|
-
await handler(pubsubMessage);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
console.error(`[AWSSNSAdapter] Error processing message: ${(error as Error).message}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.error(`[AWSSNSAdapter] Polling error: ${(error as Error).message}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Continue polling unless stopped
|
|
247
|
-
if (!this.shouldStop) {
|
|
248
|
-
const timeout = setTimeout(() => this.poll(queueUrl, config, handler), 0);
|
|
249
|
-
this.pollingIntervals.set(queueUrl, timeout);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Unsubscribe from a queue (stops polling)
|
|
255
|
-
*/
|
|
256
|
-
async unsubscribe(queueUrl: string): Promise<void> {
|
|
257
|
-
const interval = this.pollingIntervals.get(queueUrl);
|
|
258
|
-
if (interval) {
|
|
259
|
-
clearTimeout(interval);
|
|
260
|
-
this.pollingIntervals.delete(queueUrl);
|
|
261
|
-
console.log(`[AWSSNSAdapter] Unsubscribed from queue: ${queueUrl}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Check if connected to AWS
|
|
267
|
-
*/
|
|
268
|
-
isConnected(): boolean {
|
|
269
|
-
return this.connected;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* v0.7 PR 6 — publish to an SNS topic.
|
|
274
|
-
*
|
|
275
|
-
* `topic` must be the SNS topic ARN. `partitionKey` /
|
|
276
|
-
* `orderingKey` map to the FIFO `MessageGroupId` field for
|
|
277
|
-
* `.fifo` topics; ignored otherwise.
|
|
278
|
-
*/
|
|
279
|
-
async publish(
|
|
280
|
-
topic: string,
|
|
281
|
-
payload: unknown,
|
|
282
|
-
opts?: { partitionKey?: string; orderingKey?: string },
|
|
283
|
-
): Promise<void> {
|
|
284
|
-
if (!this.connected) throw new Error("[blok][pubsub-aws] not connected. Call connect() first.");
|
|
285
|
-
const moduleName = "@aws-sdk/client-sns";
|
|
286
|
-
// biome-ignore lint/suspicious/noExplicitAny: SDK loaded at runtime as a peer dep.
|
|
287
|
-
const sns: any = await import(moduleName);
|
|
288
|
-
const client = new sns.SNSClient({ region: this.config.region });
|
|
289
|
-
const isFifo = topic.endsWith(".fifo");
|
|
290
|
-
const params: Record<string, unknown> = {
|
|
291
|
-
TopicArn: topic,
|
|
292
|
-
Message: typeof payload === "string" ? payload : JSON.stringify(payload),
|
|
293
|
-
};
|
|
294
|
-
if (isFifo) {
|
|
295
|
-
params.MessageGroupId = opts?.partitionKey ?? opts?.orderingKey ?? "default";
|
|
296
|
-
params.MessageDeduplicationId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
await client.send(new sns.PublishCommand(params));
|
|
300
|
-
} finally {
|
|
301
|
-
client.destroy?.();
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Health check - verify AWS connectivity
|
|
307
|
-
*/
|
|
308
|
-
async healthCheck(): Promise<boolean> {
|
|
309
|
-
if (!this.connected) return false;
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const { ListQueuesCommand } = await import("@aws-sdk/client-sqs");
|
|
313
|
-
const command = new ListQueuesCommand({ MaxResults: 1 });
|
|
314
|
-
await this.requireSqsClient().send(command);
|
|
315
|
-
return true;
|
|
316
|
-
} catch {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export default AWSSNSAdapter;
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AzureServiceBusAdapter - Azure Service Bus adapter for PubSubTrigger
|
|
3
|
-
*
|
|
4
|
-
* Uses @azure/service-bus for Azure Service Bus connectivity.
|
|
5
|
-
* Requires: npm install @azure/service-bus
|
|
6
|
-
*
|
|
7
|
-
* Environment variables:
|
|
8
|
-
* - AZURE_SERVICE_BUS_CONNECTION_STRING: Azure Service Bus connection string
|
|
9
|
-
* - AZURE_SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE: Fully qualified namespace (if using DefaultAzureCredential)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
ProcessErrorArgs,
|
|
14
|
-
ServiceBusClient,
|
|
15
|
-
ServiceBusReceivedMessage,
|
|
16
|
-
ServiceBusReceiver,
|
|
17
|
-
} from "@azure/service-bus";
|
|
18
|
-
import type { PubSubTriggerOpts } from "@blokjs/helper";
|
|
19
|
-
import { v4 as uuid } from "uuid";
|
|
20
|
-
import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Azure Service Bus configuration
|
|
24
|
-
*/
|
|
25
|
-
export interface AzureServiceBusConfig {
|
|
26
|
-
connectionString?: string;
|
|
27
|
-
fullyQualifiedNamespace?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* AzureServiceBusAdapter - Azure Service Bus implementation
|
|
32
|
-
*/
|
|
33
|
-
export class AzureServiceBusAdapter implements PubSubAdapter {
|
|
34
|
-
readonly provider = "azure" as const;
|
|
35
|
-
|
|
36
|
-
private client: ServiceBusClient | undefined;
|
|
37
|
-
private receivers: Map<string, ServiceBusReceiver> = new Map();
|
|
38
|
-
private connected = false;
|
|
39
|
-
private config: AzureServiceBusConfig;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Type-narrowing accessor for `this.client`. Field is undefined until
|
|
43
|
-
* `connect()` runs.
|
|
44
|
-
*/
|
|
45
|
-
private requireClient(): ServiceBusClient {
|
|
46
|
-
if (!this.client) {
|
|
47
|
-
throw new Error("[AzureServiceBusAdapter] client is not initialised — call connect() first");
|
|
48
|
-
}
|
|
49
|
-
return this.client;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
constructor(config?: AzureServiceBusConfig) {
|
|
53
|
-
this.config = {
|
|
54
|
-
connectionString: config?.connectionString || process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
|
|
55
|
-
fullyQualifiedNamespace:
|
|
56
|
-
config?.fullyQualifiedNamespace || process.env.AZURE_SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Connect to Azure Service Bus
|
|
62
|
-
*/
|
|
63
|
-
async connect(): Promise<void> {
|
|
64
|
-
if (this.connected) return;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
// Dynamic import of @azure/service-bus
|
|
68
|
-
const { ServiceBusClient } = await import("@azure/service-bus");
|
|
69
|
-
|
|
70
|
-
if (this.config.connectionString) {
|
|
71
|
-
this.client = new ServiceBusClient(this.config.connectionString);
|
|
72
|
-
} else if (this.config.fullyQualifiedNamespace) {
|
|
73
|
-
// Would need @azure/identity for DefaultAzureCredential
|
|
74
|
-
throw new Error("Managed identity authentication requires @azure/identity package");
|
|
75
|
-
} else {
|
|
76
|
-
throw new Error("Either connectionString or fullyQualifiedNamespace is required");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.connected = true;
|
|
80
|
-
console.log("[AzureServiceBusAdapter] Connected to Azure Service Bus");
|
|
81
|
-
} catch (error) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`Failed to connect to Azure Service Bus: ${(error as Error).message}. Make sure @azure/service-bus is installed: npm install @azure/service-bus`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Disconnect from Azure Service Bus
|
|
90
|
-
*/
|
|
91
|
-
async disconnect(): Promise<void> {
|
|
92
|
-
if (!this.connected) return;
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
// Close all receivers
|
|
96
|
-
for (const [name, receiver] of this.receivers) {
|
|
97
|
-
await receiver.close();
|
|
98
|
-
}
|
|
99
|
-
this.receivers.clear();
|
|
100
|
-
|
|
101
|
-
await this.requireClient().close();
|
|
102
|
-
this.connected = false;
|
|
103
|
-
console.log("[AzureServiceBusAdapter] Disconnected from Azure Service Bus");
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error(`[AzureServiceBusAdapter] Error disconnecting: ${(error as Error).message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Subscribe to an Azure Service Bus topic/subscription or queue
|
|
111
|
-
*/
|
|
112
|
-
async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
|
|
113
|
-
if (!this.connected) {
|
|
114
|
-
throw new Error("Not connected to Azure Service Bus. Call connect() first.");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const client = this.requireClient();
|
|
118
|
-
let receiver: ServiceBusReceiver;
|
|
119
|
-
|
|
120
|
-
// Determine if this is a topic subscription or a queue
|
|
121
|
-
if (config.subscription && config.topic) {
|
|
122
|
-
// Topic with subscription
|
|
123
|
-
receiver = client.createReceiver(config.topic, config.subscription, {
|
|
124
|
-
receiveMode: config.ack !== false ? "peekLock" : "receiveAndDelete",
|
|
125
|
-
});
|
|
126
|
-
} else {
|
|
127
|
-
// Queue
|
|
128
|
-
receiver = client.createReceiver(config.subscription || config.topic, {
|
|
129
|
-
receiveMode: config.ack !== false ? "peekLock" : "receiveAndDelete",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const subscriptionKey = `${config.topic}/${config.subscription}`;
|
|
134
|
-
|
|
135
|
-
// Message handler
|
|
136
|
-
const processMessage = async (sbMessage: ServiceBusReceivedMessage) => {
|
|
137
|
-
// Parse message body
|
|
138
|
-
let body: unknown;
|
|
139
|
-
try {
|
|
140
|
-
if (typeof sbMessage.body === "string") {
|
|
141
|
-
body = JSON.parse(sbMessage.body);
|
|
142
|
-
} else {
|
|
143
|
-
body = sbMessage.body;
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
body = sbMessage.body;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Extract application properties as attributes
|
|
150
|
-
const attributes: Record<string, string> = {};
|
|
151
|
-
if (sbMessage.applicationProperties) {
|
|
152
|
-
for (const [key, value] of Object.entries(sbMessage.applicationProperties)) {
|
|
153
|
-
attributes[key] = String(value);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Create pub/sub message
|
|
158
|
-
const pubsubMessage: PubSubMessage = {
|
|
159
|
-
// `messageId` may be string | number | Buffer per Azure SDK; coerce to string.
|
|
160
|
-
id: sbMessage.messageId !== undefined ? String(sbMessage.messageId) : uuid(),
|
|
161
|
-
body,
|
|
162
|
-
attributes,
|
|
163
|
-
raw: sbMessage,
|
|
164
|
-
topic: config.topic,
|
|
165
|
-
subscription: config.subscription,
|
|
166
|
-
publishTime: sbMessage.enqueuedTimeUtc ? new Date(sbMessage.enqueuedTimeUtc) : new Date(),
|
|
167
|
-
ack: async () => {
|
|
168
|
-
await receiver.completeMessage(sbMessage);
|
|
169
|
-
},
|
|
170
|
-
nack: async () => {
|
|
171
|
-
await receiver.abandonMessage(sbMessage);
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// Process message
|
|
176
|
-
try {
|
|
177
|
-
await handler(pubsubMessage);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error(`[AzureServiceBusAdapter] Error processing message: ${(error as Error).message}`);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// Error handler — Azure SDK passes a `ProcessErrorArgs` with the
|
|
184
|
-
// underlying error inside `args.error` plus contextual fields.
|
|
185
|
-
const processError = async (args: ProcessErrorArgs) => {
|
|
186
|
-
console.error(`[AzureServiceBusAdapter] Error: ${args.error.message}`);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Subscribe to messages
|
|
190
|
-
receiver.subscribe({
|
|
191
|
-
processMessage,
|
|
192
|
-
processError,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
this.receivers.set(subscriptionKey, receiver);
|
|
196
|
-
|
|
197
|
-
console.log(`[AzureServiceBusAdapter] Subscribed to: ${subscriptionKey}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Unsubscribe from Azure Service Bus
|
|
202
|
-
*/
|
|
203
|
-
async unsubscribe(subscriptionKey: string): Promise<void> {
|
|
204
|
-
const receiver = this.receivers.get(subscriptionKey);
|
|
205
|
-
if (receiver) {
|
|
206
|
-
await receiver.close();
|
|
207
|
-
this.receivers.delete(subscriptionKey);
|
|
208
|
-
console.log(`[AzureServiceBusAdapter] Unsubscribed from: ${subscriptionKey}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Check if connected to Azure Service Bus
|
|
214
|
-
*/
|
|
215
|
-
isConnected(): boolean {
|
|
216
|
-
return this.connected;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* v0.7 PR 6 — publish to an Azure Service Bus topic.
|
|
221
|
-
*
|
|
222
|
-
* `partitionKey` maps to Service Bus's `partitionKey`; `orderingKey`
|
|
223
|
-
* maps to `sessionId` (for session-enabled topics).
|
|
224
|
-
*/
|
|
225
|
-
async publish(
|
|
226
|
-
topic: string,
|
|
227
|
-
payload: unknown,
|
|
228
|
-
opts?: { partitionKey?: string; orderingKey?: string },
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
if (!this.connected) throw new Error("[blok][pubsub-azure] not connected. Call connect() first.");
|
|
231
|
-
const sender = this.requireClient().createSender(topic);
|
|
232
|
-
try {
|
|
233
|
-
await sender.sendMessages({
|
|
234
|
-
body: payload,
|
|
235
|
-
partitionKey: opts?.partitionKey,
|
|
236
|
-
sessionId: opts?.orderingKey,
|
|
237
|
-
});
|
|
238
|
-
} finally {
|
|
239
|
-
await sender.close();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Health check - verify Azure Service Bus connectivity
|
|
245
|
-
*/
|
|
246
|
-
async healthCheck(): Promise<boolean> {
|
|
247
|
-
if (!this.connected) return false;
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
// Create a temporary receiver to test connectivity
|
|
251
|
-
const testReceiver = this.requireClient().createReceiver("$default", {
|
|
252
|
-
receiveMode: "peekLock",
|
|
253
|
-
});
|
|
254
|
-
await testReceiver.close();
|
|
255
|
-
return true;
|
|
256
|
-
} catch {
|
|
257
|
-
// Queue might not exist but connection is healthy if we got here
|
|
258
|
-
return this.connected;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export default AzureServiceBusAdapter;
|