@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 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"]}
@@ -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 };
@@ -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":"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","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
+ }