@geekmidas/events 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.
Files changed (211) hide show
  1. package/README.md +473 -0
  2. package/dist/BasicConnection-BNi6_olg.mjs +42 -0
  3. package/dist/BasicConnection-BNi6_olg.mjs.map +1 -0
  4. package/dist/BasicConnection-C4_Y0nIj.d.mts +26 -0
  5. package/dist/BasicConnection-CddnULuT.d.cts +26 -0
  6. package/dist/BasicConnection-CzfMubv_.cjs +48 -0
  7. package/dist/BasicConnection-CzfMubv_.cjs.map +1 -0
  8. package/dist/BasicPublisher-BzfzHIfb.cjs +20 -0
  9. package/dist/BasicPublisher-BzfzHIfb.cjs.map +1 -0
  10. package/dist/BasicPublisher-Cg3SxIVR.mjs +14 -0
  11. package/dist/BasicPublisher-Cg3SxIVR.mjs.map +1 -0
  12. package/dist/BasicPublisher-DCBxaTwC.d.mts +12 -0
  13. package/dist/BasicPublisher-V5hELHG9.d.cts +12 -0
  14. package/dist/BasicSubscriber-BB3aKeph.d.mts +12 -0
  15. package/dist/BasicSubscriber-BEv5N21f.mjs +18 -0
  16. package/dist/BasicSubscriber-BEv5N21f.mjs.map +1 -0
  17. package/dist/BasicSubscriber-DOwgFw5j.d.cts +12 -0
  18. package/dist/BasicSubscriber-DyTKOixe.cjs +24 -0
  19. package/dist/BasicSubscriber-DyTKOixe.cjs.map +1 -0
  20. package/dist/EventConnection-ClLMoIUU.mjs +36 -0
  21. package/dist/EventConnection-ClLMoIUU.mjs.map +1 -0
  22. package/dist/EventConnection-DZhn1Msl.d.cts +13 -0
  23. package/dist/EventConnection-DeXfLGom.d.mts +13 -0
  24. package/dist/EventConnection-F_N81SXu.cjs +41 -0
  25. package/dist/EventConnection-F_N81SXu.cjs.map +1 -0
  26. package/dist/EventConnection.cjs +5 -0
  27. package/dist/EventConnection.d.cts +3 -0
  28. package/dist/EventConnection.d.mts +3 -0
  29. package/dist/EventConnection.mjs +4 -0
  30. package/dist/Publisher-C3m8JFXm.d.mts +19 -0
  31. package/dist/Publisher-DS0aOh8r.mjs +65 -0
  32. package/dist/Publisher-DS0aOh8r.mjs.map +1 -0
  33. package/dist/Publisher-ceu6OxCj.cjs +70 -0
  34. package/dist/Publisher-ceu6OxCj.cjs.map +1 -0
  35. package/dist/Publisher-nXNVjL0e.d.cts +19 -0
  36. package/dist/Publisher.cjs +4 -0
  37. package/dist/Publisher.d.cts +3 -0
  38. package/dist/Publisher.d.mts +3 -0
  39. package/dist/Publisher.mjs +4 -0
  40. package/dist/RabbitMQConnection-7cWp_-_Q.d.cts +43 -0
  41. package/dist/RabbitMQConnection-BN_GybOv.cjs +113 -0
  42. package/dist/RabbitMQConnection-BN_GybOv.cjs.map +1 -0
  43. package/dist/RabbitMQConnection-C2HC7J7K.d.mts +43 -0
  44. package/dist/RabbitMQConnection-eVOVS4Dl.mjs +107 -0
  45. package/dist/RabbitMQConnection-eVOVS4Dl.mjs.map +1 -0
  46. package/dist/RabbitMQPublisher-BZX0edgL.mjs +39 -0
  47. package/dist/RabbitMQPublisher-BZX0edgL.mjs.map +1 -0
  48. package/dist/RabbitMQPublisher-CSCn6gMA.cjs +45 -0
  49. package/dist/RabbitMQPublisher-CSCn6gMA.cjs.map +1 -0
  50. package/dist/RabbitMQPublisher-CpcbgrXj.d.cts +23 -0
  51. package/dist/RabbitMQPublisher-DDZi15Ro.d.mts +23 -0
  52. package/dist/RabbitMQSubscriber-B5I-hBaT.mjs +57 -0
  53. package/dist/RabbitMQSubscriber-B5I-hBaT.mjs.map +1 -0
  54. package/dist/RabbitMQSubscriber-BJ9PTGny.d.cts +25 -0
  55. package/dist/RabbitMQSubscriber-CIvIw8MG.d.mts +25 -0
  56. package/dist/RabbitMQSubscriber-Cj7qLIi6.cjs +63 -0
  57. package/dist/RabbitMQSubscriber-Cj7qLIi6.cjs.map +1 -0
  58. package/dist/SNSConnection-CAImaMM2.d.mts +40 -0
  59. package/dist/SNSConnection-Cqbr7-Ho.d.cts +40 -0
  60. package/dist/SNSConnection-D7wCf9Si.mjs +70 -0
  61. package/dist/SNSConnection-D7wCf9Si.mjs.map +1 -0
  62. package/dist/SNSConnection-DVsC_pbB.cjs +76 -0
  63. package/dist/SNSConnection-DVsC_pbB.cjs.map +1 -0
  64. package/dist/SNSPublisher-BZ6N3JI9.cjs +48 -0
  65. package/dist/SNSPublisher-BZ6N3JI9.cjs.map +1 -0
  66. package/dist/SNSPublisher-BfWjzNiB.mjs +42 -0
  67. package/dist/SNSPublisher-BfWjzNiB.mjs.map +1 -0
  68. package/dist/SNSPublisher-Dkhb4LU-.d.mts +21 -0
  69. package/dist/SNSPublisher-RVZFa7kL.d.cts +21 -0
  70. package/dist/SNSSubscriber-B31JmkAE.d.mts +54 -0
  71. package/dist/SNSSubscriber-C9YJWa7m.mjs +143 -0
  72. package/dist/SNSSubscriber-C9YJWa7m.mjs.map +1 -0
  73. package/dist/SNSSubscriber-UE0ia4XM.cjs +149 -0
  74. package/dist/SNSSubscriber-UE0ia4XM.cjs.map +1 -0
  75. package/dist/SNSSubscriber-k2UwKUxu.d.cts +54 -0
  76. package/dist/SQSConnection-BwNjUPr4.cjs +76 -0
  77. package/dist/SQSConnection-BwNjUPr4.cjs.map +1 -0
  78. package/dist/SQSConnection-C5WFqZW7.d.mts +40 -0
  79. package/dist/SQSConnection-Eleqqre3.mjs +70 -0
  80. package/dist/SQSConnection-Eleqqre3.mjs.map +1 -0
  81. package/dist/SQSConnection-geav2Rsn.d.cts +40 -0
  82. package/dist/SQSPublisher-CdsayOLV.d.mts +23 -0
  83. package/dist/SQSPublisher-DG9o4ME3.mjs +57 -0
  84. package/dist/SQSPublisher-DG9o4ME3.mjs.map +1 -0
  85. package/dist/SQSPublisher-Dq5353oZ.cjs +63 -0
  86. package/dist/SQSPublisher-Dq5353oZ.cjs.map +1 -0
  87. package/dist/SQSPublisher-Utb_FTe5.d.cts +23 -0
  88. package/dist/SQSSubscriber-B6iL1EnX.d.cts +43 -0
  89. package/dist/SQSSubscriber-CD66P7hH.mjs +121 -0
  90. package/dist/SQSSubscriber-CD66P7hH.mjs.map +1 -0
  91. package/dist/SQSSubscriber-DBeE4XAR.d.mts +43 -0
  92. package/dist/SQSSubscriber-DZDNgLE5.cjs +127 -0
  93. package/dist/SQSSubscriber-DZDNgLE5.cjs.map +1 -0
  94. package/dist/Subscriber-Bd9hb_jp.mjs +74 -0
  95. package/dist/Subscriber-Bd9hb_jp.mjs.map +1 -0
  96. package/dist/Subscriber-Cff9EIpv.d.mts +19 -0
  97. package/dist/Subscriber-CpAwcUzb.d.cts +19 -0
  98. package/dist/Subscriber-saVOWuMq.cjs +79 -0
  99. package/dist/Subscriber-saVOWuMq.cjs.map +1 -0
  100. package/dist/Subscriber.cjs +4 -0
  101. package/dist/Subscriber.d.cts +3 -0
  102. package/dist/Subscriber.d.mts +3 -0
  103. package/dist/Subscriber.mjs +4 -0
  104. package/dist/basic/BasicConnection.cjs +4 -0
  105. package/dist/basic/BasicConnection.d.cts +3 -0
  106. package/dist/basic/BasicConnection.d.mts +3 -0
  107. package/dist/basic/BasicConnection.mjs +4 -0
  108. package/dist/basic/BasicPublisher.cjs +3 -0
  109. package/dist/basic/BasicPublisher.d.cts +4 -0
  110. package/dist/basic/BasicPublisher.d.mts +4 -0
  111. package/dist/basic/BasicPublisher.mjs +3 -0
  112. package/dist/basic/BasicSubscriber.cjs +3 -0
  113. package/dist/basic/BasicSubscriber.d.cts +4 -0
  114. package/dist/basic/BasicSubscriber.d.mts +4 -0
  115. package/dist/basic/BasicSubscriber.mjs +3 -0
  116. package/dist/basic/index.cjs +8 -0
  117. package/dist/basic/index.d.cts +5 -0
  118. package/dist/basic/index.d.mts +5 -0
  119. package/dist/basic/index.mjs +6 -0
  120. package/dist/chunk-CUT6urMc.cjs +30 -0
  121. package/dist/index.cjs +9 -0
  122. package/dist/index.d.cts +5 -0
  123. package/dist/index.d.mts +5 -0
  124. package/dist/index.mjs +6 -0
  125. package/dist/rabbitmq/RabbitMQConnection.cjs +4 -0
  126. package/dist/rabbitmq/RabbitMQConnection.d.cts +3 -0
  127. package/dist/rabbitmq/RabbitMQConnection.d.mts +3 -0
  128. package/dist/rabbitmq/RabbitMQConnection.mjs +4 -0
  129. package/dist/rabbitmq/RabbitMQPublisher.cjs +3 -0
  130. package/dist/rabbitmq/RabbitMQPublisher.d.cts +4 -0
  131. package/dist/rabbitmq/RabbitMQPublisher.d.mts +4 -0
  132. package/dist/rabbitmq/RabbitMQPublisher.mjs +3 -0
  133. package/dist/rabbitmq/RabbitMQSubscriber.cjs +3 -0
  134. package/dist/rabbitmq/RabbitMQSubscriber.d.cts +4 -0
  135. package/dist/rabbitmq/RabbitMQSubscriber.d.mts +4 -0
  136. package/dist/rabbitmq/RabbitMQSubscriber.mjs +3 -0
  137. package/dist/rabbitmq/index.cjs +8 -0
  138. package/dist/rabbitmq/index.d.cts +5 -0
  139. package/dist/rabbitmq/index.d.mts +5 -0
  140. package/dist/rabbitmq/index.mjs +6 -0
  141. package/dist/sns/SNSConnection.cjs +4 -0
  142. package/dist/sns/SNSConnection.d.cts +3 -0
  143. package/dist/sns/SNSConnection.d.mts +3 -0
  144. package/dist/sns/SNSConnection.mjs +4 -0
  145. package/dist/sns/SNSPublisher.cjs +3 -0
  146. package/dist/sns/SNSPublisher.d.cts +4 -0
  147. package/dist/sns/SNSPublisher.d.mts +4 -0
  148. package/dist/sns/SNSPublisher.mjs +3 -0
  149. package/dist/sns/SNSSubscriber.cjs +6 -0
  150. package/dist/sns/SNSSubscriber.d.cts +4 -0
  151. package/dist/sns/SNSSubscriber.d.mts +4 -0
  152. package/dist/sns/SNSSubscriber.mjs +6 -0
  153. package/dist/sns/index.cjs +10 -0
  154. package/dist/sns/index.d.cts +5 -0
  155. package/dist/sns/index.d.mts +5 -0
  156. package/dist/sns/index.mjs +8 -0
  157. package/dist/sqs/SQSConnection.cjs +4 -0
  158. package/dist/sqs/SQSConnection.d.cts +3 -0
  159. package/dist/sqs/SQSConnection.d.mts +3 -0
  160. package/dist/sqs/SQSConnection.mjs +4 -0
  161. package/dist/sqs/SQSPublisher.cjs +3 -0
  162. package/dist/sqs/SQSPublisher.d.cts +4 -0
  163. package/dist/sqs/SQSPublisher.d.mts +4 -0
  164. package/dist/sqs/SQSPublisher.mjs +3 -0
  165. package/dist/sqs/SQSSubscriber.cjs +3 -0
  166. package/dist/sqs/SQSSubscriber.d.cts +4 -0
  167. package/dist/sqs/SQSSubscriber.d.mts +4 -0
  168. package/dist/sqs/SQSSubscriber.mjs +3 -0
  169. package/dist/sqs/index.cjs +8 -0
  170. package/dist/sqs/index.d.cts +5 -0
  171. package/dist/sqs/index.d.mts +5 -0
  172. package/dist/sqs/index.mjs +6 -0
  173. package/dist/types-B63qHVOG.mjs +17 -0
  174. package/dist/types-B63qHVOG.mjs.map +1 -0
  175. package/dist/types-CvSMEO5z.cjs +23 -0
  176. package/dist/types-CvSMEO5z.cjs.map +1 -0
  177. package/dist/types-D0g-o6wX.d.mts +65 -0
  178. package/dist/types-DEGUg5BA.d.cts +65 -0
  179. package/dist/types.cjs +3 -0
  180. package/dist/types.d.cts +2 -0
  181. package/dist/types.d.mts +2 -0
  182. package/dist/types.mjs +3 -0
  183. package/package.json +49 -0
  184. package/src/EventConnection.ts +39 -0
  185. package/src/Publisher.ts +87 -0
  186. package/src/Subscriber.ts +102 -0
  187. package/src/__tests__/Subscriber.spec.ts +41 -0
  188. package/src/basic/BasicConnection.ts +47 -0
  189. package/src/basic/BasicPublisher.ts +15 -0
  190. package/src/basic/BasicSubscriber.ts +24 -0
  191. package/src/basic/__tests__/BasicSubscriber.spec.ts +166 -0
  192. package/src/basic/index.ts +3 -0
  193. package/src/index.ts +25 -0
  194. package/src/rabbitmq/RabbitMQConnection.ts +148 -0
  195. package/src/rabbitmq/RabbitMQPublisher.ts +67 -0
  196. package/src/rabbitmq/RabbitMQSubscriber.ts +113 -0
  197. package/src/rabbitmq/__tests__/RabbitMQPublisher.spec.ts +378 -0
  198. package/src/rabbitmq/__tests__/RabbitMQSubscriber.spec.ts +292 -0
  199. package/src/rabbitmq/index.ts +6 -0
  200. package/src/sns/SNSConnection.ts +98 -0
  201. package/src/sns/SNSPublisher.ts +64 -0
  202. package/src/sns/SNSSubscriber.ts +222 -0
  203. package/src/sns/__tests__/SNS-SQS-Integration.spec.ts +389 -0
  204. package/src/sns/index.ts +6 -0
  205. package/src/sqs/SQSConnection.ts +97 -0
  206. package/src/sqs/SQSPublisher.ts +105 -0
  207. package/src/sqs/SQSSubscriber.ts +189 -0
  208. package/src/sqs/__tests__/SQSPublisher.spec.ts +189 -0
  209. package/src/sqs/index.ts +6 -0
  210. package/src/types.ts +76 -0
  211. package/tsdown.config.ts +5 -0
