@fgiova/fastify-sqs-consumer 3.0.1 → 3.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
@@ -74,10 +74,39 @@ Options are an array of objects with the following properties (one for each queu
74
74
  | parallelExecution | boolean | Enable parallel execution of message handlers. |
75
75
  | credentials | {accessKeyId, secretAccessKey} | Explicit AWS credentials (alternative to environment configuration). |
76
76
  | events | object | Events functions for the consumer (detail in next table). |
77
- | sqs | MiniSQSClient | Initialized [@fgiova/mini-sqs-client](https://www.npmjs.com/package/@fgiova/mini-sqs-client) instance (useful for testing sessions). |
77
+ | sqs | MiniSQSClient \| SQSOptions | Either an initialized [@fgiova/mini-sqs-client](https://www.npmjs.com/package/@fgiova/mini-sqs-client) instance (useful for testing sessions) or an options object used to build the client (see below). |
78
78
 
79
79
  \* required
80
80
 
81
+ ### sqs options object
82
+
83
+ Instead of passing a pre-built `MiniSQSClient`, you can pass an options object so the plugin builds the client for you:
84
+
85
+ | Option | Type | Description |
86
+ |---------------|---------------------------------|----------------------------------------------------------------------------------------------|
87
+ | endpoint | string | Custom SQS endpoint (e.g. a local LocalStack/ElasticMQ instance). |
88
+ | undiciOptions | Pool.Options | [undici](https://undici.nodejs.org/#/docs/api/Pool) `Pool` options for the underlying HTTP client. |
89
+ | credentials | {accessKeyId, secretAccessKey} | Explicit AWS credentials. The top-level `credentials` option takes precedence over this one. |
90
+
91
+ ```js
92
+ app.register(sqsConsumer, [
93
+ {
94
+ arn: "arn:aws:sqs:eu-central-1:000000000000:MyQueue",
95
+ sqs: {
96
+ endpoint: "http://localhost:4566",
97
+ undiciOptions: { connections: 1 },
98
+ credentials: {
99
+ accessKeyId: "AWS_ACCESS_KEY_ID",
100
+ secretAccessKey: "AWS_SECRET_ACCESS_KEY"
101
+ }
102
+ },
103
+ handlerFunction: async (message, fastify) => {
104
+ return true;
105
+ }
106
+ }
107
+ ]);
108
+ ```
109
+
81
110
  ## Decorator
82
111
 
83
112
  The plugin decorates the Fastify instance with `sqsConsumers`, a `Record<string, { consumer: SQSConsumer, meta: { pendingMessages: number } }>` that provides access to the registered consumers and their metadata.
@@ -1,14 +1,17 @@
1
- import type { Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
1
+ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
2
2
  import { type HooksOptions, SQSConsumer } from "@fgiova/sqs-consumer";
3
3
  import type { FastifyInstance } from "fastify";
4
+ import type { Pool } from "undici";
5
+ type ConsumerEntry = {
6
+ consumer: SQSConsumer;
7
+ meta: {
8
+ pendingMessages: number;
9
+ };
10
+ ownedClient?: MiniSQSClient;
11
+ };
4
12
  declare module "fastify" {
5
13
  interface FastifyInstance {
6
- sqsConsumers: Record<string, {
7
- consumer: SQSConsumer;
8
- meta: {
9
- pendingMessages: number;
10
- };
11
- }>;
14
+ sqsConsumers: Record<string, ConsumerEntry>;
12
15
  }
13
16
  }
14
17
  declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
@@ -26,7 +29,14 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
26
29
  attributeNames?: string[];
27
30
  events?: HooksOptions;
28
31
  parallelExecution?: boolean;
29
- sqs?: MiniSQSClient;
32
+ sqs?: MiniSQSClient | {
33
+ endpoint?: string;
34
+ undiciOptions?: Pool.Options;
35
+ credentials?: {
36
+ accessKeyId: string;
37
+ secretAccessKey: string;
38
+ };
39
+ };
30
40
  }[], done: (err?: Error) => void): void;
31
41
  export declare const fastifyPlugin: typeof sqsConsumerPlugin;
32
42
  export default fastifyPlugin;
package/dist/cjs/index.js CHANGED
@@ -5,12 +5,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.fastifyPlugin = void 0;
7
7
  const node_crypto_1 = require("node:crypto");
8
+ const promises_1 = require("node:timers/promises");
9
+ const mini_sqs_client_1 = require("@fgiova/mini-sqs-client");
8
10
  const sqs_consumer_1 = require("@fgiova/sqs-consumer");
9
11
  // @ts-expect-error
10
12
  const unpromise_1 = require("@watchable/unpromise");
11
13
  const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
12
14
  function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials) {
13
15
  const meta = { pendingMessages: 0 };
16
+ let ownedClient;
17
+ if (!(sqs instanceof mini_sqs_client_1.MiniSQSClient)) {
18
+ credentials = credentials ?? sqs?.credentials;
19
+ ownedClient = new mini_sqs_client_1.MiniSQSClient(queueArn.split(":")[3], sqs?.endpoint, sqs?.undiciOptions,
20
+ /* c8 ignore next 1 */
21
+ credentials ? { credentials } : undefined);
22
+ }
14
23
  return {
15
24
  consumer: new sqs_consumer_1.SQSConsumer({
16
25
  queueARN: queueArn,
@@ -28,9 +37,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
28
37
  },
29
38
  hooks,
30
39
  clientOptions: {
31
- sqsClient: sqs,
32
- /* c8 ignore next 1 */
33
- signer: credentials ? { credentials } : undefined,
40
+ sqsClient: ownedClient ?? sqs,
34
41
  },
35
42
  handler: async function handleMessageFunction(message) {
36
43
  let timeoutId;
@@ -39,7 +46,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
39
46
  return await unpromise_1.Unpromise.race([
40
47
  handlerFunction(message, fastify),
41
48
  new Promise((_, reject) => {
42
- timeoutId = setTimeout(() => reject(new Error("Handler execution timed out")), timeout + 1000);
49
+ timeoutId = setTimeout(() => reject(new Error("Handler execution timed out")), timeout + 1000).unref();
43
50
  }),
44
51
  ]);
45
52
  }
@@ -56,6 +63,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
56
63
  },
57
64
  }),
58
65
  meta,
66
+ ownedClient,
59
67
  };
60
68
  }
