@fgiova/sqs-consumer 3.0.0 → 3.2.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
@@ -29,6 +29,25 @@ const consumer = new SQSConsumer({
29
29
 
30
30
  ```
31
31
 
32
+ ## Handler cancellation on timeout
33
+
34
+ When `handlerOptions.executionTimeout` triggers, the consumer aborts the in-flight handler via an `AbortSignal` exposed through `AsyncLocalStorage`. The handler does not need to receive the signal as an argument — it is retrieved on demand using the `getAbortSignal` helper. Pass it to any cancellable async API (`fetch`, `undici`, timers, streams) to release resources promptly and avoid zombie handlers retaining memory after a timeout.
35
+
36
+ ```typescript
37
+ import { SQSConsumer, getAbortSignal } from '@fgiova/sqs-consumer'
38
+
39
+ const consumer = new SQSConsumer({
40
+ queueARN: "arn:aws:sqs:eu-central-1:000000000000:test-queue-hooks",
41
+ handler: async (message) => {
42
+ const signal = getAbortSignal();
43
+ const res = await fetch("https://api.example.com/work", { signal });
44
+ return res.json();
45
+ }
46
+ });
47
+ ```
48
+
49
+ `getAbortSignal()` returns `undefined` when called outside a handler execution context. Handlers that ignore it keep working unchanged.
50
+
32
51
  ## Options
33
52
  | Option | Type | Default | Description |
34
53
  |-----------------|-------------------------------------|---------|----------------------------------------------------|
@@ -86,6 +105,7 @@ SQSConsumer(options: SQSConsumerOptions)
86
105
  SQSConsumer.start(): Promise<void>
87
106
  SQSConsumer.stop(destroyConsumer = false): Promise<void>
88
107
  SQSConsumer.addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this
108
+ getAbortSignal(): AbortSignal | undefined
89
109
  ```
90
110
 
91
111
  ## License
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ type HandlerContext = {
3
+ signal: AbortSignal;
4
+ };
5
+ export declare const handlerContext: AsyncLocalStorage<HandlerContext>;
6
+ export declare function getAbortSignal(): AbortSignal | undefined;
7
+ export {};
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handlerContext = void 0;
4
+ exports.getAbortSignal = getAbortSignal;
5
+ const node_async_hooks_1 = require("node:async_hooks");
6
+ exports.handlerContext = new node_async_hooks_1.AsyncLocalStorage();
7
+ function getAbortSignal() {
8
+ return exports.handlerContext.getStore()?.signal;
9
+ }
@@ -3,6 +3,7 @@ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
3
3
  import type { Pool } from "undici";
4
4
  import { type HookCallback, type HookName } from "./hooks";
5
5
  import type { Logger } from "./logger";
6
+ export { getAbortSignal } from "./context";
6
7
  type HandlerOptions = {
7
8
  deleteMessage?: boolean;
8
9
  extendVisibilityTimeout?: boolean;
@@ -61,4 +62,3 @@ export declare class SQSConsumer {
61
62
  get isRunning(): boolean;
62
63
  addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this;
63
64
  }
64
- export {};
package/dist/cjs/index.js CHANGED
@@ -3,15 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SQSConsumer = void 0;
6
+ exports.SQSConsumer = exports.getAbortSignal = void 0;
7
7
  const promises_1 = require("node:timers/promises");
8
8
  const mini_sqs_client_1 = require("@fgiova/mini-sqs-client");
9
9
  // @ts-expect-error
10
10
  const unpromise_1 = require("@watchable/unpromise");
11
11
  // @ts-expect-error
12
12
  const p_map_1 = __importDefault(require("p-map"));
13
+ const context_1 = require("./context");
13
14
  const errors_1 = require("./errors");
14
15
  const hooks_1 = require("./hooks");
16
+ var context_2 = require("./context");
17
+ Object.defineProperty(exports, "getAbortSignal", { enumerable: true, get: function () { return context_2.getAbortSignal; } });
15
18
  class SQSConsumer {
16
19
  constructor(options) {
17
20
  this.messagesOnFly = 0;
@@ -78,20 +81,27 @@ class SQSConsumer {
78
81
  }
79
82
  async runHandler(handler, message) {
80
83
  let timeoutId = null;
84
+ const abortController = new AbortController();
81
85
  try {
82
86
  let handlerResult;
83
87
  if (this.handlerOptions.executionTimeout) {
88
+ const handlerPromise = context_1.handlerContext.run({ signal: abortController.signal }, () => handler(message));
89
+ // Swallow late rejection of the loser when timeout wins the race,
90
+ // otherwise it surfaces as an unhandledRejection.
91
+ handlerPromise.catch(() => { });
84
92
  handlerResult = await unpromise_1.Unpromise.race([
85
- handler(message),
93
+ handlerPromise,
86
94
  new Promise((_resolve, reject) => {
87
95
  timeoutId = setTimeout(() => {
88
- reject(new errors_1.TimeoutError("Handler execution timed out"));
96
+ const err = new errors_1.TimeoutError("Handler execution timed out");
97
+ abortController.abort(err);
98
+ reject(err);
89
99
  }, this.handlerOptions.executionTimeout);
90
100
  }),
91
101
  ]);
92
102
  }
93
103
  else {
94
- handlerResult = await handler(message);
104
+ handlerResult = await context_1.handlerContext.run({ signal: abortController.signal }, () => handler(message));
95
105
  }
96
106
  await this.hooks.runHook("onHandlerSuccess", message);
97
107
  return handlerResult;
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ type HandlerContext = {
3
+ signal: AbortSignal;
4
+ };
5
+ export declare const handlerContext: AsyncLocalStorage<HandlerContext>;
6
+ export declare function getAbortSignal(): AbortSignal | undefined;
7
+ export {};
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const handlerContext = new AsyncLocalStorage();
3
+ export function getAbortSignal() {
4
+ return handlerContext.getStore()?.signal;
5
+ }
@@ -3,6 +3,7 @@ import { type Message, MiniSQSClient } from "@fgiova/mini-sqs-client";
3
3
  import type { Pool } from "undici";
4
4
  import { type HookCallback, type HookName } from "./hooks.mjs";
5
5
  import type { Logger } from "./logger.mjs";
6
+ export { getAbortSignal } from "./context.mjs";
6
7
  type HandlerOptions = {
7
8
  deleteMessage?: boolean;
8
9
  extendVisibilityTimeout?: boolean;
@@ -61,4 +62,3 @@ export declare class SQSConsumer {
61
62
  get isRunning(): boolean;
62
63
  addHook<H extends HookName>(hookName: H, value: HookCallback<H>): this;
63
64
  }
64
- export {};
@@ -4,8 +4,10 @@ import { MiniSQSClient } from "@fgiova/mini-sqs-client";
4
4
  import { Unpromise } from "@watchable/unpromise";
5
5
  // @ts-expect-error
6
6
  import pMap from "p-map";
7
+ import { handlerContext } from "./context.mjs";
7
8
  import { TimeoutError } from "./errors.mjs";
8
9
  import { Hooks } from "./hooks.mjs";
10
+ export { getAbortSignal } from "./context.mjs";
9
11
  export class SQSConsumer {
10
12
  constructor(options) {
11
13
  this.messagesOnFly = 0;
@@ -72,20 +74,27 @@ export class SQSConsumer {
72
74
  }
73
75
  async runHandler(handler, message) {
74
76
  let timeoutId = null;
77
+ const abortController = new AbortController();
75
78
  try {
76
79
  let handlerResult;
77
80
  if (this.handlerOptions.executionTimeout) {
81
+ const handlerPromise = handlerContext.run({ signal: abortController.signal }, () => handler(message));
82
+ // Swallow late rejection of the loser when timeout wins the race,
83
+ // otherwise it surfaces as an unhandledRejection.
84
+ handlerPromise.catch(() => { });
78
85
  handlerResult = await Unpromise.race([
79
- handler(message),
86
+ handlerPromise,
80
87
  new Promise((_resolve, reject) => {
81
88
  timeoutId = setTimeout(() => {
82
- reject(new TimeoutError("Handler execution timed out"));
89
+ const err = new TimeoutError("Handler execution timed out");
90
+ abortController.abort(err);
91
+ reject(err);
83
92
  }, this.handlerOptions.executionTimeout);
84
93
  }),
85
94
  ]);
86
95
  }
87
96
  else {
88
- handlerResult = await handler(message);
97
+ handlerResult = await handlerContext.run({ signal: abortController.signal }, () => handler(message));
89
98
  }
90
99
  await this.hooks.runHook("onHandlerSuccess", message);
91
100
  return handlerResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgiova/sqs-consumer",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "main": "./dist/cjs/index.js",
5
5
  "types": "./dist/cjs/index.d.ts",
6
6
  "exports": {
@@ -56,12 +56,12 @@
56
56
  ]
57
57
  },
58
58
  "dependencies": {
59
- "@fgiova/mini-sqs-client": "^4.0.0",
59
+ "@fgiova/mini-sqs-client": "^5.1.0",
60
60
  "@watchable/unpromise": "^1.0.2",
61
61
  "p-map": "^7.0.3"
62
62
  },
63
63
  "peerDependencies": {
64
- "@fgiova/aws-signature": "^4.0.0"
64
+ "@fgiova/aws-signature": "^5.0.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@aws-sdk/client-sqs": "^3.758.0",