@fgiova/sqs-consumer 2.2.1 → 3.0.0
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 +19 -20
- package/dist/cjs/errors.js +0 -1
- package/dist/cjs/hooks.d.ts +45 -2
- package/dist/cjs/hooks.js +15 -10
- package/dist/cjs/index.d.ts +4 -23
- package/dist/cjs/index.js +68 -54
- package/dist/esm/errors.mjs +0 -1
- package/dist/esm/hooks.d.mts +45 -2
- package/dist/esm/hooks.mjs +15 -10
- package/dist/esm/index.d.mts +4 -23
- package/dist/esm/index.mjs +68 -54
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -39,15 +39,15 @@ const consumer = new SQSConsumer({
|
|
|
39
39
|
| handlerOptions | HandlerOptions | | The options for the handler |
|
|
40
40
|
| clientOptions | ClientOptions | | The options for the client |
|
|
41
41
|
| consumerOptions | ConsumerOptions | | The options for the consumer |
|
|
42
|
-
| hooks |
|
|
42
|
+
| hooks | HooksOptions | | The hooks to be called on specific events |
|
|
43
43
|
|
|
44
44
|
### HandlerOptions
|
|
45
|
-
| Option
|
|
46
|
-
|
|
47
|
-
| deleteMessage
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
| parallelExecution
|
|
45
|
+
| Option | Type | Default | Description |
|
|
46
|
+
|-------------------------|---------|---------|----------------------------------------------------------------------------------------------------------|
|
|
47
|
+
| deleteMessage | boolean | true | Whether to delete the message after handling (if handler execute without any error) |
|
|
48
|
+
| extendVisibilityTimeout | boolean | true | Whether to extend the visibility timeout during message handling |
|
|
49
|
+
| executionTimeout | number | 30000 | The timeout for the handler execution in ms |
|
|
50
|
+
| parallelExecution | boolean | true | If true execute handler in parallel for each batch of messages received, otherwise execute consecutively |
|
|
51
51
|
|
|
52
52
|
### ClientOptions
|
|
53
53
|
| Option | Type | Default | Description |
|
|
@@ -56,7 +56,7 @@ const consumer = new SQSConsumer({
|
|
|
56
56
|
| endpoint | string | | The endpoint to be used for the client. If not provided, the endpoint will be inferred from the queueARN. |
|
|
57
57
|
| undiciOptions | Pool.Options | | The options for the undici client. |
|
|
58
58
|
| signer | Signer / SignerOptions | | The signer to be used for signing requests. If not provided, a new singleton signer will be created. |
|
|
59
|
-
| destroySigner | boolean |
|
|
59
|
+
| destroySigner | boolean | true | Whether to destroy the signer when the consumer is destroyed. |
|
|
60
60
|
|
|
61
61
|
### ConsumerOptions
|
|
62
62
|
| Option | Type | Default | Description |
|
|
@@ -68,17 +68,16 @@ const consumer = new SQSConsumer({
|
|
|
68
68
|
| attributeNames | string[] | [] | The attribute names to be included in the response |
|
|
69
69
|
|
|
70
70
|
### Hooks
|
|
71
|
-
| Option | Type | Description
|
|
72
|
-
|
|
73
|
-
| onPoll | (messages: Message[]) => Promise<Message[]> | Called when the consumer polls for messages
|
|
74
|
-
| onMessage | (message: Message) => Promise<Message> | Called when the consumer receives a message
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| onSQSError | (error: Error, message?: Message) => Promise<void> | Called when the consumer receives an error from the SQS service |
|
|
71
|
+
| Option | Type | Description |
|
|
72
|
+
|------------------|-----------------------------------------------------------------------|---------------------------------------------------------------------|
|
|
73
|
+
| onPoll | (messages: Message[]) => Promise<Message[]> | Called when the consumer polls for messages |
|
|
74
|
+
| onMessage | (message: Message) => Promise<Message> | Called when the consumer receives a message |
|
|
75
|
+
| onHandlerSuccess | (message: Message) => Promise<Message> | Called when the consumer handles a message successfully |
|
|
76
|
+
| onHandlerTimeout | (message: Message) => Promise<boolean> | Called when the consumer handler execution exceed executionTimeout |
|
|
77
|
+
| onHandlerError | (message: Message, error: Error) => Promise<boolean> | Called when the consumer handler execution throws an error |
|
|
78
|
+
| onSuccess | (message: Message) => Promise<boolean> | Called when the consumer handler execution finishes successfully |
|
|
79
|
+
| onError | (hook: HookName, message: Message, error: Error) => Promise<boolean> | Called when the consumer handler execution throws an uncaught error |
|
|
80
|
+
| onSQSError | (error: Error, message?: Message) => Promise<void> | Called when the consumer receives an error from the SQS service |
|
|
82
81
|
|
|
83
82
|
## API
|
|
84
83
|
|
|
@@ -86,7 +85,7 @@ const consumer = new SQSConsumer({
|
|
|
86
85
|
SQSConsumer(options: SQSConsumerOptions)
|
|
87
86
|
SQSConsumer.start(): Promise<void>
|
|
88
87
|
SQSConsumer.stop(destroyConsumer = false): Promise<void>
|
|
89
|
-
SQSConsumer.addHook(
|
|
88
|
+
SQSConsumer.addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this
|
|
90
89
|
```
|
|
91
90
|
|
|
92
91
|
## License
|
package/dist/cjs/errors.js
CHANGED
package/dist/cjs/hooks.d.ts
CHANGED
|
@@ -2,13 +2,56 @@
|
|
|
2
2
|
import type { Message } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Logger } from "./logger";
|
|
4
4
|
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
|
|
5
|
+
export type HookSignatureMap = {
|
|
6
|
+
onStart: {
|
|
7
|
+
args: [sqsConsumer: unknown];
|
|
8
|
+
return: undefined;
|
|
9
|
+
};
|
|
10
|
+
onStop: {
|
|
11
|
+
args: [sqsConsumer: unknown];
|
|
12
|
+
return: undefined;
|
|
13
|
+
};
|
|
14
|
+
onPoll: {
|
|
15
|
+
args: [messages: Message[]];
|
|
16
|
+
return: Message[];
|
|
17
|
+
};
|
|
18
|
+
onMessage: {
|
|
19
|
+
args: [message: Message];
|
|
20
|
+
return: Message;
|
|
21
|
+
};
|
|
22
|
+
onHandlerSuccess: {
|
|
23
|
+
args: [message: Message];
|
|
24
|
+
return: Message;
|
|
25
|
+
};
|
|
26
|
+
onHandlerTimeout: {
|
|
27
|
+
args: [message: Message];
|
|
28
|
+
return: boolean;
|
|
29
|
+
};
|
|
30
|
+
onHandlerError: {
|
|
31
|
+
args: [message: Message, error: Error];
|
|
32
|
+
return: boolean;
|
|
33
|
+
};
|
|
34
|
+
onSuccess: {
|
|
35
|
+
args: [message: Message];
|
|
36
|
+
return: boolean;
|
|
37
|
+
};
|
|
38
|
+
onError: {
|
|
39
|
+
args: [hook: HookName, message: Message, error: Error];
|
|
40
|
+
return: boolean;
|
|
41
|
+
};
|
|
42
|
+
onSQSError: {
|
|
43
|
+
args: [error: Error, message?: Message];
|
|
44
|
+
return: undefined;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export type HookCallback<H extends HookName> = (...args: HookSignatureMap[H]["args"]) => HookSignatureMap[H]["return"] | Promise<HookSignatureMap[H]["return"]> | void | Promise<void> | undefined;
|
|
5
48
|
export declare class Hooks {
|
|
6
49
|
private readonly hookSymbols;
|
|
7
50
|
private readonly hooks;
|
|
8
51
|
private readonly logger;
|
|
9
52
|
constructor(logger?: Logger);
|
|
10
|
-
addHook(hook:
|
|
11
|
-
runHook(hook:
|
|
53
|
+
addHook<H extends HookName>(hook: H, fn: HookCallback<H>): void;
|
|
54
|
+
runHook<H extends HookName>(hook: H, ...args: HookSignatureMap[H]["args"]): Promise<HookSignatureMap[H]["return"]>;
|
|
12
55
|
private runHookWithVoidReturn;
|
|
13
56
|
private runHookWithMessageReturn;
|
|
14
57
|
private runHookWithBooleanReturn;
|
package/dist/cjs/hooks.js
CHANGED
|
@@ -75,31 +75,36 @@ class Hooks {
|
|
|
75
75
|
throw new Error(`Invalid hook ${hook}`);
|
|
76
76
|
this.hooks[hookSymbol.S]?.push(fn);
|
|
77
77
|
}
|
|
78
|
-
// biome-ignore lint/suspicious/noExplicitAny: args can be anything
|
|
79
78
|
async runHook(hook, ...args) {
|
|
80
79
|
const hookSymbol = this.hookSymbols[hook];
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: internal cast for generic dispatch
|
|
81
|
+
const hookArgs = args;
|
|
81
82
|
try {
|
|
82
83
|
if (!hookSymbol)
|
|
83
84
|
throw new Error(`Invalid hook ${hook}`);
|
|
84
85
|
switch (hookSymbol.type) {
|
|
85
86
|
case "boolean-return":
|
|
86
|
-
return await this.runHookWithBooleanReturn(hookSymbol, ...
|
|
87
|
+
return (await this.runHookWithBooleanReturn(hookSymbol, ...hookArgs));
|
|
87
88
|
case "message-return": {
|
|
88
|
-
const [message, ...someArgs] =
|
|
89
|
-
return await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs);
|
|
89
|
+
const [message, ...someArgs] = hookArgs;
|
|
90
|
+
return (await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs));
|
|
90
91
|
}
|
|
91
92
|
case "void":
|
|
92
|
-
return await this.runHookWithVoidReturn(hookSymbol, ...
|
|
93
|
+
return (await this.runHookWithVoidReturn(hookSymbol, ...hookArgs));
|
|
93
94
|
}
|
|
94
|
-
/* c8 ignore next 1 */
|
|
95
95
|
// biome-ignore lint/suspicious/noExplicitAny: error type is not important
|
|
96
96
|
}
|
|
97
|
-
catch (
|
|
98
|
-
|
|
97
|
+
catch (originalError) {
|
|
98
|
+
const newError = new Error(
|
|
99
|
+
/* c8 ignore next */
|
|
100
|
+
`Error running hook ${hook}: ${originalError?.message ?? originalError}`);
|
|
101
|
+
newError.cause = originalError;
|
|
99
102
|
if (hookSymbol.throwable !== false)
|
|
100
|
-
throw
|
|
101
|
-
this.logger.error(
|
|
103
|
+
throw newError;
|
|
104
|
+
this.logger.error(newError);
|
|
102
105
|
}
|
|
106
|
+
// biome-ignore lint/suspicious/noExplicitAny: fallthrough for non-throwable error paths
|
|
107
|
+
return undefined;
|
|
103
108
|
}
|
|
104
109
|
async runHookWithVoidReturn(hookSymbol,
|
|
105
110
|
// biome-ignore lint/suspicious/noExplicitAny: args can be anything
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Signer, SignerOptions } from "@fgiova/aws-signature";
|
|
2
2
|
import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Pool } from "undici";
|
|
4
|
-
import { type HookName } from "./hooks";
|
|
4
|
+
import { type HookCallback, type HookName } from "./hooks";
|
|
5
5
|
import type { Logger } from "./logger";
|
|
6
6
|
type HandlerOptions = {
|
|
7
7
|
deleteMessage?: boolean;
|
|
@@ -23,18 +23,9 @@ type ConsumerOptions = {
|
|
|
23
23
|
messageAttributeNames?: string[];
|
|
24
24
|
attributeNames?: string[];
|
|
25
25
|
};
|
|
26
|
-
type
|
|
27
|
-
type hookReturnMessage = Promise<Message> | Message | Promise<void> | void | undefined;
|
|
28
|
-
type hookReturnBoolean = Promise<boolean> | boolean | Promise<void> | void | undefined;
|
|
26
|
+
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
29
27
|
export type HooksOptions = {
|
|
30
|
-
|
|
31
|
-
onMessage?: (message: Message) => hookReturnMessage;
|
|
32
|
-
onHandlerSuccess?: (message: Message) => hookReturnMessage;
|
|
33
|
-
onHandlerTimeout?: (message: Message) => hookReturnBoolean;
|
|
34
|
-
onHandlerError?: (message: Message, error: Error) => hookReturnBoolean;
|
|
35
|
-
onSuccess?: (message: Message) => hookReturnBoolean;
|
|
36
|
-
onError?: (hook: HookName, message: Message, error: Error) => hookReturnBoolean;
|
|
37
|
-
onSQSError?: (error: Error, message?: Message) => Promise<void>;
|
|
28
|
+
[H in ConsumerHookName]?: HookCallback<H>;
|
|
38
29
|
};
|
|
39
30
|
export type SQSConsumerOptions = {
|
|
40
31
|
queueARN: string;
|
|
@@ -68,16 +59,6 @@ export declare class SQSConsumer {
|
|
|
68
59
|
start(): Promise<void>;
|
|
69
60
|
stop(destroy?: boolean): Promise<void>;
|
|
70
61
|
get isRunning(): boolean;
|
|
71
|
-
addHook(hookName:
|
|
72
|
-
addHook(hookName: "onStop", value: (sqsConsumer: SQSConsumerOptions) => void): this;
|
|
73
|
-
addHook(hookName: "onPoll", value: (messages: Message[]) => Promise<Message[]>): this;
|
|
74
|
-
addHook(hookName: "onMessage", value: (message: Message) => Promise<Message>): this;
|
|
75
|
-
addHook(hookName: "onHandlerSuccess", value: (message: Message) => Promise<Message>): this;
|
|
76
|
-
addHook(hookName: "onHandlerTimeout", value: (message: Message) => Promise<boolean>): this;
|
|
77
|
-
addHook(hookName: "onHandlerError", value: (message: Message, error: Error) => Promise<boolean>): this;
|
|
78
|
-
addHook(hookName: "onSuccess", value: (message: Message) => Promise<boolean>): this;
|
|
79
|
-
addHook(hookName: "onError", value: (hook: HookName, message: Message, error: Error) => Promise<boolean>): this;
|
|
80
|
-
addHook(hookName: "onSQSError", value: (error: Error, message?: Message) => Promise<void>): this;
|
|
81
|
-
addHook(hookName: HookName, value: Function): this;
|
|
62
|
+
addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this;
|
|
82
63
|
}
|
|
83
64
|
export {};
|
package/dist/cjs/index.js
CHANGED
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SQSConsumer = void 0;
|
|
7
|
-
/** biome-ignore-all lint/complexity/noBannedTypes: Function required as generic */
|
|
8
7
|
const promises_1 = require("node:timers/promises");
|
|
9
8
|
const mini_sqs_client_1 = require("@fgiova/mini-sqs-client");
|
|
10
9
|
// @ts-expect-error
|
|
@@ -50,6 +49,7 @@ class SQSConsumer {
|
|
|
50
49
|
for (const [key, value] of Object.entries(options.hooks)) {
|
|
51
50
|
if (typeof value !== "function")
|
|
52
51
|
throw new Error(`${key} must be a function`);
|
|
52
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic registration loses type correlation
|
|
53
53
|
this.addHook(key, value);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -77,13 +77,14 @@ class SQSConsumer {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
async runHandler(handler, message) {
|
|
80
|
+
let timeoutId = null;
|
|
80
81
|
try {
|
|
81
82
|
let handlerResult;
|
|
82
83
|
if (this.handlerOptions.executionTimeout) {
|
|
83
84
|
handlerResult = await unpromise_1.Unpromise.race([
|
|
84
85
|
handler(message),
|
|
85
86
|
new Promise((_resolve, reject) => {
|
|
86
|
-
setTimeout(() => {
|
|
87
|
+
timeoutId = setTimeout(() => {
|
|
87
88
|
reject(new errors_1.TimeoutError("Handler execution timed out"));
|
|
88
89
|
}, this.handlerOptions.executionTimeout);
|
|
89
90
|
}),
|
|
@@ -103,6 +104,10 @@ class SQSConsumer {
|
|
|
103
104
|
await this.hooks.runHook("onHandlerError", message, error);
|
|
104
105
|
}
|
|
105
106
|
throw error;
|
|
107
|
+
} /* c8 ignore next 2 */
|
|
108
|
+
finally {
|
|
109
|
+
if (timeoutId)
|
|
110
|
+
clearTimeout(timeoutId);
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
haExtendVisibilityTimeout(messages, visibilityTimeout) {
|
|
@@ -133,65 +138,75 @@ class SQSConsumer {
|
|
|
133
138
|
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
134
139
|
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
135
140
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
}
|
|
142
|
+
if (candidatesToDelete.length) {
|
|
143
|
+
await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
|
|
144
|
+
}
|
|
145
|
+
if (candidateToRelease.length) {
|
|
146
|
+
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease, 0);
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
async pollMessages() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
150
|
+
let consecutiveErrors = 0;
|
|
151
|
+
while (this.running) {
|
|
152
|
+
try {
|
|
153
|
+
this.polling = true;
|
|
154
|
+
/* c8 ignore next 1 */
|
|
155
|
+
const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
|
|
156
|
+
const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
|
|
157
|
+
WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
|
|
158
|
+
MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
|
|
159
|
+
VisibilityTimeout: visibilityTimeout,
|
|
160
|
+
MessageAttributeNames: this.consumerOptions.messageAttributeNames,
|
|
161
|
+
AttributeNames: this.consumerOptions.attributeNames,
|
|
162
|
+
});
|
|
163
|
+
const messages = await this.hooks.runHook("onPoll",
|
|
164
|
+
/* c8 ignore next 1 */
|
|
165
|
+
messagesResult.Messages ?? []);
|
|
166
|
+
this.messagesOnFly += messages.length;
|
|
167
|
+
if (messages.length) {
|
|
168
|
+
const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
|
|
169
|
+
try {
|
|
170
|
+
if (this.handlerOptions.parallelExecution !== false) {
|
|
171
|
+
try {
|
|
172
|
+
const results = await (0, p_map_1.default)(messages, async (message) => {
|
|
173
|
+
return await this.runOnMessageHook(message);
|
|
174
|
+
}, {
|
|
175
|
+
concurrency: 10,
|
|
176
|
+
stopOnError: false,
|
|
177
|
+
});
|
|
178
|
+
await this.deleteMessages(results);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.messagesOnFly -= messages.length;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const handlingMessages = [...messages];
|
|
186
|
+
for (let i = 0; i < handlingMessages.length; i++) {
|
|
187
|
+
try {
|
|
188
|
+
const result = await this.runOnMessageHook(handlingMessages[i]);
|
|
189
|
+
await this.deleteMessages([result]);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
this.messagesOnFly -= 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (haTimeout)
|
|
199
|
+
clearInterval(haTimeout);
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
|
-
|
|
184
|
-
clearInterval(haTimeout);
|
|
202
|
+
consecutiveErrors = 0;
|
|
185
203
|
}
|
|
186
|
-
|
|
187
|
-
this.
|
|
204
|
+
catch (e) {
|
|
205
|
+
await this.hooks.runHook("onSQSError", e);
|
|
206
|
+
await (0, promises_1.setTimeout)(Math.min(1000 * 2 ** consecutiveErrors++, 30000));
|
|
188
207
|
}
|
|
208
|
+
this.polling = false;
|
|
189
209
|
}
|
|
190
|
-
catch (e) {
|
|
191
|
-
await this.hooks.runHook("onSQSError", e);
|
|
192
|
-
}
|
|
193
|
-
this.polling = false;
|
|
194
|
-
await this.pollMessages();
|
|
195
210
|
}
|
|
196
211
|
async start() {
|
|
197
212
|
try {
|
|
@@ -223,7 +238,6 @@ class SQSConsumer {
|
|
|
223
238
|
await this.hooks.runHook("onStop", this);
|
|
224
239
|
}
|
|
225
240
|
}
|
|
226
|
-
/* c8 ignore next 3 */
|
|
227
241
|
get isRunning() {
|
|
228
242
|
return this.running;
|
|
229
243
|
}
|
package/dist/esm/errors.mjs
CHANGED
package/dist/esm/hooks.d.mts
CHANGED
|
@@ -2,13 +2,56 @@
|
|
|
2
2
|
import type { Message } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Logger } from "./logger.mjs";
|
|
4
4
|
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
|
|
5
|
+
export type HookSignatureMap = {
|
|
6
|
+
onStart: {
|
|
7
|
+
args: [sqsConsumer: unknown];
|
|
8
|
+
return: undefined;
|
|
9
|
+
};
|
|
10
|
+
onStop: {
|
|
11
|
+
args: [sqsConsumer: unknown];
|
|
12
|
+
return: undefined;
|
|
13
|
+
};
|
|
14
|
+
onPoll: {
|
|
15
|
+
args: [messages: Message[]];
|
|
16
|
+
return: Message[];
|
|
17
|
+
};
|
|
18
|
+
onMessage: {
|
|
19
|
+
args: [message: Message];
|
|
20
|
+
return: Message;
|
|
21
|
+
};
|
|
22
|
+
onHandlerSuccess: {
|
|
23
|
+
args: [message: Message];
|
|
24
|
+
return: Message;
|
|
25
|
+
};
|
|
26
|
+
onHandlerTimeout: {
|
|
27
|
+
args: [message: Message];
|
|
28
|
+
return: boolean;
|
|
29
|
+
};
|
|
30
|
+
onHandlerError: {
|
|
31
|
+
args: [message: Message, error: Error];
|
|
32
|
+
return: boolean;
|
|
33
|
+
};
|
|
34
|
+
onSuccess: {
|
|
35
|
+
args: [message: Message];
|
|
36
|
+
return: boolean;
|
|
37
|
+
};
|
|
38
|
+
onError: {
|
|
39
|
+
args: [hook: HookName, message: Message, error: Error];
|
|
40
|
+
return: boolean;
|
|
41
|
+
};
|
|
42
|
+
onSQSError: {
|
|
43
|
+
args: [error: Error, message?: Message];
|
|
44
|
+
return: undefined;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export type HookCallback<H extends HookName> = (...args: HookSignatureMap[H]["args"]) => HookSignatureMap[H]["return"] | Promise<HookSignatureMap[H]["return"]> | void | Promise<void> | undefined;
|
|
5
48
|
export declare class Hooks {
|
|
6
49
|
private readonly hookSymbols;
|
|
7
50
|
private readonly hooks;
|
|
8
51
|
private readonly logger;
|
|
9
52
|
constructor(logger?: Logger);
|
|
10
|
-
addHook(hook:
|
|
11
|
-
runHook(hook:
|
|
53
|
+
addHook<H extends HookName>(hook: H, fn: HookCallback<H>): void;
|
|
54
|
+
runHook<H extends HookName>(hook: H, ...args: HookSignatureMap[H]["args"]): Promise<HookSignatureMap[H]["return"]>;
|
|
12
55
|
private runHookWithVoidReturn;
|
|
13
56
|
private runHookWithMessageReturn;
|
|
14
57
|
private runHookWithBooleanReturn;
|
package/dist/esm/hooks.mjs
CHANGED
|
@@ -72,31 +72,36 @@ export class Hooks {
|
|
|
72
72
|
throw new Error(`Invalid hook ${hook}`);
|
|
73
73
|
this.hooks[hookSymbol.S]?.push(fn);
|
|
74
74
|
}
|
|
75
|
-
// biome-ignore lint/suspicious/noExplicitAny: args can be anything
|
|
76
75
|
async runHook(hook, ...args) {
|
|
77
76
|
const hookSymbol = this.hookSymbols[hook];
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: internal cast for generic dispatch
|
|
78
|
+
const hookArgs = args;
|
|
78
79
|
try {
|
|
79
80
|
if (!hookSymbol)
|
|
80
81
|
throw new Error(`Invalid hook ${hook}`);
|
|
81
82
|
switch (hookSymbol.type) {
|
|
82
83
|
case "boolean-return":
|
|
83
|
-
return await this.runHookWithBooleanReturn(hookSymbol, ...
|
|
84
|
+
return (await this.runHookWithBooleanReturn(hookSymbol, ...hookArgs));
|
|
84
85
|
case "message-return": {
|
|
85
|
-
const [message, ...someArgs] =
|
|
86
|
-
return await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs);
|
|
86
|
+
const [message, ...someArgs] = hookArgs;
|
|
87
|
+
return (await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs));
|
|
87
88
|
}
|
|
88
89
|
case "void":
|
|
89
|
-
return await this.runHookWithVoidReturn(hookSymbol, ...
|
|
90
|
+
return (await this.runHookWithVoidReturn(hookSymbol, ...hookArgs));
|
|
90
91
|
}
|
|
91
|
-
/* c8 ignore next 1 */
|
|
92
92
|
// biome-ignore lint/suspicious/noExplicitAny: error type is not important
|
|
93
93
|
}
|
|
94
|
-
catch (
|
|
95
|
-
|
|
94
|
+
catch (originalError) {
|
|
95
|
+
const newError = new Error(
|
|
96
|
+
/* c8 ignore next */
|
|
97
|
+
`Error running hook ${hook}: ${originalError?.message ?? originalError}`);
|
|
98
|
+
newError.cause = originalError;
|
|
96
99
|
if (hookSymbol.throwable !== false)
|
|
97
|
-
throw
|
|
98
|
-
this.logger.error(
|
|
100
|
+
throw newError;
|
|
101
|
+
this.logger.error(newError);
|
|
99
102
|
}
|
|
103
|
+
// biome-ignore lint/suspicious/noExplicitAny: fallthrough for non-throwable error paths
|
|
104
|
+
return undefined;
|
|
100
105
|
}
|
|
101
106
|
async runHookWithVoidReturn(hookSymbol,
|
|
102
107
|
// biome-ignore lint/suspicious/noExplicitAny: args can be anything
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Signer, SignerOptions } from "@fgiova/aws-signature";
|
|
2
2
|
import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Pool } from "undici";
|
|
4
|
-
import { type HookName } from "./hooks.mjs";
|
|
4
|
+
import { type HookCallback, type HookName } from "./hooks.mjs";
|
|
5
5
|
import type { Logger } from "./logger.mjs";
|
|
6
6
|
type HandlerOptions = {
|
|
7
7
|
deleteMessage?: boolean;
|
|
@@ -23,18 +23,9 @@ type ConsumerOptions = {
|
|
|
23
23
|
messageAttributeNames?: string[];
|
|
24
24
|
attributeNames?: string[];
|
|
25
25
|
};
|
|
26
|
-
type
|
|
27
|
-
type hookReturnMessage = Promise<Message> | Message | Promise<void> | void | undefined;
|
|
28
|
-
type hookReturnBoolean = Promise<boolean> | boolean | Promise<void> | void | undefined;
|
|
26
|
+
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
29
27
|
export type HooksOptions = {
|
|
30
|
-
|
|
31
|
-
onMessage?: (message: Message) => hookReturnMessage;
|
|
32
|
-
onHandlerSuccess?: (message: Message) => hookReturnMessage;
|
|
33
|
-
onHandlerTimeout?: (message: Message) => hookReturnBoolean;
|
|
34
|
-
onHandlerError?: (message: Message, error: Error) => hookReturnBoolean;
|
|
35
|
-
onSuccess?: (message: Message) => hookReturnBoolean;
|
|
36
|
-
onError?: (hook: HookName, message: Message, error: Error) => hookReturnBoolean;
|
|
37
|
-
onSQSError?: (error: Error, message?: Message) => Promise<void>;
|
|
28
|
+
[H in ConsumerHookName]?: HookCallback<H>;
|
|
38
29
|
};
|
|
39
30
|
export type SQSConsumerOptions = {
|
|
40
31
|
queueARN: string;
|
|
@@ -68,16 +59,6 @@ export declare class SQSConsumer {
|
|
|
68
59
|
start(): Promise<void>;
|
|
69
60
|
stop(destroy?: boolean): Promise<void>;
|
|
70
61
|
get isRunning(): boolean;
|
|
71
|
-
addHook(hookName:
|
|
72
|
-
addHook(hookName: "onStop", value: (sqsConsumer: SQSConsumerOptions) => void): this;
|
|
73
|
-
addHook(hookName: "onPoll", value: (messages: Message[]) => Promise<Message[]>): this;
|
|
74
|
-
addHook(hookName: "onMessage", value: (message: Message) => Promise<Message>): this;
|
|
75
|
-
addHook(hookName: "onHandlerSuccess", value: (message: Message) => Promise<Message>): this;
|
|
76
|
-
addHook(hookName: "onHandlerTimeout", value: (message: Message) => Promise<boolean>): this;
|
|
77
|
-
addHook(hookName: "onHandlerError", value: (message: Message, error: Error) => Promise<boolean>): this;
|
|
78
|
-
addHook(hookName: "onSuccess", value: (message: Message) => Promise<boolean>): this;
|
|
79
|
-
addHook(hookName: "onError", value: (hook: HookName, message: Message, error: Error) => Promise<boolean>): this;
|
|
80
|
-
addHook(hookName: "onSQSError", value: (error: Error, message?: Message) => Promise<void>): this;
|
|
81
|
-
addHook(hookName: HookName, value: Function): this;
|
|
62
|
+
addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this;
|
|
82
63
|
}
|
|
83
64
|
export {};
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** biome-ignore-all lint/complexity/noBannedTypes: Function required as generic */
|
|
2
1
|
import { setTimeout as setTimeoutAsync } from "node:timers/promises";
|
|
3
2
|
import { MiniSQSClient } from "@fgiova/mini-sqs-client";
|
|
4
3
|
// @ts-expect-error
|
|
@@ -44,6 +43,7 @@ export class SQSConsumer {
|
|
|
44
43
|
for (const [key, value] of Object.entries(options.hooks)) {
|
|
45
44
|
if (typeof value !== "function")
|
|
46
45
|
throw new Error(`${key} must be a function`);
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic registration loses type correlation
|
|
47
47
|
this.addHook(key, value);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -71,13 +71,14 @@ export class SQSConsumer {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
async runHandler(handler, message) {
|
|
74
|
+
let timeoutId = null;
|
|
74
75
|
try {
|
|
75
76
|
let handlerResult;
|
|
76
77
|
if (this.handlerOptions.executionTimeout) {
|
|
77
78
|
handlerResult = await Unpromise.race([
|
|
78
79
|
handler(message),
|
|
79
80
|
new Promise((_resolve, reject) => {
|
|
80
|
-
setTimeout(() => {
|
|
81
|
+
timeoutId = setTimeout(() => {
|
|
81
82
|
reject(new TimeoutError("Handler execution timed out"));
|
|
82
83
|
}, this.handlerOptions.executionTimeout);
|
|
83
84
|
}),
|
|
@@ -97,6 +98,10 @@ export class SQSConsumer {
|
|
|
97
98
|
await this.hooks.runHook("onHandlerError", message, error);
|
|
98
99
|
}
|
|
99
100
|
throw error;
|
|
101
|
+
} /* c8 ignore next 2 */
|
|
102
|
+
finally {
|
|
103
|
+
if (timeoutId)
|
|
104
|
+
clearTimeout(timeoutId);
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
haExtendVisibilityTimeout(messages, visibilityTimeout) {
|
|
@@ -127,65 +132,75 @@ export class SQSConsumer {
|
|
|
127
132
|
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
128
133
|
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
}
|
|
136
|
+
if (candidatesToDelete.length) {
|
|
137
|
+
await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
|
|
138
|
+
}
|
|
139
|
+
if (candidateToRelease.length) {
|
|
140
|
+
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease, 0);
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
async pollMessages() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
144
|
+
let consecutiveErrors = 0;
|
|
145
|
+
while (this.running) {
|
|
146
|
+
try {
|
|
147
|
+
this.polling = true;
|
|
148
|
+
/* c8 ignore next 1 */
|
|
149
|
+
const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
|
|
150
|
+
const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
|
|
151
|
+
WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
|
|
152
|
+
MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
|
|
153
|
+
VisibilityTimeout: visibilityTimeout,
|
|
154
|
+
MessageAttributeNames: this.consumerOptions.messageAttributeNames,
|
|
155
|
+
AttributeNames: this.consumerOptions.attributeNames,
|
|
156
|
+
});
|
|
157
|
+
const messages = await this.hooks.runHook("onPoll",
|
|
158
|
+
/* c8 ignore next 1 */
|
|
159
|
+
messagesResult.Messages ?? []);
|
|
160
|
+
this.messagesOnFly += messages.length;
|
|
161
|
+
if (messages.length) {
|
|
162
|
+
const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
|
|
163
|
+
try {
|
|
164
|
+
if (this.handlerOptions.parallelExecution !== false) {
|
|
165
|
+
try {
|
|
166
|
+
const results = await pMap(messages, async (message) => {
|
|
167
|
+
return await this.runOnMessageHook(message);
|
|
168
|
+
}, {
|
|
169
|
+
concurrency: 10,
|
|
170
|
+
stopOnError: false,
|
|
171
|
+
});
|
|
172
|
+
await this.deleteMessages(results);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
this.messagesOnFly -= messages.length;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const handlingMessages = [...messages];
|
|
180
|
+
for (let i = 0; i < handlingMessages.length; i++) {
|
|
181
|
+
try {
|
|
182
|
+
const result = await this.runOnMessageHook(handlingMessages[i]);
|
|
183
|
+
await this.deleteMessages([result]);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
this.messagesOnFly -= 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
if (haTimeout)
|
|
193
|
+
clearInterval(haTimeout);
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
|
-
|
|
178
|
-
clearInterval(haTimeout);
|
|
196
|
+
consecutiveErrors = 0;
|
|
179
197
|
}
|
|
180
|
-
|
|
181
|
-
this.
|
|
198
|
+
catch (e) {
|
|
199
|
+
await this.hooks.runHook("onSQSError", e);
|
|
200
|
+
await setTimeoutAsync(Math.min(1000 * 2 ** consecutiveErrors++, 30000));
|
|
182
201
|
}
|
|
202
|
+
this.polling = false;
|
|
183
203
|
}
|
|
184
|
-
catch (e) {
|
|
185
|
-
await this.hooks.runHook("onSQSError", e);
|
|
186
|
-
}
|
|
187
|
-
this.polling = false;
|
|
188
|
-
await this.pollMessages();
|
|
189
204
|
}
|
|
190
205
|
async start() {
|
|
191
206
|
try {
|
|
@@ -217,7 +232,6 @@ export class SQSConsumer {
|
|
|
217
232
|
await this.hooks.runHook("onStop", this);
|
|
218
233
|
}
|
|
219
234
|
}
|
|
220
|
-
/* c8 ignore next 3 */
|
|
221
235
|
get isRunning() {
|
|
222
236
|
return this.running;
|
|
223
237
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgiova/sqs-consumer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"main": "./dist/cjs/index.js",
|
|
5
5
|
"types": "./dist/cjs/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
|
-
"node": "
|
|
47
|
+
"node": "^22.14.0 || >= 24.10.0"
|
|
48
48
|
},
|
|
49
49
|
"tap": {
|
|
50
50
|
"show-full-coverage": true,
|
|
@@ -56,25 +56,27 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@fgiova/mini-sqs-client": "^
|
|
59
|
+
"@fgiova/mini-sqs-client": "^4.0.0",
|
|
60
60
|
"@watchable/unpromise": "^1.0.2",
|
|
61
61
|
"p-map": "^7.0.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@fgiova/aws-signature": "^
|
|
64
|
+
"@fgiova/aws-signature": "^4.0.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@aws-sdk/client-sqs": "^3.758.0",
|
|
68
|
-
"@biomejs/biome": "2.
|
|
68
|
+
"@biomejs/biome": "2.3.15",
|
|
69
69
|
"@fgiova/cjs-esm-ts-builder": "^1.2.0",
|
|
70
70
|
"@semantic-release/changelog": "^6.0.3",
|
|
71
71
|
"@semantic-release/git": "^10.0.1",
|
|
72
|
-
"@
|
|
73
|
-
"@
|
|
74
|
-
"
|
|
75
|
-
"
|
|
72
|
+
"@semantic-release/npm": "^13.1.4",
|
|
73
|
+
"@tsconfig/node22": "^22.0.5",
|
|
74
|
+
"@types/node": "^22.19.11",
|
|
75
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
76
|
+
"dotenv": "^17.2.4",
|
|
77
|
+
"semantic-release": "^25.0.3",
|
|
76
78
|
"tap": "^21.1.0",
|
|
77
|
-
"testcontainers": "^11.
|
|
79
|
+
"testcontainers": "^11.11.0",
|
|
78
80
|
"ts-node": "^10.9.2",
|
|
79
81
|
"typescript": "^5.8.2"
|
|
80
82
|
}
|