@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.
- package/README.md +473 -0
- package/dist/BasicConnection-BNi6_olg.mjs +42 -0
- package/dist/BasicConnection-BNi6_olg.mjs.map +1 -0
- package/dist/BasicConnection-C4_Y0nIj.d.mts +26 -0
- package/dist/BasicConnection-CddnULuT.d.cts +26 -0
- package/dist/BasicConnection-CzfMubv_.cjs +48 -0
- package/dist/BasicConnection-CzfMubv_.cjs.map +1 -0
- package/dist/BasicPublisher-BzfzHIfb.cjs +20 -0
- package/dist/BasicPublisher-BzfzHIfb.cjs.map +1 -0
- package/dist/BasicPublisher-Cg3SxIVR.mjs +14 -0
- package/dist/BasicPublisher-Cg3SxIVR.mjs.map +1 -0
- package/dist/BasicPublisher-DCBxaTwC.d.mts +12 -0
- package/dist/BasicPublisher-V5hELHG9.d.cts +12 -0
- package/dist/BasicSubscriber-BB3aKeph.d.mts +12 -0
- package/dist/BasicSubscriber-BEv5N21f.mjs +18 -0
- package/dist/BasicSubscriber-BEv5N21f.mjs.map +1 -0
- package/dist/BasicSubscriber-DOwgFw5j.d.cts +12 -0
- package/dist/BasicSubscriber-DyTKOixe.cjs +24 -0
- package/dist/BasicSubscriber-DyTKOixe.cjs.map +1 -0
- package/dist/EventConnection-ClLMoIUU.mjs +36 -0
- package/dist/EventConnection-ClLMoIUU.mjs.map +1 -0
- package/dist/EventConnection-DZhn1Msl.d.cts +13 -0
- package/dist/EventConnection-DeXfLGom.d.mts +13 -0
- package/dist/EventConnection-F_N81SXu.cjs +41 -0
- package/dist/EventConnection-F_N81SXu.cjs.map +1 -0
- package/dist/EventConnection.cjs +5 -0
- package/dist/EventConnection.d.cts +3 -0
- package/dist/EventConnection.d.mts +3 -0
- package/dist/EventConnection.mjs +4 -0
- package/dist/Publisher-C3m8JFXm.d.mts +19 -0
- package/dist/Publisher-DS0aOh8r.mjs +65 -0
- package/dist/Publisher-DS0aOh8r.mjs.map +1 -0
- package/dist/Publisher-ceu6OxCj.cjs +70 -0
- package/dist/Publisher-ceu6OxCj.cjs.map +1 -0
- package/dist/Publisher-nXNVjL0e.d.cts +19 -0
- package/dist/Publisher.cjs +4 -0
- package/dist/Publisher.d.cts +3 -0
- package/dist/Publisher.d.mts +3 -0
- package/dist/Publisher.mjs +4 -0
- package/dist/RabbitMQConnection-7cWp_-_Q.d.cts +43 -0
- package/dist/RabbitMQConnection-BN_GybOv.cjs +113 -0
- package/dist/RabbitMQConnection-BN_GybOv.cjs.map +1 -0
- package/dist/RabbitMQConnection-C2HC7J7K.d.mts +43 -0
- package/dist/RabbitMQConnection-eVOVS4Dl.mjs +107 -0
- package/dist/RabbitMQConnection-eVOVS4Dl.mjs.map +1 -0
- package/dist/RabbitMQPublisher-BZX0edgL.mjs +39 -0
- package/dist/RabbitMQPublisher-BZX0edgL.mjs.map +1 -0
- package/dist/RabbitMQPublisher-CSCn6gMA.cjs +45 -0
- package/dist/RabbitMQPublisher-CSCn6gMA.cjs.map +1 -0
- package/dist/RabbitMQPublisher-CpcbgrXj.d.cts +23 -0
- package/dist/RabbitMQPublisher-DDZi15Ro.d.mts +23 -0
- package/dist/RabbitMQSubscriber-B5I-hBaT.mjs +57 -0
- package/dist/RabbitMQSubscriber-B5I-hBaT.mjs.map +1 -0
- package/dist/RabbitMQSubscriber-BJ9PTGny.d.cts +25 -0
- package/dist/RabbitMQSubscriber-CIvIw8MG.d.mts +25 -0
- package/dist/RabbitMQSubscriber-Cj7qLIi6.cjs +63 -0
- package/dist/RabbitMQSubscriber-Cj7qLIi6.cjs.map +1 -0
- package/dist/SNSConnection-CAImaMM2.d.mts +40 -0
- package/dist/SNSConnection-Cqbr7-Ho.d.cts +40 -0
- package/dist/SNSConnection-D7wCf9Si.mjs +70 -0
- package/dist/SNSConnection-D7wCf9Si.mjs.map +1 -0
- package/dist/SNSConnection-DVsC_pbB.cjs +76 -0
- package/dist/SNSConnection-DVsC_pbB.cjs.map +1 -0
- package/dist/SNSPublisher-BZ6N3JI9.cjs +48 -0
- package/dist/SNSPublisher-BZ6N3JI9.cjs.map +1 -0
- package/dist/SNSPublisher-BfWjzNiB.mjs +42 -0
- package/dist/SNSPublisher-BfWjzNiB.mjs.map +1 -0
- package/dist/SNSPublisher-Dkhb4LU-.d.mts +21 -0
- package/dist/SNSPublisher-RVZFa7kL.d.cts +21 -0
- package/dist/SNSSubscriber-B31JmkAE.d.mts +54 -0
- package/dist/SNSSubscriber-C9YJWa7m.mjs +143 -0
- package/dist/SNSSubscriber-C9YJWa7m.mjs.map +1 -0
- package/dist/SNSSubscriber-UE0ia4XM.cjs +149 -0
- package/dist/SNSSubscriber-UE0ia4XM.cjs.map +1 -0
- package/dist/SNSSubscriber-k2UwKUxu.d.cts +54 -0
- package/dist/SQSConnection-BwNjUPr4.cjs +76 -0
- package/dist/SQSConnection-BwNjUPr4.cjs.map +1 -0
- package/dist/SQSConnection-C5WFqZW7.d.mts +40 -0
- package/dist/SQSConnection-Eleqqre3.mjs +70 -0
- package/dist/SQSConnection-Eleqqre3.mjs.map +1 -0
- package/dist/SQSConnection-geav2Rsn.d.cts +40 -0
- package/dist/SQSPublisher-CdsayOLV.d.mts +23 -0
- package/dist/SQSPublisher-DG9o4ME3.mjs +57 -0
- package/dist/SQSPublisher-DG9o4ME3.mjs.map +1 -0
- package/dist/SQSPublisher-Dq5353oZ.cjs +63 -0
- package/dist/SQSPublisher-Dq5353oZ.cjs.map +1 -0
- package/dist/SQSPublisher-Utb_FTe5.d.cts +23 -0
- package/dist/SQSSubscriber-B6iL1EnX.d.cts +43 -0
- package/dist/SQSSubscriber-CD66P7hH.mjs +121 -0
- package/dist/SQSSubscriber-CD66P7hH.mjs.map +1 -0
- package/dist/SQSSubscriber-DBeE4XAR.d.mts +43 -0
- package/dist/SQSSubscriber-DZDNgLE5.cjs +127 -0
- package/dist/SQSSubscriber-DZDNgLE5.cjs.map +1 -0
- package/dist/Subscriber-Bd9hb_jp.mjs +74 -0
- package/dist/Subscriber-Bd9hb_jp.mjs.map +1 -0
- package/dist/Subscriber-Cff9EIpv.d.mts +19 -0
- package/dist/Subscriber-CpAwcUzb.d.cts +19 -0
- package/dist/Subscriber-saVOWuMq.cjs +79 -0
- package/dist/Subscriber-saVOWuMq.cjs.map +1 -0
- package/dist/Subscriber.cjs +4 -0
- package/dist/Subscriber.d.cts +3 -0
- package/dist/Subscriber.d.mts +3 -0
- package/dist/Subscriber.mjs +4 -0
- package/dist/basic/BasicConnection.cjs +4 -0
- package/dist/basic/BasicConnection.d.cts +3 -0
- package/dist/basic/BasicConnection.d.mts +3 -0
- package/dist/basic/BasicConnection.mjs +4 -0
- package/dist/basic/BasicPublisher.cjs +3 -0
- package/dist/basic/BasicPublisher.d.cts +4 -0
- package/dist/basic/BasicPublisher.d.mts +4 -0
- package/dist/basic/BasicPublisher.mjs +3 -0
- package/dist/basic/BasicSubscriber.cjs +3 -0
- package/dist/basic/BasicSubscriber.d.cts +4 -0
- package/dist/basic/BasicSubscriber.d.mts +4 -0
- package/dist/basic/BasicSubscriber.mjs +3 -0
- package/dist/basic/index.cjs +8 -0
- package/dist/basic/index.d.cts +5 -0
- package/dist/basic/index.d.mts +5 -0
- package/dist/basic/index.mjs +6 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/index.cjs +9 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +6 -0
- package/dist/rabbitmq/RabbitMQConnection.cjs +4 -0
- package/dist/rabbitmq/RabbitMQConnection.d.cts +3 -0
- package/dist/rabbitmq/RabbitMQConnection.d.mts +3 -0
- package/dist/rabbitmq/RabbitMQConnection.mjs +4 -0
- package/dist/rabbitmq/RabbitMQPublisher.cjs +3 -0
- package/dist/rabbitmq/RabbitMQPublisher.d.cts +4 -0
- package/dist/rabbitmq/RabbitMQPublisher.d.mts +4 -0
- package/dist/rabbitmq/RabbitMQPublisher.mjs +3 -0
- package/dist/rabbitmq/RabbitMQSubscriber.cjs +3 -0
- package/dist/rabbitmq/RabbitMQSubscriber.d.cts +4 -0
- package/dist/rabbitmq/RabbitMQSubscriber.d.mts +4 -0
- package/dist/rabbitmq/RabbitMQSubscriber.mjs +3 -0
- package/dist/rabbitmq/index.cjs +8 -0
- package/dist/rabbitmq/index.d.cts +5 -0
- package/dist/rabbitmq/index.d.mts +5 -0
- package/dist/rabbitmq/index.mjs +6 -0
- package/dist/sns/SNSConnection.cjs +4 -0
- package/dist/sns/SNSConnection.d.cts +3 -0
- package/dist/sns/SNSConnection.d.mts +3 -0
- package/dist/sns/SNSConnection.mjs +4 -0
- package/dist/sns/SNSPublisher.cjs +3 -0
- package/dist/sns/SNSPublisher.d.cts +4 -0
- package/dist/sns/SNSPublisher.d.mts +4 -0
- package/dist/sns/SNSPublisher.mjs +3 -0
- package/dist/sns/SNSSubscriber.cjs +6 -0
- package/dist/sns/SNSSubscriber.d.cts +4 -0
- package/dist/sns/SNSSubscriber.d.mts +4 -0
- package/dist/sns/SNSSubscriber.mjs +6 -0
- package/dist/sns/index.cjs +10 -0
- package/dist/sns/index.d.cts +5 -0
- package/dist/sns/index.d.mts +5 -0
- package/dist/sns/index.mjs +8 -0
- package/dist/sqs/SQSConnection.cjs +4 -0
- package/dist/sqs/SQSConnection.d.cts +3 -0
- package/dist/sqs/SQSConnection.d.mts +3 -0
- package/dist/sqs/SQSConnection.mjs +4 -0
- package/dist/sqs/SQSPublisher.cjs +3 -0
- package/dist/sqs/SQSPublisher.d.cts +4 -0
- package/dist/sqs/SQSPublisher.d.mts +4 -0
- package/dist/sqs/SQSPublisher.mjs +3 -0
- package/dist/sqs/SQSSubscriber.cjs +3 -0
- package/dist/sqs/SQSSubscriber.d.cts +4 -0
- package/dist/sqs/SQSSubscriber.d.mts +4 -0
- package/dist/sqs/SQSSubscriber.mjs +3 -0
- package/dist/sqs/index.cjs +8 -0
- package/dist/sqs/index.d.cts +5 -0
- package/dist/sqs/index.d.mts +5 -0
- package/dist/sqs/index.mjs +6 -0
- package/dist/types-B63qHVOG.mjs +17 -0
- package/dist/types-B63qHVOG.mjs.map +1 -0
- package/dist/types-CvSMEO5z.cjs +23 -0
- package/dist/types-CvSMEO5z.cjs.map +1 -0
- package/dist/types-D0g-o6wX.d.mts +65 -0
- package/dist/types-DEGUg5BA.d.cts +65 -0
- package/dist/types.cjs +3 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +3 -0
- package/package.json +49 -0
- package/src/EventConnection.ts +39 -0
- package/src/Publisher.ts +87 -0
- package/src/Subscriber.ts +102 -0
- package/src/__tests__/Subscriber.spec.ts +41 -0
- package/src/basic/BasicConnection.ts +47 -0
- package/src/basic/BasicPublisher.ts +15 -0
- package/src/basic/BasicSubscriber.ts +24 -0
- package/src/basic/__tests__/BasicSubscriber.spec.ts +166 -0
- package/src/basic/index.ts +3 -0
- package/src/index.ts +25 -0
- package/src/rabbitmq/RabbitMQConnection.ts +148 -0
- package/src/rabbitmq/RabbitMQPublisher.ts +67 -0
- package/src/rabbitmq/RabbitMQSubscriber.ts +113 -0
- package/src/rabbitmq/__tests__/RabbitMQPublisher.spec.ts +378 -0
- package/src/rabbitmq/__tests__/RabbitMQSubscriber.spec.ts +292 -0
- package/src/rabbitmq/index.ts +6 -0
- package/src/sns/SNSConnection.ts +98 -0
- package/src/sns/SNSPublisher.ts +64 -0
- package/src/sns/SNSSubscriber.ts +222 -0
- package/src/sns/__tests__/SNS-SQS-Integration.spec.ts +389 -0
- package/src/sns/index.ts +6 -0
- package/src/sqs/SQSConnection.ts +97 -0
- package/src/sqs/SQSPublisher.ts +105 -0
- package/src/sqs/SQSSubscriber.ts +189 -0
- package/src/sqs/__tests__/SQSPublisher.spec.ts +189 -0
- package/src/sqs/index.ts +6 -0
- package/src/types.ts +76 -0
- 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
|
+
});
|
package/src/sns/index.ts
ADDED
|
@@ -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®ion=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®ion=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
|
+
}
|