@geekmidas/events 10.0.0-alpha.9 → 10.0.0-alpha.91
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 +46 -1
- package/dist/SQSSubscriber-BEJBfNTe.mjs +264 -0
- package/dist/SQSSubscriber-BEJBfNTe.mjs.map +1 -0
- package/dist/SQSSubscriber-COTqWvOk.cjs +288 -0
- package/dist/SQSSubscriber-COTqWvOk.cjs.map +1 -0
- package/dist/basic/index.cjs +41 -11
- package/dist/basic/index.cjs.map +1 -1
- package/dist/basic/index.d.cts +11 -10
- package/dist/basic/index.d.cts.map +1 -1
- package/dist/basic/index.d.mts +11 -10
- package/dist/basic/index.d.mts.map +1 -1
- package/dist/basic/index.mjs +39 -10
- package/dist/basic/index.mjs.map +1 -1
- package/dist/index.cjs +47 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -26
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +82 -26
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +34 -151
- package/dist/index.mjs.map +1 -1
- package/dist/pgboss/index.cjs +198 -34
- package/dist/pgboss/index.cjs.map +1 -1
- package/dist/pgboss/index.d.cts +54 -12
- package/dist/pgboss/index.d.cts.map +1 -1
- package/dist/pgboss/index.d.mts +54 -12
- package/dist/pgboss/index.d.mts.map +1 -1
- package/dist/pgboss/index.mjs +193 -34
- package/dist/pgboss/index.mjs.map +1 -1
- package/dist/rabbitmq/index.cjs +173 -27
- package/dist/rabbitmq/index.cjs.map +1 -1
- package/dist/rabbitmq/index.d.cts +19 -10
- package/dist/rabbitmq/index.d.cts.map +1 -1
- package/dist/rabbitmq/index.d.mts +19 -10
- package/dist/rabbitmq/index.d.mts.map +1 -1
- package/dist/rabbitmq/index.mjs +169 -27
- package/dist/rabbitmq/index.mjs.map +1 -1
- package/dist/registry-DIx25Q5s.d.cts +255 -0
- package/dist/registry-DIx25Q5s.d.cts.map +1 -0
- package/dist/registry-DIx25Q5s.d.mts +255 -0
- package/dist/registry-DIx25Q5s.d.mts.map +1 -0
- package/dist/registry-DXLY1Xf6.mjs +99 -0
- package/dist/registry-DXLY1Xf6.mjs.map +1 -0
- package/dist/registry-DlT36229.cjs +122 -0
- package/dist/registry-DlT36229.cjs.map +1 -0
- package/dist/{chunk-CsX-DzYB.cjs → rolldown-runtime-C0BPl7ul.cjs} +17 -16
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/sns/index.cjs +403 -40
- package/dist/sns/index.cjs.map +1 -1
- package/dist/sns/index.d.cts +175 -11
- package/dist/sns/index.d.cts.map +1 -1
- package/dist/sns/index.d.mts +175 -11
- package/dist/sns/index.d.mts.map +1 -1
- package/dist/sns/index.mjs +372 -24
- package/dist/sns/index.mjs.map +1 -1
- package/dist/sns/snsUrl.cjs +91 -6
- package/dist/sns/snsUrl.cjs.map +1 -0
- package/dist/sns/snsUrl.d.cts +57 -2
- package/dist/sns/snsUrl.d.cts.map +1 -0
- package/dist/sns/snsUrl.d.mts +57 -2
- package/dist/sns/snsUrl.d.mts.map +1 -0
- package/dist/sns/snsUrl.mjs +81 -2
- package/dist/sns/snsUrl.mjs.map +1 -0
- package/dist/sqs/index.cjs +31 -65
- package/dist/sqs/index.cjs.map +1 -1
- package/dist/sqs/index.d.cts +28 -11
- package/dist/sqs/index.d.cts.map +1 -1
- package/dist/sqs/index.d.mts +28 -11
- package/dist/sqs/index.d.mts.map +1 -1
- package/dist/sqs/index.mjs +22 -58
- package/dist/sqs/index.mjs.map +1 -1
- package/dist/sqs/sqsUrl.cjs +92 -6
- package/dist/sqs/sqsUrl.cjs.map +1 -0
- package/dist/sqs/sqsUrl.d.cts +56 -2
- package/dist/sqs/sqsUrl.d.cts.map +1 -0
- package/dist/sqs/sqsUrl.d.mts +56 -2
- package/dist/sqs/sqsUrl.d.mts.map +1 -0
- package/dist/sqs/sqsUrl.mjs +82 -2
- package/dist/sqs/sqsUrl.mjs.map +1 -0
- package/dist/telemetry-DG3l807O.cjs +309 -0
- package/dist/telemetry-DG3l807O.cjs.map +1 -0
- package/dist/telemetry-DjjgJMul.mjs +244 -0
- package/dist/telemetry-DjjgJMul.mjs.map +1 -0
- package/package.json +8 -3
- package/dist/PgBossConnection-23soZ_aM.cjs +0 -80
- package/dist/PgBossConnection-23soZ_aM.cjs.map +0 -1
- package/dist/PgBossConnection-CCXhoNzb.cjs +0 -4
- package/dist/PgBossConnection-DvNbd4Zk.mjs +0 -74
- package/dist/PgBossConnection-DvNbd4Zk.mjs.map +0 -1
- package/dist/PgBossConnection-_gdM40JB.mjs +0 -4
- package/dist/RabbitMQConnection-BKEdCUro.cjs +0 -113
- package/dist/RabbitMQConnection-BKEdCUro.cjs.map +0 -1
- package/dist/RabbitMQConnection-CCOqDDnb.cjs +0 -4
- package/dist/RabbitMQConnection-DJEyM-Bu.mjs +0 -4
- package/dist/RabbitMQConnection-DJgUPc19.mjs +0 -107
- package/dist/RabbitMQConnection-DJgUPc19.mjs.map +0 -1
- package/dist/SNSConnection-B2K86hpz.cjs +0 -75
- package/dist/SNSConnection-B2K86hpz.cjs.map +0 -1
- package/dist/SNSConnection-BVo84FeW.mjs +0 -5
- package/dist/SNSConnection-DIHRjrQF.cjs +0 -5
- package/dist/SNSConnection-v9jjF99w.mjs +0 -69
- package/dist/SNSConnection-v9jjF99w.mjs.map +0 -1
- package/dist/SQSConnection-4tBMkVZE.mjs +0 -69
- package/dist/SQSConnection-4tBMkVZE.mjs.map +0 -1
- package/dist/SQSConnection-BBLAvF-1.cjs +0 -5
- package/dist/SQSConnection-BaC7rGhI.cjs +0 -75
- package/dist/SQSConnection-BaC7rGhI.cjs.map +0 -1
- package/dist/SQSConnection-DbavOq8_.mjs +0 -5
- package/dist/SQSSubscriber-BMWZ3s5q.mjs +0 -114
- package/dist/SQSSubscriber-BMWZ3s5q.mjs.map +0 -1
- package/dist/SQSSubscriber-YXPlg2rm.cjs +0 -120
- package/dist/SQSSubscriber-YXPlg2rm.cjs.map +0 -1
- package/dist/chunk-B9dir_RE.mjs +0 -11
- package/dist/snsUrl-ASdc0D1v.d.cts +0 -61
- package/dist/snsUrl-ASdc0D1v.d.cts.map +0 -1
- package/dist/snsUrl-CifYhPAa.mjs +0 -79
- package/dist/snsUrl-CifYhPAa.mjs.map +0 -1
- package/dist/snsUrl-FRbRcwF0.d.mts +0 -61
- package/dist/snsUrl-FRbRcwF0.d.mts.map +0 -1
- package/dist/snsUrl-f2gU3CzK.cjs +0 -114
- package/dist/snsUrl-f2gU3CzK.cjs.map +0 -1
- package/dist/sqsUrl-BV6zS-mi.d.mts +0 -60
- package/dist/sqsUrl-BV6zS-mi.d.mts.map +0 -1
- package/dist/sqsUrl-CbAsBFHI.mjs +0 -80
- package/dist/sqsUrl-CbAsBFHI.mjs.map +0 -1
- package/dist/sqsUrl-CdisN3GU.cjs +0 -115
- package/dist/sqsUrl-CdisN3GU.cjs.map +0 -1
- package/dist/sqsUrl-DZDPHxIk.d.cts +0 -60
- package/dist/sqsUrl-DZDPHxIk.d.cts.map +0 -1
- package/dist/types-BmixGrOD.d.cts +0 -67
- package/dist/types-BmixGrOD.d.cts.map +0 -1
- package/dist/types-Byv8N9ma.cjs +0 -24
- package/dist/types-Byv8N9ma.cjs.map +0 -1
- package/dist/types-CYDt-2RQ.mjs +0 -18
- package/dist/types-CYDt-2RQ.mjs.map +0 -1
- package/dist/types-DycjsYms.d.mts +0 -67
- package/dist/types-DycjsYms.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -28,8 +28,38 @@ pnpm add amqplib
|
|
|
28
28
|
|
|
29
29
|
# For AWS SQS/SNS
|
|
30
30
|
pnpm add @aws-sdk/client-sqs @aws-sdk/client-sns
|
|
31
|
+
|
|
32
|
+
# For pg-boss (PostgreSQL)
|
|
33
|
+
pnpm add pg-boss
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
### Registering a Broker
|
|
37
|
+
|
|
38
|
+
Each broker is a driver on its own subpath — the only module that imports its
|
|
39
|
+
peer dependency — and the core uses whichever ones the process registered. A
|
|
40
|
+
bundle therefore contains only the brokers its entry point registers.
|
|
41
|
+
|
|
42
|
+
| Subpath | Driver |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `@geekmidas/events/basic` | `basicEventsDriver` |
|
|
45
|
+
| `@geekmidas/events/pgboss` | `pgbossEventsDriver` |
|
|
46
|
+
| `@geekmidas/events/rabbitmq` | `rabbitmqEventsDriver` |
|
|
47
|
+
| `@geekmidas/events/sns` | `snsEventsDriver` |
|
|
48
|
+
| `@geekmidas/events/sqs` | `sqsEventsDriver` |
|
|
49
|
+
|
|
50
|
+
Code generated by `gkm` (dev, build, test, Lambda handlers) registers its
|
|
51
|
+
target's broker. A script that builds its own publisher registers it once:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { registerEventsDriver } from '@geekmidas/events';
|
|
55
|
+
import { pgbossEventsDriver } from '@geekmidas/events/pgboss';
|
|
56
|
+
|
|
57
|
+
registerEventsDriver(pgbossEventsDriver);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without it, `Publisher.fromConnectionString('pgboss://…')` throws
|
|
61
|
+
`UnregisteredEventsScheme`, which names the subpath and the call to add.
|
|
62
|
+
|
|
33
63
|
## Quick Start
|
|
34
64
|
|
|
35
65
|
### 1. Define Your Message Types
|
|
@@ -46,7 +76,14 @@ type UserEvents =
|
|
|
46
76
|
### 2. Create Publisher and Subscriber
|
|
47
77
|
|
|
48
78
|
```typescript
|
|
49
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
Publisher,
|
|
81
|
+
registerEventsDriver,
|
|
82
|
+
Subscriber,
|
|
83
|
+
} from '@geekmidas/events';
|
|
84
|
+
import { rabbitmqEventsDriver } from '@geekmidas/events/rabbitmq';
|
|
85
|
+
|
|
86
|
+
registerEventsDriver(rabbitmqEventsDriver);
|
|
50
87
|
|
|
51
88
|
// Create publisher from connection string
|
|
52
89
|
const publisher = await Publisher.fromConnectionString<UserEvents>(
|
|
@@ -411,6 +448,14 @@ Then use `endpoint: 'http://localhost:4566'` in your configuration.
|
|
|
411
448
|
|
|
412
449
|
## API Reference
|
|
413
450
|
|
|
451
|
+
### Registry
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
function registerEventsDriver(driver: EventsDriver): void
|
|
455
|
+
function eventsDriverFor(scheme: string): EventsDriver // throws UnregisteredEventsScheme
|
|
456
|
+
function registeredEventsSchemes(): string[]
|
|
457
|
+
```
|
|
458
|
+
|
|
414
459
|
### Publisher
|
|
415
460
|
|
|
416
461
|
```typescript
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { a as consumeTraced, n as carrierAsAttributes, r as carrierFromAttributes, s as publishTraced } from "./telemetry-DjjgJMul.mjs";
|
|
3
|
+
import { parse } from "./sqs/sqsUrl.mjs";
|
|
4
|
+
import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
import chunk from "lodash.chunk";
|
|
6
|
+
//#region src/sqs/SQSConnection.ts
|
|
7
|
+
var SQSConnection_exports = /* @__PURE__ */ __exportAll({ SQSConnection: () => SQSConnection });
|
|
8
|
+
var SQSConnection = class SQSConnection {
|
|
9
|
+
config;
|
|
10
|
+
type = "sqs";
|
|
11
|
+
client;
|
|
12
|
+
connected = false;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.config = config;
|
|
16
|
+
const clientConfig = {
|
|
17
|
+
region: config.region,
|
|
18
|
+
endpoint: config.endpoint,
|
|
19
|
+
credentials: config.credentials
|
|
20
|
+
};
|
|
21
|
+
this.client = new SQSClient(clientConfig);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create an SQSConnection from a connection string
|
|
25
|
+
* Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566
|
|
26
|
+
*/
|
|
27
|
+
static async fromConnectionString(connectionString) {
|
|
28
|
+
const { queueUrl, region, endpoint } = parse(connectionString);
|
|
29
|
+
const params = new URL(connectionString).searchParams;
|
|
30
|
+
const config = {
|
|
31
|
+
queueUrl,
|
|
32
|
+
region,
|
|
33
|
+
endpoint
|
|
34
|
+
};
|
|
35
|
+
const accessKeyId = params.get("accessKeyId");
|
|
36
|
+
const secretAccessKey = params.get("secretAccessKey");
|
|
37
|
+
if (accessKeyId && secretAccessKey) config.credentials = {
|
|
38
|
+
accessKeyId,
|
|
39
|
+
secretAccessKey,
|
|
40
|
+
sessionToken: params.get("sessionToken") || void 0
|
|
41
|
+
};
|
|
42
|
+
const connection = new SQSConnection(config);
|
|
43
|
+
await connection.connect();
|
|
44
|
+
return connection;
|
|
45
|
+
}
|
|
46
|
+
async connect() {
|
|
47
|
+
this.connected = true;
|
|
48
|
+
}
|
|
49
|
+
async close() {
|
|
50
|
+
this.client.destroy();
|
|
51
|
+
this.connected = false;
|
|
52
|
+
}
|
|
53
|
+
isConnected() {
|
|
54
|
+
return this.connected;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the underlying SQS client
|
|
58
|
+
*/
|
|
59
|
+
get sqsClient() {
|
|
60
|
+
return this.client;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the queue URL for this connection
|
|
64
|
+
*/
|
|
65
|
+
get queueUrl() {
|
|
66
|
+
return this.config.queueUrl;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/sqs/SQSPublisher.ts
|
|
71
|
+
var SQSPublisher = class SQSPublisher {
|
|
72
|
+
connection;
|
|
73
|
+
options;
|
|
74
|
+
constructor(connection, options = {}) {
|
|
75
|
+
this.connection = connection;
|
|
76
|
+
this.options = { maxBatchSize: options.maxBatchSize ?? 10 };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create an SQSPublisher from a connection string
|
|
80
|
+
* Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566&maxBatchSize=10
|
|
81
|
+
*/
|
|
82
|
+
static async fromConnectionString(connectionString) {
|
|
83
|
+
const params = new URL(connectionString).searchParams;
|
|
84
|
+
const { SQSConnection } = await Promise.resolve().then(() => SQSConnection_exports);
|
|
85
|
+
const connection = await SQSConnection.fromConnectionString(connectionString);
|
|
86
|
+
const options = { maxBatchSize: params.get("maxBatchSize") ? Number.parseInt(params.get("maxBatchSize"), 10) : void 0 };
|
|
87
|
+
return new SQSPublisher(connection, options);
|
|
88
|
+
}
|
|
89
|
+
async publish(messages) {
|
|
90
|
+
if (messages.length === 0) return;
|
|
91
|
+
if (!this.connection.isConnected()) await this.connection.connect();
|
|
92
|
+
const batches = chunk(messages, this.options.maxBatchSize);
|
|
93
|
+
await Promise.all(batches.map((batch) => this.sendBatch(batch)));
|
|
94
|
+
}
|
|
95
|
+
async sendBatch(messages) {
|
|
96
|
+
const destination = sqsQueueName(this.connection.queueUrl);
|
|
97
|
+
await publishTraced(messages, (message) => ({
|
|
98
|
+
system: "aws_sqs",
|
|
99
|
+
destination,
|
|
100
|
+
type: message.type
|
|
101
|
+
}), (carriers) => this.send(messages.map((message, index) => ({
|
|
102
|
+
Id: `${index}`,
|
|
103
|
+
MessageBody: JSON.stringify({
|
|
104
|
+
type: message.type,
|
|
105
|
+
payload: message.payload
|
|
106
|
+
}),
|
|
107
|
+
MessageAttributes: {
|
|
108
|
+
type: {
|
|
109
|
+
DataType: "String",
|
|
110
|
+
StringValue: message.type
|
|
111
|
+
},
|
|
112
|
+
...carrierAsAttributes(carriers[index])
|
|
113
|
+
}
|
|
114
|
+
}))));
|
|
115
|
+
}
|
|
116
|
+
async send(entries) {
|
|
117
|
+
const input = {
|
|
118
|
+
QueueUrl: this.connection.queueUrl,
|
|
119
|
+
Entries: entries
|
|
120
|
+
};
|
|
121
|
+
const command = new SendMessageBatchCommand(input);
|
|
122
|
+
const response = await this.connection.sqsClient.send(command);
|
|
123
|
+
if (response.Failed && response.Failed.length > 0) throw new SqsBatchPartlyFailed(response.Failed.map((f) => ({
|
|
124
|
+
id: f.Id,
|
|
125
|
+
code: f.Code,
|
|
126
|
+
message: f.Message
|
|
127
|
+
})));
|
|
128
|
+
}
|
|
129
|
+
async close() {}
|
|
130
|
+
};
|
|
131
|
+
/** SQS accepted the batch but refused some of its messages; the rest were sent. */
|
|
132
|
+
var SqsBatchPartlyFailed = class extends Error {
|
|
133
|
+
failed;
|
|
134
|
+
constructor(failed) {
|
|
135
|
+
super(`Failed to send ${failed.length} messages: ${failed.map((f) => `${f.id}: ${f.code} - ${f.message}`).join(", ")}. The others in the batch were sent; retry only these.`);
|
|
136
|
+
this.failed = failed;
|
|
137
|
+
this.name = "SqsBatchPartlyFailed";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/** The queue's name, the last segment of its URL. */
|
|
141
|
+
function sqsQueueName(queueUrl) {
|
|
142
|
+
return queueUrl.split("/").filter(Boolean).pop() ?? queueUrl;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/sqs/SQSSubscriber.ts
|
|
146
|
+
/**
|
|
147
|
+
* SQS Subscriber - receives messages from an SQS queue
|
|
148
|
+
*
|
|
149
|
+
* This subscriber can receive:
|
|
150
|
+
* 1. Direct SQS messages
|
|
151
|
+
* 2. SNS messages that were pushed to the queue (SNS → SQS pattern)
|
|
152
|
+
*
|
|
153
|
+
* When receiving SNS messages, it:
|
|
154
|
+
* - Automatically detects and parses the SNS wrapper format
|
|
155
|
+
* - Filters by TopicArn (if expectedTopicArn is provided)
|
|
156
|
+
* - Filters by message type
|
|
157
|
+
* - Processes the actual message payload
|
|
158
|
+
*/
|
|
159
|
+
var SQSSubscriber = class {
|
|
160
|
+
connection;
|
|
161
|
+
polling = false;
|
|
162
|
+
messageTypes = /* @__PURE__ */ new Set();
|
|
163
|
+
listener;
|
|
164
|
+
options;
|
|
165
|
+
constructor(connection, options = {}) {
|
|
166
|
+
this.connection = connection;
|
|
167
|
+
this.options = {
|
|
168
|
+
waitTimeSeconds: options.waitTimeSeconds ?? 20,
|
|
169
|
+
maxMessages: options.maxMessages ?? 10,
|
|
170
|
+
expectedTopicArn: options.expectedTopicArn ?? ""
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async subscribe(messages, listener) {
|
|
174
|
+
this.messageTypes = new Set(messages);
|
|
175
|
+
this.listener = listener;
|
|
176
|
+
if (!this.connection.isConnected()) await this.connection.connect();
|
|
177
|
+
this.polling = true;
|
|
178
|
+
this.poll();
|
|
179
|
+
}
|
|
180
|
+
stop() {
|
|
181
|
+
this.polling = false;
|
|
182
|
+
}
|
|
183
|
+
async poll() {
|
|
184
|
+
while (this.polling) try {
|
|
185
|
+
const command = new ReceiveMessageCommand({
|
|
186
|
+
QueueUrl: this.connection.queueUrl,
|
|
187
|
+
MaxNumberOfMessages: this.options.maxMessages,
|
|
188
|
+
WaitTimeSeconds: this.options.waitTimeSeconds,
|
|
189
|
+
MessageAttributeNames: ["All"]
|
|
190
|
+
});
|
|
191
|
+
const response = await this.connection.sqsClient.send(command);
|
|
192
|
+
if (response.Messages && response.Messages.length > 0) await this.processMessages(response.Messages);
|
|
193
|
+
} catch (_error) {
|
|
194
|
+
await this.sleep(1e3);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async processMessages(messages) {
|
|
198
|
+
for (const message of messages) try {
|
|
199
|
+
if (!message.Body) continue;
|
|
200
|
+
const parsedMessage = this.parseMessage(message.Body, message.MessageAttributes);
|
|
201
|
+
if (!parsedMessage) {
|
|
202
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const { payload, messageType, topicArn, carrier } = parsedMessage;
|
|
206
|
+
if (this.options.expectedTopicArn && topicArn) {
|
|
207
|
+
if (topicArn !== this.options.expectedTopicArn) {
|
|
208
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (messageType && this.messageTypes.has(messageType)) {
|
|
213
|
+
const listener = this.listener;
|
|
214
|
+
if (listener) await consumeTraced({
|
|
215
|
+
system: "aws_sqs",
|
|
216
|
+
destination: sqsQueueName(this.connection.queueUrl),
|
|
217
|
+
type: messageType,
|
|
218
|
+
messageId: message.MessageId
|
|
219
|
+
}, carrier, () => listener(payload));
|
|
220
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
221
|
+
} else await this.deleteMessage(message.ReceiptHandle);
|
|
222
|
+
} catch (_error) {}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parse message body - handles both direct SQS messages and SNS-wrapped messages
|
|
226
|
+
*/
|
|
227
|
+
parseMessage(body, attributes) {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(body);
|
|
230
|
+
if (parsed.Type === "Notification" && parsed.Message) {
|
|
231
|
+
const snsMessage = JSON.parse(parsed.Message);
|
|
232
|
+
return {
|
|
233
|
+
payload: snsMessage,
|
|
234
|
+
messageType: parsed.MessageAttributes?.type?.Value || snsMessage.type,
|
|
235
|
+
topicArn: parsed.TopicArn,
|
|
236
|
+
carrier: carrierFromAttributes(parsed.MessageAttributes)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
payload: parsed,
|
|
241
|
+
messageType: parsed.type,
|
|
242
|
+
carrier: carrierFromAttributes(attributes)
|
|
243
|
+
};
|
|
244
|
+
} catch (_error) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async deleteMessage(receiptHandle) {
|
|
249
|
+
try {
|
|
250
|
+
const command = new DeleteMessageCommand({
|
|
251
|
+
QueueUrl: this.connection.queueUrl,
|
|
252
|
+
ReceiptHandle: receiptHandle
|
|
253
|
+
});
|
|
254
|
+
await this.connection.sqsClient.send(command);
|
|
255
|
+
} catch (_error) {}
|
|
256
|
+
}
|
|
257
|
+
sleep(ms) {
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
export { SQSConnection as i, SQSPublisher as n, SqsBatchPartlyFailed as r, SQSSubscriber as t };
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=SQSSubscriber-BEJBfNTe.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SQSSubscriber-BEJBfNTe.mjs","names":["sqsUrl.parse"],"sources":["../src/sqs/SQSConnection.ts","../src/sqs/SQSPublisher.ts","../src/sqs/SQSSubscriber.ts"],"sourcesContent":["import { SQSClient, type SQSClientConfig } from '@aws-sdk/client-sqs';\nimport type { EventConnection } from '../types';\nimport { EventPublisherType } from '../types';\nimport * as sqsUrl from './sqsUrl';\n\nexport interface SQSConnectionConfig {\n\tqueueUrl: string;\n\tregion?: string;\n\tendpoint?: string; // Custom endpoint (e.g., for LocalStack)\n\tcredentials?: {\n\t\taccessKeyId: string;\n\t\tsecretAccessKey: string;\n\t\tsessionToken?: string;\n\t};\n}\n\nexport class SQSConnection implements EventConnection {\n\treadonly type = EventPublisherType.SQS;\n\tprivate client: SQSClient;\n\tprivate connected = false;\n\n\tconstructor(readonly config: SQSConnectionConfig) {\n\t\tthis.config = config;\n\n\t\tconst clientConfig: SQSClientConfig = {\n\t\t\tregion: config.region,\n\t\t\tendpoint: config.endpoint,\n\t\t\tcredentials: config.credentials,\n\t\t};\n\n\t\tthis.client = new SQSClient(clientConfig);\n\t}\n\n\t/**\n\t * Create an SQSConnection from a connection string\n\t * Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566\n\t */\n\tstatic async fromConnectionString(\n\t\tconnectionString: string,\n\t): Promise<SQSConnection> {\n\t\t// Through the codec, not a second hand-rolled parse: the deploy target\n\t\t// composes these with `sqsUrl.build`, and a string it cannot read back is\n\t\t// exactly the drift one shared module rules out.\n\t\tconst { queueUrl, region, endpoint } = sqsUrl.parse(connectionString);\n\t\tconst params = new URL(connectionString).searchParams;\n\n\t\tconst config: SQSConnectionConfig = {\n\t\t\tqueueUrl,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t};\n\n\t\t// Credentials are not part of the address — see `sqsUrl` — but a caller\n\t\t// may still pin them here, so they are read beside it rather than in it.\n\t\tconst accessKeyId = params.get('accessKeyId');\n\t\tconst secretAccessKey = params.get('secretAccessKey');\n\t\tif (accessKeyId && secretAccessKey) {\n\t\t\tconfig.credentials = {\n\t\t\t\taccessKeyId,\n\t\t\t\tsecretAccessKey,\n\t\t\t\tsessionToken: params.get('sessionToken') || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst connection = new SQSConnection(config);\n\t\tawait connection.connect();\n\t\treturn connection;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\t// SQS doesn't have a traditional connection, but we mark as connected\n\t\tthis.connected = true;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.client.destroy();\n\t\tthis.connected = false;\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected;\n\t}\n\n\t/**\n\t * Get the underlying SQS client\n\t */\n\tget sqsClient(): SQSClient {\n\t\treturn this.client;\n\t}\n\n\t/**\n\t * Get the queue URL for this connection\n\t */\n\tget queueUrl(): string {\n\t\treturn this.config.queueUrl;\n\t}\n}\n","import {\n\tSendMessageBatchCommand,\n\ttype SendMessageBatchCommandInput,\n\ttype SendMessageBatchRequestEntry,\n} from '@aws-sdk/client-sqs';\nimport chunk from 'lodash.chunk';\nimport { carrierAsAttributes, publishTraced } from '../telemetry';\nimport type { EventPublisher, PublishableMessage } from '../types';\nimport type { SQSConnection } from './SQSConnection';\n\nexport interface SQSPublisherOptions {\n\tmaxBatchSize?: number; // Default: 10 (SQS limit)\n}\n\nexport class SQSPublisher<TMessage extends PublishableMessage<string, any>>\n\timplements EventPublisher<TMessage>\n{\n\treadonly options: Required<SQSPublisherOptions>;\n\n\tconstructor(\n\t\treadonly connection: SQSConnection,\n\t\toptions: SQSPublisherOptions = {},\n\t) {\n\t\tthis.options = {\n\t\t\tmaxBatchSize: options.maxBatchSize ?? 10, // SQS limit\n\t\t};\n\t}\n\n\t/**\n\t * Create an SQSPublisher from a connection string\n\t * Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566&maxBatchSize=10\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<SQSPublisher<TMessage>> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst { SQSConnection } = await import('./SQSConnection');\n\t\tconst connection =\n\t\t\tawait SQSConnection.fromConnectionString(connectionString);\n\n\t\tconst options: SQSPublisherOptions = {\n\t\t\tmaxBatchSize: params.get('maxBatchSize')\n\t\t\t\t? Number.parseInt(params.get('maxBatchSize')!, 10)\n\t\t\t\t: undefined,\n\t\t};\n\n\t\treturn new SQSPublisher<TMessage>(connection, options);\n\t}\n\n\tasync publish(messages: TMessage[]): Promise<void> {\n\t\tif (messages.length === 0) return;\n\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\t// Split messages into batches (SQS limit is 10 messages per batch)\n\t\tconst batches = chunk(messages, this.options.maxBatchSize);\n\n\t\t// Send all batches\n\t\tawait Promise.all(batches.map((batch) => this.sendBatch(batch)));\n\t}\n\n\tprivate async sendBatch(messages: TMessage[]): Promise<void> {\n\t\tconst destination = sqsQueueName(this.connection.queueUrl);\n\t\tawait publishTraced(\n\t\t\tmessages,\n\t\t\t(message) => ({ system: 'aws_sqs', destination, type: message.type }),\n\t\t\t(carriers) =>\n\t\t\t\tthis.send(\n\t\t\t\t\tmessages.map((message, index) => ({\n\t\t\t\t\t\tId: `${index}`,\n\t\t\t\t\t\tMessageBody: JSON.stringify({\n\t\t\t\t\t\t\ttype: message.type,\n\t\t\t\t\t\t\tpayload: message.payload,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t// The trace context as message attributes — SQS's own header\n\t\t\t\t\t\t// field — beside the type.\n\t\t\t\t\t\tMessageAttributes: {\n\t\t\t\t\t\t\ttype: {\n\t\t\t\t\t\t\t\tDataType: 'String',\n\t\t\t\t\t\t\t\tStringValue: message.type,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...carrierAsAttributes(carriers[index]),\n\t\t\t\t\t\t},\n\t\t\t\t\t})),\n\t\t\t\t),\n\t\t);\n\t}\n\n\tprivate async send(entries: SendMessageBatchRequestEntry[]): Promise<void> {\n\t\tconst input: SendMessageBatchCommandInput = {\n\t\t\tQueueUrl: this.connection.queueUrl,\n\t\t\tEntries: entries,\n\t\t};\n\n\t\tconst command = new SendMessageBatchCommand(input);\n\t\tconst response = await this.connection.sqsClient.send(command);\n\n\t\t// Check for failures\n\t\tif (response.Failed && response.Failed.length > 0) {\n\t\t\tthrow new SqsBatchPartlyFailed(\n\t\t\t\tresponse.Failed.map((f) => ({\n\t\t\t\t\tid: f.Id,\n\t\t\t\t\tcode: f.Code,\n\t\t\t\t\tmessage: f.Message,\n\t\t\t\t})),\n\t\t\t);\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\t// Publisher doesn't own the connection\n\t\t// Connection should be closed by whoever created it\n\t}\n}\n\n/** SQS accepted the batch but refused some of its messages; the rest were sent. */\nexport class SqsBatchPartlyFailed extends Error {\n\tconstructor(\n\t\treadonly failed: readonly {\n\t\t\tid?: string;\n\t\t\tcode?: string;\n\t\t\tmessage?: string;\n\t\t}[],\n\t) {\n\t\tsuper(\n\t\t\t`Failed to send ${failed.length} messages: ${failed.map((f) => `${f.id}: ${f.code} - ${f.message}`).join(', ')}. The others in the batch were sent; retry only these.`,\n\t\t);\n\t\tthis.name = 'SqsBatchPartlyFailed';\n\t}\n}\n\n/** The queue's name, the last segment of its URL. */\nexport function sqsQueueName(queueUrl: string): string {\n\treturn queueUrl.split('/').filter(Boolean).pop() ?? queueUrl;\n}\n","import {\n\tDeleteMessageCommand,\n\ttype Message,\n\tReceiveMessageCommand,\n} from '@aws-sdk/client-sqs';\nimport {\n\tcarrierFromAttributes,\n\tconsumeTraced,\n\ttype TraceCarrier,\n} from '../telemetry';\nimport type { EventSubscriber, PublishableMessage } from '../types';\nimport type { SQSConnection } from './SQSConnection';\nimport { sqsQueueName } from './SQSPublisher';\n\nexport interface SQSSubscriberOptions {\n\twaitTimeSeconds?: number; // SQS long polling (default: 20)\n\tmaxMessages?: number; // Max messages per poll (default: 10)\n\texpectedTopicArn?: string; // Optional: verify messages are from specific SNS topic\n}\n\n/**\n * SQS Subscriber - receives messages from an SQS queue\n *\n * This subscriber can receive:\n * 1. Direct SQS messages\n * 2. SNS messages that were pushed to the queue (SNS → SQS pattern)\n *\n * When receiving SNS messages, it:\n * - Automatically detects and parses the SNS wrapper format\n * - Filters by TopicArn (if expectedTopicArn is provided)\n * - Filters by message type\n * - Processes the actual message payload\n */\nexport class SQSSubscriber<TMessage extends PublishableMessage<string, any>>\n\timplements EventSubscriber<TMessage>\n{\n\tprivate polling = false;\n\tprivate messageTypes: Set<TMessage['type']> = new Set();\n\tprivate listener?: (payload: TMessage) => Promise<void>;\n\tprivate options: Required<SQSSubscriberOptions>;\n\n\tconstructor(\n\t\tprivate connection: SQSConnection,\n\t\toptions: SQSSubscriberOptions = {},\n\t) {\n\t\tthis.options = {\n\t\t\twaitTimeSeconds: options.waitTimeSeconds ?? 20,\n\t\t\tmaxMessages: options.maxMessages ?? 10,\n\t\t\texpectedTopicArn: options.expectedTopicArn ?? '',\n\t\t};\n\t}\n\n\tasync subscribe(\n\t\tmessages: TMessage['type'][],\n\t\tlistener: (payload: TMessage) => Promise<void>,\n\t): Promise<void> {\n\t\tthis.messageTypes = new Set(messages);\n\t\tthis.listener = listener;\n\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\tthis.polling = true;\n\t\tthis.poll();\n\t}\n\n\tstop(): void {\n\t\tthis.polling = false;\n\t}\n\n\tprivate async poll(): Promise<void> {\n\t\twhile (this.polling) {\n\t\t\ttry {\n\t\t\t\tconst command = new ReceiveMessageCommand({\n\t\t\t\t\tQueueUrl: this.connection.queueUrl,\n\t\t\t\t\tMaxNumberOfMessages: this.options.maxMessages,\n\t\t\t\t\tWaitTimeSeconds: this.options.waitTimeSeconds,\n\t\t\t\t\tMessageAttributeNames: ['All'],\n\t\t\t\t});\n\n\t\t\t\tconst response = await this.connection.sqsClient.send(command);\n\n\t\t\t\tif (response.Messages && response.Messages.length > 0) {\n\t\t\t\t\tawait this.processMessages(response.Messages);\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\tawait this.sleep(1000);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async processMessages(messages: Message[]): Promise<void> {\n\t\tfor (const message of messages) {\n\t\t\ttry {\n\t\t\t\tif (!message.Body) continue;\n\n\t\t\t\t// Try to parse as SNS message first\n\t\t\t\tconst parsedMessage = this.parseMessage(\n\t\t\t\t\tmessage.Body,\n\t\t\t\t\tmessage.MessageAttributes,\n\t\t\t\t);\n\n\t\t\t\tif (!parsedMessage) {\n\t\t\t\t\t// Not a valid message format\n\t\t\t\t\tawait this.deleteMessage(message.ReceiptHandle!);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst { payload, messageType, topicArn, carrier } = parsedMessage;\n\n\t\t\t\t// If expectedTopicArn is set, verify the message is from that topic\n\t\t\t\tif (this.options.expectedTopicArn && topicArn) {\n\t\t\t\t\tif (topicArn !== this.options.expectedTopicArn) {\n\t\t\t\t\t\tawait this.deleteMessage(message.ReceiptHandle!);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Check if we're subscribed to this message type\n\t\t\t\tif (\n\t\t\t\t\tmessageType &&\n\t\t\t\t\tthis.messageTypes.has(messageType as TMessage['type'])\n\t\t\t\t) {\n\t\t\t\t\t// Call listener\n\t\t\t\t\tconst listener = this.listener;\n\t\t\t\t\tif (listener) {\n\t\t\t\t\t\tawait consumeTraced(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tsystem: 'aws_sqs',\n\t\t\t\t\t\t\t\tdestination: sqsQueueName(this.connection.queueUrl),\n\t\t\t\t\t\t\t\ttype: messageType,\n\t\t\t\t\t\t\t\tmessageId: message.MessageId,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcarrier,\n\t\t\t\t\t\t\t() => listener(payload as TMessage),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Delete message after successful processing\n\t\t\t\t\tawait this.deleteMessage(message.ReceiptHandle!);\n\t\t\t\t} else {\n\t\t\t\t\t// Delete messages we're not subscribed to\n\t\t\t\t\tawait this.deleteMessage(message.ReceiptHandle!);\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\t// Message will become visible again\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Parse message body - handles both direct SQS messages and SNS-wrapped messages\n\t */\n\tprivate parseMessage(\n\t\tbody: string,\n\t\tattributes: Record<string, unknown> | undefined,\n\t): {\n\t\tpayload: unknown;\n\t\tmessageType: string | undefined;\n\t\ttopicArn?: string;\n\t\tcarrier?: TraceCarrier;\n\t} | null {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(body);\n\n\t\t\t// Check if this is an SNS notification wrapper\n\t\t\tif (parsed.Type === 'Notification' && parsed.Message) {\n\t\t\t\t// This is an SNS message\n\t\t\t\tconst snsMessage = JSON.parse(parsed.Message);\n\t\t\t\tconst messageType =\n\t\t\t\t\tparsed.MessageAttributes?.type?.Value || snsMessage.type;\n\n\t\t\t\treturn {\n\t\t\t\t\tpayload: snsMessage,\n\t\t\t\t\tmessageType,\n\t\t\t\t\ttopicArn: parsed.TopicArn,\n\t\t\t\t\t// SNS's message attributes, carried in its envelope.\n\t\t\t\t\tcarrier: carrierFromAttributes(parsed.MessageAttributes),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// This is a direct SQS message\n\t\t\treturn {\n\t\t\t\tpayload: parsed,\n\t\t\t\tmessageType: parsed.type,\n\t\t\t\tcarrier: carrierFromAttributes(attributes),\n\t\t\t};\n\t\t} catch (_error) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async deleteMessage(receiptHandle: string): Promise<void> {\n\t\ttry {\n\t\t\tconst command = new DeleteMessageCommand({\n\t\t\t\tQueueUrl: this.connection.queueUrl,\n\t\t\t\tReceiptHandle: receiptHandle,\n\t\t\t});\n\t\t\tawait this.connection.sqsClient.send(command);\n\t\t} catch (_error) {}\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
const require_telemetry = require("./telemetry-DG3l807O.cjs");
|
|
3
|
+
const require_sqs_sqsUrl = require("./sqs/sqsUrl.cjs");
|
|
4
|
+
let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
|
|
5
|
+
let lodash_chunk = require("lodash.chunk");
|
|
6
|
+
lodash_chunk = require_rolldown_runtime.__toESM(lodash_chunk, 1);
|
|
7
|
+
//#region src/sqs/SQSConnection.ts
|
|
8
|
+
var SQSConnection_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ SQSConnection: () => SQSConnection });
|
|
9
|
+
var SQSConnection = class SQSConnection {
|
|
10
|
+
config;
|
|
11
|
+
type = "sqs";
|
|
12
|
+
client;
|
|
13
|
+
connected = false;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.config = config;
|
|
17
|
+
const clientConfig = {
|
|
18
|
+
region: config.region,
|
|
19
|
+
endpoint: config.endpoint,
|
|
20
|
+
credentials: config.credentials
|
|
21
|
+
};
|
|
22
|
+
this.client = new _aws_sdk_client_sqs.SQSClient(clientConfig);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create an SQSConnection from a connection string
|
|
26
|
+
* Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566
|
|
27
|
+
*/
|
|
28
|
+
static async fromConnectionString(connectionString) {
|
|
29
|
+
const { queueUrl, region, endpoint } = require_sqs_sqsUrl.parse(connectionString);
|
|
30
|
+
const params = new URL(connectionString).searchParams;
|
|
31
|
+
const config = {
|
|
32
|
+
queueUrl,
|
|
33
|
+
region,
|
|
34
|
+
endpoint
|
|
35
|
+
};
|
|
36
|
+
const accessKeyId = params.get("accessKeyId");
|
|
37
|
+
const secretAccessKey = params.get("secretAccessKey");
|
|
38
|
+
if (accessKeyId && secretAccessKey) config.credentials = {
|
|
39
|
+
accessKeyId,
|
|
40
|
+
secretAccessKey,
|
|
41
|
+
sessionToken: params.get("sessionToken") || void 0
|
|
42
|
+
};
|
|
43
|
+
const connection = new SQSConnection(config);
|
|
44
|
+
await connection.connect();
|
|
45
|
+
return connection;
|
|
46
|
+
}
|
|
47
|
+
async connect() {
|
|
48
|
+
this.connected = true;
|
|
49
|
+
}
|
|
50
|
+
async close() {
|
|
51
|
+
this.client.destroy();
|
|
52
|
+
this.connected = false;
|
|
53
|
+
}
|
|
54
|
+
isConnected() {
|
|
55
|
+
return this.connected;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the underlying SQS client
|
|
59
|
+
*/
|
|
60
|
+
get sqsClient() {
|
|
61
|
+
return this.client;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the queue URL for this connection
|
|
65
|
+
*/
|
|
66
|
+
get queueUrl() {
|
|
67
|
+
return this.config.queueUrl;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/sqs/SQSPublisher.ts
|
|
72
|
+
var SQSPublisher = class SQSPublisher {
|
|
73
|
+
connection;
|
|
74
|
+
options;
|
|
75
|
+
constructor(connection, options = {}) {
|
|
76
|
+
this.connection = connection;
|
|
77
|
+
this.options = { maxBatchSize: options.maxBatchSize ?? 10 };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create an SQSPublisher from a connection string
|
|
81
|
+
* Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName®ion=us-east-1&endpoint=http://localhost:4566&maxBatchSize=10
|
|
82
|
+
*/
|
|
83
|
+
static async fromConnectionString(connectionString) {
|
|
84
|
+
const params = new URL(connectionString).searchParams;
|
|
85
|
+
const { SQSConnection } = await Promise.resolve().then(() => SQSConnection_exports);
|
|
86
|
+
const connection = await SQSConnection.fromConnectionString(connectionString);
|
|
87
|
+
const options = { maxBatchSize: params.get("maxBatchSize") ? Number.parseInt(params.get("maxBatchSize"), 10) : void 0 };
|
|
88
|
+
return new SQSPublisher(connection, options);
|
|
89
|
+
}
|
|
90
|
+
async publish(messages) {
|
|
91
|
+
if (messages.length === 0) return;
|
|
92
|
+
if (!this.connection.isConnected()) await this.connection.connect();
|
|
93
|
+
const batches = (0, lodash_chunk.default)(messages, this.options.maxBatchSize);
|
|
94
|
+
await Promise.all(batches.map((batch) => this.sendBatch(batch)));
|
|
95
|
+
}
|
|
96
|
+
async sendBatch(messages) {
|
|
97
|
+
const destination = sqsQueueName(this.connection.queueUrl);
|
|
98
|
+
await require_telemetry.publishTraced(messages, (message) => ({
|
|
99
|
+
system: "aws_sqs",
|
|
100
|
+
destination,
|
|
101
|
+
type: message.type
|
|
102
|
+
}), (carriers) => this.send(messages.map((message, index) => ({
|
|
103
|
+
Id: `${index}`,
|
|
104
|
+
MessageBody: JSON.stringify({
|
|
105
|
+
type: message.type,
|
|
106
|
+
payload: message.payload
|
|
107
|
+
}),
|
|
108
|
+
MessageAttributes: {
|
|
109
|
+
type: {
|
|
110
|
+
DataType: "String",
|
|
111
|
+
StringValue: message.type
|
|
112
|
+
},
|
|
113
|
+
...require_telemetry.carrierAsAttributes(carriers[index])
|
|
114
|
+
}
|
|
115
|
+
}))));
|
|
116
|
+
}
|
|
117
|
+
async send(entries) {
|
|
118
|
+
const input = {
|
|
119
|
+
QueueUrl: this.connection.queueUrl,
|
|
120
|
+
Entries: entries
|
|
121
|
+
};
|
|
122
|
+
const command = new _aws_sdk_client_sqs.SendMessageBatchCommand(input);
|
|
123
|
+
const response = await this.connection.sqsClient.send(command);
|
|
124
|
+
if (response.Failed && response.Failed.length > 0) throw new SqsBatchPartlyFailed(response.Failed.map((f) => ({
|
|
125
|
+
id: f.Id,
|
|
126
|
+
code: f.Code,
|
|
127
|
+
message: f.Message
|
|
128
|
+
})));
|
|
129
|
+
}
|
|
130
|
+
async close() {}
|
|
131
|
+
};
|
|
132
|
+
/** SQS accepted the batch but refused some of its messages; the rest were sent. */
|
|
133
|
+
var SqsBatchPartlyFailed = class extends Error {
|
|
134
|
+
failed;
|
|
135
|
+
constructor(failed) {
|
|
136
|
+
super(`Failed to send ${failed.length} messages: ${failed.map((f) => `${f.id}: ${f.code} - ${f.message}`).join(", ")}. The others in the batch were sent; retry only these.`);
|
|
137
|
+
this.failed = failed;
|
|
138
|
+
this.name = "SqsBatchPartlyFailed";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/** The queue's name, the last segment of its URL. */
|
|
142
|
+
function sqsQueueName(queueUrl) {
|
|
143
|
+
return queueUrl.split("/").filter(Boolean).pop() ?? queueUrl;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/sqs/SQSSubscriber.ts
|
|
147
|
+
/**
|
|
148
|
+
* SQS Subscriber - receives messages from an SQS queue
|
|
149
|
+
*
|
|
150
|
+
* This subscriber can receive:
|
|
151
|
+
* 1. Direct SQS messages
|
|
152
|
+
* 2. SNS messages that were pushed to the queue (SNS → SQS pattern)
|
|
153
|
+
*
|
|
154
|
+
* When receiving SNS messages, it:
|
|
155
|
+
* - Automatically detects and parses the SNS wrapper format
|
|
156
|
+
* - Filters by TopicArn (if expectedTopicArn is provided)
|
|
157
|
+
* - Filters by message type
|
|
158
|
+
* - Processes the actual message payload
|
|
159
|
+
*/
|
|
160
|
+
var SQSSubscriber = class {
|
|
161
|
+
connection;
|
|
162
|
+
polling = false;
|
|
163
|
+
messageTypes = /* @__PURE__ */ new Set();
|
|
164
|
+
listener;
|
|
165
|
+
options;
|
|
166
|
+
constructor(connection, options = {}) {
|
|
167
|
+
this.connection = connection;
|
|
168
|
+
this.options = {
|
|
169
|
+
waitTimeSeconds: options.waitTimeSeconds ?? 20,
|
|
170
|
+
maxMessages: options.maxMessages ?? 10,
|
|
171
|
+
expectedTopicArn: options.expectedTopicArn ?? ""
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async subscribe(messages, listener) {
|
|
175
|
+
this.messageTypes = new Set(messages);
|
|
176
|
+
this.listener = listener;
|
|
177
|
+
if (!this.connection.isConnected()) await this.connection.connect();
|
|
178
|
+
this.polling = true;
|
|
179
|
+
this.poll();
|
|
180
|
+
}
|
|
181
|
+
stop() {
|
|
182
|
+
this.polling = false;
|
|
183
|
+
}
|
|
184
|
+
async poll() {
|
|
185
|
+
while (this.polling) try {
|
|
186
|
+
const command = new _aws_sdk_client_sqs.ReceiveMessageCommand({
|
|
187
|
+
QueueUrl: this.connection.queueUrl,
|
|
188
|
+
MaxNumberOfMessages: this.options.maxMessages,
|
|
189
|
+
WaitTimeSeconds: this.options.waitTimeSeconds,
|
|
190
|
+
MessageAttributeNames: ["All"]
|
|
191
|
+
});
|
|
192
|
+
const response = await this.connection.sqsClient.send(command);
|
|
193
|
+
if (response.Messages && response.Messages.length > 0) await this.processMessages(response.Messages);
|
|
194
|
+
} catch (_error) {
|
|
195
|
+
await this.sleep(1e3);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async processMessages(messages) {
|
|
199
|
+
for (const message of messages) try {
|
|
200
|
+
if (!message.Body) continue;
|
|
201
|
+
const parsedMessage = this.parseMessage(message.Body, message.MessageAttributes);
|
|
202
|
+
if (!parsedMessage) {
|
|
203
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const { payload, messageType, topicArn, carrier } = parsedMessage;
|
|
207
|
+
if (this.options.expectedTopicArn && topicArn) {
|
|
208
|
+
if (topicArn !== this.options.expectedTopicArn) {
|
|
209
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (messageType && this.messageTypes.has(messageType)) {
|
|
214
|
+
const listener = this.listener;
|
|
215
|
+
if (listener) await require_telemetry.consumeTraced({
|
|
216
|
+
system: "aws_sqs",
|
|
217
|
+
destination: sqsQueueName(this.connection.queueUrl),
|
|
218
|
+
type: messageType,
|
|
219
|
+
messageId: message.MessageId
|
|
220
|
+
}, carrier, () => listener(payload));
|
|
221
|
+
await this.deleteMessage(message.ReceiptHandle);
|
|
222
|
+
} else await this.deleteMessage(message.ReceiptHandle);
|
|
223
|
+
} catch (_error) {}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Parse message body - handles both direct SQS messages and SNS-wrapped messages
|
|
227
|
+
*/
|
|
228
|
+
parseMessage(body, attributes) {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(body);
|
|
231
|
+
if (parsed.Type === "Notification" && parsed.Message) {
|
|
232
|
+
const snsMessage = JSON.parse(parsed.Message);
|
|
233
|
+
return {
|
|
234
|
+
payload: snsMessage,
|
|
235
|
+
messageType: parsed.MessageAttributes?.type?.Value || snsMessage.type,
|
|
236
|
+
topicArn: parsed.TopicArn,
|
|
237
|
+
carrier: require_telemetry.carrierFromAttributes(parsed.MessageAttributes)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
payload: parsed,
|
|
242
|
+
messageType: parsed.type,
|
|
243
|
+
carrier: require_telemetry.carrierFromAttributes(attributes)
|
|
244
|
+
};
|
|
245
|
+
} catch (_error) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async deleteMessage(receiptHandle) {
|
|
250
|
+
try {
|
|
251
|
+
const command = new _aws_sdk_client_sqs.DeleteMessageCommand({
|
|
252
|
+
QueueUrl: this.connection.queueUrl,
|
|
253
|
+
ReceiptHandle: receiptHandle
|
|
254
|
+
});
|
|
255
|
+
await this.connection.sqsClient.send(command);
|
|
256
|
+
} catch (_error) {}
|
|
257
|
+
}
|
|
258
|
+
sleep(ms) {
|
|
259
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
263
|
+
Object.defineProperty(exports, "SQSConnection", {
|
|
264
|
+
enumerable: true,
|
|
265
|
+
get: function() {
|
|
266
|
+
return SQSConnection;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
Object.defineProperty(exports, "SQSPublisher", {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
get: function() {
|
|
272
|
+
return SQSPublisher;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(exports, "SQSSubscriber", {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
get: function() {
|
|
278
|
+
return SQSSubscriber;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(exports, "SqsBatchPartlyFailed", {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get: function() {
|
|
284
|
+
return SqsBatchPartlyFailed;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
//# sourceMappingURL=SQSSubscriber-COTqWvOk.cjs.map
|