@@ -0,0 +1,389 @@
1
+ import {
2
+ CreateTopicCommand,
3
+ DeleteTopicCommand,
4
+ SNSClient,
5
+ SubscribeCommand,
6
+ UnsubscribeCommand,
7
+ } from '@aws-sdk/client-sns';
8
+ import {
9
+ CreateQueueCommand,
10
+ DeleteQueueCommand,
11
+ GetQueueAttributesCommand,
12
+ SQSClient,
13
+ SetQueueAttributesCommand,
14
+ } from '@aws-sdk/client-sqs';
15
+ import { describe, expect, it } from 'vitest';
16
+ import { SQSConnection } from '../../sqs/SQSConnection';
17
+ import { SQSSubscriber } from '../../sqs/SQSSubscriber';
18
+ import type { PublishableMessage } from '../../types';
19
+ import { SNSConnection } from '../SNSConnection';
20
+ import { SNSPublisher } from '../SNSPublisher';
21
+
22
+ type TestMessage = PublishableMessage<'user.created' | 'user.updated', any>;
23
+
24
+ const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
25
+ const AWS_REGION = 'us-east-1';
26
+ const AWS_CREDENTIALS = {
27
+ accessKeyId: 'test',
28
+ secretAccessKey: 'test',
29
+ };
30
+
31
+ // Helper to create a unique topic name
32
+ const uniqueTopicName = () =>
33
+ `test-topic-${Date.now()}-${Math.random().toString(36).substring(7)}`;
34
+
35
+ // Helper to create a unique queue name
36
+ const uniqueQueueName = () =>
37
+ `test-queue-${Date.now()}-${Math.random().toString(36).substring(7)}`;
38
+
39
+ // Helper to create an SNS topic and return its ARN
40
+ async function createTopic(topicName: string): Promise<string> {
41
+ const client = new SNSClient({
42
+ region: AWS_REGION,
43
+ endpoint: LOCALSTACK_ENDPOINT,
44
+ credentials: AWS_CREDENTIALS,
45
+ });
46
+
47
+ const command = new CreateTopicCommand({
48
+ Name: topicName,
49
+ });
50
+
51
+ const response = await client.send(command);
52
+ client.destroy();
53
+
54
+ if (!response.TopicArn) {
55
+ throw new Error('Failed to create topic');
56
+ }
57
+
58
+ return response.TopicArn;
59
+ }
60
+
61
+ // Helper to delete an SNS topic
62
+ async function deleteTopic(topicArn: string): Promise<void> {
63
+ const client = new SNSClient({
64
+ region: AWS_REGION,
65
+ endpoint: LOCALSTACK_ENDPOINT,
66
+ credentials: AWS_CREDENTIALS,
67
+ });
68
+
69
+ const command = new DeleteTopicCommand({
70
+ TopicArn: topicArn,
71
+ });
72
+
73
+ await client.send(command);
74
+ client.destroy();
75
+ }
76
+
77
+ // Helper to create an SQS queue and return its URL
78
+ async function createQueue(queueName: string): Promise<string> {
79
+ const client = new SQSClient({
80
+ region: AWS_REGION,
81
+ endpoint: LOCALSTACK_ENDPOINT,
82
+ credentials: AWS_CREDENTIALS,
83
+ });
84
+
85
+ const command = new CreateQueueCommand({
86
+ QueueName: queueName,
87
+ });
88
+
89
+ const response = await client.send(command);
90
+ client.destroy();
91
+
92
+ if (!response.QueueUrl) {
93
+ throw new Error('Failed to create queue');
94
+ }
95
+
96
+ return response.QueueUrl;
97
+ }
98
+
99
+ // Helper to get queue ARN
100
+ async function getQueueArn(queueUrl: string): Promise<string> {
101
+ const client = new SQSClient({
102
+ region: AWS_REGION,
103
+ endpoint: LOCALSTACK_ENDPOINT,
104
+ credentials: AWS_CREDENTIALS,
105
+ });
106
+
107
+ const command = new GetQueueAttributesCommand({
108
+ QueueUrl: queueUrl,
109
+ AttributeNames: ['QueueArn'],
110
+ });
111
+
112
+ const response = await client.send(command);
113
+ client.destroy();
114
+
115
+ const queueArn = response.Attributes?.QueueArn;
116
+ if (!queueArn) {
117
+ throw new Error('Failed to get queue ARN');
118
+ }
119
+
120
+ return queueArn;
121
+ }
122
+
123
+ // Helper to set queue policy to allow SNS to send messages
124
+ async function setQueuePolicy(
125
+ queueUrl: string,
126
+ queueArn: string,
127
+ topicArn: string,
128
+ ): Promise<void> {
129
+ const client = new SQSClient({
130
+ region: AWS_REGION,
131
+ endpoint: LOCALSTACK_ENDPOINT,
132
+ credentials: AWS_CREDENTIALS,
133
+ });
134
+
135
+ const policy = {
136
+ Version: '2012-10-17',
137
+ Statement: [
138
+ {
139
+ Effect: 'Allow',
140
+ Principal: '*',
141
+ Action: 'SQS:SendMessage',
142
+ Resource: queueArn,
143
+ Condition: {
144
+ ArnEquals: {
145
+ 'aws:SourceArn': topicArn,
146
+ },
147
+ },
148
+ },
149
+ ],
150
+ };
151
+
152
+ const command = new SetQueueAttributesCommand({
153
+ QueueUrl: queueUrl,
154
+ Attributes: {
155
+ Policy: JSON.stringify(policy),
156
+ },
157
+ });
158
+
159
+ await client.send(command);
160
+ client.destroy();
161
+ }
162
+
163
+ // Helper to subscribe SQS queue to SNS topic
164
+ async function subscribeQueueToTopic(
165
+ topicArn: string,
166
+ queueArn: string,
167
+ ): Promise<string> {
168
+ const client = new SNSClient({
169
+ region: AWS_REGION,
170
+ endpoint: LOCALSTACK_ENDPOINT,
171
+ credentials: AWS_CREDENTIALS,
172
+ });
173
+
174
+ const command = new SubscribeCommand({
175
+ TopicArn: topicArn,
176
+ Protocol: 'sqs',
177
+ Endpoint: queueArn,
178
+ });
179
+
180
+ const response = await client.send(command);
181
+ client.destroy();
182
+
183
+ if (!response.SubscriptionArn) {
184
+ throw new Error('Failed to subscribe queue to topic');
185
+ }
186
+
187
+ return response.SubscriptionArn;
188
+ }
189
+
190
+ // Helper to unsubscribe
191
+ async function unsubscribe(subscriptionArn: string): Promise<void> {
192
+ const client = new SNSClient({
193
+ region: AWS_REGION,
194
+ endpoint: LOCALSTACK_ENDPOINT,
195
+ credentials: AWS_CREDENTIALS,
196
+ });
197
+
198
+ const command = new UnsubscribeCommand({
199
+ SubscriptionArn: subscriptionArn,
200
+ });
201
+
202
+ await client.send(command);
203
+ client.destroy();
204
+ }
205
+
206
+ // Helper to delete a queue
207
+ async function deleteQueue(queueUrl: string): Promise<void> {
208
+ const client = new SQSClient({
209
+ region: AWS_REGION,
210
+ endpoint: LOCALSTACK_ENDPOINT,
211
+ credentials: AWS_CREDENTIALS,
212
+ });
213
+
214
+ const command = new DeleteQueueCommand({
215
+ QueueUrl: queueUrl,
216
+ });
217
+
218
+ await client.send(command);
219
+ client.destroy();
220
+ }
221
+
222
+ describe('SNS to SQS Integration Tests', () => {
223
+ it('should publish to SNS and receive via SQS subscription', async () => {
224
+ const topicName = uniqueTopicName();
225
+ const queueName = uniqueQueueName();
226
+
227
+ const topicArn = await createTopic(topicName);
228
+ const queueUrl = await createQueue(queueName);
229
+ const queueArn = await getQueueArn(queueUrl);
230
+
231
+ // Set queue policy to allow SNS
232
+ await setQueuePolicy(queueUrl, queueArn, topicArn);
233
+
234
+ // Subscribe queue to topic
235
+ const subscriptionArn = await subscribeQueueToTopic(topicArn, queueArn);
236
+
237
+ try {
238
+ // Wait a moment for subscription to be ready
239
+ await new Promise((resolve) => setTimeout(resolve, 1000));
240
+
241
+ // Create SNS publisher
242
+ const snsConnection = new SNSConnection({
243
+ topicArn,
244
+ endpoint: LOCALSTACK_ENDPOINT,
245
+ region: AWS_REGION,
246
+ credentials: AWS_CREDENTIALS,
247
+ });
248
+ await snsConnection.connect();
249
+
250
+ const snsPublisher = new SNSPublisher<TestMessage>(snsConnection);
251
+
252
+ // Create SQS subscriber
253
+ const sqsConnection = new SQSConnection({
254
+ queueUrl,
255
+ endpoint: LOCALSTACK_ENDPOINT,
256
+ region: AWS_REGION,
257
+ credentials: AWS_CREDENTIALS,
258
+ });
259
+ await sqsConnection.connect();
260
+
261
+ const sqsSubscriber = new SQSSubscriber<TestMessage>(sqsConnection, {
262
+ waitTimeSeconds: 1,
263
+ });
264
+
265
+ const receivedMessages: TestMessage[] = [];
266
+
267
+ await sqsSubscriber.subscribe(['user.created'], async (message) => {
268
+ receivedMessages.push(message);
269
+ });
270
+
271
+ // Publish via SNS
272
+ await snsPublisher.publish([
273
+ { type: 'user.created', payload: { userId: '123' } },
274
+ ]);
275
+
276
+ // Wait for message to be delivered (SNS to SQS is nearly instant)
277
+ await new Promise((resolve) => setTimeout(resolve, 2000));
278
+
279
+ sqsSubscriber.stop();
280
+
281
+ expect(receivedMessages).toHaveLength(1);
282
+ expect(receivedMessages[0].type).toBe('user.created');
283
+ expect(receivedMessages[0].payload.userId).toBe('123');
284
+
285
+ await snsConnection.close();
286
+ await sqsConnection.close();
287
+ } finally {
288
+ await unsubscribe(subscriptionArn);
289
+ await deleteQueue(queueUrl);
290
+ await deleteTopic(topicArn);
291
+ }
292
+ }, 15000);
293
+
294
+ it('should fan out to multiple SQS queues', async () => {
295
+ const topicName = uniqueTopicName();
296
+ const queue1Name = uniqueQueueName();
297
+ const queue2Name = uniqueQueueName();
298
+
299
+ const topicArn = await createTopic(topicName);
300
+ const queue1Url = await createQueue(queue1Name);
301
+ const queue2Url = await createQueue(queue2Name);
302
+ const queue1Arn = await getQueueArn(queue1Url);
303
+ const queue2Arn = await getQueueArn(queue2Url);
304
+
305
+ await setQueuePolicy(queue1Url, queue1Arn, topicArn);
306
+ await setQueuePolicy(queue2Url, queue2Arn, topicArn);
307
+
308
+ const subscription1Arn = await subscribeQueueToTopic(topicArn, queue1Arn);
309
+ const subscription2Arn = await subscribeQueueToTopic(topicArn, queue2Arn);
310
+
311
+ try {
312
+ await new Promise((resolve) => setTimeout(resolve, 1000));
313
+
314
+ // Create SNS publisher
315
+ const snsConnection = new SNSConnection({
316
+ topicArn,
317
+ endpoint: LOCALSTACK_ENDPOINT,
318
+ region: AWS_REGION,
319
+ credentials: AWS_CREDENTIALS,
320
+ });
321
+ await snsConnection.connect();
322
+
323
+ const snsPublisher = new SNSPublisher<TestMessage>(snsConnection);
324
+
325
+ // Create SQS subscribers for both queues
326
+ const sqsConnection1 = new SQSConnection({
327
+ queueUrl: queue1Url,
328
+ endpoint: LOCALSTACK_ENDPOINT,
329
+ region: AWS_REGION,
330
+ credentials: AWS_CREDENTIALS,
331
+ });
332
+ await sqsConnection1.connect();
333
+
334
+ const sqsConnection2 = new SQSConnection({
335
+ queueUrl: queue2Url,
336
+ endpoint: LOCALSTACK_ENDPOINT,
337
+ region: AWS_REGION,
338
+ credentials: AWS_CREDENTIALS,
339
+ });
340
+ await sqsConnection2.connect();
341
+
342
+ const sqsSubscriber1 = new SQSSubscriber<TestMessage>(sqsConnection1, {
343
+ waitTimeSeconds: 1,
344
+ });
345
+
346
+ const sqsSubscriber2 = new SQSSubscriber<TestMessage>(sqsConnection2, {
347
+ waitTimeSeconds: 1,
348
+ });
349
+
350
+ const queue1Messages: TestMessage[] = [];
351
+ const queue2Messages: TestMessage[] = [];
352
+
353
+ await sqsSubscriber1.subscribe(['user.created'], async (message) => {
354
+ queue1Messages.push(message);
355
+ });
356
+
357
+ await sqsSubscriber2.subscribe(['user.created'], async (message) => {
358
+ queue2Messages.push(message);
359
+ });
360
+
361
+ // Publish via SNS
362
+ await snsPublisher.publish([
363
+ { type: 'user.created', payload: { userId: '456' } },
364
+ ]);
365
+
366
+ // Wait for messages
367
+ await new Promise((resolve) => setTimeout(resolve, 3000));
368
+
369
+ sqsSubscriber1.stop();
370
+ sqsSubscriber2.stop();
371
+
372
+ // Both queues should receive the message
373
+ expect(queue1Messages).toHaveLength(1);
374
+ expect(queue2Messages).toHaveLength(1);
375
+ expect(queue1Messages[0].payload.userId).toBe('456');
376
+ expect(queue2Messages[0].payload.userId).toBe('456');
377
+
378
+ await snsConnection.close();
379
+ await sqsConnection1.close();
380
+ await sqsConnection2.close();
381
+ } finally {
382
+ await unsubscribe(subscription1Arn);
383
+ await unsubscribe(subscription2Arn);
384
+ await deleteQueue(queue1Url);
385
+ await deleteQueue(queue2Url);
386
+ await deleteTopic(topicArn);
387
+ }
388
+ }, 20000);
389
+ });
@@ -0,0 +1,6 @@
1
+ export { SNSConnection } from './SNSConnection';
2
+ export type { SNSConnectionConfig } from './SNSConnection';
3
+ export { SNSPublisher } from './SNSPublisher';
4
+ export type { SNSPublisherOptions } from './SNSPublisher';
5
+ export { SNSSubscriber } from './SNSSubscriber';
6
+ export type { SNSSubscriberOptions } from './SNSSubscriber';
@@ -0,0 +1,97 @@
1
+ import { SQSClient, type SQSClientConfig } from '@aws-sdk/client-sqs';
2
+ import type { EventConnection } from '../types';
3
+ import { EventPublisherType } from '../types';
4
+
5
+ export interface SQSConnectionConfig {
6
+ queueUrl: string;
7
+ region?: string;
8
+ endpoint?: string; // Custom endpoint (e.g., for LocalStack)
9
+ credentials?: {
10
+ accessKeyId: string;
11
+ secretAccessKey: string;
12
+ sessionToken?: string;
13
+ };
14
+ }
15
+
16
+ export class SQSConnection implements EventConnection {
17
+ readonly type = EventPublisherType.SQS;
18
+ private client: SQSClient;
19
+ private connected = false;
20
+
21
+ constructor(readonly config: SQSConnectionConfig) {
22
+ this.config = config;
23
+
24
+ const clientConfig: SQSClientConfig = {
25
+ region: config.region,
26
+ endpoint: config.endpoint,
27
+ credentials: config.credentials,
28
+ };
29
+
30
+ this.client = new SQSClient(clientConfig);
31
+ }
32
+
33
+ /**
34
+ * Create an SQSConnection from a connection string
35
+ * Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName&region=us-east-1&endpoint=http://localhost:4566
36
+ */
37
+ static async fromConnectionString(
38
+ connectionString: string,
39
+ ): Promise<SQSConnection> {
40
+ const url = new URL(connectionString);
41
+ const params = url.searchParams;
42
+
43
+ const queueUrl = params.get('queueUrl');
44
+ if (!queueUrl) {
45
+ throw new Error('queueUrl parameter is required in connection string');
46
+ }
47
+
48
+ const config: SQSConnectionConfig = {
49
+ queueUrl,
50
+ region: params.get('region') || undefined,
51
+ endpoint: params.get('endpoint') || undefined,
52
+ };
53
+
54
+ // Parse credentials if provided
55
+ const accessKeyId = params.get('accessKeyId');
56
+ const secretAccessKey = params.get('secretAccessKey');
57
+ if (accessKeyId && secretAccessKey) {
58
+ config.credentials = {
59
+ accessKeyId,
60
+ secretAccessKey,
61
+ sessionToken: params.get('sessionToken') || undefined,
62
+ };
63
+ }
64
+
65
+ const connection = new SQSConnection(config);
66
+ await connection.connect();
67
+ return connection;
68
+ }
69
+
70
+ async connect(): Promise<void> {
71
+ // SQS doesn't have a traditional connection, but we mark as connected
72
+ this.connected = true;
73
+ }
74
+
75
+ async close(): Promise<void> {
76
+ this.client.destroy();
77
+ this.connected = false;
78
+ }
79
+
80
+ isConnected(): boolean {
81
+ return this.connected;
82
+ }
83
+
84
+ /**
85
+ * Get the underlying SQS client
86
+ */
87
+ get sqsClient(): SQSClient {
88
+ return this.client;
89
+ }
90
+
91
+ /**
92
+ * Get the queue URL for this connection
93
+ */
94
+ get queueUrl(): string {
95
+ return this.config.queueUrl;
96
+ }
97
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ SendMessageBatchCommand,
3
+ type SendMessageBatchCommandInput,
4
+ type SendMessageBatchRequestEntry,
5
+ } from '@aws-sdk/client-sqs';
6
+ import chunk from 'lodash.chunk';
7
+ import type { EventPublisher, PublishableMessage } from '../types';
8
+ import type { SQSConnection } from './SQSConnection';
9
+
10
+ export interface SQSPublisherOptions {
11
+ maxBatchSize?: number; // Default: 10 (SQS limit)
12
+ }
13
+
14
+ export class SQSPublisher<TMessage extends PublishableMessage<string, any>>
15
+ implements EventPublisher<TMessage>
16
+ {
17
+ readonly options: Required<SQSPublisherOptions>;
18
+
19
+ constructor(
20
+ readonly connection: SQSConnection,
21
+ options: SQSPublisherOptions = {},
22
+ ) {
23
+ this.options = {
24
+ maxBatchSize: options.maxBatchSize ?? 10, // SQS limit
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Create an SQSPublisher from a connection string
30
+ * Format: sqs://?queueUrl=https://sqs.region.amazonaws.com/accountId/queueName&region=us-east-1&endpoint=http://localhost:4566&maxBatchSize=10
31
+ */
32
+ static async fromConnectionString<
33
+ TMessage extends PublishableMessage<string, any>,
34
+ >(connectionString: string): Promise<SQSPublisher<TMessage>> {
35
+ const url = new URL(connectionString);
36
+ const params = url.searchParams;
37
+
38
+ const { SQSConnection } = await import('./SQSConnection');
39
+ const connection =
40
+ await SQSConnection.fromConnectionString(connectionString);
41
+
42
+ const options: SQSPublisherOptions = {
43
+ maxBatchSize: params.get('maxBatchSize')
44
+ ? Number.parseInt(params.get('maxBatchSize')!, 10)
45
+ : undefined,
46
+ };
47
+
48
+ return new SQSPublisher<TMessage>(connection, options);
49
+ }
50
+
51
+ async publish(messages: TMessage[]): Promise<void> {
52
+ if (messages.length === 0) return;
53
+
54
+ if (!this.connection.isConnected()) {
55
+ await this.connection.connect();
56
+ }
57
+
58
+ // Split messages into batches (SQS limit is 10 messages per batch)
59
+ const batches = chunk(messages, this.options.maxBatchSize);
60
+
61
+ // Send all batches
62
+ await Promise.all(batches.map((batch) => this.sendBatch(batch)));
63
+ }
64
+
65
+ private async sendBatch(messages: TMessage[]): Promise<void> {
66
+ const entries: SendMessageBatchRequestEntry[] = messages.map(
67
+ (message, index) => ({
68
+ Id: `${index}`,
69
+ MessageBody: JSON.stringify({
70
+ type: message.type,
71
+ payload: message.payload,
72
+ }),
73
+ MessageAttributes: {
74
+ type: {
75
+ DataType: 'String',
76
+ StringValue: message.type,
77
+ },
78
+ },
79
+ }),
80
+ );
81
+
82
+ const input: SendMessageBatchCommandInput = {
83
+ QueueUrl: this.connection.queueUrl,
84
+ Entries: entries,
85
+ };
86
+
87
+ const command = new SendMessageBatchCommand(input);
88
+ const response = await this.connection.sqsClient.send(command);
89
+
90
+ // Check for failures
91
+ if (response.Failed && response.Failed.length > 0) {
92
+ const errors = response.Failed.map(
93
+ (f) => `${f.Id}: ${f.Code} - ${f.Message}`,
94
+ ).join(', ');
95
+ throw new Error(
96
+ `Failed to send ${response.Failed.length} messages: ${errors}`,
97
+ );
98
+ }
99
+ }
100
+
101
+ async close(): Promise<void> {
102
+ // Publisher doesn't own the connection
103
+ // Connection should be closed by whoever created it
104
+ }
105
+ }