@fgiova/sqs-consumer 2.0.1 → 2.1.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 CHANGED
@@ -3,8 +3,8 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/@fgiova/sqs-consumer.svg?style=flat)](https://www.npmjs.com/package/@fgiova/sqs-consumer)
4
4
  ![CI workflow](https://github.com/fgiova/sqs-consumer/actions/workflows/node.js.yml/badge.svg)
5
5
  [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
6
- [![Maintainability](https://api.codeclimate.com/v1/badges/0c10549e75ef6c798dfd/maintainability)](https://codeclimate.com/github/fgiova/sqs-consumer/maintainability)
7
- [![Test Coverage](https://api.codeclimate.com/v1/badges/0c10549e75ef6c798dfd/test_coverage)](https://codeclimate.com/github/fgiova/mini-sqs-client/test_coverage)
6
+ [![Maintainability](https://qlty.sh/gh/fgiova/projects/sqs-consumer/maintainability.svg)](https://qlty.sh/gh/fgiova/projects/sqs-consumer)
7
+ [![Code Coverage](https://qlty.sh/gh/fgiova/projects/sqs-consumer/coverage.svg)](https://qlty.sh/gh/fgiova/projects/sqs-consumer)
8
8
 
9
9
  ## Description
10
10
  This module allows consuming SQS messages using @fgiova/mini-sqs-client thorough the aws-json protocol with "undici" as http agent .<br />
@@ -58,12 +58,13 @@ const consumer = new SQSConsumer({
58
58
  | destroySigner | boolean | false | Whether to destroy the signer when the consumer is destroyed. |
59
59
 
60
60
  ### ConsumerOptions
61
- | Option | Type | Default | Description |
62
- |-------------------------|----------|---------|------------------------------------------------------------|
63
- | visibilityTimeout | number | 30 | The visibility timeout for the messages in seconds |
64
- | waitTimeSeconds | number | 20 | The wait time for the receiveMessage call in seconds |
65
- | itemsPerRequest | number | 10 | The maximum number of messages to be received at once |
66
- | messageAttributeNames | string[] | [] | The message attribute names to be included in the response |
61
+ | Option | Type | Default | Description |
62
+ |-----------------------|----------|---------|------------------------------------------------------------|
63
+ | visibilityTimeout | number | 30 | The visibility timeout for the messages in seconds |
64
+ | waitTimeSeconds | number | 20 | The wait time for the receiveMessage call in seconds |
65
+ | itemsPerRequest | number | 10 | The maximum number of messages to be received at once |
66
+ | messageAttributeNames | string[] | [] | The message attribute names to be included in the response |
67
+ | attributeNames | string[] | [] | The attribute names to be included in the response |
67
68
 
68
69
  ### Hooks
69
70
  | Option | Type | Description |
@@ -88,4 +89,4 @@ SQSConsumer.addHook(hookname: string, func: Function): void
88
89
  ```
89
90
 
90
91
  ## License
91
- Licensed under [MIT](./LICENSE).
92
+ Licensed under [MIT](./LICENSE).
@@ -1,5 +1,4 @@
1
- import { Logger } from "./logger";
2
- import { Message } from "@fgiova/mini-sqs-client";
1
+ import type { Logger } from "./logger";
3
2
  export type HookName = "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
4
3
  export declare class Hooks {
5
4
  private readonly hookSymbols;
@@ -7,7 +6,7 @@ export declare class Hooks {
7
6
  private readonly logger;
8
7
  constructor(logger?: Logger);
9
8
  addHook(hook: HookName, fn: Function): void;
10
- runHook(hook: HookName, ...args: any[]): Promise<boolean | void | Message | Message[]>;
9
+ runHook(hook: HookName, ...args: any[]): Promise<any>;
11
10
  private runHookWithVoidReturn;
12
11
  private runHookWithMessageReturn;
13
12
  private runHookWithBooleanReturn;
@@ -2,52 +2,49 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Hooks = void 0;
4
4
  class Hooks {
5
- hookSymbols = {
6
- onPoll: {
7
- S: Symbol("poll"),
8
- throwable: true,
9
- type: "message-return"
10
- },
11
- onMessage: {
12
- S: Symbol("message"),
13
- throwable: true,
14
- type: "message-return"
15
- },
16
- onHandlerSuccess: {
17
- S: Symbol("handlerSuccess"),
18
- throwable: false,
19
- type: "message-return"
20
- },
21
- onHandlerTimeout: {
22
- S: Symbol("handlerTimeout"),
23
- throwable: false,
24
- type: "boolean-return"
25
- },
26
- onHandlerError: {
27
- S: Symbol("handlerError"),
28
- throwable: false,
29
- type: "boolean-return"
30
- },
31
- onSuccess: {
32
- S: Symbol("success"),
33
- throwable: false,
34
- type: "boolean-return"
35
- },
36
- onError: {
37
- S: Symbol("error"),
38
- throwable: false,
39
- type: "boolean-return"
40
- },
41
- onSQSError: {
42
- S: Symbol("sqsError"),
43
- throwable: false,
44
- type: "void"
45
- },
46
- };
47
- // eslint-disable-next-line @typescript-eslint/ban-types
48
- hooks;
49
- logger;
50
5
  constructor(logger = console) {
6
+ this.hookSymbols = {
7
+ onPoll: {
8
+ S: Symbol("poll"),
9
+ throwable: true,
10
+ type: "message-return",
11
+ },
12
+ onMessage: {
13
+ S: Symbol("message"),
14
+ throwable: true,
15
+ type: "message-return",
16
+ },
17
+ onHandlerSuccess: {
18
+ S: Symbol("handlerSuccess"),
19
+ throwable: false,
20
+ type: "message-return",
21
+ },
22
+ onHandlerTimeout: {
23
+ S: Symbol("handlerTimeout"),
24
+ throwable: false,
25
+ type: "boolean-return",
26
+ },
27
+ onHandlerError: {
28
+ S: Symbol("handlerError"),
29
+ throwable: false,
30
+ type: "boolean-return",
31
+ },
32
+ onSuccess: {
33
+ S: Symbol("success"),
34
+ throwable: false,
35
+ type: "boolean-return",
36
+ },
37
+ onError: {
38
+ S: Symbol("error"),
39
+ throwable: false,
40
+ type: "boolean-return",
41
+ },
42
+ onSQSError: {
43
+ S: Symbol("sqsError"),
44
+ throwable: false,
45
+ type: "void",
46
+ },
47
+ };
51
48
  this.logger = logger;
52
49
  this.hooks = {
53
50
  [this.hookSymbols.onPoll.S]: [],
@@ -60,13 +57,13 @@ class Hooks {
60
57
  [this.hookSymbols.onSQSError.S]: [],
61
58
  };
62
59
  }
63
- // eslint-disable-next-line @typescript-eslint/ban-types
64
60
  addHook(hook, fn) {
65
61
  const hookSymbol = this.hookSymbols[hook];
66
62
  if (!hookSymbol)
67
63
  throw new Error(`Invalid hook ${hook}`);
68
64
  this.hooks[hookSymbol.S]?.push(fn);
69
65
  }
66
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
70
67
  async runHook(hook, ...args) {
71
68
  const hookSymbol = this.hookSymbols[hook];
72
69
  try {
@@ -75,13 +72,15 @@ class Hooks {
75
72
  switch (hookSymbol.type) {
76
73
  case "boolean-return":
77
74
  return await this.runHookWithBooleanReturn(hookSymbol, ...args);
78
- case "message-return":
75
+ case "message-return": {
79
76
  const [message, ...someArgs] = args;
80
77
  return await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs);
78
+ }
81
79
  case "void":
82
80
  return await this.runHookWithVoidReturn(hookSymbol, ...args);
83
81
  }
84
82
  /* c8 ignore next 1 */
83
+ // biome-ignore lint/suspicious/noExplicitAny: error type is not important
85
84
  }
86
85
  catch (error) {
87
86
  error.message = `Error running hook ${hook}: ${error.message}`;
@@ -90,22 +89,31 @@ class Hooks {
90
89
  this.logger.error(error);
91
90
  }
92
91
  }
93
- async runHookWithVoidReturn(hookSymbol, ...args) {
92
+ async runHookWithVoidReturn(hookSymbol,
93
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
94
+ ...args) {
94
95
  for (const fn of this.hooks[hookSymbol.S]) {
95
96
  await fn(...args);
96
97
  }
97
98
  return;
98
99
  }
99
- async runHookWithMessageReturn(hookSymbol, message, ...args) {
100
+ async runHookWithMessageReturn(hookSymbol, message,
101
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
102
+ ...args) {
100
103
  for (const fn of this.hooks[hookSymbol.S]) {
101
- message = await fn(message, ...args);
104
+ const returnMessage = await fn(message, ...args);
105
+ if (returnMessage) {
106
+ message = returnMessage;
107
+ }
102
108
  }
103
109
  return message;
104
110
  }
105
- async runHookWithBooleanReturn(hookSymbol, ...args) {
111
+ async runHookWithBooleanReturn(hookSymbol,
112
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
113
+ ...args) {
106
114
  for (const fn of this.hooks[hookSymbol.S]) {
107
115
  const continueLoop = await fn(...args);
108
- if (!continueLoop) {
116
+ if (continueLoop === false) {
109
117
  return false;
110
118
  }
111
119
  }
@@ -1,8 +1,8 @@
1
- import { Signer, SignerOptions } from "@fgiova/aws-signature";
1
+ import type { Signer, SignerOptions } from "@fgiova/aws-signature";
2
+ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
2
3
  import type { Pool } from "undici";
3
- import { MiniSQSClient, Message } from "@fgiova/mini-sqs-client";
4
- import { HookName } from "./hooks";
5
- import { Logger } from "./logger";
4
+ import { type HookName } from "./hooks";
5
+ import type { Logger } from "./logger";
6
6
  type HandlerOptions = {
7
7
  deleteMessage?: boolean;
8
8
  extendVisibilityTimeout?: boolean;
@@ -21,15 +21,19 @@ type ConsumerOptions = {
21
21
  waitTimeSeconds?: number;
22
22
  itemsPerRequest?: number;
23
23
  messageAttributeNames?: string[];
24
+ attributeNames?: string[];
24
25
  };
25
- type HooksOptions = {
26
- onPoll?: (messages: Message[]) => Promise<Message[]>;
27
- onMessage?: (message: Message) => Promise<Message>;
28
- onHandlerSuccess?: (message: Message) => Promise<Message>;
29
- onHandlerTimeout?: (message: Message) => Promise<boolean>;
30
- onHandlerError?: (message: Message, error: Error) => Promise<boolean>;
31
- onSuccess?: (message: Message) => Promise<boolean>;
32
- onError?: (hook: HookName, message: Message, error: Error) => Promise<boolean>;
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;
29
+ 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;
33
37
  onSQSError?: (error: Error, message?: Message) => Promise<void>;
34
38
  };
35
39
  export type SQSConsumerOptions = {
@@ -63,14 +67,15 @@ export declare class SQSConsumer {
63
67
  private pollMessages;
64
68
  start(): Promise<void>;
65
69
  stop(destroy?: boolean): Promise<void>;
66
- addHook(hookName: "onPoll", value: (messages: Message[]) => Promise<Message[]>): void;
67
- addHook(hookName: "onMessage", value: (message: Message) => Promise<Message>): void;
68
- addHook(hookName: "onHandlerSuccess", value: (message: Message) => Promise<Message>): void;
69
- addHook(hookName: "onHandlerTimeout", value: (message: Message) => Promise<boolean>): void;
70
- addHook(hookName: "onHandlerError", value: (message: Message, error: Error) => Promise<boolean>): void;
71
- addHook(hookName: "onSuccess", value: (message: Message) => Promise<boolean>): void;
72
- addHook(hookName: "onError", value: (hook: HookName, message: Message, error: Error) => Promise<boolean>): void;
73
- addHook(hookName: "onSQSError", value: (error: Error, message?: Message) => Promise<void>): void;
74
- addHook(hookName: HookName, value: Function): void;
70
+ get isRunning(): boolean;
71
+ addHook(hookName: "onPoll", value: (messages: Message[]) => Promise<Message[]>): this;
72
+ addHook(hookName: "onMessage", value: (message: Message) => Promise<Message>): this;
73
+ addHook(hookName: "onHandlerSuccess", value: (message: Message) => Promise<Message>): this;
74
+ addHook(hookName: "onHandlerTimeout", value: (message: Message) => Promise<boolean>): this;
75
+ addHook(hookName: "onHandlerError", value: (message: Message, error: Error) => Promise<boolean>): this;
76
+ addHook(hookName: "onSuccess", value: (message: Message) => Promise<boolean>): this;
77
+ addHook(hookName: "onError", value: (hook: HookName, message: Message, error: Error) => Promise<boolean>): this;
78
+ addHook(hookName: "onSQSError", value: (error: Error, message?: Message) => Promise<void>): this;
79
+ addHook(hookName: HookName, value: Function): this;
75
80
  }
76
81
  export {};
@@ -4,26 +4,21 @@ 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
+ const promises_1 = require("node:timers/promises");
7
9
  const mini_sqs_client_1 = require("@fgiova/mini-sqs-client");
8
- const hooks_1 = require("./hooks");
9
- const errors_1 = require("./errors");
10
- const promises_1 = require("timers/promises");
11
- const p_map_1 = __importDefault(require("p-map"));
10
+ // @ts-expect-error
12
11
  const unpromise_1 = require("@watchable/unpromise");
12
+ // @ts-expect-error
13
+ const p_map_1 = __importDefault(require("p-map"));
14
+ const errors_1 = require("./errors");
15
+ const hooks_1 = require("./hooks");
13
16
  class SQSConsumer {
14
- queueARN;
15
- handlerOptions;
16
- clientOptions;
17
- consumerOptions;
18
- sqsClient;
19
- hooks;
20
- messageHandler;
21
- messagesOnFly = 0;
22
- running = false;
23
- polling = false;
24
- destroyed = false;
25
- logger;
26
17
  constructor(options) {
18
+ this.messagesOnFly = 0;
19
+ this.running = false;
20
+ this.polling = false;
21
+ this.destroyed = false;
27
22
  if (!options.queueARN)
28
23
  throw new Error("queueARN is required");
29
24
  if (!options.handler || typeof options.handler !== "function")
@@ -32,20 +27,25 @@ class SQSConsumer {
32
27
  this.hooks = new hooks_1.Hooks(this.logger);
33
28
  this.queueARN = options.queueARN;
34
29
  const region = this.queueARN.split(":").reverse()[2];
35
- this.clientOptions = { destroySigner: options.clientOptions?.destroySigner ?? true };
30
+ this.clientOptions = {
31
+ destroySigner: options.clientOptions?.destroySigner ?? true,
32
+ };
36
33
  this.handlerOptions = {
37
34
  deleteMessage: options.handlerOptions?.deleteMessage ?? true,
38
35
  extendVisibilityTimeout: options.handlerOptions?.extendVisibilityTimeout ?? true,
39
- executionTimeout: options.handlerOptions?.executionTimeout ?? 30_000,
40
- parallelExecution: options.handlerOptions?.parallelExecution ?? true
36
+ executionTimeout: options.handlerOptions?.executionTimeout ?? 30000,
37
+ parallelExecution: options.handlerOptions?.parallelExecution ?? true,
41
38
  };
42
39
  this.consumerOptions = {
43
40
  visibilityTimeout: options.consumerOptions?.visibilityTimeout ?? 30,
44
41
  waitTimeSeconds: options.consumerOptions?.waitTimeSeconds ?? 20,
45
42
  itemsPerRequest: options.consumerOptions?.itemsPerRequest ?? 10,
46
- messageAttributeNames: options.consumerOptions?.messageAttributeNames ?? []
43
+ messageAttributeNames: options.consumerOptions?.messageAttributeNames ?? [],
44
+ attributeNames: options.consumerOptions?.attributeNames ?? [],
47
45
  };
48
- this.sqsClient = options.clientOptions?.sqsClient ?? new mini_sqs_client_1.MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
46
+ this.sqsClient =
47
+ options.clientOptions?.sqsClient ??
48
+ new mini_sqs_client_1.MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
49
49
  if (options.hooks) {
50
50
  for (const [key, value] of Object.entries(options.hooks)) {
51
51
  if (typeof value !== "function")
@@ -65,14 +65,14 @@ class SQSConsumer {
65
65
  await this.hooks.runHook("onSuccess", message);
66
66
  return {
67
67
  message,
68
- result
68
+ result,
69
69
  };
70
70
  }
71
71
  catch (e) {
72
72
  await this.hooks.runHook("onError", "onMessage", message, e);
73
73
  return {
74
74
  message,
75
- error: e
75
+ error: e,
76
76
  };
77
77
  }
78
78
  }
@@ -82,20 +82,18 @@ class SQSConsumer {
82
82
  if (this.handlerOptions.executionTimeout) {
83
83
  const response = await unpromise_1.Unpromise.race([
84
84
  handler(message),
85
- new Promise((resolve, reject) => {
85
+ new Promise((_resolve, reject) => {
86
86
  setTimeout(() => {
87
87
  reject(new errors_1.TimeoutError("Handler execution timed out"));
88
88
  }, this.handlerOptions.executionTimeout);
89
- })
89
+ }),
90
90
  ]);
91
91
  handlerResult = response;
92
92
  }
93
93
  else {
94
94
  handlerResult = await handler(message);
95
95
  }
96
- await Promise.all([
97
- this.hooks.runHook("onHandlerSuccess", message),
98
- ]);
96
+ await this.hooks.runHook("onHandlerSuccess", message);
99
97
  return handlerResult;
100
98
  }
101
99
  catch (error) {
@@ -110,16 +108,17 @@ class SQSConsumer {
110
108
  }
111
109
  haExtendVisibilityTimeout(messages, visibilityTimeout) {
112
110
  if (this.handlerOptions.extendVisibilityTimeout !== false) {
113
- const processingMessages = messages.map(message => message.ReceiptHandle);
111
+ const processingMessages = messages.map(
112
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
113
+ (message) => message.ReceiptHandle);
114
114
  return setInterval(async () => {
115
115
  try {
116
116
  await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, processingMessages, visibilityTimeout);
117
- }
118
- /* c8 ignore next 3 */
117
+ } /* c8 ignore next 3 */
119
118
  catch (e) {
120
119
  await this.hooks.runHook("onSQSError", e);
121
120
  }
122
- }, (visibilityTimeout * 1000 - 5));
121
+ }, visibilityTimeout * 1000 - 5);
123
122
  }
124
123
  return null;
125
124
  }
@@ -128,9 +127,11 @@ class SQSConsumer {
128
127
  const candidateToRelease = [];
129
128
  for (const result of messagesResults) {
130
129
  if (result.result && this.handlerOptions.deleteMessage !== false) {
130
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
131
131
  candidatesToDelete.push(result.message.ReceiptHandle);
132
132
  }
133
133
  else if (result.error) {
134
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
134
135
  candidateToRelease.push(result.message.ReceiptHandle);
135
136
  }
136
137
  if (candidatesToDelete.length)
@@ -144,12 +145,14 @@ class SQSConsumer {
144
145
  return;
145
146
  try {
146
147
  this.polling = true;
147
- const visibilityTimeout = this.consumerOptions.visibilityTimeout;
148
+ /* c8 ignore next 1 */
149
+ const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
148
150
  const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
149
151
  WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
150
152
  MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
151
153
  VisibilityTimeout: visibilityTimeout,
152
154
  MessageAttributeNames: this.consumerOptions.messageAttributeNames,
155
+ AttributeNames: this.consumerOptions.attributeNames,
153
156
  });
154
157
  /* c8 ignore next 1 */
155
158
  const messages = messagesResult.Messages ?? [];
@@ -158,7 +161,7 @@ class SQSConsumer {
158
161
  await this.hooks.runHook("onPoll", messages);
159
162
  if (messages.length) {
160
163
  const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
161
- this.messagesOnFly += (messages.length - totalMessages);
164
+ this.messagesOnFly += messages.length - totalMessages;
162
165
  if (this.handlerOptions.parallelExecution !== false) {
163
166
  const results = await (0, p_map_1.default)(messages, async (message) => {
164
167
  return await this.runOnMessageHook(message);
@@ -212,9 +215,13 @@ class SQSConsumer {
212
215
  this.destroyed = true;
213
216
  }
214
217
  }
215
- // eslint-disable-next-line @typescript-eslint/ban-types
218
+ /* c8 ignore next 3 */
219
+ get isRunning() {
220
+ return this.running;
221
+ }
216
222
  addHook(hookName, value) {
217
- return this.hooks.addHook(hookName, value);
223
+ this.hooks.addHook(hookName, value);
224
+ return this;
218
225
  }
219
226
  }
220
227
  exports.SQSConsumer = SQSConsumer;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Represents a generic logger that could be a simple console, pino etc.
3
3
  */
4
+ /** biome-ignore-all lint/suspicious/noExplicitAny: leave message logger as any */
4
5
  export interface Logger {
5
6
  trace(message?: any, ...optionalParams: any[]): void;
6
7
  debug(message?: any, ...optionalParams: any[]): void;
@@ -0,0 +1,3 @@
1
+ export declare class TimeoutError extends Error {
2
+ constructor(message?: string);
3
+ }
@@ -0,0 +1,7 @@
1
+ export class TimeoutError extends Error {
2
+ constructor(message) {
3
+ /* c8 ignore next 1 */
4
+ super(message || "Timeout Error");
5
+ this.name = "TimeoutError";
6
+ }
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { Logger } from "./logger.mjs";
2
+ export type HookName = "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
3
+ export declare class Hooks {
4
+ private readonly hookSymbols;
5
+ private readonly hooks;
6
+ private readonly logger;
7
+ constructor(logger?: Logger);
8
+ addHook(hook: HookName, fn: Function): void;
9
+ runHook(hook: HookName, ...args: any[]): Promise<any>;
10
+ private runHookWithVoidReturn;
11
+ private runHookWithMessageReturn;
12
+ private runHookWithBooleanReturn;
13
+ }
@@ -0,0 +1,119 @@
1
+ export class Hooks {
2
+ constructor(logger = console) {
3
+ this.hookSymbols = {
4
+ onPoll: {
5
+ S: Symbol("poll"),
6
+ throwable: true,
7
+ type: "message-return",
8
+ },
9
+ onMessage: {
10
+ S: Symbol("message"),
11
+ throwable: true,
12
+ type: "message-return",
13
+ },
14
+ onHandlerSuccess: {
15
+ S: Symbol("handlerSuccess"),
16
+ throwable: false,
17
+ type: "message-return",
18
+ },
19
+ onHandlerTimeout: {
20
+ S: Symbol("handlerTimeout"),
21
+ throwable: false,
22
+ type: "boolean-return",
23
+ },
24
+ onHandlerError: {
25
+ S: Symbol("handlerError"),
26
+ throwable: false,
27
+ type: "boolean-return",
28
+ },
29
+ onSuccess: {
30
+ S: Symbol("success"),
31
+ throwable: false,
32
+ type: "boolean-return",
33
+ },
34
+ onError: {
35
+ S: Symbol("error"),
36
+ throwable: false,
37
+ type: "boolean-return",
38
+ },
39
+ onSQSError: {
40
+ S: Symbol("sqsError"),
41
+ throwable: false,
42
+ type: "void",
43
+ },
44
+ };
45
+ this.logger = logger;
46
+ this.hooks = {
47
+ [this.hookSymbols.onPoll.S]: [],
48
+ [this.hookSymbols.onMessage.S]: [],
49
+ [this.hookSymbols.onHandlerSuccess.S]: [],
50
+ [this.hookSymbols.onHandlerTimeout.S]: [],
51
+ [this.hookSymbols.onHandlerError.S]: [],
52
+ [this.hookSymbols.onSuccess.S]: [],
53
+ [this.hookSymbols.onError.S]: [],
54
+ [this.hookSymbols.onSQSError.S]: [],
55
+ };
56
+ }
57
+ addHook(hook, fn) {
58
+ const hookSymbol = this.hookSymbols[hook];
59
+ if (!hookSymbol)
60
+ throw new Error(`Invalid hook ${hook}`);
61
+ this.hooks[hookSymbol.S]?.push(fn);
62
+ }
63
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
64
+ async runHook(hook, ...args) {
65
+ const hookSymbol = this.hookSymbols[hook];
66
+ try {
67
+ if (!hookSymbol)
68
+ throw new Error(`Invalid hook ${hook}`);
69
+ switch (hookSymbol.type) {
70
+ case "boolean-return":
71
+ return await this.runHookWithBooleanReturn(hookSymbol, ...args);
72
+ case "message-return": {
73
+ const [message, ...someArgs] = args;
74
+ return await this.runHookWithMessageReturn(hookSymbol, message, ...someArgs);
75
+ }
76
+ case "void":
77
+ return await this.runHookWithVoidReturn(hookSymbol, ...args);
78
+ }
79
+ /* c8 ignore next 1 */
80
+ // biome-ignore lint/suspicious/noExplicitAny: error type is not important
81
+ }
82
+ catch (error) {
83
+ error.message = `Error running hook ${hook}: ${error.message}`;
84
+ if (hookSymbol.throwable !== false)
85
+ throw error;
86
+ this.logger.error(error);
87
+ }
88
+ }
89
+ async runHookWithVoidReturn(hookSymbol,
90
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
91
+ ...args) {
92
+ for (const fn of this.hooks[hookSymbol.S]) {
93
+ await fn(...args);
94
+ }
95
+ return;
96
+ }
97
+ async runHookWithMessageReturn(hookSymbol, message,
98
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
99
+ ...args) {
100
+ for (const fn of this.hooks[hookSymbol.S]) {
101
+ const returnMessage = await fn(message, ...args);
102
+ if (returnMessage) {
103
+ message = returnMessage;
104
+ }
105
+ }
106
+ return message;
107
+ }
108
+ async runHookWithBooleanReturn(hookSymbol,
109
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
110
+ ...args) {
111
+ for (const fn of this.hooks[hookSymbol.S]) {
112
+ const continueLoop = await fn(...args);
113
+ if (continueLoop === false) {
114
+ return false;
115
+ }
116
+ }
117
+ return true;
118
+ }
119
+ }
@@ -0,0 +1,81 @@
1
+ import type { Signer, SignerOptions } from "@fgiova/aws-signature";
2
+ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
3
+ import type { Pool } from "undici";
4
+ import { type HookName } from "./hooks.mjs";
5
+ import type { Logger } from "./logger.mjs";
6
+ type HandlerOptions = {
7
+ deleteMessage?: boolean;
8
+ extendVisibilityTimeout?: boolean;
9
+ executionTimeout?: number;
10
+ parallelExecution?: boolean;
11
+ };
12
+ type ClientOptions = {
13
+ sqsClient?: MiniSQSClient;
14
+ endpoint?: string;
15
+ undiciOptions?: Pool.Options;
16
+ signer?: Signer | SignerOptions;
17
+ destroySigner?: boolean;
18
+ };
19
+ type ConsumerOptions = {
20
+ visibilityTimeout?: number;
21
+ waitTimeSeconds?: number;
22
+ itemsPerRequest?: number;
23
+ messageAttributeNames?: string[];
24
+ attributeNames?: string[];
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;
29
+ 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>;
38
+ };
39
+ export type SQSConsumerOptions = {
40
+ queueARN: string;
41
+ handler: (message: Message) => Promise<any>;
42
+ logger?: Logger;
43
+ autostart?: boolean;
44
+ handlerOptions?: HandlerOptions;
45
+ clientOptions?: ClientOptions;
46
+ consumerOptions?: ConsumerOptions;
47
+ hooks?: HooksOptions;
48
+ };
49
+ export declare class SQSConsumer {
50
+ private readonly queueARN;
51
+ private readonly handlerOptions;
52
+ private readonly clientOptions;
53
+ private readonly consumerOptions;
54
+ private readonly sqsClient;
55
+ private readonly hooks;
56
+ private readonly messageHandler;
57
+ private messagesOnFly;
58
+ private running;
59
+ private polling;
60
+ private destroyed;
61
+ private readonly logger;
62
+ constructor(options: SQSConsumerOptions);
63
+ private runOnMessageHook;
64
+ private runHandler;
65
+ private haExtendVisibilityTimeout;
66
+ private deleteMessages;
67
+ private pollMessages;
68
+ start(): Promise<void>;
69
+ stop(destroy?: boolean): Promise<void>;
70
+ get isRunning(): boolean;
71
+ addHook(hookName: "onPoll", value: (messages: Message[]) => Promise<Message[]>): this;
72
+ addHook(hookName: "onMessage", value: (message: Message) => Promise<Message>): this;
73
+ addHook(hookName: "onHandlerSuccess", value: (message: Message) => Promise<Message>): this;
74
+ addHook(hookName: "onHandlerTimeout", value: (message: Message) => Promise<boolean>): this;
75
+ addHook(hookName: "onHandlerError", value: (message: Message, error: Error) => Promise<boolean>): this;
76
+ addHook(hookName: "onSuccess", value: (message: Message) => Promise<boolean>): this;
77
+ addHook(hookName: "onError", value: (hook: HookName, message: Message, error: Error) => Promise<boolean>): this;
78
+ addHook(hookName: "onSQSError", value: (error: Error, message?: Message) => Promise<void>): this;
79
+ addHook(hookName: HookName, value: Function): this;
80
+ }
81
+ export {};
@@ -0,0 +1,220 @@
1
+ /** biome-ignore-all lint/complexity/noBannedTypes: Function required as generic */
2
+ import { setTimeout as setTimeoutAsync } from "node:timers/promises";
3
+ import { MiniSQSClient } from "@fgiova/mini-sqs-client";
4
+ // @ts-expect-error
5
+ import { Unpromise } from "@watchable/unpromise";
6
+ // @ts-expect-error
7
+ import pMap from "p-map";
8
+ import { TimeoutError } from "./errors.mjs";
9
+ import { Hooks } from "./hooks.mjs";
10
+ export class SQSConsumer {
11
+ constructor(options) {
12
+ this.messagesOnFly = 0;
13
+ this.running = false;
14
+ this.polling = false;
15
+ this.destroyed = false;
16
+ if (!options.queueARN)
17
+ throw new Error("queueARN is required");
18
+ if (!options.handler || typeof options.handler !== "function")
19
+ throw new Error("handler is required and must be a function");
20
+ this.logger = options.logger ?? console;
21
+ this.hooks = new Hooks(this.logger);
22
+ this.queueARN = options.queueARN;
23
+ const region = this.queueARN.split(":").reverse()[2];
24
+ this.clientOptions = {
25
+ destroySigner: options.clientOptions?.destroySigner ?? true,
26
+ };
27
+ this.handlerOptions = {
28
+ deleteMessage: options.handlerOptions?.deleteMessage ?? true,
29
+ extendVisibilityTimeout: options.handlerOptions?.extendVisibilityTimeout ?? true,
30
+ executionTimeout: options.handlerOptions?.executionTimeout ?? 30000,
31
+ parallelExecution: options.handlerOptions?.parallelExecution ?? true,
32
+ };
33
+ this.consumerOptions = {
34
+ visibilityTimeout: options.consumerOptions?.visibilityTimeout ?? 30,
35
+ waitTimeSeconds: options.consumerOptions?.waitTimeSeconds ?? 20,
36
+ itemsPerRequest: options.consumerOptions?.itemsPerRequest ?? 10,
37
+ messageAttributeNames: options.consumerOptions?.messageAttributeNames ?? [],
38
+ attributeNames: options.consumerOptions?.attributeNames ?? [],
39
+ };
40
+ this.sqsClient =
41
+ options.clientOptions?.sqsClient ??
42
+ new MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
43
+ if (options.hooks) {
44
+ for (const [key, value] of Object.entries(options.hooks)) {
45
+ if (typeof value !== "function")
46
+ throw new Error(`${key} must be a function`);
47
+ this.addHook(key, value);
48
+ }
49
+ }
50
+ this.messageHandler = options.handler;
51
+ if (options.autostart !== false) {
52
+ void this.start();
53
+ }
54
+ }
55
+ async runOnMessageHook(message) {
56
+ try {
57
+ await this.hooks.runHook("onMessage", message);
58
+ const result = await this.runHandler(this.messageHandler, message);
59
+ await this.hooks.runHook("onSuccess", message);
60
+ return {
61
+ message,
62
+ result,
63
+ };
64
+ }
65
+ catch (e) {
66
+ await this.hooks.runHook("onError", "onMessage", message, e);
67
+ return {
68
+ message,
69
+ error: e,
70
+ };
71
+ }
72
+ }
73
+ async runHandler(handler, message) {
74
+ try {
75
+ let handlerResult;
76
+ if (this.handlerOptions.executionTimeout) {
77
+ const response = await Unpromise.race([
78
+ handler(message),
79
+ new Promise((_resolve, reject) => {
80
+ setTimeout(() => {
81
+ reject(new TimeoutError("Handler execution timed out"));
82
+ }, this.handlerOptions.executionTimeout);
83
+ }),
84
+ ]);
85
+ handlerResult = response;
86
+ }
87
+ else {
88
+ handlerResult = await handler(message);
89
+ }
90
+ await this.hooks.runHook("onHandlerSuccess", message);
91
+ return handlerResult;
92
+ }
93
+ catch (error) {
94
+ if (error instanceof TimeoutError) {
95
+ await this.hooks.runHook("onHandlerTimeout", message);
96
+ }
97
+ else {
98
+ await this.hooks.runHook("onHandlerError", message, error);
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ haExtendVisibilityTimeout(messages, visibilityTimeout) {
104
+ if (this.handlerOptions.extendVisibilityTimeout !== false) {
105
+ const processingMessages = messages.map(
106
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
107
+ (message) => message.ReceiptHandle);
108
+ return setInterval(async () => {
109
+ try {
110
+ await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, processingMessages, visibilityTimeout);
111
+ } /* c8 ignore next 3 */
112
+ catch (e) {
113
+ await this.hooks.runHook("onSQSError", e);
114
+ }
115
+ }, visibilityTimeout * 1000 - 5);
116
+ }
117
+ return null;
118
+ }
119
+ async deleteMessages(messagesResults) {
120
+ const candidatesToDelete = [];
121
+ const candidateToRelease = [];
122
+ for (const result of messagesResults) {
123
+ if (result.result && this.handlerOptions.deleteMessage !== false) {
124
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
125
+ candidatesToDelete.push(result.message.ReceiptHandle);
126
+ }
127
+ else if (result.error) {
128
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
129
+ candidateToRelease.push(result.message.ReceiptHandle);
130
+ }
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
+ }
137
+ 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;
176
+ }
177
+ }
178
+ if (haTimeout)
179
+ clearInterval(haTimeout);
180
+ }
181
+ else {
182
+ this.messagesOnFly -= totalMessages;
183
+ }
184
+ }
185
+ catch (e) {
186
+ await this.hooks.runHook("onSQSError", e);
187
+ }
188
+ this.polling = false;
189
+ await this.pollMessages();
190
+ }
191
+ // eslint-disable-next-line @typescript-eslint/require-await
192
+ async start() {
193
+ if (this.running)
194
+ throw new Error("Consumer is already running");
195
+ if (this.destroyed)
196
+ throw new Error("Consumer is destroyed");
197
+ this.running = true;
198
+ void this.pollMessages();
199
+ }
200
+ async stop(destroy = false) {
201
+ if (!this.running)
202
+ throw new Error("Consumer is not running");
203
+ this.running = false;
204
+ while (this.messagesOnFly > 0 || this.polling) {
205
+ await setTimeoutAsync(500);
206
+ }
207
+ if (destroy) {
208
+ await this.sqsClient.destroy(this.clientOptions.destroySigner);
209
+ this.destroyed = true;
210
+ }
211
+ }
212
+ /* c8 ignore next 3 */
213
+ get isRunning() {
214
+ return this.running;
215
+ }
216
+ addHook(hookName, value) {
217
+ this.hooks.addHook(hookName, value);
218
+ return this;
219
+ }
220
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Represents a generic logger that could be a simple console, pino etc.
3
+ */
4
+ /** biome-ignore-all lint/suspicious/noExplicitAny: leave message logger as any */
5
+ export interface Logger {
6
+ trace(message?: any, ...optionalParams: any[]): void;
7
+ debug(message?: any, ...optionalParams: any[]): void;
8
+ info(message?: any, ...optionalParams: any[]): void;
9
+ warn(message?: any, ...optionalParams: any[]): void;
10
+ error(message?: any, ...optionalParams: any[]): void;
11
+ [x: string]: any;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,14 +1,29 @@
1
1
  {
2
2
  "name": "@fgiova/sqs-consumer",
3
- "version": "2.0.1",
4
- "main": "dist/index.js",
3
+ "version": "2.1.1",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./dist/esm/index.d.mts",
10
+ "default": "./dist/esm/index.mjs"
11
+ },
12
+ "require": {
13
+ "types": "./dist/cjs/index.d.ts",
14
+ "default": "./dist/cjs/index.js"
15
+ }
16
+ }
17
+ },
5
18
  "files": [
6
19
  "dist"
7
20
  ],
8
21
  "scripts": {
9
- "prebuild": "rimraf dist",
10
- "build": "tsc",
11
- "lint": "eslint 'src/**/*.ts'",
22
+ "clean": "rimraf dist",
23
+ "preBuild": "npm run clean",
24
+ "build": "esmBuilder",
25
+ "lint": "biome ci ./src ./test",
26
+ "fix": "biome check --write ./src ./test",
12
27
  "test": "tap",
13
28
  "test:debug": "tap --only --timeout=0",
14
29
  "test:coverage": "tap --coverage-report=lcovonly --coverage-report=text",
@@ -41,26 +56,25 @@
41
56
  ]
42
57
  },
43
58
  "dependencies": {
44
- "@fgiova/mini-sqs-client": "^2.2.1",
59
+ "@fgiova/mini-sqs-client": "^3.2.0",
45
60
  "@watchable/unpromise": "^1.0.2",
46
- "p-map": "^4.0.0"
61
+ "p-map": "^7.0.3"
47
62
  },
48
63
  "peerDependencies": {
49
- "@fgiova/aws-signature": "^2.0.0"
64
+ "@fgiova/aws-signature": "^3.1.0"
50
65
  },
51
66
  "devDependencies": {
52
67
  "@aws-sdk/client-sqs": "^3.758.0",
68
+ "@biomejs/biome": "2.2.2",
69
+ "@fgiova/cjs-esm-ts-builder": "^1.2.0",
53
70
  "@semantic-release/changelog": "^6.0.3",
54
71
  "@semantic-release/git": "^10.0.1",
72
+ "@tsconfig/node20": "^20.1.4",
55
73
  "@types/node": "^20.17.23",
56
- "@typescript-eslint/eslint-plugin": "^6.21.0",
57
- "@typescript-eslint/parser": "^6.21.0",
58
74
  "conventional-changelog-conventionalcommits": "^8.0.0",
59
- "eslint": "^8.57.1",
60
- "eslint-plugin-import": "^2.31.0",
61
75
  "semantic-release": "^24.2.3",
62
76
  "tap": "^21.1.0",
63
- "testcontainers": "^10.18.0",
77
+ "testcontainers": "^11.5.1",
64
78
  "ts-node": "^10.9.2",
65
79
  "typescript": "^5.8.2"
66
80
  }
File without changes
File without changes
File without changes