61
69
  function sqsConsumerPlugin(fastify, options, done) {
@@ -76,48 +84,32 @@ function sqsConsumerPlugin(fastify, options, done) {
76
84
  for (const consumerName of Object.keys(consumers)) {
77
85
  consumers[consumerName].consumer.start().catch(async (e) => {
78
86
  fastify.log.error(e);
79
- await consumers[consumerName].consumer
80
- .stop()
81
- .catch((stopErr) => fastify.log.error(stopErr));
87
+ const { consumer, ownedClient } = consumers[consumerName];
82
88
  delete consumers[consumerName];
89
+ await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
90
+ await ownedClient?.destroy().catch((err) => fastify.log.error(err));
83
91
  });
84
92
  }
85
93
  done();
86
94
  });
87
- fastify.addHook("onClose", (fastify, done) => {
88
- let allStopped = false;
95
+ fastify.addHook("onClose", async () => {
89
96
  const arrayConsumers = Object.values(consumers);
90
- const timeStart = Date.now();
91
- for (const consumer of arrayConsumers) {
92
- consumer.consumer
93
- .stop()
94
- .catch((e) => fastify.log.error(`[fastify-sqs-consumer] Error stopping consumer: ${e}`));
95
- }
96
- const interval = setInterval(() => {
97
- const isRunning = arrayConsumers.some((consumer) => consumer.consumer.isRunning);
98
- const pendingMessages = arrayConsumers.reduce((acc, consumer) => acc + consumer.meta.pendingMessages, 0);
99
- if (Date.now() - timeStart > maxExecutionTimeout + 2000) {
97
+ const deadline = Date.now() + maxExecutionTimeout + 2000;
98
+ let stopped = false;
99
+ Promise.allSettled(arrayConsumers.map(({ consumer }) => consumer
100
+ .stop()
101
+ .catch((e) => fastify.log.error(`[fastify-sqs-consumer] Error stopping consumer: ${e}`)))).then(() => {
102
+ stopped = true;
103
+ });
104
+ while (!stopped ||
105
+ arrayConsumers.some(({ meta }) => meta.pendingMessages > 0)) {
106
+ if (Date.now() > deadline) {
100
107
  fastify.log.warn("[fastify-sqs-consumer] Consumers are taking too long to stop... forcing shutdown");
101
- clearInterval(interval);
102
- return done();
103
- }
104
- /* c8 ignore next 5 */
105
- if (isRunning) {
106
- fastify.log.debug("[fastify-sqs-consumer] Some consumers still running... please wait");
107
- return null;
108
- }
109
- else if (!allStopped) {
110
- allStopped = true;
111
- fastify.log.debug("[fastify-sqs-consumer] All consumers are stopped");
108
+ break;
112
109
  }
113
- if (pendingMessages <= 0) {
114
- clearInterval(interval);
115
- return done();
116
- }
117
- else {
118
- fastify.log.debug(`[fastify-sqs-consumer] ${pendingMessages} pending messages`);
119
- }
120
- }, 500);
110
+ await (0, promises_1.setTimeout)(500);
111
+ }
112
+ await Promise.allSettled(arrayConsumers.map(({ ownedClient }) => ownedClient?.destroy()));
121
113
  });
122
114
  done();
123
115
  }
@@ -1,14 +1,17 @@
1
- import type { Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
1
+ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
2
2
  import { type HooksOptions, SQSConsumer } from "@fgiova/sqs-consumer";
3
3
  import type { FastifyInstance } from "fastify";
4
+ import type { Pool } from "undici";
5
+ type ConsumerEntry = {
6
+ consumer: SQSConsumer;
7
+ meta: {
8
+ pendingMessages: number;
9
+ };
10
+ ownedClient?: MiniSQSClient;
11
+ };
4
12
  declare module "fastify" {
5
13
  interface FastifyInstance {
6
- sqsConsumers: Record<string, {
7
- consumer: SQSConsumer;
8
- meta: {
9
- pendingMessages: number;
10
- };
11
- }>;
14
+ sqsConsumers: Record<string, ConsumerEntry>;
12
15
  }
13
16
  }
14
17
  declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
@@ -26,7 +29,14 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
26
29
  attributeNames?: string[];
27
30
  events?: HooksOptions;
28
31
  parallelExecution?: boolean;
29
- sqs?: MiniSQSClient;
32
+ sqs?: MiniSQSClient | {
33
+ endpoint?: string;
34
+ undiciOptions?: Pool.Options;
35
+ credentials?: {
36
+ accessKeyId: string;
37
+ secretAccessKey: string;
38
+ };
39
+ };
30
40
  }[], done: (err?: Error) => void): void;
31
41
  export declare const fastifyPlugin: typeof sqsConsumerPlugin;
32
42
  export default fastifyPlugin;
@@ -1,10 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import { MiniSQSClient } from "@fgiova/mini-sqs-client";
2
4
  import { SQSConsumer } from "@fgiova/sqs-consumer";
3
5
  // @ts-expect-error
4
6
  import { Unpromise } from "@watchable/unpromise";
5
7
  import fp from "fastify-plugin";
6
8
  function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials) {
7
9
  const meta = { pendingMessages: 0 };
10
+ let ownedClient;
11
+ if (!(sqs instanceof MiniSQSClient)) {
12
+ credentials = credentials ?? sqs?.credentials;
13
+ ownedClient = new MiniSQSClient(queueArn.split(":")[3], sqs?.endpoint, sqs?.undiciOptions,
14
+ /* c8 ignore next 1 */
15
+ credentials ? { credentials } : undefined);
16
+ }
8
17
  return {
9
18
  consumer: new SQSConsumer({
10
19
  queueARN: queueArn,
@@ -22,9 +31,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
22
31
  },
23
32
  hooks,
24
33
  clientOptions: {
25
- sqsClient: sqs,
26
- /* c8 ignore next 1 */
27
- signer: credentials ? { credentials } : undefined,
34
+ sqsClient: ownedClient ?? sqs,
28
35
  },
29
36
  handler: async function handleMessageFunction(message) {
30
37
  let timeoutId;
@@ -33,7 +40,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
33
40
  return await Unpromise.race([
34
41
  handlerFunction(message, fastify),
35
42
  new Promise((_, reject) => {
36
- timeoutId = setTimeout(() => reject(new Error("Handler execution timed out")), timeout + 1000);
43
+ timeoutId = setTimeout(() => reject(new Error("Handler execution timed out")), timeout + 1000).unref();
37
44
  }),
38
45
  ]);
39
46
  }
@@ -50,6 +57,7 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
50
57
  },
51
58
  }),
