@fgiova/sqs-consumer 2.2.0 → 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 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 | Hooks | | The hooks to be called on specific events |
42
+ | hooks | HooksOptions | | The hooks to be called on specific events |
43
43
 
44
44
  ### HandlerOptions
45
- | Option | Type | Default | Description |
46
- |-------------------|---------|---------|----------------------------------------------------------------------------------------------------------|
47
- | deleteMessage | boolean | true | Whether to delete the message after handling (if handler execute without any error) |
48
- | extendVisibility | boolean | true | Whether to extend the visibility timeout during message handling |
49
- | excuteTimeout | 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 |
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 | false | Whether to destroy the signer when the consumer is destroyed. |
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
- | onHandle | (message: Message) => Promise<Message> | Called when the consumer handles a message |
76
- | onHandlerSuccess | (message: Message) => Promise<Message> | Called when the consumer handles a message successfully |
77
- | onHandlerTimeout | (message: Message) => Promise<Message> | Called when the consumer handler execution exceed executionTimeout |
78
- | onHandlerError | (message: Message, error: Error) => Promise<Boolean> | Called when the consumer handler execution throws an error |
79
- | onSuccess | (message: Message) => Promise<Message> | Called when the consumer handler execution finishes successfully |
80
- | onError | ( hook: HookName, message: Message, error: Error) => Promise<Boolean> | Called when the consumer handler execution throws an uncaught error |
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(hookname: string, func: Function): void
88
+ SQSConsumer.addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this
90
89
  ```
91
90
 
92
91
  ## License
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TimeoutError = void 0;
4
4
  class TimeoutError extends Error {
5
5
  constructor(message) {
6
- /* c8 ignore next 1 */
7
6
  super(message || "Timeout Error");
8
7
  this.name = "TimeoutError";
9
8
  }
@@ -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: HookName, fn: Function): void;
11
- runHook(hook: HookName, ...args: any[]): Promise<boolean | void | Message | Message[]>;
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, ...args);
87
+ return (await this.runHookWithBooleanReturn(hookSymbol, ...hookArgs));
87
88
  case "message-return": {
88
- const [message, ...someArgs] = args;
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, ...args);
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 (error) {
98
- error.message = `Error running hook ${hook}: ${error.message}`;
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 error;
101
- this.logger.error(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
@@ -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,22 +23,13 @@ type ConsumerOptions = {
23
23
  messageAttributeNames?: string[];
24
24
  attributeNames?: string[];
25
25
  };
26
- type hookReturnMessages = Promise<Message[]> | Message[] | Promise<void> | void | undefined;
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
- onPoll?: (messages: Message[]) => hookReturnMessages;
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;
41
- handler: (message: Message) => Promise<any>;
32
+ handler: (message: Message) => Promise<unknown>;
42
33
  logger?: Logger;
43
34
  autostart?: boolean;
44
35
  handlerOptions?: HandlerOptions;
@@ -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: "onStart", value: (sqsConsumer: SQSConsumerOptions) => void): this;
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,18 +77,18 @@ 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
- const response = await unpromise_1.Unpromise.race([
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
  }),
90
91
  ]);
91
- handlerResult = response;
92
92
  }
93
93
  else {
94
94
  handlerResult = await handler(message);
@@ -104,6 +104,10 @@ class SQSConsumer {
104
104
  await this.hooks.runHook("onHandlerError", message, error);
105
105
  }
106
106
  throw error;
107
+ } /* c8 ignore next 2 */
108
+ finally {
109
+ if (timeoutId)
110
+ clearTimeout(timeoutId);
107
111
  }
108
112
  }
109
113
  haExtendVisibilityTimeout(messages, visibilityTimeout) {
@@ -134,67 +138,76 @@ class SQSConsumer {
134
138
  // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
135
139
  candidatesToDelete.push(result.message.ReceiptHandle);
136
140
  }
137
- if (candidatesToDelete.length)
138
- await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
139
- if (candidateToRelease.length)
140
- await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease, 0);
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);
141
147
  }
142
148
  }
143
149
  async pollMessages() {
144
- if (!this.running)
145
- return;
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
- /* c8 ignore next 1 */
158
- const messages = messagesResult.Messages ?? [];
159
- const totalMessages = messages.length;
160
- this.messagesOnFly += totalMessages;
161
- await this.hooks.runHook("onPoll", messages);
162
- if (messages.length) {
163
- const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
164
- this.messagesOnFly += messages.length - totalMessages;
165
- if (this.handlerOptions.parallelExecution !== false) {
166
- const results = await (0, p_map_1.default)(messages, async (message) => {
167
- return await this.runOnMessageHook(message);
168
- }, {
169
- concurrency: 10,
170
- stopOnError: false,
171
- });
172
- await this.deleteMessages(results);
173
- this.messagesOnFly -= messages.length;
174
- }
175
- else {
176
- const handlingMessages = [...messages];
177
- for (let i = 0; i < handlingMessages.length; i++) {
178
- const result = await this.runOnMessageHook(handlingMessages[i]);
179
- await this.deleteMessages([result]);
180
- messages.splice(i, 1);
181
- this.messagesOnFly -= 1;
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);
182
200
  }
183
201
  }
184
- if (haTimeout)
185
- clearInterval(haTimeout);
202
+ consecutiveErrors = 0;
186
203
  }
187
- else {
188
- this.messagesOnFly -= totalMessages;
204
+ catch (e) {
205
+ await this.hooks.runHook("onSQSError", e);
206
+ await (0, promises_1.setTimeout)(Math.min(1000 * 2 ** consecutiveErrors++, 30000));
189
207
  }
208
+ this.polling = false;
190
209
  }
191
- catch (e) {
192
- await this.hooks.runHook("onSQSError", e);
193
- }
194
- this.polling = false;
195
- await this.pollMessages();
196
210
  }
197
- // eslint-disable-next-line @typescript-eslint/require-await
198
211
  async start() {
199
212
  try {
200
213
  if (this.running)
@@ -225,7 +238,6 @@ class SQSConsumer {
225
238
  await this.hooks.runHook("onStop", this);
226
239
  }
227
240
  }
228
- /* c8 ignore next 3 */
229
241
  get isRunning() {
230
242
  return this.running;
231
243
  }
@@ -1,6 +1,5 @@
1
1
  export class TimeoutError extends Error {
2
2
  constructor(message) {
3
- /* c8 ignore next 1 */
4
3
  super(message || "Timeout Error");
5
4
  this.name = "TimeoutError";
6
5
  }
@@ -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: HookName, fn: Function): void;
11
- runHook(hook: HookName, ...args: any[]): Promise<boolean | void | Message | Message[]>;
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;
@@ -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, ...args);
84
+ return (await this.runHookWithBooleanReturn(hookSymbol, ...hookArgs));
84
85
  case "message-return": {
85
- const [message, ...someArgs] = args;
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, ...args);
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 (error) {
95
- error.message = `Error running hook ${hook}: ${error.message}`;
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 error;
98
- this.logger.error(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
@@ -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,22 +23,13 @@ type ConsumerOptions = {
23
23
  messageAttributeNames?: string[];
24
24
  attributeNames?: string[];
25
25
  };
26
- type hookReturnMessages = Promise<Message[]> | Message[] | Promise<void> | void | undefined;
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
- onPoll?: (messages: Message[]) => hookReturnMessages;
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;
41
- handler: (message: Message) => Promise<any>;
32
+ handler: (message: Message) => Promise<unknown>;
42
33
  logger?: Logger;
43
34
  autostart?: boolean;
44
35
  handlerOptions?: HandlerOptions;
@@ -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: "onStart", value: (sqsConsumer: SQSConsumerOptions) => void): this;
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 {};
@@ -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,18 +71,18 @@ 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
- const response = await Unpromise.race([
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
  }),
84
85
  ]);
85
- handlerResult = response;
86
86
  }
87
87
  else {
88
88
  handlerResult = await handler(message);
@@ -98,6 +98,10 @@ export class SQSConsumer {
98
98
  await this.hooks.runHook("onHandlerError", message, error);
99
99
  }
100
100
  throw error;
101
+ } /* c8 ignore next 2 */
102
+ finally {
103
+ if (timeoutId)
104
+ clearTimeout(timeoutId);
101
105
  }
102
106
  }
103
107
  haExtendVisibilityTimeout(messages, visibilityTimeout) {
@@ -128,67 +132,76 @@ export class SQSConsumer {
128
132
  // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
129
133
  candidatesToDelete.push(result.message.ReceiptHandle);
130
134
  }
131
- if (candidatesToDelete.length)
132
- await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
133
- if (candidateToRelease.length)
134
- await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease, 0);
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);
135
141
  }
136
142
  }
137
143
  async pollMessages() {
138
- if (!this.running)
139
- return;
140
- try {
141
- this.polling = true;
142
- /* c8 ignore next 1 */
143
- const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
144
- const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
145
- WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
146
- MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
147
- VisibilityTimeout: visibilityTimeout,
148
- MessageAttributeNames: this.consumerOptions.messageAttributeNames,
149
- AttributeNames: this.consumerOptions.attributeNames,
150
- });
151
- /* c8 ignore next 1 */
152
- const messages = messagesResult.Messages ?? [];
153
- const totalMessages = messages.length;
154
- this.messagesOnFly += totalMessages;
155
- await this.hooks.runHook("onPoll", messages);
156
- if (messages.length) {
157
- const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
158
- this.messagesOnFly += messages.length - totalMessages;
159
- if (this.handlerOptions.parallelExecution !== false) {
160
- const results = await pMap(messages, async (message) => {
161
- return await this.runOnMessageHook(message);
162
- }, {
163
- concurrency: 10,
164
- stopOnError: false,
165
- });
166
- await this.deleteMessages(results);
167
- this.messagesOnFly -= messages.length;
168
- }
169
- else {
170
- const handlingMessages = [...messages];
171
- for (let i = 0; i < handlingMessages.length; i++) {
172
- const result = await this.runOnMessageHook(handlingMessages[i]);
173
- await this.deleteMessages([result]);
174
- messages.splice(i, 1);
175
- this.messagesOnFly -= 1;
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);
176
194
  }
177
195
  }
178
- if (haTimeout)
179
- clearInterval(haTimeout);
196
+ consecutiveErrors = 0;
180
197
  }
181
- else {
182
- this.messagesOnFly -= totalMessages;
198
+ catch (e) {
199
+ await this.hooks.runHook("onSQSError", e);
200
+ await setTimeoutAsync(Math.min(1000 * 2 ** consecutiveErrors++, 30000));
183
201
  }
202
+ this.polling = false;
184
203
  }
185
- catch (e) {
186
- await this.hooks.runHook("onSQSError", e);
187
- }
188
- this.polling = false;
189
- await this.pollMessages();
190
204
  }
191
- // eslint-disable-next-line @typescript-eslint/require-await
192
205
  async start() {
193
206
  try {
194
207
  if (this.running)
@@ -219,7 +232,6 @@ export class SQSConsumer {
219
232
  await this.hooks.runHook("onStop", this);
220
233
  }
221
234
  }
222
- /* c8 ignore next 3 */
223
235
  get isRunning() {
224
236
  return this.running;
225
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgiova/sqs-consumer",
3
- "version": "2.2.0",
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": ">=20.14.1"
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": "^3.3.0",
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": "^3.2.0"
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.2.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
- "@tsconfig/node20": "^20.1.4",
73
- "@types/node": "^20.17.23",
74
- "conventional-changelog-conventionalcommits": "^8.0.0",
75
- "semantic-release": "^24.2.3",
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.5.1",
79
+ "testcontainers": "^11.11.0",
78
80
  "ts-node": "^10.9.2",
79
81
  "typescript": "^5.8.2"
80
82
  }