@fgiova/fastify-sqs-consumer 3.1.1 → 3.3.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
@@ -73,6 +73,7 @@ Options are an array of objects with the following properties (one for each queu
73
73
  | messageAttributeNames | string[] | Array of custom message attributes to retrieve for each message. |
74
74
  | parallelExecution | boolean | Enable parallel execution of message handlers. |
75
75
  | credentials | {accessKeyId, secretAccessKey} | Explicit AWS credentials (alternative to environment configuration). |
76
+ | backoff | number \| (attempt: number) => number | Delay (in seconds, 0-43200) before a failed message becomes visible again. A number sets a fixed delay; a function receives the attempt number (`ApproximateReceiveCount`, requested automatically) and returns the delay. Default: 0 (immediate retry). |
76
77
  | events | object | Events functions for the consumer (detail in next table). |
77
78
  | 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
79
 
@@ -107,9 +108,38 @@ app.register(sqsConsumer, [
107
108
  ]);
108
109
  ```
109
110
 
110
- ## Decorator
111
+ ## Decorators
111
112
 
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.
113
+ ### sqsConsumers
114
+
115
+ `sqsConsumers` is a `Record<string, { consumer: SQSConsumer, meta: { pendingMessages: number }, ownedClient?: MiniSQSClient }>` that provides access to the registered consumers and their metadata, keyed by consumer name.
116
+ It is a null-prototype object (`Object.create(null)`): use `Object.hasOwn(app.sqsConsumers, name)` or `name in app.sqsConsumers` instead of `app.sqsConsumers.hasOwnProperty(name)`.
117
+
118
+ The consumer lifecycle (start/stop) is managed by the plugin: do not call `start()` or `stop()` on these instances directly.
119
+
120
+ ### addSQSConsumer
121
+
122
+ `addSQSConsumer(options: ConsumerOptions): string | null` adds a consumer at runtime. `options` accepts the same object used for each entry of the plugin options array (see [Options](#options)); the `ConsumerOptions` type is exported by the package.
123
+
124
+ - Returns the consumer name (the generated uuid when `name` is not provided), or `null` if the consumer was not created.
125
+ - Before the server is ready, the consumer is started together with the others in the `onReady` hook; after that, it is started immediately.
126
+ - Returns `null` (and logs a warning) when a consumer with the same name already exists or when the server is closing.
127
+
128
+ ```js
129
+ import fastify from "fastify";
130
+ import sqsConsumer from "@fgiova/fastify-sqs-consumer";
131
+
132
+ const app = fastify();
133
+ await app.register(sqsConsumer, []);
134
+ await app.listen({ port: 3000 });
135
+
136
+ const name = app.addSQSConsumer({
137
+ arn: "arn:aws:sqs:eu-central-1:000000000000:MyOtherQueue",
138
+ handlerFunction: async (message, fastify) => {
139
+ return true;
140
+ }
141
+ });
142
+ ```
113
143
 
114
144
  ## handlerFunction
115
145
 
@@ -9,12 +9,7 @@ type ConsumerEntry = {
9
9
  };
10
10
  ownedClient?: MiniSQSClient;
11
11
  };
12
- declare module "fastify" {
13
- interface FastifyInstance {
14
- sqsConsumers: Record<string, ConsumerEntry>;
15
- }
16
- }
17
- declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
12
+ export type ConsumerOptions = {
18
13
  arn: string;
19
14
  credentials?: {
20
15
  accessKeyId: string;
@@ -29,6 +24,7 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
29
24
  attributeNames?: string[];
30
25
  events?: HooksOptions;
31
26
  parallelExecution?: boolean;
27
+ backoff?: ((attempt: number) => number) | number;
32
28
  sqs?: MiniSQSClient | {
33
29
  endpoint?: string;
34
30
  undiciOptions?: Pool.Options;
@@ -37,6 +33,13 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
37
33
  secretAccessKey: string;
38
34
  };
39
35
  };
40
- }[], done: (err?: Error) => void): void;
36
+ };
37
+ declare module "fastify" {
38
+ interface FastifyInstance {
39
+ sqsConsumers: Record<string, ConsumerEntry>;
40
+ addSQSConsumer: (consumerOptions: ConsumerOptions) => string | null;
41
+ }
42
+ }
43
+ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: ConsumerOptions[], done: (err?: Error) => void): void;
41
44
  export declare const fastifyPlugin: typeof sqsConsumerPlugin;
42
45
  export default fastifyPlugin;
package/dist/cjs/index.js CHANGED
@@ -11,7 +11,7 @@ const sqs_consumer_1 = require("@fgiova/sqs-consumer");
11
11
  // @ts-expect-error
12
12
  const unpromise_1 = require("@watchable/unpromise");
13
13
  const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
14
- function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials) {
14
+ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials, backoff) {
15
15
  const meta = { pendingMessages: 0 };
16
16
  let ownedClient;
17
17
  if (!(sqs instanceof mini_sqs_client_1.MiniSQSClient)) {
@@ -30,6 +30,8 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
30
30
  attributeNames,
31
31
  messageAttributeNames,
32
32
  visibilityTimeout: timeout / 1000,
33
+ errorDelaySeconds: typeof backoff === "number" ? backoff : undefined,
34
+ errorDelayBackoff: typeof backoff === "function" ? backoff : undefined,
33
35
  },
34
36
  handlerOptions: {
35
37
  executionTimeout: timeout,
@@ -67,31 +69,60 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
67
69
  };
68
70
  }
69
71
  function sqsConsumerPlugin(fastify, options, done) {
70
- const consumers = {};
72
+ const consumers = Object.create(null);
71
73
  let maxExecutionTimeout = 90000;
74
+ let isClosing = false;
75
+ let isReady = false;
72
76
  fastify.decorate("sqsConsumers", consumers);
73
- for (const handler of options) {
74
- maxExecutionTimeout = Math.max(maxExecutionTimeout, handler.timeout ?? 0);
75
- const { name, arn: queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, events, sqs, parallelExecution, credentials, } = handler;
77
+ const startConsumer = (consumerName) => {
78
+ const { consumer, ownedClient } = consumers[consumerName];
79
+ const consumerStart = consumer.start();
80
+ consumerStart.catch(async (e) => {
81
+ fastify.log.error(e);
82
+ delete consumers[consumerName];
83
+ await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
84
+ await ownedClient?.destroy().catch((err) => fastify.log.error(err));
85
+ });
86
+ return consumerStart;
87
+ };
88
+ const addSQSConsumer = (consumerOptions) => {
89
+ const { name, arn: queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, events, sqs, parallelExecution, credentials, backoff, } = consumerOptions;
90
+ const consumerName = name || (0, node_crypto_1.randomUUID)();
91
+ if (consumers[consumerName]) {
92
+ fastify.log.warn(`[fastify-sqs-consumer] Consumer with name ${consumerName} already exists. Skipping creation.`);
93
+ return null;
94
+ }
95
+ if (isClosing) {
96
+ fastify.log.warn(`[fastify-sqs-consumer] Cannot add consumer ${consumerName} while closing. Skipping creation.`);
97
+ return null;
98
+ }
99
+ maxExecutionTimeout = Math.max(maxExecutionTimeout, consumerOptions.timeout ?? 0);
76
100
  try {
77
- consumers[name || (0, node_crypto_1.randomUUID)()] = createConsumer(fastify, queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, parallelExecution, events, sqs, credentials);
78
- } /* c8 ignore next 3 */
101
+ consumers[consumerName] = createConsumer(fastify, queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, parallelExecution, events, sqs, credentials, backoff);
102
+ if (isReady) {
103
+ void startConsumer(consumerName);
104
+ }
105
+ return consumerName;
106
+ } /* c8 ignore next 4 */
79
107
  catch (e) {
80
108
  fastify.log.error(e);
109
+ return null;
81
110
  }
111
+ };
112
+ fastify.decorate("addSQSConsumer", addSQSConsumer);
113
+ for (const handler of options) {
114
+ addSQSConsumer(handler);
82
115
  }
83
116
  fastify.addHook("onReady", (done) => {
117
+ isReady = true;
84
118
  for (const consumerName of Object.keys(consumers)) {
85
- consumers[consumerName].consumer.start().catch(async (e) => {
86
- fastify.log.error(e);
87
- const { consumer, ownedClient } = consumers[consumerName];
88
- delete consumers[consumerName];
89
- await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
90
- await ownedClient?.destroy().catch((err) => fastify.log.error(err));
91
- });
119
+ void startConsumer(consumerName);
92
120
  }
93
121
  done();
94
122
  });
123
+ fastify.addHook("preClose", async () => {
124
+ isClosing = true;
125
+ });
95
126
  fastify.addHook("onClose", async () => {
96
127
  const arrayConsumers = Object.values(consumers);
97
128
  const deadline = Date.now() + maxExecutionTimeout + 2000;
@@ -9,12 +9,7 @@ type ConsumerEntry = {
9
9
  };
10
10
  ownedClient?: MiniSQSClient;
11
11
  };
12
- declare module "fastify" {
13
- interface FastifyInstance {
14
- sqsConsumers: Record<string, ConsumerEntry>;
15
- }
16
- }
17
- declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
12
+ export type ConsumerOptions = {
18
13
  arn: string;
19
14
  credentials?: {
20
15
  accessKeyId: string;
@@ -29,6 +24,7 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
29
24
  attributeNames?: string[];
30
25
  events?: HooksOptions;
31
26
  parallelExecution?: boolean;
27
+ backoff?: ((attempt: number) => number) | number;
32
28
  sqs?: MiniSQSClient | {
33
29
  endpoint?: string;
34
30
  undiciOptions?: Pool.Options;
@@ -37,6 +33,13 @@ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: {
37
33
  secretAccessKey: string;
38
34
  };
39
35
  };
40
- }[], done: (err?: Error) => void): void;
36
+ };
37
+ declare module "fastify" {
38
+ interface FastifyInstance {
39
+ sqsConsumers: Record<string, ConsumerEntry>;
40
+ addSQSConsumer: (consumerOptions: ConsumerOptions) => string | null;
41
+ }
42
+ }
43
+ declare function sqsConsumerPlugin(fastify: FastifyInstance, options: ConsumerOptions[], done: (err?: Error) => void): void;
41
44
  export declare const fastifyPlugin: typeof sqsConsumerPlugin;
42
45
  export default fastifyPlugin;
@@ -5,7 +5,7 @@ import { SQSConsumer } from "@fgiova/sqs-consumer";
5
5
  // @ts-expect-error
6
6
  import { Unpromise } from "@watchable/unpromise";
7
7
  import fp from "fastify-plugin";
8
- function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials) {
8
+ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, waitTimeSeconds = 20, batchSize = 1, attributeNames = [], messageAttributeNames = [], parallelExecution, hooks, sqs, credentials, backoff) {
9
9
  const meta = { pendingMessages: 0 };
10
10
  let ownedClient;
11
11
  if (!(sqs instanceof MiniSQSClient)) {
@@ -24,6 +24,8 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
24
24
  attributeNames,
25
25
  messageAttributeNames,
26
26
  visibilityTimeout: timeout / 1000,
27
+ errorDelaySeconds: typeof backoff === "number" ? backoff : undefined,
28
+ errorDelayBackoff: typeof backoff === "function" ? backoff : undefined,
27
29
  },
28
30
  handlerOptions: {
29
31
  executionTimeout: timeout,
@@ -61,31 +63,60 @@ function createConsumer(fastify, queueArn, handlerFunction, timeout = 90000, wai
61
63
  };
62
64
  }
63
65
  function sqsConsumerPlugin(fastify, options, done) {
64
- const consumers = {};
66
+ const consumers = Object.create(null);
65
67
  let maxExecutionTimeout = 90000;
68
+ let isClosing = false;
69
+ let isReady = false;
66
70
  fastify.decorate("sqsConsumers", consumers);
67
- for (const handler of options) {
68
- maxExecutionTimeout = Math.max(maxExecutionTimeout, handler.timeout ?? 0);
69
- const { name, arn: queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, events, sqs, parallelExecution, credentials, } = handler;
71
+ const startConsumer = (consumerName) => {
72
+ const { consumer, ownedClient } = consumers[consumerName];
73
+ const consumerStart = consumer.start();
74
+ consumerStart.catch(async (e) => {
75
+ fastify.log.error(e);
76
+ delete consumers[consumerName];
77
+ await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
78
+ await ownedClient?.destroy().catch((err) => fastify.log.error(err));
79
+ });
80
+ return consumerStart;
81
+ };
82
+ const addSQSConsumer = (consumerOptions) => {
83
+ const { name, arn: queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, events, sqs, parallelExecution, credentials, backoff, } = consumerOptions;
84
+ const consumerName = name || randomUUID();
85
+ if (consumers[consumerName]) {
86
+ fastify.log.warn(`[fastify-sqs-consumer] Consumer with name ${consumerName} already exists. Skipping creation.`);
87
+ return null;
88
+ }
89
+ if (isClosing) {
90
+ fastify.log.warn(`[fastify-sqs-consumer] Cannot add consumer ${consumerName} while closing. Skipping creation.`);
91
+ return null;
92
+ }
93
+ maxExecutionTimeout = Math.max(maxExecutionTimeout, consumerOptions.timeout ?? 0);
70
94
  try {
71
- consumers[name || randomUUID()] = createConsumer(fastify, queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, parallelExecution, events, sqs, credentials);
72
- } /* c8 ignore next 3 */
95
+ consumers[consumerName] = createConsumer(fastify, queueArn, handlerFunction, timeout, waitTimeSeconds, batchSize, attributeNames, messageAttributeNames, parallelExecution, events, sqs, credentials, backoff);
96
+ if (isReady) {
97
+ void startConsumer(consumerName);
98
+ }
99
+ return consumerName;
100
+ } /* c8 ignore next 4 */
73
101
  catch (e) {
74
102
  fastify.log.error(e);
103
+ return null;
75
104
  }
105
+ };
106
+ fastify.decorate("addSQSConsumer", addSQSConsumer);
107
+ for (const handler of options) {
108
+ addSQSConsumer(handler);
76
109
  }
77
110
  fastify.addHook("onReady", (done) => {
111
+ isReady = true;
78
112
  for (const consumerName of Object.keys(consumers)) {
79
- consumers[consumerName].consumer.start().catch(async (e) => {
80
- fastify.log.error(e);
81
- const { consumer, ownedClient } = consumers[consumerName];
82
- delete consumers[consumerName];
83
- await consumer.stop().catch((stopErr) => fastify.log.error(stopErr));
84
- await ownedClient?.destroy().catch((err) => fastify.log.error(err));
85
- });
113
+ void startConsumer(consumerName);
86
114
  }
87
115
  done();
88
116
  });
117
+ fastify.addHook("preClose", async () => {
118
+ isClosing = true;
119
+ });
89
120
  fastify.addHook("onClose", async () => {
90
121
  const arrayConsumers = Object.values(consumers);
91
122
  const deadline = Date.now() + maxExecutionTimeout + 2000;
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.1.1",
4
+ "version": "3.3.0",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "types": "./dist/cjs/index.d.ts",
7
7
  "exports": {
@@ -77,7 +77,7 @@
77
77
  "clock-mock": "^2.0.4",
78
78
  "conventional-changelog-conventionalcommits": "^9.1.0",
79
79
  "dotenv": "^10.0.0",
80
- "fastify": "^5.8.5",
80
+ "fastify": "^5.12.5",
81
81
  "semantic-release": "^25.0.3",
82
82
  "tap": "^21.1.0",
83
83
  "testcontainers": "^11.5.1",
@@ -85,7 +85,7 @@
85
85
  "typescript": "^5.1.6"
86
86
  },
87
87
  "dependencies": {
88
- "@fgiova/sqs-consumer": "^3.2.0",
88
+ "@fgiova/sqs-consumer": "^3.3.0",
89
89
  "@watchable/unpromise": "^1.0.2",
90
90
  "fastify-plugin": "^5.1.0"
91
91
  }