52
59
  meta,
60
+ ownedClient,
53
61
  };
54
62
  }
55
63
  function sqsConsumerPlugin(fastify, options, done) {
@@ -70,48 +78,32 @@ function sqsConsumerPlugin(fastify, options, done) {
70
78
  for (const consumerName of Object.keys(consumers)) {
71
79
  consumers[consumerName].consumer.start().catch(async (e) => {
72
80
  fastify.log.error(e);
73
- await consumers[consumerName].consumer
74
- .stop()
75
- .catch((stopErr) => fastify.log.error(stopErr));
81
+ const { consumer, ownedClient } = consumers[consumerName];
76
82
  delete consumers[consumerName];
83
+ await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
84
+ await ownedClient?.destroy().catch((err) => fastify.log.error(err));
77
85
  });
78
86
  }
79
87
  done();
80
88
  });
81
- fastify.addHook("onClose", (fastify, done) => {
82
- let allStopped = false;
89
+ fastify.addHook("onClose", async () => {
83
90
  const arrayConsumers = Object.values(consumers);
84
- const timeStart = Date.now();
85
- for (const consumer of arrayConsumers) {
86
- consumer.consumer
87
- .stop()
88
- .catch((e) => fastify.log.error(`[fastify-sqs-consumer] Error stopping consumer: ${e}`));
89
- }
90
- const interval = setInterval(() => {
91
- const isRunning = arrayConsumers.some((consumer) => consumer.consumer.isRunning);
92
- const pendingMessages = arrayConsumers.reduce((acc, consumer) => acc + consumer.meta.pendingMessages, 0);
93
- if (Date.now() - timeStart > maxExecutionTimeout + 2000) {
91
+ const deadline = Date.now() + maxExecutionTimeout + 2000;
92
+ let stopped = false;
93
+ Promise.allSettled(arrayConsumers.map(({ consumer }) => consumer
94
+ .stop()
95
+ .catch((e) => fastify.log.error(`[fastify-sqs-consumer] Error stopping consumer: ${e}`)))).then(() => {
96
+ stopped = true;
97
+ });
98
+ while (!stopped ||
99
+ arrayConsumers.some(({ meta }) => meta.pendingMessages > 0)) {
100
+ if (Date.now() > deadline) {
94
101
  fastify.log.warn("[fastify-sqs-consumer] Consumers are taking too long to stop... forcing shutdown");
95
- clearInterval(interval);
96
- return done();
97
- }
98
- /* c8 ignore next 5 */
99
- if (isRunning) {
100
- fastify.log.debug("[fastify-sqs-consumer] Some consumers still running... please wait");
101
- return null;
102
- }
103
- else if (!allStopped) {
104
- allStopped = true;
105
- fastify.log.debug("[fastify-sqs-consumer] All consumers are stopped");
102
+ break;
106
103
  }
107
- if (pendingMessages <= 0) {
108
- clearInterval(interval);
109
- return done();
110
- }
111
- else {
112
- fastify.log.debug(`[fastify-sqs-consumer] ${pendingMessages} pending messages`);
113
- }
114
- }, 500);
104
+ await sleep(500);
105
+ }
106
+ await Promise.allSettled(arrayConsumers.map(({ ownedClient }) => ownedClient?.destroy()));
115
107
  });
116
108
  done();
117
109
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fgiova/fastify-sqs-consumer",
3
3
  "description": "sqs-consumer fastify plugin",
4
- "version": "3.0.1",
4
+ "version": "3.1.1",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "types": "./dist/cjs/index.d.ts",
7
7
  "exports": {
@@ -29,7 +29,7 @@
29
29
  "test:debug": "tap --only --timeout=0",
30
30
  "test:local": "TEST_LOCAL=true tap",
31
31
  "test:local:debug": "TEST_LOCAL=true tap --only --timeout=0",
32
- "test:coverage": "tap --coverage-report=lcovonly --coverage-report=text"
32
+ "test:coverage": "tap --timeout=90 --coverage-report=lcovonly --coverage-report=text"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",