@dbos-inc/sqs-receive 3.0.10-preview
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 +178 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/sqs.test.d.ts +2 -0
- package/dist/sqs.test.d.ts.map +1 -0
- package/dist/sqs.test.js +108 -0
- package/dist/sqs.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/index.ts +226 -0
- package/jest.config.js +8 -0
- package/package.json +34 -0
- package/sqs-test-dbos-config.yaml +10 -0
- package/sqs.test.ts +111 -0
- package/tsconfig.json +9 -0
package/index.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { DBOS, Error as DBOSError, DBOSLifecycleCallback } from '@dbos-inc/dbos-sdk';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DeleteMessageCommand,
|
|
5
|
+
GetQueueAttributesCommand,
|
|
6
|
+
GetQueueAttributesCommandInput,
|
|
7
|
+
Message,
|
|
8
|
+
ReceiveMessageCommand,
|
|
9
|
+
ReceiveMessageCommandOutput,
|
|
10
|
+
SQSClient,
|
|
11
|
+
} from '@aws-sdk/client-sqs';
|
|
12
|
+
|
|
13
|
+
interface SQSConfig {
|
|
14
|
+
client?: SQSClient | (() => SQSClient);
|
|
15
|
+
queueUrl?: string;
|
|
16
|
+
getWorkflowKey?: (m: Message) => string;
|
|
17
|
+
workflowQueueName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SQSRecvClassConfig {
|
|
21
|
+
config?: SQSConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SQSRecvMethodConfig {
|
|
25
|
+
config?: SQSConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class SQSReceiver extends DBOSLifecycleCallback {
|
|
29
|
+
config?: SQSConfig;
|
|
30
|
+
|
|
31
|
+
constructor(config?: SQSConfig) {
|
|
32
|
+
super();
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
listeners: Promise<void>[] = [];
|
|
37
|
+
isShuttingDown = false;
|
|
38
|
+
|
|
39
|
+
override async destroy() {
|
|
40
|
+
this.isShuttingDown = true;
|
|
41
|
+
try {
|
|
42
|
+
await Promise.allSettled(this.listeners);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// yawn
|
|
45
|
+
}
|
|
46
|
+
this.listeners = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// async function that uses .then/.catch to handle potentially unreliable library calls
|
|
50
|
+
static async sendReceiveMessageCommandSafe(
|
|
51
|
+
sqs: SQSClient,
|
|
52
|
+
params: ReceiveMessageCommand,
|
|
53
|
+
): Promise<ReceiveMessageCommandOutput> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
sqs
|
|
56
|
+
.send(params)
|
|
57
|
+
.then((response) => resolve(response))
|
|
58
|
+
.catch((error) => reject(error as Error));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async validateConnection(client: SQSClient, url: string) {
|
|
63
|
+
const params: GetQueueAttributesCommandInput = {
|
|
64
|
+
QueueUrl: url,
|
|
65
|
+
AttributeNames: ['All'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const _validateSQSConfiguration = await client.send(new GetQueueAttributesCommand(params));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override async initialize() {
|
|
72
|
+
const regops = DBOS.getAssociatedInfo(this);
|
|
73
|
+
|
|
74
|
+
for (const regop of regops) {
|
|
75
|
+
const { methodConfig, classConfig, methodReg } = regop;
|
|
76
|
+
const sqsclass = classConfig as SQSRecvClassConfig;
|
|
77
|
+
const sqsmethod = methodConfig as SQSRecvMethodConfig;
|
|
78
|
+
if (!sqsmethod) continue;
|
|
79
|
+
|
|
80
|
+
const url = sqsclass.config?.queueUrl ?? sqsmethod.config?.queueUrl ?? this.config?.queueUrl;
|
|
81
|
+
if (!url) continue;
|
|
82
|
+
|
|
83
|
+
const cname = methodReg.className;
|
|
84
|
+
const mname = methodReg.name;
|
|
85
|
+
|
|
86
|
+
if (!methodReg.workflowConfig) {
|
|
87
|
+
throw new DBOSError.DBOSError(
|
|
88
|
+
`Error registering method ${cname}.${mname}: An SQS decorator can only be assigned to a workflow!`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let sqs: SQSClient | undefined = undefined;
|
|
93
|
+
if (sqsmethod.config?.client) {
|
|
94
|
+
sqs = typeof sqsmethod.config.client === 'function' ? sqsmethod.config.client() : sqsmethod.config.client;
|
|
95
|
+
} else if (sqsclass.config?.client) {
|
|
96
|
+
sqs = typeof sqsclass.config.client === 'function' ? sqsclass.config.client() : sqsclass.config.client;
|
|
97
|
+
} else if (this.config?.client) {
|
|
98
|
+
sqs = typeof this.config.client === 'function' ? this.config.client() : this.config.client;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!sqs) {
|
|
102
|
+
throw new DBOSError.DBOSError(`Error initializing SQS method ${cname}.${mname}: No SQSClient provided`);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await SQSReceiver.validateConnection(sqs, url);
|
|
106
|
+
DBOS.logger.info(`Successfully connected to SQS queue ${url} for ${cname}.${mname}`);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
const err = e as Error;
|
|
109
|
+
DBOS.logger.error(err);
|
|
110
|
+
throw new DBOSError.DBOSError(
|
|
111
|
+
`SQS Receiver for ${cname}.${mname} was unable to connect to ${url}: ${err.message}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
116
|
+
const sqsr = this;
|
|
117
|
+
this.listeners.push(
|
|
118
|
+
(async (client: SQSClient, url: string) => {
|
|
119
|
+
while (!this.isShuttingDown) {
|
|
120
|
+
// Get message
|
|
121
|
+
try {
|
|
122
|
+
const response = await SQSReceiver.sendReceiveMessageCommandSafe(
|
|
123
|
+
client,
|
|
124
|
+
new ReceiveMessageCommand({
|
|
125
|
+
QueueUrl: url,
|
|
126
|
+
MaxNumberOfMessages: 1,
|
|
127
|
+
WaitTimeSeconds: 5,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!response.Messages || response.Messages.length === 0) {
|
|
132
|
+
DBOS.logger.debug(`No messages for ${url} - `);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const message = response.Messages[0];
|
|
137
|
+
|
|
138
|
+
// Start workflow
|
|
139
|
+
let wfid = sqsmethod.config?.getWorkflowKey ? sqsmethod.config.getWorkflowKey(message) : undefined;
|
|
140
|
+
if (!wfid) {
|
|
141
|
+
wfid = sqsclass.config?.getWorkflowKey ? sqsclass.config.getWorkflowKey(message) : undefined;
|
|
142
|
+
}
|
|
143
|
+
if (!wfid) {
|
|
144
|
+
wfid = sqsr.config?.getWorkflowKey ? sqsr.config.getWorkflowKey(message) : undefined;
|
|
145
|
+
}
|
|
146
|
+
if (!wfid) wfid = message.MessageId;
|
|
147
|
+
await DBOS.startWorkflow(methodReg.registeredFunction as (...args: unknown[]) => Promise<unknown>, {
|
|
148
|
+
workflowID: wfid,
|
|
149
|
+
queueName:
|
|
150
|
+
sqsmethod.config?.workflowQueueName ??
|
|
151
|
+
sqsclass.config?.workflowQueueName ??
|
|
152
|
+
sqsr.config?.workflowQueueName,
|
|
153
|
+
})(message);
|
|
154
|
+
|
|
155
|
+
// Delete the message from the queue after starting workflow (which will take over retries)
|
|
156
|
+
await client.send(
|
|
157
|
+
new DeleteMessageCommand({
|
|
158
|
+
QueueUrl: url,
|
|
159
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
DBOS.logger.error(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
})(sqs, url),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
override logRegisteredEndpoints() {
|
|
172
|
+
DBOS.logger.info('SQS receiver endpoints:');
|
|
173
|
+
|
|
174
|
+
const eps = DBOS.getAssociatedInfo(this);
|
|
175
|
+
|
|
176
|
+
for (const e of eps) {
|
|
177
|
+
const { methodConfig, classConfig, methodReg } = e;
|
|
178
|
+
const sqsclass = classConfig as SQSRecvClassConfig;
|
|
179
|
+
const sqsmethod = methodConfig as SQSRecvMethodConfig;
|
|
180
|
+
if (sqsmethod) {
|
|
181
|
+
const url = sqsclass.config?.queueUrl ?? sqsmethod.config?.queueUrl ?? this.config?.queueUrl;
|
|
182
|
+
if (url) {
|
|
183
|
+
const cname = methodReg.className;
|
|
184
|
+
const mname = methodReg.name;
|
|
185
|
+
DBOS.logger.info(` ${url} -> ${cname}.${mname}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Decorator - class
|
|
192
|
+
configuration(config: SQSConfig) {
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
194
|
+
const er = this;
|
|
195
|
+
|
|
196
|
+
function clsdec<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
197
|
+
const erInfo = DBOS.associateClassWithInfo(er, ctor) as SQSRecvClassConfig;
|
|
198
|
+
erInfo.config = config;
|
|
199
|
+
}
|
|
200
|
+
return clsdec;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Decorators - method
|
|
204
|
+
messageConsumer(config?: SQSConfig) {
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
206
|
+
const er = this;
|
|
207
|
+
function mtddec<This, Args extends [Message], Return>(
|
|
208
|
+
target: object,
|
|
209
|
+
propertyKey: string,
|
|
210
|
+
inDescriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
|
|
211
|
+
) {
|
|
212
|
+
const { regInfo: receiverInfo } = DBOS.associateFunctionWithInfo(er, inDescriptor.value!, {
|
|
213
|
+
classOrInst: target,
|
|
214
|
+
name: propertyKey,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const mRegistration = receiverInfo as SQSRecvMethodConfig;
|
|
218
|
+
mRegistration.config = config;
|
|
219
|
+
|
|
220
|
+
return inDescriptor;
|
|
221
|
+
}
|
|
222
|
+
return mtddec;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { SQSConfig, SQSReceiver };
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbos-inc/sqs-receive",
|
|
3
|
+
"version": "3.0.10-preview",
|
|
4
|
+
"description": "DBOS event receiver for AWS SQS queues",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/dbos-inc/dbos-transact-ts",
|
|
9
|
+
"directory": "packages/sqs-receive"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.dbos.dev/",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc --project tsconfig.json",
|
|
16
|
+
"test": "echo 'no tests'",
|
|
17
|
+
"testsqs": "npm run build && jest --detectOpenHandles"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/jest": "^29.5.12",
|
|
21
|
+
"@types/supertest": "^6.0.2",
|
|
22
|
+
"jest": "^29.7.0",
|
|
23
|
+
"supertest": "^7.0.0",
|
|
24
|
+
"ts-jest": "^29.1.4",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@dbos-inc/dbos-sdk": "*",
|
|
29
|
+
"@dbos-inc/aws-config": "*"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aws-sdk/client-sqs": "^3.596.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# To enable auto-completion and validation for this file in VSCode, install the RedHat YAML extension
|
|
2
|
+
# https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml
|
|
3
|
+
|
|
4
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/dbos-inc/dbos-transact/main/dbos-config.schema.json
|
|
5
|
+
|
|
6
|
+
database:
|
|
7
|
+
hostname: 'localhost'
|
|
8
|
+
port: 5432
|
|
9
|
+
username: 'postgres'
|
|
10
|
+
password: ${PGPASSWORD}
|
package/sqs.test.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Message, SendMessageCommand, SendMessageCommandInput, SQSClient } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { SQSReceiver } from './index';
|
|
3
|
+
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
4
|
+
|
|
5
|
+
const sleepms = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
|
|
7
|
+
interface ValueObj {
|
|
8
|
+
val: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createSQS() {
|
|
12
|
+
return new SQSClient({
|
|
13
|
+
region: process.env['AWS_REGION'] ?? '',
|
|
14
|
+
endpoint: process.env['AWS_ENDPOINT_URL_SQS'],
|
|
15
|
+
credentials: {
|
|
16
|
+
accessKeyId: process.env['AWS_ACCESS_KEY_ID'] ?? '',
|
|
17
|
+
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'] ?? '',
|
|
18
|
+
},
|
|
19
|
+
//logger: console,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create a new type that omits the QueueUrl property
|
|
24
|
+
type MessageWithoutQueueUrl = Omit<SendMessageCommandInput, 'QueueUrl'>;
|
|
25
|
+
|
|
26
|
+
// Create a new type that allows QueueUrl to be added later
|
|
27
|
+
type MessageWithOptionalQueueUrl = MessageWithoutQueueUrl & { QueueUrl?: string };
|
|
28
|
+
|
|
29
|
+
async function sendMessageInternal(msg: MessageWithOptionalQueueUrl) {
|
|
30
|
+
try {
|
|
31
|
+
const smsg = { ...msg, QueueUrl: msg.QueueUrl || process.env['SQS_QUEUE_URL']! };
|
|
32
|
+
return await createSQS().send(new SendMessageCommand(smsg));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
DBOS.logger.error(e);
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sendMessageStep = DBOS.registerStep(sendMessageInternal, { name: 'Send SQS Message' });
|
|
40
|
+
|
|
41
|
+
const sqsReceiver = new SQSReceiver({
|
|
42
|
+
client: createSQS,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
class SQSRcv {
|
|
46
|
+
static msgRcvCount: number = 0;
|
|
47
|
+
static msgValueSum: number = 0;
|
|
48
|
+
@sqsReceiver.messageConsumer({ queueUrl: process.env['SQS_QUEUE_URL'] })
|
|
49
|
+
@DBOS.workflow()
|
|
50
|
+
static async recvMessage(msg: Message) {
|
|
51
|
+
const ms = msg.Body!;
|
|
52
|
+
const res = JSON.parse(ms) as ValueObj;
|
|
53
|
+
SQSRcv.msgRcvCount++;
|
|
54
|
+
SQSRcv.msgValueSum += res.val;
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('sqs-tests', () => {
|
|
60
|
+
let sqsIsAvailable = true;
|
|
61
|
+
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
// Check if SES is available and update app config, skip the test if it's not
|
|
64
|
+
if (!process.env['AWS_REGION'] || !process.env['SQS_QUEUE_URL']) {
|
|
65
|
+
sqsIsAvailable = false;
|
|
66
|
+
} else {
|
|
67
|
+
// This would normally be a global or static or something
|
|
68
|
+
DBOS.setConfig({ name: 'dbossqstest' });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
if (sqsIsAvailable) {
|
|
74
|
+
DBOS.registerLifecycleCallback(sqsReceiver);
|
|
75
|
+
await DBOS.launch();
|
|
76
|
+
} else {
|
|
77
|
+
console.log(
|
|
78
|
+
'SQS Test is not configured. To run, set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and SQS_QUEUE_URL',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(async () => {
|
|
84
|
+
if (sqsIsAvailable) {
|
|
85
|
+
await DBOS.shutdown();
|
|
86
|
+
}
|
|
87
|
+
}, 10000);
|
|
88
|
+
|
|
89
|
+
// This tests receive also; which is already wired up
|
|
90
|
+
test('sqs-send', async () => {
|
|
91
|
+
if (!sqsIsAvailable) {
|
|
92
|
+
console.log('SQS unavailable, skipping SQS tests');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const sv: ValueObj = {
|
|
96
|
+
val: 10,
|
|
97
|
+
};
|
|
98
|
+
const ser = await sendMessageStep({
|
|
99
|
+
MessageBody: JSON.stringify(sv),
|
|
100
|
+
});
|
|
101
|
+
expect(ser.MessageId).toBeDefined();
|
|
102
|
+
|
|
103
|
+
// Wait for receipt
|
|
104
|
+
for (let i = 0; i < 100; ++i) {
|
|
105
|
+
if (SQSRcv.msgRcvCount === 1) break;
|
|
106
|
+
await sleepms(100);
|
|
107
|
+
}
|
|
108
|
+
expect(SQSRcv.msgRcvCount).toBe(1);
|
|
109
|
+
expect(SQSRcv.msgValueSum).toBe(10);
|
|
110
|
+
});
|
|
111
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Visit https://aka.ms/tsconfig to read more about this file */
|
|
2
|
+
{
|
|
3
|
+
"extends": "../../tsconfig.shared.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "./dist"
|
|
6
|
+
},
|
|
7
|
+
"include": [/* Specifies an array of filenames or patterns to include in the program. */ "."],
|
|
8
|
+
"exclude": ["dist"]
|
|
9
|
+
}
|