@pubber-subber/aws-sqs 0.0.1
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 +127 -0
- package/dist/index.cjs +183 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +181 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/src/index.ts +246 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sami Mishal
|
|
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,127 @@
|
|
|
1
|
+
# @pubber-subber/aws-sqs
|
|
2
|
+
|
|
3
|
+
AWS SQS **subscribe-only** adapter for [`@pubber-subber/core`](https://www.npmjs.com/package/@pubber-subber/core). Backed by [`@aws-sdk/client-sqs`](https://www.npmjs.com/package/@aws-sdk/client-sqs).
|
|
4
|
+
|
|
5
|
+
Long-polls a queue, delivers messages to your handler, auto-acks on success and nacks on failure. Unwraps SNS notification envelopes transparently for SNS → SQS pipelines.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @pubber-subber/core @pubber-subber/aws-sqs @aws-sdk/client-sqs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@aws-sdk/client-sqs` is a **peer dependency**. AWS auth follows the SDK's default credential chain.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { PubSub } from '@pubber-subber/core';
|
|
19
|
+
import { awsSqs } from '@pubber-subber/aws-sqs';
|
|
20
|
+
|
|
21
|
+
const pubsub = new PubSub({
|
|
22
|
+
adapter: awsSqs({
|
|
23
|
+
region: 'us-east-1',
|
|
24
|
+
queueUrl: 'https://sqs.us-east-1.amazonaws.com/000000000000/orders',
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await pubsub.subscribe('orders', (msg) => {
|
|
29
|
+
console.log(msg.payload); // SNS envelope auto-unwrapped if present
|
|
30
|
+
// call msg.ack() / msg.nack() manually if you need fine control
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pair with SNS for full-duplex
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { PubSub, compose } from '@pubber-subber/core';
|
|
38
|
+
import { awsSns } from '@pubber-subber/aws-sns';
|
|
39
|
+
import { awsSqs } from '@pubber-subber/aws-sqs';
|
|
40
|
+
|
|
41
|
+
const pubsub = new PubSub({
|
|
42
|
+
adapter: compose({
|
|
43
|
+
publisher: awsSns({ region: 'us-east-1' }),
|
|
44
|
+
subscriber: awsSqs({ region: 'us-east-1', queueUrl: '...' }),
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(The SNS → SQS subscription must already be wired — typically via Terraform/CDK.)
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
awsSqs({
|
|
55
|
+
region?: string;
|
|
56
|
+
queueUrl?: string; // default for subscribes
|
|
57
|
+
client?: SQSClient;
|
|
58
|
+
options?: SQSClientConfig;
|
|
59
|
+
codec?: Codec;
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Option | Notes |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `region` | AWS region. |
|
|
66
|
+
| `queueUrl` | Default queue URL — used when `meta.queueUrl` isn't passed on subscribe. |
|
|
67
|
+
| `client` | Pre-constructed `SQSClient`. |
|
|
68
|
+
| `options` | Full `SQSClientConfig` (endpoint override for LocalStack, retry strategy, etc.). |
|
|
69
|
+
| `codec` | Payload encoder/decoder. Default `jsonCodec()`. |
|
|
70
|
+
|
|
71
|
+
## Subscribe meta
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await pubsub.subscribe('orders', handler, {
|
|
75
|
+
queueUrl: 'https://...',
|
|
76
|
+
waitTimeSeconds: 20,
|
|
77
|
+
maxMessages: 10,
|
|
78
|
+
handlerConcurrency: 5,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Field | Default | Notes |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `queueUrl` | `opts.queueUrl` | One of the two **must** be set, otherwise `SubscriptionError` is thrown. |
|
|
85
|
+
| `waitTimeSeconds` | 20 | SQS long-poll wait. 0–20. Higher → fewer empty receives, lower API cost. |
|
|
86
|
+
| `maxMessages` | 10 | Per `ReceiveMessage` batch. 1–10. |
|
|
87
|
+
| `visibilityTimeout` | (queue default) | Override per subscription. |
|
|
88
|
+
| `handlerConcurrency` | 5 | Maximum in-flight handler invocations. Increase for I/O-bound handlers. |
|
|
89
|
+
|
|
90
|
+
## Capabilities
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{ publish: false, subscribe: true, patternSubscribe: false, ack: true }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Calling `publish()` throws `NotSupportedError` with a pointer to `@pubber-subber/aws-sns` + `compose()`.
|
|
97
|
+
|
|
98
|
+
## Ack semantics
|
|
99
|
+
|
|
100
|
+
- Handler resolves → `DeleteMessage` (ack).
|
|
101
|
+
- Handler throws → `ChangeMessageVisibility(VisibilityTimeout=0)` so the message is immediately retried (nack).
|
|
102
|
+
- For manual control, call `msg.ack()` or `msg.nack()` inside your handler. The adapter tracks whether either was called and skips the auto-resolve to avoid double-ack.
|
|
103
|
+
|
|
104
|
+
## SNS → SQS envelope unwrapping
|
|
105
|
+
|
|
106
|
+
If a message's Body is an SNS notification envelope:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "Type": "Notification", "MessageId": "...", "Message": "<inner>", "MessageAttributes": {...} }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
the adapter:
|
|
113
|
+
|
|
114
|
+
- decodes the inner `Message` through the codec (so the JSON SNS publishes is unwrapped to your original payload),
|
|
115
|
+
- exposes the SNS attributes on `AdapterMessage.meta.snsAttributes`.
|
|
116
|
+
|
|
117
|
+
Your handler sees the original payload, not the envelope.
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
- The receive loop runs in the background; `unsubscribe()` halts it and drains any in-flight handler invocations before resolving.
|
|
122
|
+
- For FIFO queues, set `meta.maxMessages = 1` and `meta.handlerConcurrency = 1` to preserve ordering.
|
|
123
|
+
- LocalStack: pass `options: { endpoint: 'http://localhost:4566' }`.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clientSqs = require('@aws-sdk/client-sqs');
|
|
4
|
+
var core = require('@pubber-subber/core');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
function awsSqs(opts = {}) {
|
|
8
|
+
const codec = opts.codec ?? core.jsonCodec();
|
|
9
|
+
let client = null;
|
|
10
|
+
let idCounter = 0;
|
|
11
|
+
const ensure = () => {
|
|
12
|
+
if (client) return client;
|
|
13
|
+
if (opts.client) {
|
|
14
|
+
client = opts.client;
|
|
15
|
+
} else {
|
|
16
|
+
client = new clientSqs.SQSClient({ region: opts.region, ...opts.options });
|
|
17
|
+
}
|
|
18
|
+
return client;
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: "aws-sqs",
|
|
22
|
+
capabilities: { publish: false, subscribe: true, patternSubscribe: false, ack: true },
|
|
23
|
+
async connect() {
|
|
24
|
+
ensure();
|
|
25
|
+
},
|
|
26
|
+
async disconnect() {
|
|
27
|
+
if (client) client.destroy();
|
|
28
|
+
client = null;
|
|
29
|
+
},
|
|
30
|
+
async publish() {
|
|
31
|
+
throw new core.NotSupportedError(
|
|
32
|
+
"aws-sqs is subscribe-only. Use @pubber-subber/aws-sns (or another publish-capable adapter) and compose them: `compose({ publisher: awsSns(), subscriber: awsSqs() })`."
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
async subscribe(topic, handler, meta) {
|
|
36
|
+
const c = ensure();
|
|
37
|
+
const queueUrl = meta?.queueUrl ?? opts.queueUrl;
|
|
38
|
+
if (!queueUrl) {
|
|
39
|
+
throw new core.SubscriptionError(
|
|
40
|
+
"queueUrl is required (pass it in awsSqs({ queueUrl }) or meta.queueUrl)."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const waitTimeSeconds = meta?.waitTimeSeconds ?? 20;
|
|
44
|
+
const maxMessages = meta?.maxMessages ?? 10;
|
|
45
|
+
const concurrency = meta?.handlerConcurrency ?? 5;
|
|
46
|
+
const visibilityTimeout = meta?.visibilityTimeout;
|
|
47
|
+
let stopped = false;
|
|
48
|
+
const semaphore = new Semaphore(concurrency);
|
|
49
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
50
|
+
const loop = async () => {
|
|
51
|
+
while (!stopped) {
|
|
52
|
+
try {
|
|
53
|
+
const resp = await c.send(
|
|
54
|
+
new clientSqs.ReceiveMessageCommand({
|
|
55
|
+
QueueUrl: queueUrl,
|
|
56
|
+
MaxNumberOfMessages: maxMessages,
|
|
57
|
+
WaitTimeSeconds: waitTimeSeconds,
|
|
58
|
+
VisibilityTimeout: visibilityTimeout,
|
|
59
|
+
MessageAttributeNames: ["All"],
|
|
60
|
+
MessageSystemAttributeNames: ["All"]
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
for (const m of resp.Messages ?? []) {
|
|
64
|
+
if (stopped) break;
|
|
65
|
+
await semaphore.acquire();
|
|
66
|
+
const task = processMessage(c, queueUrl, topic, m, handler, codec).finally(() => {
|
|
67
|
+
semaphore.release();
|
|
68
|
+
inflight.delete(task);
|
|
69
|
+
});
|
|
70
|
+
inflight.add(task);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (stopped) break;
|
|
74
|
+
await sleep(1e3);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
void loop();
|
|
79
|
+
idCounter += 1;
|
|
80
|
+
const id = `sqs-${idCounter}`;
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
topic,
|
|
84
|
+
unsubscribe: async () => {
|
|
85
|
+
stopped = true;
|
|
86
|
+
await Promise.allSettled([...inflight]);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function processMessage(client, queueUrl, topic, message, handler, codec) {
|
|
93
|
+
const body = message.Body;
|
|
94
|
+
let payload = body;
|
|
95
|
+
let snsAttributes;
|
|
96
|
+
if (typeof body === "string") {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(body);
|
|
99
|
+
if (parsed && typeof parsed === "object" && parsed.Type === "Notification" && typeof parsed.Message === "string") {
|
|
100
|
+
snsAttributes = parsed.MessageAttributes;
|
|
101
|
+
try {
|
|
102
|
+
payload = codec.decode(parsed.Message);
|
|
103
|
+
} catch {
|
|
104
|
+
payload = parsed.Message;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
payload = codec.decode(body);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
payload = body;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const receiptHandle = message.ReceiptHandle ?? "";
|
|
114
|
+
let resolved = false;
|
|
115
|
+
const doAck = async () => {
|
|
116
|
+
if (resolved) return;
|
|
117
|
+
resolved = true;
|
|
118
|
+
await client.send(
|
|
119
|
+
new clientSqs.DeleteMessageCommand({
|
|
120
|
+
QueueUrl: queueUrl,
|
|
121
|
+
ReceiptHandle: receiptHandle
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
const doNack = async () => {
|
|
126
|
+
if (resolved) return;
|
|
127
|
+
resolved = true;
|
|
128
|
+
await client.send(
|
|
129
|
+
new clientSqs.ChangeMessageVisibilityCommand({
|
|
130
|
+
QueueUrl: queueUrl,
|
|
131
|
+
ReceiptHandle: receiptHandle,
|
|
132
|
+
VisibilityTimeout: 0
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
const adapterMsg = {
|
|
137
|
+
topic,
|
|
138
|
+
payload,
|
|
139
|
+
raw: message,
|
|
140
|
+
meta: {
|
|
141
|
+
messageId: message.MessageId,
|
|
142
|
+
receiptHandle: message.ReceiptHandle,
|
|
143
|
+
attributes: message.MessageAttributes,
|
|
144
|
+
systemAttributes: message.Attributes,
|
|
145
|
+
snsAttributes
|
|
146
|
+
},
|
|
147
|
+
ack: doAck,
|
|
148
|
+
nack: doNack
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
await handler(adapterMsg);
|
|
152
|
+
await doAck();
|
|
153
|
+
} catch {
|
|
154
|
+
await doNack().catch(() => void 0);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function sleep(ms) {
|
|
158
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
159
|
+
}
|
|
160
|
+
var Semaphore = class {
|
|
161
|
+
#permits;
|
|
162
|
+
#waiters = [];
|
|
163
|
+
constructor(permits) {
|
|
164
|
+
this.#permits = permits;
|
|
165
|
+
}
|
|
166
|
+
async acquire() {
|
|
167
|
+
if (this.#permits > 0) {
|
|
168
|
+
this.#permits -= 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await new Promise((resolve) => this.#waiters.push(resolve));
|
|
172
|
+
this.#permits -= 1;
|
|
173
|
+
}
|
|
174
|
+
release() {
|
|
175
|
+
this.#permits += 1;
|
|
176
|
+
const next = this.#waiters.shift();
|
|
177
|
+
if (next) next();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
exports.awsSqs = awsSqs;
|
|
182
|
+
//# sourceMappingURL=index.cjs.map
|
|
183
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["jsonCodec","SQSClient","NotSupportedError","SubscriptionError","ReceiveMessageCommand","DeleteMessageCommand","ChangeMessageVisibilityCommand"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n ChangeMessageVisibilityCommand,\n DeleteMessageCommand,\n ReceiveMessageCommand,\n SQSClient,\n type SQSClientConfig,\n type Message as SqsMessage,\n} from '@aws-sdk/client-sqs';\nimport {\n type AdapterMessage,\n type Codec,\n NotSupportedError,\n type PubSubAdapter,\n SubscriptionError,\n jsonCodec,\n} from '@pubber-subber/core';\n\nexport interface AwsSqsAdapterOptions {\n region?: string;\n /** Default queue URL, used when no `meta.queueUrl` is provided on subscribe. */\n queueUrl?: string;\n client?: SQSClient;\n options?: SQSClientConfig;\n codec?: Codec;\n}\n\nexport interface AwsSqsSubscribeMeta {\n queueUrl?: string;\n /** 0–20s. Default 20 (long polling). */\n waitTimeSeconds?: number;\n /** 1–10 per ReceiveMessage call. Default 10. */\n maxMessages?: number;\n /** Override the queue's default visibility timeout for received messages. */\n visibilityTimeout?: number;\n /** Max number of handler invocations in flight at once. Default 5. */\n handlerConcurrency?: number;\n}\n\nexport function awsSqs(opts: AwsSqsAdapterOptions = {}): PubSubAdapter<never, AwsSqsSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n let client: SQSClient | null = null;\n let idCounter = 0;\n\n const ensure = (): SQSClient => {\n if (client) return client;\n if (opts.client) {\n client = opts.client;\n } else {\n client = new SQSClient({ region: opts.region, ...opts.options });\n }\n return client;\n };\n\n return {\n name: 'aws-sqs',\n capabilities: { publish: false, subscribe: true, patternSubscribe: false, ack: true },\n\n async connect() {\n ensure();\n },\n\n async disconnect() {\n if (client) client.destroy();\n client = null;\n },\n\n async publish() {\n throw new NotSupportedError(\n 'aws-sqs is subscribe-only. Use @pubber-subber/aws-sns (or another publish-capable adapter) ' +\n 'and compose them: `compose({ publisher: awsSns(), subscriber: awsSqs() })`.',\n );\n },\n\n async subscribe(topic, handler, meta) {\n const c = ensure();\n const queueUrl = meta?.queueUrl ?? opts.queueUrl;\n if (!queueUrl) {\n throw new SubscriptionError(\n 'queueUrl is required (pass it in awsSqs({ queueUrl }) or meta.queueUrl).',\n );\n }\n const waitTimeSeconds = meta?.waitTimeSeconds ?? 20;\n const maxMessages = meta?.maxMessages ?? 10;\n const concurrency = meta?.handlerConcurrency ?? 5;\n const visibilityTimeout = meta?.visibilityTimeout;\n\n let stopped = false;\n const semaphore = new Semaphore(concurrency);\n const inflight = new Set<Promise<unknown>>();\n\n const loop = async (): Promise<void> => {\n while (!stopped) {\n try {\n const resp = await c.send(\n new ReceiveMessageCommand({\n QueueUrl: queueUrl,\n MaxNumberOfMessages: maxMessages,\n WaitTimeSeconds: waitTimeSeconds,\n VisibilityTimeout: visibilityTimeout,\n MessageAttributeNames: ['All'],\n MessageSystemAttributeNames: ['All'] as never,\n }),\n );\n for (const m of resp.Messages ?? []) {\n if (stopped) break;\n await semaphore.acquire();\n const task = processMessage(c, queueUrl, topic, m, handler, codec).finally(() => {\n semaphore.release();\n inflight.delete(task);\n });\n inflight.add(task);\n }\n } catch (err) {\n if (stopped) break;\n // Backoff briefly on receive errors so we don't hot-loop on a permission issue.\n await sleep(1000);\n }\n }\n };\n\n void loop();\n\n idCounter += 1;\n const id = `sqs-${idCounter}`;\n return {\n id,\n topic,\n unsubscribe: async () => {\n stopped = true;\n await Promise.allSettled([...inflight]);\n },\n };\n },\n };\n}\n\nasync function processMessage(\n client: SQSClient,\n queueUrl: string,\n topic: string,\n message: SqsMessage,\n handler: (msg: AdapterMessage) => void | Promise<void>,\n codec: Codec,\n): Promise<void> {\n const body = message.Body;\n let payload: unknown = body;\n let snsAttributes: Record<string, unknown> | undefined;\n\n if (typeof body === 'string') {\n try {\n const parsed = JSON.parse(body);\n if (\n parsed &&\n typeof parsed === 'object' &&\n parsed.Type === 'Notification' &&\n typeof parsed.Message === 'string'\n ) {\n // SNS→SQS envelope. Unwrap.\n snsAttributes = parsed.MessageAttributes;\n try {\n payload = codec.decode(parsed.Message);\n } catch {\n payload = parsed.Message;\n }\n } else {\n payload = codec.decode(body);\n }\n } catch {\n payload = body;\n }\n }\n\n const receiptHandle = message.ReceiptHandle ?? '';\n let resolved = false;\n const doAck = async (): Promise<void> => {\n if (resolved) return;\n resolved = true;\n await client.send(\n new DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: receiptHandle,\n }),\n );\n };\n const doNack = async (): Promise<void> => {\n if (resolved) return;\n resolved = true;\n await client.send(\n new ChangeMessageVisibilityCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: receiptHandle,\n VisibilityTimeout: 0,\n }),\n );\n };\n\n const adapterMsg: AdapterMessage = {\n topic,\n payload,\n raw: message,\n meta: {\n messageId: message.MessageId,\n receiptHandle: message.ReceiptHandle,\n attributes: message.MessageAttributes,\n systemAttributes: message.Attributes,\n snsAttributes,\n },\n ack: doAck,\n nack: doNack,\n };\n\n try {\n await handler(adapterMsg);\n await doAck();\n } catch {\n await doNack().catch(() => undefined);\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nclass Semaphore {\n #permits: number;\n #waiters: Array<() => void> = [];\n\n constructor(permits: number) {\n this.#permits = permits;\n }\n\n async acquire(): Promise<void> {\n if (this.#permits > 0) {\n this.#permits -= 1;\n return;\n }\n await new Promise<void>((resolve) => this.#waiters.push(resolve));\n this.#permits -= 1;\n }\n\n release(): void {\n this.#permits += 1;\n const next = this.#waiters.shift();\n if (next) next();\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
interface AwsSqsAdapterOptions {
|
|
5
|
+
region?: string;
|
|
6
|
+
/** Default queue URL, used when no `meta.queueUrl` is provided on subscribe. */
|
|
7
|
+
queueUrl?: string;
|
|
8
|
+
client?: SQSClient;
|
|
9
|
+
options?: SQSClientConfig;
|
|
10
|
+
codec?: Codec;
|
|
11
|
+
}
|
|
12
|
+
interface AwsSqsSubscribeMeta {
|
|
13
|
+
queueUrl?: string;
|
|
14
|
+
/** 0–20s. Default 20 (long polling). */
|
|
15
|
+
waitTimeSeconds?: number;
|
|
16
|
+
/** 1–10 per ReceiveMessage call. Default 10. */
|
|
17
|
+
maxMessages?: number;
|
|
18
|
+
/** Override the queue's default visibility timeout for received messages. */
|
|
19
|
+
visibilityTimeout?: number;
|
|
20
|
+
/** Max number of handler invocations in flight at once. Default 5. */
|
|
21
|
+
handlerConcurrency?: number;
|
|
22
|
+
}
|
|
23
|
+
declare function awsSqs(opts?: AwsSqsAdapterOptions): PubSubAdapter<never, AwsSqsSubscribeMeta>;
|
|
24
|
+
|
|
25
|
+
export { type AwsSqsAdapterOptions, type AwsSqsSubscribeMeta, awsSqs };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
interface AwsSqsAdapterOptions {
|
|
5
|
+
region?: string;
|
|
6
|
+
/** Default queue URL, used when no `meta.queueUrl` is provided on subscribe. */
|
|
7
|
+
queueUrl?: string;
|
|
8
|
+
client?: SQSClient;
|
|
9
|
+
options?: SQSClientConfig;
|
|
10
|
+
codec?: Codec;
|
|
11
|
+
}
|
|
12
|
+
interface AwsSqsSubscribeMeta {
|
|
13
|
+
queueUrl?: string;
|
|
14
|
+
/** 0–20s. Default 20 (long polling). */
|
|
15
|
+
waitTimeSeconds?: number;
|
|
16
|
+
/** 1–10 per ReceiveMessage call. Default 10. */
|
|
17
|
+
maxMessages?: number;
|
|
18
|
+
/** Override the queue's default visibility timeout for received messages. */
|
|
19
|
+
visibilityTimeout?: number;
|
|
20
|
+
/** Max number of handler invocations in flight at once. Default 5. */
|
|
21
|
+
handlerConcurrency?: number;
|
|
22
|
+
}
|
|
23
|
+
declare function awsSqs(opts?: AwsSqsAdapterOptions): PubSubAdapter<never, AwsSqsSubscribeMeta>;
|
|
24
|
+
|
|
25
|
+
export { type AwsSqsAdapterOptions, type AwsSqsSubscribeMeta, awsSqs };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { SQSClient, ReceiveMessageCommand, ChangeMessageVisibilityCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { jsonCodec, SubscriptionError, NotSupportedError } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
function awsSqs(opts = {}) {
|
|
6
|
+
const codec = opts.codec ?? jsonCodec();
|
|
7
|
+
let client = null;
|
|
8
|
+
let idCounter = 0;
|
|
9
|
+
const ensure = () => {
|
|
10
|
+
if (client) return client;
|
|
11
|
+
if (opts.client) {
|
|
12
|
+
client = opts.client;
|
|
13
|
+
} else {
|
|
14
|
+
client = new SQSClient({ region: opts.region, ...opts.options });
|
|
15
|
+
}
|
|
16
|
+
return client;
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
name: "aws-sqs",
|
|
20
|
+
capabilities: { publish: false, subscribe: true, patternSubscribe: false, ack: true },
|
|
21
|
+
async connect() {
|
|
22
|
+
ensure();
|
|
23
|
+
},
|
|
24
|
+
async disconnect() {
|
|
25
|
+
if (client) client.destroy();
|
|
26
|
+
client = null;
|
|
27
|
+
},
|
|
28
|
+
async publish() {
|
|
29
|
+
throw new NotSupportedError(
|
|
30
|
+
"aws-sqs is subscribe-only. Use @pubber-subber/aws-sns (or another publish-capable adapter) and compose them: `compose({ publisher: awsSns(), subscriber: awsSqs() })`."
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
async subscribe(topic, handler, meta) {
|
|
34
|
+
const c = ensure();
|
|
35
|
+
const queueUrl = meta?.queueUrl ?? opts.queueUrl;
|
|
36
|
+
if (!queueUrl) {
|
|
37
|
+
throw new SubscriptionError(
|
|
38
|
+
"queueUrl is required (pass it in awsSqs({ queueUrl }) or meta.queueUrl)."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const waitTimeSeconds = meta?.waitTimeSeconds ?? 20;
|
|
42
|
+
const maxMessages = meta?.maxMessages ?? 10;
|
|
43
|
+
const concurrency = meta?.handlerConcurrency ?? 5;
|
|
44
|
+
const visibilityTimeout = meta?.visibilityTimeout;
|
|
45
|
+
let stopped = false;
|
|
46
|
+
const semaphore = new Semaphore(concurrency);
|
|
47
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
48
|
+
const loop = async () => {
|
|
49
|
+
while (!stopped) {
|
|
50
|
+
try {
|
|
51
|
+
const resp = await c.send(
|
|
52
|
+
new ReceiveMessageCommand({
|
|
53
|
+
QueueUrl: queueUrl,
|
|
54
|
+
MaxNumberOfMessages: maxMessages,
|
|
55
|
+
WaitTimeSeconds: waitTimeSeconds,
|
|
56
|
+
VisibilityTimeout: visibilityTimeout,
|
|
57
|
+
MessageAttributeNames: ["All"],
|
|
58
|
+
MessageSystemAttributeNames: ["All"]
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
for (const m of resp.Messages ?? []) {
|
|
62
|
+
if (stopped) break;
|
|
63
|
+
await semaphore.acquire();
|
|
64
|
+
const task = processMessage(c, queueUrl, topic, m, handler, codec).finally(() => {
|
|
65
|
+
semaphore.release();
|
|
66
|
+
inflight.delete(task);
|
|
67
|
+
});
|
|
68
|
+
inflight.add(task);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (stopped) break;
|
|
72
|
+
await sleep(1e3);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
void loop();
|
|
77
|
+
idCounter += 1;
|
|
78
|
+
const id = `sqs-${idCounter}`;
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
topic,
|
|
82
|
+
unsubscribe: async () => {
|
|
83
|
+
stopped = true;
|
|
84
|
+
await Promise.allSettled([...inflight]);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function processMessage(client, queueUrl, topic, message, handler, codec) {
|
|
91
|
+
const body = message.Body;
|
|
92
|
+
let payload = body;
|
|
93
|
+
let snsAttributes;
|
|
94
|
+
if (typeof body === "string") {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(body);
|
|
97
|
+
if (parsed && typeof parsed === "object" && parsed.Type === "Notification" && typeof parsed.Message === "string") {
|
|
98
|
+
snsAttributes = parsed.MessageAttributes;
|
|
99
|
+
try {
|
|
100
|
+
payload = codec.decode(parsed.Message);
|
|
101
|
+
} catch {
|
|
102
|
+
payload = parsed.Message;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
payload = codec.decode(body);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
payload = body;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const receiptHandle = message.ReceiptHandle ?? "";
|
|
112
|
+
let resolved = false;
|
|
113
|
+
const doAck = async () => {
|
|
114
|
+
if (resolved) return;
|
|
115
|
+
resolved = true;
|
|
116
|
+
await client.send(
|
|
117
|
+
new DeleteMessageCommand({
|
|
118
|
+
QueueUrl: queueUrl,
|
|
119
|
+
ReceiptHandle: receiptHandle
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
const doNack = async () => {
|
|
124
|
+
if (resolved) return;
|
|
125
|
+
resolved = true;
|
|
126
|
+
await client.send(
|
|
127
|
+
new ChangeMessageVisibilityCommand({
|
|
128
|
+
QueueUrl: queueUrl,
|
|
129
|
+
ReceiptHandle: receiptHandle,
|
|
130
|
+
VisibilityTimeout: 0
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
const adapterMsg = {
|
|
135
|
+
topic,
|
|
136
|
+
payload,
|
|
137
|
+
raw: message,
|
|
138
|
+
meta: {
|
|
139
|
+
messageId: message.MessageId,
|
|
140
|
+
receiptHandle: message.ReceiptHandle,
|
|
141
|
+
attributes: message.MessageAttributes,
|
|
142
|
+
systemAttributes: message.Attributes,
|
|
143
|
+
snsAttributes
|
|
144
|
+
},
|
|
145
|
+
ack: doAck,
|
|
146
|
+
nack: doNack
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
await handler(adapterMsg);
|
|
150
|
+
await doAck();
|
|
151
|
+
} catch {
|
|
152
|
+
await doNack().catch(() => void 0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function sleep(ms) {
|
|
156
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
157
|
+
}
|
|
158
|
+
var Semaphore = class {
|
|
159
|
+
#permits;
|
|
160
|
+
#waiters = [];
|
|
161
|
+
constructor(permits) {
|
|
162
|
+
this.#permits = permits;
|
|
163
|
+
}
|
|
164
|
+
async acquire() {
|
|
165
|
+
if (this.#permits > 0) {
|
|
166
|
+
this.#permits -= 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await new Promise((resolve) => this.#waiters.push(resolve));
|
|
170
|
+
this.#permits -= 1;
|
|
171
|
+
}
|
|
172
|
+
release() {
|
|
173
|
+
this.#permits += 1;
|
|
174
|
+
const next = this.#waiters.shift();
|
|
175
|
+
if (next) next();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export { awsSqs };
|
|
180
|
+
//# sourceMappingURL=index.js.map
|
|
181
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AAsCO,SAAS,MAAA,CAAO,IAAA,GAA6B,EAAC,EAA8C;AACjG,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,SAAA,EAAU;AACtC,EAAA,IAAI,MAAA,GAA2B,IAAA;AAC/B,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,MAAM,SAAS,MAAiB;AAC9B,IAAA,IAAI,QAAQ,OAAO,MAAA;AACnB,IAAA,IAAI,KAAK,MAAA,EAAQ;AACf,MAAA,MAAA,GAAS,IAAA,CAAK,MAAA;AAAA,IAChB,CAAA,MAAO;AACL,MAAA,MAAA,GAAS,IAAI,UAAU,EAAE,MAAA,EAAQ,KAAK,MAAA,EAAQ,GAAG,IAAA,CAAK,OAAA,EAAS,CAAA;AAAA,IACjE;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IACN,YAAA,EAAc,EAAE,OAAA,EAAS,KAAA,EAAO,WAAW,IAAA,EAAM,gBAAA,EAAkB,KAAA,EAAO,GAAA,EAAK,IAAA,EAAK;AAAA,IAEpF,MAAM,OAAA,GAAU;AACd,MAAA,MAAA,EAAO;AAAA,IACT,CAAA;AAAA,IAEA,MAAM,UAAA,GAAa;AACjB,MAAA,IAAI,MAAA,SAAe,OAAA,EAAQ;AAC3B,MAAA,MAAA,GAAS,IAAA;AAAA,IACX,CAAA;AAAA,IAEA,MAAM,OAAA,GAAU;AACd,MAAA,MAAM,IAAI,iBAAA;AAAA,QACR;AAAA,OAEF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM;AACpC,MAAA,MAAM,IAAI,MAAA,EAAO;AACjB,MAAA,MAAM,QAAA,GAAW,IAAA,EAAM,QAAA,IAAY,IAAA,CAAK,QAAA;AACxC,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,iBAAA;AAAA,UACR;AAAA,SACF;AAAA,MACF;AACA,MAAA,MAAM,eAAA,GAAkB,MAAM,eAAA,IAAmB,EAAA;AACjD,MAAA,MAAM,WAAA,GAAc,MAAM,WAAA,IAAe,EAAA;AACzC,MAAA,MAAM,WAAA,GAAc,MAAM,kBAAA,IAAsB,CAAA;AAChD,MAAA,MAAM,oBAAoB,IAAA,EAAM,iBAAA;AAEhC,MAAA,IAAI,OAAA,GAAU,KAAA;AACd,MAAA,MAAM,SAAA,GAAY,IAAI,SAAA,CAAU,WAAW,CAAA;AAC3C,MAAA,MAAM,QAAA,uBAAe,GAAA,EAAsB;AAE3C,MAAA,MAAM,OAAO,YAA2B;AACtC,QAAA,OAAO,CAAC,OAAA,EAAS;AACf,UAAA,IAAI;AACF,YAAA,MAAM,IAAA,GAAO,MAAM,CAAA,CAAE,IAAA;AAAA,cACnB,IAAI,qBAAA,CAAsB;AAAA,gBACxB,QAAA,EAAU,QAAA;AAAA,gBACV,mBAAA,EAAqB,WAAA;AAAA,gBACrB,eAAA,EAAiB,eAAA;AAAA,gBACjB,iBAAA,EAAmB,iBAAA;AAAA,gBACnB,qBAAA,EAAuB,CAAC,KAAK,CAAA;AAAA,gBAC7B,2BAAA,EAA6B,CAAC,KAAK;AAAA,eACpC;AAAA,aACH;AACA,YAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,QAAA,IAAY,EAAC,EAAG;AACnC,cAAA,IAAI,OAAA,EAAS;AACb,cAAA,MAAM,UAAU,OAAA,EAAQ;AACxB,cAAA,MAAM,IAAA,GAAO,cAAA,CAAe,CAAA,EAAG,QAAA,EAAU,KAAA,EAAO,GAAG,OAAA,EAAS,KAAK,CAAA,CAAE,OAAA,CAAQ,MAAM;AAC/E,gBAAA,SAAA,CAAU,OAAA,EAAQ;AAClB,gBAAA,QAAA,CAAS,OAAO,IAAI,CAAA;AAAA,cACtB,CAAC,CAAA;AACD,cAAA,QAAA,CAAS,IAAI,IAAI,CAAA;AAAA,YACnB;AAAA,UACF,SAAS,GAAA,EAAK;AACZ,YAAA,IAAI,OAAA,EAAS;AAEb,YAAA,MAAM,MAAM,GAAI,CAAA;AAAA,UAClB;AAAA,QACF;AAAA,MACF,CAAA;AAEA,MAAA,KAAK,IAAA,EAAK;AAEV,MAAA,SAAA,IAAa,CAAA;AACb,MAAA,MAAM,EAAA,GAAK,OAAO,SAAS,CAAA,CAAA;AAC3B,MAAA,OAAO;AAAA,QACL,EAAA;AAAA,QACA,KAAA;AAAA,QACA,aAAa,YAAY;AACvB,UAAA,OAAA,GAAU,IAAA;AACV,UAAA,MAAM,OAAA,CAAQ,UAAA,CAAW,CAAC,GAAG,QAAQ,CAAC,CAAA;AAAA,QACxC;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAEA,eAAe,eACb,MAAA,EACA,QAAA,EACA,KAAA,EACA,OAAA,EACA,SACA,KAAA,EACe;AACf,EAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,EAAA,IAAI,OAAA,GAAmB,IAAA;AACvB,EAAA,IAAI,aAAA;AAEJ,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC9B,MAAA,IACE,MAAA,IACA,OAAO,MAAA,KAAW,QAAA,IAClB,MAAA,CAAO,SAAS,cAAA,IAChB,OAAO,MAAA,CAAO,OAAA,KAAY,QAAA,EAC1B;AAEA,QAAA,aAAA,GAAgB,MAAA,CAAO,iBAAA;AACvB,QAAA,IAAI;AACF,UAAA,OAAA,GAAU,KAAA,CAAM,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA;AAAA,QACvC,CAAA,CAAA,MAAQ;AACN,UAAA,OAAA,GAAU,MAAA,CAAO,OAAA;AAAA,QACnB;AAAA,MACF,CAAA,MAAO;AACL,QAAA,OAAA,GAAU,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,MAC7B;AAAA,IACF,CAAA,CAAA,MAAQ;AACN,MAAA,OAAA,GAAU,IAAA;AAAA,IACZ;AAAA,EACF;AAEA,EAAA,MAAM,aAAA,GAAgB,QAAQ,aAAA,IAAiB,EAAA;AAC/C,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,MAAM,QAAQ,YAA2B;AACvC,IAAA,IAAI,QAAA,EAAU;AACd,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACX,IAAI,oBAAA,CAAqB;AAAA,QACvB,QAAA,EAAU,QAAA;AAAA,QACV,aAAA,EAAe;AAAA,OAChB;AAAA,KACH;AAAA,EACF,CAAA;AACA,EAAA,MAAM,SAAS,YAA2B;AACxC,IAAA,IAAI,QAAA,EAAU;AACd,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACX,IAAI,8BAAA,CAA+B;AAAA,QACjC,QAAA,EAAU,QAAA;AAAA,QACV,aAAA,EAAe,aAAA;AAAA,QACf,iBAAA,EAAmB;AAAA,OACpB;AAAA,KACH;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAA,GAA6B;AAAA,IACjC,KAAA;AAAA,IACA,OAAA;AAAA,IACA,GAAA,EAAK,OAAA;AAAA,IACL,IAAA,EAAM;AAAA,MACJ,WAAW,OAAA,CAAQ,SAAA;AAAA,MACnB,eAAe,OAAA,CAAQ,aAAA;AAAA,MACvB,YAAY,OAAA,CAAQ,iBAAA;AAAA,MACpB,kBAAkB,OAAA,CAAQ,UAAA;AAAA,MAC1B;AAAA,KACF;AAAA,IACA,GAAA,EAAK,KAAA;AAAA,IACL,IAAA,EAAM;AAAA,GACR;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,QAAQ,UAAU,CAAA;AACxB,IAAA,MAAM,KAAA,EAAM;AAAA,EACd,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,MAAA,EAAO,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAAA,EACtC;AACF;AAEA,SAAS,MAAM,EAAA,EAA2B;AACxC,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,EAAE,CAAC,CAAA;AAC7C;AAEA,IAAM,YAAN,MAAgB;AAAA,EACd,QAAA;AAAA,EACA,WAA8B,EAAC;AAAA,EAE/B,YAAY,OAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA,EAEA,MAAM,OAAA,GAAyB;AAC7B,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrB,MAAA,IAAA,CAAK,QAAA,IAAY,CAAA;AACjB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,IAAI,QAAc,CAAC,OAAA,KAAY,KAAK,QAAA,CAAS,IAAA,CAAK,OAAO,CAAC,CAAA;AAChE,IAAA,IAAA,CAAK,QAAA,IAAY,CAAA;AAAA,EACnB;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,QAAA,IAAY,CAAA;AACjB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,KAAA,EAAM;AACjC,IAAA,IAAI,MAAM,IAAA,EAAK;AAAA,EACjB;AACF,CAAA","file":"index.js","sourcesContent":["import {\n ChangeMessageVisibilityCommand,\n DeleteMessageCommand,\n ReceiveMessageCommand,\n SQSClient,\n type SQSClientConfig,\n type Message as SqsMessage,\n} from '@aws-sdk/client-sqs';\nimport {\n type AdapterMessage,\n type Codec,\n NotSupportedError,\n type PubSubAdapter,\n SubscriptionError,\n jsonCodec,\n} from '@pubber-subber/core';\n\nexport interface AwsSqsAdapterOptions {\n region?: string;\n /** Default queue URL, used when no `meta.queueUrl` is provided on subscribe. */\n queueUrl?: string;\n client?: SQSClient;\n options?: SQSClientConfig;\n codec?: Codec;\n}\n\nexport interface AwsSqsSubscribeMeta {\n queueUrl?: string;\n /** 0–20s. Default 20 (long polling). */\n waitTimeSeconds?: number;\n /** 1–10 per ReceiveMessage call. Default 10. */\n maxMessages?: number;\n /** Override the queue's default visibility timeout for received messages. */\n visibilityTimeout?: number;\n /** Max number of handler invocations in flight at once. Default 5. */\n handlerConcurrency?: number;\n}\n\nexport function awsSqs(opts: AwsSqsAdapterOptions = {}): PubSubAdapter<never, AwsSqsSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n let client: SQSClient | null = null;\n let idCounter = 0;\n\n const ensure = (): SQSClient => {\n if (client) return client;\n if (opts.client) {\n client = opts.client;\n } else {\n client = new SQSClient({ region: opts.region, ...opts.options });\n }\n return client;\n };\n\n return {\n name: 'aws-sqs',\n capabilities: { publish: false, subscribe: true, patternSubscribe: false, ack: true },\n\n async connect() {\n ensure();\n },\n\n async disconnect() {\n if (client) client.destroy();\n client = null;\n },\n\n async publish() {\n throw new NotSupportedError(\n 'aws-sqs is subscribe-only. Use @pubber-subber/aws-sns (or another publish-capable adapter) ' +\n 'and compose them: `compose({ publisher: awsSns(), subscriber: awsSqs() })`.',\n );\n },\n\n async subscribe(topic, handler, meta) {\n const c = ensure();\n const queueUrl = meta?.queueUrl ?? opts.queueUrl;\n if (!queueUrl) {\n throw new SubscriptionError(\n 'queueUrl is required (pass it in awsSqs({ queueUrl }) or meta.queueUrl).',\n );\n }\n const waitTimeSeconds = meta?.waitTimeSeconds ?? 20;\n const maxMessages = meta?.maxMessages ?? 10;\n const concurrency = meta?.handlerConcurrency ?? 5;\n const visibilityTimeout = meta?.visibilityTimeout;\n\n let stopped = false;\n const semaphore = new Semaphore(concurrency);\n const inflight = new Set<Promise<unknown>>();\n\n const loop = async (): Promise<void> => {\n while (!stopped) {\n try {\n const resp = await c.send(\n new ReceiveMessageCommand({\n QueueUrl: queueUrl,\n MaxNumberOfMessages: maxMessages,\n WaitTimeSeconds: waitTimeSeconds,\n VisibilityTimeout: visibilityTimeout,\n MessageAttributeNames: ['All'],\n MessageSystemAttributeNames: ['All'] as never,\n }),\n );\n for (const m of resp.Messages ?? []) {\n if (stopped) break;\n await semaphore.acquire();\n const task = processMessage(c, queueUrl, topic, m, handler, codec).finally(() => {\n semaphore.release();\n inflight.delete(task);\n });\n inflight.add(task);\n }\n } catch (err) {\n if (stopped) break;\n // Backoff briefly on receive errors so we don't hot-loop on a permission issue.\n await sleep(1000);\n }\n }\n };\n\n void loop();\n\n idCounter += 1;\n const id = `sqs-${idCounter}`;\n return {\n id,\n topic,\n unsubscribe: async () => {\n stopped = true;\n await Promise.allSettled([...inflight]);\n },\n };\n },\n };\n}\n\nasync function processMessage(\n client: SQSClient,\n queueUrl: string,\n topic: string,\n message: SqsMessage,\n handler: (msg: AdapterMessage) => void | Promise<void>,\n codec: Codec,\n): Promise<void> {\n const body = message.Body;\n let payload: unknown = body;\n let snsAttributes: Record<string, unknown> | undefined;\n\n if (typeof body === 'string') {\n try {\n const parsed = JSON.parse(body);\n if (\n parsed &&\n typeof parsed === 'object' &&\n parsed.Type === 'Notification' &&\n typeof parsed.Message === 'string'\n ) {\n // SNS→SQS envelope. Unwrap.\n snsAttributes = parsed.MessageAttributes;\n try {\n payload = codec.decode(parsed.Message);\n } catch {\n payload = parsed.Message;\n }\n } else {\n payload = codec.decode(body);\n }\n } catch {\n payload = body;\n }\n }\n\n const receiptHandle = message.ReceiptHandle ?? '';\n let resolved = false;\n const doAck = async (): Promise<void> => {\n if (resolved) return;\n resolved = true;\n await client.send(\n new DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: receiptHandle,\n }),\n );\n };\n const doNack = async (): Promise<void> => {\n if (resolved) return;\n resolved = true;\n await client.send(\n new ChangeMessageVisibilityCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: receiptHandle,\n VisibilityTimeout: 0,\n }),\n );\n };\n\n const adapterMsg: AdapterMessage = {\n topic,\n payload,\n raw: message,\n meta: {\n messageId: message.MessageId,\n receiptHandle: message.ReceiptHandle,\n attributes: message.MessageAttributes,\n systemAttributes: message.Attributes,\n snsAttributes,\n },\n ack: doAck,\n nack: doNack,\n };\n\n try {\n await handler(adapterMsg);\n await doAck();\n } catch {\n await doNack().catch(() => undefined);\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nclass Semaphore {\n #permits: number;\n #waiters: Array<() => void> = [];\n\n constructor(permits: number) {\n this.#permits = permits;\n }\n\n async acquire(): Promise<void> {\n if (this.#permits > 0) {\n this.#permits -= 1;\n return;\n }\n await new Promise<void>((resolve) => this.#waiters.push(resolve));\n this.#permits -= 1;\n }\n\n release(): void {\n this.#permits += 1;\n const next = this.#waiters.shift();\n if (next) next();\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pubber-subber/aws-sqs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AWS SQS subscribe-only adapter for @pubber-subber. Pair with @pubber-subber/aws-sns via compose() for SNS→SQS fan-out.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pubsub",
|
|
7
|
+
"pub-sub",
|
|
8
|
+
"messaging",
|
|
9
|
+
"events",
|
|
10
|
+
"adapter",
|
|
11
|
+
"aws",
|
|
12
|
+
"sqs",
|
|
13
|
+
"amazon",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/samishal1998/pubber-subber/tree/main/packages/aws-sqs#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/samishal1998/pubber-subber.git",
|
|
20
|
+
"directory": "packages/aws-sqs"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/samishal1998/pubber-subber/issues"
|
|
24
|
+
},
|
|
25
|
+
"author": "Sami Mishal",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/index.d.cts",
|
|
37
|
+
"default": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.cjs",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prebuild": "node ../../scripts/swap-package-json.mjs build",
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:integration": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"prepublishOnly": "node ../../scripts/swap-package-json.mjs publish",
|
|
54
|
+
"postpublish": "node ../../scripts/swap-package-json.mjs build"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@aws-sdk/client-sqs": "^3.0.0",
|
|
58
|
+
"@pubber-subber/core": "^0.0.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@pubber-subber/core": "workspace:*",
|
|
62
|
+
"@aws-sdk/client-sqs": "^3.700.0"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeMessageVisibilityCommand,
|
|
3
|
+
DeleteMessageCommand,
|
|
4
|
+
ReceiveMessageCommand,
|
|
5
|
+
SQSClient,
|
|
6
|
+
type SQSClientConfig,
|
|
7
|
+
type Message as SqsMessage,
|
|
8
|
+
} from '@aws-sdk/client-sqs';
|
|
9
|
+
import {
|
|
10
|
+
type AdapterMessage,
|
|
11
|
+
type Codec,
|
|
12
|
+
NotSupportedError,
|
|
13
|
+
type PubSubAdapter,
|
|
14
|
+
SubscriptionError,
|
|
15
|
+
jsonCodec,
|
|
16
|
+
} from '@pubber-subber/core';
|
|
17
|
+
|
|
18
|
+
export interface AwsSqsAdapterOptions {
|
|
19
|
+
region?: string;
|
|
20
|
+
/** Default queue URL, used when no `meta.queueUrl` is provided on subscribe. */
|
|
21
|
+
queueUrl?: string;
|
|
22
|
+
client?: SQSClient;
|
|
23
|
+
options?: SQSClientConfig;
|
|
24
|
+
codec?: Codec;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AwsSqsSubscribeMeta {
|
|
28
|
+
queueUrl?: string;
|
|
29
|
+
/** 0–20s. Default 20 (long polling). */
|
|
30
|
+
waitTimeSeconds?: number;
|
|
31
|
+
/** 1–10 per ReceiveMessage call. Default 10. */
|
|
32
|
+
maxMessages?: number;
|
|
33
|
+
/** Override the queue's default visibility timeout for received messages. */
|
|
34
|
+
visibilityTimeout?: number;
|
|
35
|
+
/** Max number of handler invocations in flight at once. Default 5. */
|
|
36
|
+
handlerConcurrency?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function awsSqs(opts: AwsSqsAdapterOptions = {}): PubSubAdapter<never, AwsSqsSubscribeMeta> {
|
|
40
|
+
const codec = opts.codec ?? jsonCodec();
|
|
41
|
+
let client: SQSClient | null = null;
|
|
42
|
+
let idCounter = 0;
|
|
43
|
+
|
|
44
|
+
const ensure = (): SQSClient => {
|
|
45
|
+
if (client) return client;
|
|
46
|
+
if (opts.client) {
|
|
47
|
+
client = opts.client;
|
|
48
|
+
} else {
|
|
49
|
+
client = new SQSClient({ region: opts.region, ...opts.options });
|
|
50
|
+
}
|
|
51
|
+
return client;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: 'aws-sqs',
|
|
56
|
+
capabilities: { publish: false, subscribe: true, patternSubscribe: false, ack: true },
|
|
57
|
+
|
|
58
|
+
async connect() {
|
|
59
|
+
ensure();
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async disconnect() {
|
|
63
|
+
if (client) client.destroy();
|
|
64
|
+
client = null;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async publish() {
|
|
68
|
+
throw new NotSupportedError(
|
|
69
|
+
'aws-sqs is subscribe-only. Use @pubber-subber/aws-sns (or another publish-capable adapter) ' +
|
|
70
|
+
'and compose them: `compose({ publisher: awsSns(), subscriber: awsSqs() })`.',
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async subscribe(topic, handler, meta) {
|
|
75
|
+
const c = ensure();
|
|
76
|
+
const queueUrl = meta?.queueUrl ?? opts.queueUrl;
|
|
77
|
+
if (!queueUrl) {
|
|
78
|
+
throw new SubscriptionError(
|
|
79
|
+
'queueUrl is required (pass it in awsSqs({ queueUrl }) or meta.queueUrl).',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const waitTimeSeconds = meta?.waitTimeSeconds ?? 20;
|
|
83
|
+
const maxMessages = meta?.maxMessages ?? 10;
|
|
84
|
+
const concurrency = meta?.handlerConcurrency ?? 5;
|
|
85
|
+
const visibilityTimeout = meta?.visibilityTimeout;
|
|
86
|
+
|
|
87
|
+
let stopped = false;
|
|
88
|
+
const semaphore = new Semaphore(concurrency);
|
|
89
|
+
const inflight = new Set<Promise<unknown>>();
|
|
90
|
+
|
|
91
|
+
const loop = async (): Promise<void> => {
|
|
92
|
+
while (!stopped) {
|
|
93
|
+
try {
|
|
94
|
+
const resp = await c.send(
|
|
95
|
+
new ReceiveMessageCommand({
|
|
96
|
+
QueueUrl: queueUrl,
|
|
97
|
+
MaxNumberOfMessages: maxMessages,
|
|
98
|
+
WaitTimeSeconds: waitTimeSeconds,
|
|
99
|
+
VisibilityTimeout: visibilityTimeout,
|
|
100
|
+
MessageAttributeNames: ['All'],
|
|
101
|
+
MessageSystemAttributeNames: ['All'] as never,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
for (const m of resp.Messages ?? []) {
|
|
105
|
+
if (stopped) break;
|
|
106
|
+
await semaphore.acquire();
|
|
107
|
+
const task = processMessage(c, queueUrl, topic, m, handler, codec).finally(() => {
|
|
108
|
+
semaphore.release();
|
|
109
|
+
inflight.delete(task);
|
|
110
|
+
});
|
|
111
|
+
inflight.add(task);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (stopped) break;
|
|
115
|
+
// Backoff briefly on receive errors so we don't hot-loop on a permission issue.
|
|
116
|
+
await sleep(1000);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
void loop();
|
|
122
|
+
|
|
123
|
+
idCounter += 1;
|
|
124
|
+
const id = `sqs-${idCounter}`;
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
topic,
|
|
128
|
+
unsubscribe: async () => {
|
|
129
|
+
stopped = true;
|
|
130
|
+
await Promise.allSettled([...inflight]);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function processMessage(
|
|
138
|
+
client: SQSClient,
|
|
139
|
+
queueUrl: string,
|
|
140
|
+
topic: string,
|
|
141
|
+
message: SqsMessage,
|
|
142
|
+
handler: (msg: AdapterMessage) => void | Promise<void>,
|
|
143
|
+
codec: Codec,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const body = message.Body;
|
|
146
|
+
let payload: unknown = body;
|
|
147
|
+
let snsAttributes: Record<string, unknown> | undefined;
|
|
148
|
+
|
|
149
|
+
if (typeof body === 'string') {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(body);
|
|
152
|
+
if (
|
|
153
|
+
parsed &&
|
|
154
|
+
typeof parsed === 'object' &&
|
|
155
|
+
parsed.Type === 'Notification' &&
|
|
156
|
+
typeof parsed.Message === 'string'
|
|
157
|
+
) {
|
|
158
|
+
// SNS→SQS envelope. Unwrap.
|
|
159
|
+
snsAttributes = parsed.MessageAttributes;
|
|
160
|
+
try {
|
|
161
|
+
payload = codec.decode(parsed.Message);
|
|
162
|
+
} catch {
|
|
163
|
+
payload = parsed.Message;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
payload = codec.decode(body);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
payload = body;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const receiptHandle = message.ReceiptHandle ?? '';
|
|
174
|
+
let resolved = false;
|
|
175
|
+
const doAck = async (): Promise<void> => {
|
|
176
|
+
if (resolved) return;
|
|
177
|
+
resolved = true;
|
|
178
|
+
await client.send(
|
|
179
|
+
new DeleteMessageCommand({
|
|
180
|
+
QueueUrl: queueUrl,
|
|
181
|
+
ReceiptHandle: receiptHandle,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
const doNack = async (): Promise<void> => {
|
|
186
|
+
if (resolved) return;
|
|
187
|
+
resolved = true;
|
|
188
|
+
await client.send(
|
|
189
|
+
new ChangeMessageVisibilityCommand({
|
|
190
|
+
QueueUrl: queueUrl,
|
|
191
|
+
ReceiptHandle: receiptHandle,
|
|
192
|
+
VisibilityTimeout: 0,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const adapterMsg: AdapterMessage = {
|
|
198
|
+
topic,
|
|
199
|
+
payload,
|
|
200
|
+
raw: message,
|
|
201
|
+
meta: {
|
|
202
|
+
messageId: message.MessageId,
|
|
203
|
+
receiptHandle: message.ReceiptHandle,
|
|
204
|
+
attributes: message.MessageAttributes,
|
|
205
|
+
systemAttributes: message.Attributes,
|
|
206
|
+
snsAttributes,
|
|
207
|
+
},
|
|
208
|
+
ack: doAck,
|
|
209
|
+
nack: doNack,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await handler(adapterMsg);
|
|
214
|
+
await doAck();
|
|
215
|
+
} catch {
|
|
216
|
+
await doNack().catch(() => undefined);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sleep(ms: number): Promise<void> {
|
|
221
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class Semaphore {
|
|
225
|
+
#permits: number;
|
|
226
|
+
#waiters: Array<() => void> = [];
|
|
227
|
+
|
|
228
|
+
constructor(permits: number) {
|
|
229
|
+
this.#permits = permits;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async acquire(): Promise<void> {
|
|
233
|
+
if (this.#permits > 0) {
|
|
234
|
+
this.#permits -= 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
await new Promise<void>((resolve) => this.#waiters.push(resolve));
|
|
238
|
+
this.#permits -= 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
release(): void {
|
|
242
|
+
this.#permits += 1;
|
|
243
|
+
const next = this.#waiters.shift();
|
|
244
|
+
if (next) next();
|
|
245
|
+
}
|
|
246
|
+
}
|