@fgiova/sqs-consumer 2.0.1 → 2.1.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
@@ -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 />
@@ -88,4 +88,4 @@ SQSConsumer.addHook(hookname: string, func: Function): void
88
88
  ```
89
89
 
90
90
  ## License
91
- Licensed under [MIT](./LICENSE).
91
+ 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,19 +89,25 @@ 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
104
  message = await fn(message, ...args);
102
105
  }
103
106
  return message;
104
107
  }
105
- async runHookWithBooleanReturn(hookSymbol, ...args) {
108
+ async runHookWithBooleanReturn(hookSymbol,
109
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
110
+ ...args) {
106
111
  for (const fn of this.hooks[hookSymbol.S]) {
107
112
  const continueLoop = await fn(...args);
108
113
  if (!continueLoop) {
@@ -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;
@@ -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,24 @@ 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 ?? [],
47
44
  };
48
- this.sqsClient = options.clientOptions?.sqsClient ?? new mini_sqs_client_1.MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
45
+ this.sqsClient =
46
+ options.clientOptions?.sqsClient ??
47
+ new mini_sqs_client_1.MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
49
48
  if (options.hooks) {
50
49
  for (const [key, value] of Object.entries(options.hooks)) {
51
50
  if (typeof value !== "function")
@@ -65,14 +64,14 @@ class SQSConsumer {
65
64
  await this.hooks.runHook("onSuccess", message);
66
65
  return {
67
66
  message,
68
- result
67
+ result,
69
68
  };
70
69
  }
71
70
  catch (e) {
72
71
  await this.hooks.runHook("onError", "onMessage", message, e);
73
72
  return {
74
73
  message,
75
- error: e
74
+ error: e,
76
75
  };
77
76
  }
78
77
  }
@@ -82,20 +81,18 @@ class SQSConsumer {
82
81
  if (this.handlerOptions.executionTimeout) {
83
82
  const response = await unpromise_1.Unpromise.race([
84
83
  handler(message),
85
- new Promise((resolve, reject) => {
84
+ new Promise((_resolve, reject) => {
86
85
  setTimeout(() => {
87
86
  reject(new errors_1.TimeoutError("Handler execution timed out"));
88
87
  }, this.handlerOptions.executionTimeout);
89
- })
88
+ }),
90
89
  ]);
91
90
  handlerResult = response;
92
91
  }
93
92
  else {
94
93
  handlerResult = await handler(message);
95
94
  }
96
- await Promise.all([
97
- this.hooks.runHook("onHandlerSuccess", message),
98
- ]);
95
+ await Promise.all([this.hooks.runHook("onHandlerSuccess", message)]);
99
96
  return handlerResult;
100
97
  }
101
98
  catch (error) {
@@ -110,16 +107,17 @@ class SQSConsumer {
110
107
  }
111
108
  haExtendVisibilityTimeout(messages, visibilityTimeout) {
112
109
  if (this.handlerOptions.extendVisibilityTimeout !== false) {
113
- const processingMessages = messages.map(message => message.ReceiptHandle);
110
+ const processingMessages = messages.map(
111
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
112
+ (message) => message.ReceiptHandle);
114
113
  return setInterval(async () => {
115
114
  try {
116
115
  await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, processingMessages, visibilityTimeout);
117
- }
118
- /* c8 ignore next 3 */
116
+ } /* c8 ignore next 3 */
119
117
  catch (e) {
120
118
  await this.hooks.runHook("onSQSError", e);
121
119
  }
122
- }, (visibilityTimeout * 1000 - 5));
120
+ }, visibilityTimeout * 1000 - 5);
123
121
  }
124
122
  return null;
125
123
  }
@@ -128,9 +126,11 @@ class SQSConsumer {
128
126
  const candidateToRelease = [];
129
127
  for (const result of messagesResults) {
130
128
  if (result.result && this.handlerOptions.deleteMessage !== false) {
129
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
131
130
  candidatesToDelete.push(result.message.ReceiptHandle);
132
131
  }
133
132
  else if (result.error) {
133
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
134
134
  candidateToRelease.push(result.message.ReceiptHandle);
135
135
  }
136
136
  if (candidatesToDelete.length)
@@ -144,7 +144,8 @@ class SQSConsumer {
144
144
  return;
145
145
  try {
146
146
  this.polling = true;
147
- const visibilityTimeout = this.consumerOptions.visibilityTimeout;
147
+ /* c8 ignore next 1 */
148
+ const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
148
149
  const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
149
150
  WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
150
151
  MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
@@ -158,7 +159,7 @@ class SQSConsumer {
158
159
  await this.hooks.runHook("onPoll", messages);
159
160
  if (messages.length) {
160
161
  const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
161
- this.messagesOnFly += (messages.length - totalMessages);
162
+ this.messagesOnFly += messages.length - totalMessages;
162
163
  if (this.handlerOptions.parallelExecution !== false) {
163
164
  const results = await (0, p_map_1.default)(messages, async (message) => {
164
165
  return await this.runOnMessageHook(message);
@@ -212,7 +213,6 @@ class SQSConsumer {
212
213
  this.destroyed = true;
213
214
  }
214
215
  }
215
- // eslint-disable-next-line @typescript-eslint/ban-types
216
216
  addHook(hookName, value) {
217
217
  return this.hooks.addHook(hookName, value);
218
218
  }
@@ -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,116 @@
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
+ message = await fn(message, ...args);
102
+ }
103
+ return message;
104
+ }
105
+ async runHookWithBooleanReturn(hookSymbol,
106
+ // biome-ignore lint/suspicious/noExplicitAny: args can be anything
107
+ ...args) {
108
+ for (const fn of this.hooks[hookSymbol.S]) {
109
+ const continueLoop = await fn(...args);
110
+ if (!continueLoop) {
111
+ return false;
112
+ }
113
+ }
114
+ return true;
115
+ }
116
+ }
@@ -0,0 +1,76 @@
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
+ };
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>;
33
+ onSQSError?: (error: Error, message?: Message) => Promise<void>;
34
+ };
35
+ export type SQSConsumerOptions = {
36
+ queueARN: string;
37
+ handler: (message: Message) => Promise<any>;
38
+ logger?: Logger;
39
+ autostart?: boolean;
40
+ handlerOptions?: HandlerOptions;
41
+ clientOptions?: ClientOptions;
42
+ consumerOptions?: ConsumerOptions;
43
+ hooks?: HooksOptions;
44
+ };
45
+ export declare class SQSConsumer {
46
+ private readonly queueARN;
47
+ private readonly handlerOptions;
48
+ private readonly clientOptions;
49
+ private readonly consumerOptions;
50
+ private readonly sqsClient;
51
+ private readonly hooks;
52
+ private readonly messageHandler;
53
+ private messagesOnFly;
54
+ private running;
55
+ private polling;
56
+ private destroyed;
57
+ private readonly logger;
58
+ constructor(options: SQSConsumerOptions);
59
+ private runOnMessageHook;
60
+ private runHandler;
61
+ private haExtendVisibilityTimeout;
62
+ private deleteMessages;
63
+ private pollMessages;
64
+ start(): Promise<void>;
65
+ 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;
75
+ }
76
+ export {};
@@ -0,0 +1,213 @@
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
+ };
39
+ this.sqsClient =
40
+ options.clientOptions?.sqsClient ??
41
+ new MiniSQSClient(region, options.clientOptions?.endpoint, options.clientOptions?.undiciOptions, options.clientOptions?.signer);
42
+ if (options.hooks) {
43
+ for (const [key, value] of Object.entries(options.hooks)) {
44
+ if (typeof value !== "function")
45
+ throw new Error(`${key} must be a function`);
46
+ this.addHook(key, value);
47
+ }
48
+ }
49
+ this.messageHandler = options.handler;
50
+ if (options.autostart !== false) {
51
+ void this.start();
52
+ }
53
+ }
54
+ async runOnMessageHook(message) {
55
+ try {
56
+ await this.hooks.runHook("onMessage", message);
57
+ const result = await this.runHandler(this.messageHandler, message);
58
+ await this.hooks.runHook("onSuccess", message);
59
+ return {
60
+ message,
61
+ result,
62
+ };
63
+ }
64
+ catch (e) {
65
+ await this.hooks.runHook("onError", "onMessage", message, e);
66
+ return {
67
+ message,
68
+ error: e,
69
+ };
70
+ }
71
+ }
72
+ async runHandler(handler, message) {
73
+ try {
74
+ let handlerResult;
75
+ if (this.handlerOptions.executionTimeout) {
76
+ const response = await Unpromise.race([
77
+ handler(message),
78
+ new Promise((_resolve, reject) => {
79
+ setTimeout(() => {
80
+ reject(new TimeoutError("Handler execution timed out"));
81
+ }, this.handlerOptions.executionTimeout);
82
+ }),
83
+ ]);
84
+ handlerResult = response;
85
+ }
86
+ else {
87
+ handlerResult = await handler(message);
88
+ }
89
+ await Promise.all([this.hooks.runHook("onHandlerSuccess", message)]);
90
+ return handlerResult;
91
+ }
92
+ catch (error) {
93
+ if (error instanceof TimeoutError) {
94
+ await this.hooks.runHook("onHandlerTimeout", message);
95
+ }
96
+ else {
97
+ await this.hooks.runHook("onHandlerError", message, error);
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ haExtendVisibilityTimeout(messages, visibilityTimeout) {
103
+ if (this.handlerOptions.extendVisibilityTimeout !== false) {
104
+ const processingMessages = messages.map(
105
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
106
+ (message) => message.ReceiptHandle);
107
+ return setInterval(async () => {
108
+ try {
109
+ await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, processingMessages, visibilityTimeout);
110
+ } /* c8 ignore next 3 */
111
+ catch (e) {
112
+ await this.hooks.runHook("onSQSError", e);
113
+ }
114
+ }, visibilityTimeout * 1000 - 5);
115
+ }
116
+ return null;
117
+ }
118
+ async deleteMessages(messagesResults) {
119
+ const candidatesToDelete = [];
120
+ const candidateToRelease = [];
121
+ for (const result of messagesResults) {
122
+ if (result.result && this.handlerOptions.deleteMessage !== false) {
123
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
124
+ candidatesToDelete.push(result.message.ReceiptHandle);
125
+ }
126
+ else if (result.error) {
127
+ // biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
128
+ candidateToRelease.push(result.message.ReceiptHandle);
129
+ }
130
+ if (candidatesToDelete.length)
131
+ await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
132
+ if (candidateToRelease.length)
133
+ await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease, 0);
134
+ }
135
+ }
136
+ async pollMessages() {
137
+ if (!this.running)
138
+ return;
139
+ try {
140
+ this.polling = true;
141
+ /* c8 ignore next 1 */
142
+ const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
143
+ const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
144
+ WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
145
+ MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
146
+ VisibilityTimeout: visibilityTimeout,
147
+ MessageAttributeNames: this.consumerOptions.messageAttributeNames,
148
+ });
149
+ /* c8 ignore next 1 */
150
+ const messages = messagesResult.Messages ?? [];
151
+ const totalMessages = messages.length;
152
+ this.messagesOnFly += totalMessages;
153
+ await this.hooks.runHook("onPoll", messages);
154
+ if (messages.length) {
155
+ const haTimeout = this.haExtendVisibilityTimeout(messages, visibilityTimeout);
156
+ this.messagesOnFly += messages.length - totalMessages;
157
+ if (this.handlerOptions.parallelExecution !== false) {
158
+ const results = await pMap(messages, async (message) => {
159
+ return await this.runOnMessageHook(message);
160
+ }, {
161
+ concurrency: 10,
162
+ stopOnError: false,
163
+ });
164
+ await this.deleteMessages(results);
165
+ this.messagesOnFly -= messages.length;
166
+ }
167
+ else {
168
+ const handlingMessages = [...messages];
169
+ for (let i = 0; i < handlingMessages.length; i++) {
170
+ const result = await this.runOnMessageHook(handlingMessages[i]);
171
+ await this.deleteMessages([result]);
172
+ messages.splice(i, 1);
173
+ this.messagesOnFly -= 1;
174
+ }
175
+ }
176
+ if (haTimeout)
177
+ clearInterval(haTimeout);
178
+ }
179
+ else {
180
+ this.messagesOnFly -= totalMessages;
181
+ }
182
+ }
183
+ catch (e) {
184
+ await this.hooks.runHook("onSQSError", e);
185
+ }
186
+ this.polling = false;
187
+ await this.pollMessages();
188
+ }
189
+ // eslint-disable-next-line @typescript-eslint/require-await
190
+ async start() {
191
+ if (this.running)
192
+ throw new Error("Consumer is already running");
193
+ if (this.destroyed)
194
+ throw new Error("Consumer is destroyed");
195
+ this.running = true;
196
+ void this.pollMessages();
197
+ }
198
+ async stop(destroy = false) {
199
+ if (!this.running)
200
+ throw new Error("Consumer is not running");
201
+ this.running = false;
202
+ while (this.messagesOnFly > 0 || this.polling) {
203
+ await setTimeoutAsync(500);
204
+ }
205
+ if (destroy) {
206
+ await this.sqsClient.destroy(this.clientOptions.destroySigner);
207
+ this.destroyed = true;
208
+ }
209
+ }
210
+ addHook(hookName, value) {
211
+ return this.hooks.addHook(hookName, value);
212
+ }
213
+ }
@@ -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.0",
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