sqs-consumer 15.0.1 → 15.0.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## [15.0.2](https://github.com/bbc/sqs-consumer/compare/v15.0.1...v15.0.2) (2026-05-26)
2
+
3
+ ### Bug Fixes
4
+
5
+ * handle batch failures differently ([#807](https://github.com/bbc/sqs-consumer/issues/807)) ([0988bca](https://github.com/bbc/sqs-consumer/commit/0988bca2b2bc0dee9d97c25268aeee99585e9d5c))
6
+ * make coverage reports genhtml-compatible ([#803](https://github.com/bbc/sqs-consumer/issues/803)) ([8da9097](https://github.com/bbc/sqs-consumer/commit/8da9097f870d5cf2bf02dd970f2e90d76cd82d42))
7
+
8
+ ### Chores
9
+
10
+ * change to only run on dispatch ([d35f4b3](https://github.com/bbc/sqs-consumer/commit/d35f4b366de9e52d405ff830c8d5c168a45c481e))
11
+ * **deps-dev:** bump @cucumber/cucumber from 12.7.0 to 12.8.2 ([#796](https://github.com/bbc/sqs-consumer/issues/796)) ([de53034](https://github.com/bbc/sqs-consumer/commit/de53034e34e7c84d88dd716d4f94a100bac51fb5))
12
+ * **deps-dev:** bump @cucumber/cucumber from 12.8.2 to 12.8.3 ([#808](https://github.com/bbc/sqs-consumer/issues/808)) ([7144968](https://github.com/bbc/sqs-consumer/commit/7144968c51d0e646b9d852a6fa72a6c94d70baad))
13
+ * **deps-dev:** bump @semantic-release/github from 12.0.6 to 12.0.8 ([#800](https://github.com/bbc/sqs-consumer/issues/800)) ([c5fe1f8](https://github.com/bbc/sqs-consumer/commit/c5fe1f888fec145d61b313e38688fb6799c88b23))
14
+ * **deps-dev:** bump @semantic-release/release-notes-generator ([#799](https://github.com/bbc/sqs-consumer/issues/799)) ([6c24b1b](https://github.com/bbc/sqs-consumer/commit/6c24b1bbfa9e98eedc9ee23712187b1aa5be6344))
15
+ * **deps-dev:** bump @types/node from 25.5.0 to 25.6.2 ([#798](https://github.com/bbc/sqs-consumer/issues/798)) ([cecfd8d](https://github.com/bbc/sqs-consumer/commit/cecfd8da9bb8bf360ae16ebcf81796fa0f1c955e))
16
+ * **deps-dev:** bump @vitest/coverage-v8 from 4.1.5 to 4.1.6 ([#809](https://github.com/bbc/sqs-consumer/issues/809)) ([efe4953](https://github.com/bbc/sqs-consumer/commit/efe49535be6732ae8a7ed47add521f1ce5f128d3))
17
+ * **deps-dev:** bump oxfmt from 0.44.0 to 0.46.0 ([#786](https://github.com/bbc/sqs-consumer/issues/786)) ([a61d0b7](https://github.com/bbc/sqs-consumer/commit/a61d0b7308f40b0836198a8dfbd17181400fd8c7))
18
+ * **deps-dev:** bump sinon and @types/sinon ([#788](https://github.com/bbc/sqs-consumer/issues/788)) ([da2a4d9](https://github.com/bbc/sqs-consumer/commit/da2a4d9ebc84fb0929c7131b3c57d293064e623a))
19
+ * **deps-dev:** bump sqs-producer from 8.0.2 to 8.0.9 ([#810](https://github.com/bbc/sqs-consumer/issues/810)) ([da4afb0](https://github.com/bbc/sqs-consumer/commit/da4afb0ff29e296e6d3044b6db39880bdec62c19))
20
+ * **deps-dev:** bump typedoc from 0.28.17 to 0.28.19 ([#787](https://github.com/bbc/sqs-consumer/issues/787)) ([9467cec](https://github.com/bbc/sqs-consumer/commit/9467ceca1e844f5934c0f3d9ba654f62b8739c60))
21
+ * **deps-dev:** bump vitest from 4.1.2 to 4.1.5 ([#790](https://github.com/bbc/sqs-consumer/issues/790)) ([7720d4f](https://github.com/bbc/sqs-consumer/commit/7720d4fb0dab1dd1db766597cf1798014d388435))
22
+ * **deps-dev:** bump vitest from 4.1.5 to 4.1.6 ([#811](https://github.com/bbc/sqs-consumer/issues/811)) ([f14a0ef](https://github.com/bbc/sqs-consumer/commit/f14a0ef5fd53443abdea215af4301d669066ef68))
23
+ * **deps:** bump @aws-sdk/client-sqs from 3.1034.0 to 3.1036.0 ([#789](https://github.com/bbc/sqs-consumer/issues/789)) ([e54e664](https://github.com/bbc/sqs-consumer/commit/e54e664b474dc97f40cc82664f664b40b180b713))
24
+ * **deps:** bump actions/add-to-project from 1.0.2 to 2.0.0 ([#792](https://github.com/bbc/sqs-consumer/issues/792)) ([b1eeec1](https://github.com/bbc/sqs-consumer/commit/b1eeec1efa8dc9e2d419e41e3393b02f3b6e7d67))
25
+ * **deps:** bump actions/dependency-review-action from 4.9.0 to 5.0.0 ([#795](https://github.com/bbc/sqs-consumer/issues/795)) ([ea16075](https://github.com/bbc/sqs-consumer/commit/ea1607596771ed5b55e65c77c92c774cae1070c3))
26
+ * **deps:** bump actions/stale from 10.2.0 to 10.3.0 ([#805](https://github.com/bbc/sqs-consumer/issues/805)) ([f689b90](https://github.com/bbc/sqs-consumer/commit/f689b90faa61e87f142e8dbb6d4f22fab3d81e96))
27
+ * **deps:** bump dessant/lock-threads from 6.0.0 to 6.0.2 ([#812](https://github.com/bbc/sqs-consumer/issues/812)) ([96b83ad](https://github.com/bbc/sqs-consumer/commit/96b83adf35accacc504d0a7679350b4d9b1b5fa2))
28
+ * **deps:** bump fast-xml-builder ([#794](https://github.com/bbc/sqs-consumer/issues/794)) ([dc325d0](https://github.com/bbc/sqs-consumer/commit/dc325d0ed59e006e58e51fb71de3c58f968856c7))
29
+ * **deps:** bump github/codeql-action from 4.35.2 to 4.35.4 ([#793](https://github.com/bbc/sqs-consumer/issues/793)) ([fb6e76e](https://github.com/bbc/sqs-consumer/commit/fb6e76eb86e8307df849445f467f0085ae81e463))
30
+ * **deps:** bump github/codeql-action from 4.35.4 to 4.35.5 ([#802](https://github.com/bbc/sqs-consumer/issues/802)) ([a232427](https://github.com/bbc/sqs-consumer/commit/a232427a7455a536e8adf81db0824ecadb9126f5))
31
+ * **deps:** bump github/codeql-action from 4.35.5 to 4.36.0 ([#814](https://github.com/bbc/sqs-consumer/issues/814)) ([2725d58](https://github.com/bbc/sqs-consumer/commit/2725d5894510fd7945e16469be3222dfc3e76b01))
32
+ * **deps:** bump zgosalvez/github-actions-report-lcov ([#801](https://github.com/bbc/sqs-consumer/issues/801)) ([b1de26f](https://github.com/bbc/sqs-consumer/commit/b1de26ff931a74c38c1bf1263441a04aaabbc357))
33
+ * **deps:** bump zgosalvez/github-actions-report-lcov ([#813](https://github.com/bbc/sqs-consumer/issues/813)) ([2bc51e0](https://github.com/bbc/sqs-consumer/commit/2bc51e0671cb92e2c25b1fd5388b669e90d292d4))
34
+ * remove git step ([a2b699c](https://github.com/bbc/sqs-consumer/commit/a2b699cf26ae1de480b086ac8f23cbb12eb16e3a))
35
+ * remove unused action ([7b027a0](https://github.com/bbc/sqs-consumer/commit/7b027a03964910123534e8f63d6e55712f5b059b))
36
+ * update security policy ([464c7f0](https://github.com/bbc/sqs-consumer/commit/464c7f03fe82a3a4ff73bcbcb611805c8bc7c38d))
37
+ * updating brace expansion ([704890e](https://github.com/bbc/sqs-consumer/commit/704890e36c264ec2044fa07cf30130c11e1cd7e3))
38
+
1
39
  ## [15.0.1](https://github.com/bbc/sqs-consumer/compare/v15.0.0...v15.0.1) (2026-05-02)
2
40
 
3
41
  ### Chores
@@ -118,6 +118,7 @@ export declare class Consumer extends TypedEventEmitter {
118
118
  * @param timeout The new timeout that should be set
119
119
  */
120
120
  private changeVisibilityTimeoutBatch;
121
+ private emitBatchFailures;
121
122
  /**
122
123
  * Trigger the applications handleMessage function
123
124
  * @param message The message that was received from SQS
@@ -138,4 +139,5 @@ export declare class Consumer extends TypedEventEmitter {
138
139
  * @param messages The messages that should be deleted from SQS
139
140
  */
140
141
  private deleteMessageBatch;
142
+ private getSuccessfulBatchMessages;
141
143
  }
@@ -39,12 +39,14 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
39
39
  this.alwaysAcknowledge = options.alwaysAcknowledge ?? false;
40
40
  this.extendedAWSErrors = options.extendedAWSErrors ?? false;
41
41
  this.strictReturn = options.strictReturn ?? false;
42
- this.sqs =
43
- options.sqs ||
44
- new client_sqs_1.SQSClient({
45
- useQueueUrlAsEndpoint: options.useQueueUrlAsEndpoint ?? true,
46
- region: options.region || process.env.AWS_REGION || "eu-west-1",
47
- });
42
+ if (options.sqs) {
43
+ this.sqs = options.sqs;
44
+ }
45
+ else {
46
+ const useQueueUrlAsEndpoint = options.useQueueUrlAsEndpoint ?? true;
47
+ const region = options.region || process.env.AWS_REGION || "eu-west-1";
48
+ this.sqs = new client_sqs_1.SQSClient({ useQueueUrlAsEndpoint, region });
49
+ }
48
50
  }
49
51
  /**
50
52
  * Creates a new SQS consumer.
@@ -72,10 +74,11 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
72
74
  * A reusable options object for sqs.send that's used to avoid duplication.
73
75
  */
74
76
  get sqsSendOptions() {
77
+ const abortSignal = this.abortController?.signal || new AbortController().signal;
75
78
  return {
76
79
  // return the current abortController signal or a fresh signal that has not been aborted.
77
80
  // This effectively defaults the signal sent to the AWS SDK to not aborted
78
- abortSignal: this.abortController?.signal || new AbortController().signal,
81
+ abortSignal,
79
82
  };
80
83
  }
81
84
  /**
@@ -185,11 +188,13 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
185
188
  this.emitError(err);
186
189
  }
187
190
  catch (listenerErr) {
188
- logger_js_1.logger.warn(`An error event listener threw an error: ${listenerErr instanceof Error ? listenerErr.message : String(listenerErr)}`);
191
+ const listenerErrorMessage = listenerErr instanceof Error ? listenerErr.message : String(listenerErr);
192
+ logger_js_1.logger.warn(`An error event listener threw an error: ${listenerErrorMessage}`);
189
193
  }
190
194
  if ((0, errors_js_1.isConnectionError)(err)) {
195
+ const errorCode = err.code || "Unknown";
191
196
  logger_js_1.logger.debug("authentication_error", {
192
- code: err.code || "Unknown",
197
+ code: errorCode,
193
198
  detail: "There was an authentication error. Pausing before retrying.",
194
199
  });
195
200
  currentPollingTimeout = this.authenticationErrorTimeout;
@@ -295,8 +300,8 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
295
300
  }
296
301
  const ackedMessages = await this.executeBatchHandler(messages);
297
302
  if (ackedMessages?.length > 0) {
298
- await this.deleteMessageBatch(ackedMessages);
299
- ackedMessages.forEach((message) => {
303
+ const deletedMessages = await this.deleteMessageBatch(ackedMessages);
304
+ deletedMessages.forEach((message) => {
300
305
  this.emit("message_processed", message);
301
306
  });
302
307
  }
@@ -363,12 +368,23 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
363
368
  })),
364
369
  };
365
370
  try {
366
- return await this.sqs.send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(params), this.sqsSendOptions);
371
+ const response = await this.sqs.send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(params), this.sqsSendOptions);
372
+ if (response) {
373
+ this.emitBatchFailures(response.Failed, messages);
374
+ }
375
+ return response;
367
376
  }
368
377
  catch (err) {
369
378
  this.emit("error", (0, errors_js_1.toSQSError)(err, `Error changing visibility timeout: ${err.message}`, this.extendedAWSErrors, this.queueUrl, messages), messages);
370
379
  }
371
380
  }
381
+ emitBatchFailures(failed, messages) {
382
+ if (!failed?.length) {
383
+ return;
384
+ }
385
+ const failedIds = failed.map((entry) => entry.Id).filter((id) => Boolean(id));
386
+ this.emit("error", new Error(`Batch operation failed for entries with Ids: ${failedIds.join(", ")}`), messages);
387
+ }
372
388
  /**
373
389
  * Trigger the applications handleMessage function
374
390
  * @param message The message that was received from SQS
@@ -485,7 +501,7 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
485
501
  logger_js_1.logger.debug("skipping_delete", {
486
502
  detail: "Skipping message delete since shouldDeleteMessages is set to false",
487
503
  });
488
- return;
504
+ return messages;
489
505
  }
490
506
  logger_js_1.logger.debug("deleting_messages", {
491
507
  messageIds: messages.map((msg) => msg.MessageId),
@@ -498,11 +514,23 @@ class Consumer extends emitter_js_1.TypedEventEmitter {
498
514
  })),
499
515
  };
500
516
  try {
501
- await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand(deleteParams), this.sqsSendOptions);
517
+ const response = await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand(deleteParams), this.sqsSendOptions);
518
+ if (!response) {
519
+ return messages;
520
+ }
521
+ this.emitBatchFailures(response.Failed, messages);
522
+ return this.getSuccessfulBatchMessages(response, messages);
502
523
  }
503
524
  catch (err) {
504
525
  throw (0, errors_js_1.toSQSError)(err, `SQS delete message failed: ${err.message}`, this.extendedAWSErrors, this.queueUrl, messages);
505
526
  }
506
527
  }
528
+ getSuccessfulBatchMessages(response, messages) {
529
+ if (!response.Successful) {
530
+ return [];
531
+ }
532
+ const successfulIds = new Set(response.Successful.map(({ Id }) => Id));
533
+ return messages.filter((message) => successfulIds.has(message.MessageId));
534
+ }
507
535
  }
508
536
  exports.Consumer = Consumer;
@@ -5,6 +5,8 @@ exports.isConnectionError = isConnectionError;
5
5
  exports.toSQSError = toSQSError;
6
6
  exports.toStandardError = toStandardError;
7
7
  exports.toTimeoutError = toTimeoutError;
8
+ const DEFAULT_TIMEOUT_ERROR_MESSAGE = "Operation timed out.";
9
+ const DEFAULT_STANDARD_ERROR_MESSAGE = "An unexpected error occurred:";
8
10
  class SQSError extends Error {
9
11
  constructor(message) {
10
12
  super(message);
@@ -13,18 +15,20 @@ class SQSError extends Error {
13
15
  }
14
16
  exports.SQSError = SQSError;
15
17
  class TimeoutError extends Error {
16
- constructor(message = "Operation timed out.") {
17
- super(message);
18
- this.message = message;
18
+ constructor(message) {
19
+ const errorMessage = message === undefined ? DEFAULT_TIMEOUT_ERROR_MESSAGE : message;
20
+ super(errorMessage);
21
+ this.message = errorMessage;
19
22
  this.name = "TimeoutError";
20
23
  this.messageIds = [];
21
24
  }
22
25
  }
23
26
  exports.TimeoutError = TimeoutError;
24
27
  class StandardError extends Error {
25
- constructor(message = "An unexpected error occurred:") {
26
- super(message);
27
- this.message = message;
28
+ constructor(message) {
29
+ const errorMessage = message === undefined ? DEFAULT_STANDARD_ERROR_MESSAGE : message;
30
+ super(errorMessage);
31
+ this.message = errorMessage;
28
32
  this.name = "StandardError";
29
33
  this.messageIds = [];
30
34
  }
@@ -118,6 +118,7 @@ export declare class Consumer extends TypedEventEmitter {
118
118
  * @param timeout The new timeout that should be set
119
119
  */
120
120
  private changeVisibilityTimeoutBatch;
121
+ private emitBatchFailures;
121
122
  /**
122
123
  * Trigger the applications handleMessage function
123
124
  * @param message The message that was received from SQS
@@ -138,4 +139,5 @@ export declare class Consumer extends TypedEventEmitter {
138
139
  * @param messages The messages that should be deleted from SQS
139
140
  */
140
141
  private deleteMessageBatch;
142
+ private getSuccessfulBatchMessages;
141
143
  }
@@ -36,12 +36,14 @@ export class Consumer extends TypedEventEmitter {
36
36
  this.alwaysAcknowledge = options.alwaysAcknowledge ?? false;
37
37
  this.extendedAWSErrors = options.extendedAWSErrors ?? false;
38
38
  this.strictReturn = options.strictReturn ?? false;
39
- this.sqs =
40
- options.sqs ||
41
- new SQSClient({
42
- useQueueUrlAsEndpoint: options.useQueueUrlAsEndpoint ?? true,
43
- region: options.region || process.env.AWS_REGION || "eu-west-1",
44
- });
39
+ if (options.sqs) {
40
+ this.sqs = options.sqs;
41
+ }
42
+ else {
43
+ const useQueueUrlAsEndpoint = options.useQueueUrlAsEndpoint ?? true;
44
+ const region = options.region || process.env.AWS_REGION || "eu-west-1";
45
+ this.sqs = new SQSClient({ useQueueUrlAsEndpoint, region });
46
+ }
45
47
  }
46
48
  /**
47
49
  * Creates a new SQS consumer.
@@ -69,10 +71,11 @@ export class Consumer extends TypedEventEmitter {
69
71
  * A reusable options object for sqs.send that's used to avoid duplication.
70
72
  */
71
73
  get sqsSendOptions() {
74
+ const abortSignal = this.abortController?.signal || new AbortController().signal;
72
75
  return {
73
76
  // return the current abortController signal or a fresh signal that has not been aborted.
74
77
  // This effectively defaults the signal sent to the AWS SDK to not aborted
75
- abortSignal: this.abortController?.signal || new AbortController().signal,
78
+ abortSignal,
76
79
  };
77
80
  }
78
81
  /**
@@ -182,11 +185,13 @@ export class Consumer extends TypedEventEmitter {
182
185
  this.emitError(err);
183
186
  }
184
187
  catch (listenerErr) {
185
- logger.warn(`An error event listener threw an error: ${listenerErr instanceof Error ? listenerErr.message : String(listenerErr)}`);
188
+ const listenerErrorMessage = listenerErr instanceof Error ? listenerErr.message : String(listenerErr);
189
+ logger.warn(`An error event listener threw an error: ${listenerErrorMessage}`);
186
190
  }
187
191
  if (isConnectionError(err)) {
192
+ const errorCode = err.code || "Unknown";
188
193
  logger.debug("authentication_error", {
189
- code: err.code || "Unknown",
194
+ code: errorCode,
190
195
  detail: "There was an authentication error. Pausing before retrying.",
191
196
  });
192
197
  currentPollingTimeout = this.authenticationErrorTimeout;
@@ -292,8 +297,8 @@ export class Consumer extends TypedEventEmitter {
292
297
  }
293
298
  const ackedMessages = await this.executeBatchHandler(messages);
294
299
  if (ackedMessages?.length > 0) {
295
- await this.deleteMessageBatch(ackedMessages);
296
- ackedMessages.forEach((message) => {
300
+ const deletedMessages = await this.deleteMessageBatch(ackedMessages);
301
+ deletedMessages.forEach((message) => {
297
302
  this.emit("message_processed", message);
298
303
  });
299
304
  }
@@ -360,12 +365,23 @@ export class Consumer extends TypedEventEmitter {
360
365
  })),
361
366
  };
362
367
  try {
363
- return await this.sqs.send(new ChangeMessageVisibilityBatchCommand(params), this.sqsSendOptions);
368
+ const response = await this.sqs.send(new ChangeMessageVisibilityBatchCommand(params), this.sqsSendOptions);
369
+ if (response) {
370
+ this.emitBatchFailures(response.Failed, messages);
371
+ }
372
+ return response;
364
373
  }
365
374
  catch (err) {
366
375
  this.emit("error", toSQSError(err, `Error changing visibility timeout: ${err.message}`, this.extendedAWSErrors, this.queueUrl, messages), messages);
367
376
  }
368
377
  }
378
+ emitBatchFailures(failed, messages) {
379
+ if (!failed?.length) {
380
+ return;
381
+ }
382
+ const failedIds = failed.map((entry) => entry.Id).filter((id) => Boolean(id));
383
+ this.emit("error", new Error(`Batch operation failed for entries with Ids: ${failedIds.join(", ")}`), messages);
384
+ }
369
385
  /**
370
386
  * Trigger the applications handleMessage function
371
387
  * @param message The message that was received from SQS
@@ -482,7 +498,7 @@ export class Consumer extends TypedEventEmitter {
482
498
  logger.debug("skipping_delete", {
483
499
  detail: "Skipping message delete since shouldDeleteMessages is set to false",
484
500
  });
485
- return;
501
+ return messages;
486
502
  }
487
503
  logger.debug("deleting_messages", {
488
504
  messageIds: messages.map((msg) => msg.MessageId),
@@ -495,10 +511,22 @@ export class Consumer extends TypedEventEmitter {
495
511
  })),
496
512
  };
497
513
  try {
498
- await this.sqs.send(new DeleteMessageBatchCommand(deleteParams), this.sqsSendOptions);
514
+ const response = await this.sqs.send(new DeleteMessageBatchCommand(deleteParams), this.sqsSendOptions);
515
+ if (!response) {
516
+ return messages;
517
+ }
518
+ this.emitBatchFailures(response.Failed, messages);
519
+ return this.getSuccessfulBatchMessages(response, messages);
499
520
  }
500
521
  catch (err) {
501
522
  throw toSQSError(err, `SQS delete message failed: ${err.message}`, this.extendedAWSErrors, this.queueUrl, messages);
502
523
  }
503
524
  }
525
+ getSuccessfulBatchMessages(response, messages) {
526
+ if (!response.Successful) {
527
+ return [];
528
+ }
529
+ const successfulIds = new Set(response.Successful.map(({ Id }) => Id));
530
+ return messages.filter((message) => successfulIds.has(message.MessageId));
531
+ }
504
532
  }
@@ -1,3 +1,5 @@
1
+ const DEFAULT_TIMEOUT_ERROR_MESSAGE = "Operation timed out.";
2
+ const DEFAULT_STANDARD_ERROR_MESSAGE = "An unexpected error occurred:";
1
3
  class SQSError extends Error {
2
4
  constructor(message) {
3
5
  super(message);
@@ -5,17 +7,19 @@ class SQSError extends Error {
5
7
  }
6
8
  }
7
9
  class TimeoutError extends Error {
8
- constructor(message = "Operation timed out.") {
9
- super(message);
10
- this.message = message;
10
+ constructor(message) {
11
+ const errorMessage = message === undefined ? DEFAULT_TIMEOUT_ERROR_MESSAGE : message;
12
+ super(errorMessage);
13
+ this.message = errorMessage;
11
14
  this.name = "TimeoutError";
12
15
  this.messageIds = [];
13
16
  }
14
17
  }
15
18
  class StandardError extends Error {
16
- constructor(message = "An unexpected error occurred:") {
17
- super(message);
18
- this.message = message;
19
+ constructor(message) {
20
+ const errorMessage = message === undefined ? DEFAULT_STANDARD_ERROR_MESSAGE : message;
21
+ super(errorMessage);
22
+ this.message = errorMessage;
19
23
  this.name = "StandardError";
20
24
  this.messageIds = [];
21
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqs-consumer",
3
- "version": "15.0.1",
3
+ "version": "15.0.2",
4
4
  "description": "Build SQS-based Node applications without the boilerplate",
5
5
  "keywords": [
6
6
  "aws-sqs",
@@ -63,38 +63,38 @@
63
63
  "generate-docs": "typedoc"
64
64
  },
65
65
  "dependencies": {
66
- "@aws-sdk/client-sqs": "^3.1034.0",
66
+ "@aws-sdk/client-sqs": "^3.1036.0",
67
67
  "debug": "^4.4.3"
68
68
  },
69
69
  "devDependencies": {
70
- "@cucumber/cucumber": "^12.7.0",
70
+ "@cucumber/cucumber": "^12.8.3",
71
71
  "@sebbo2002/semantic-release-jsr": "^3.2.1",
72
72
  "@semantic-release/changelog": "^6.0.3",
73
73
  "@semantic-release/commit-analyzer": "^13.0.1",
74
74
  "@semantic-release/git": "^10.0.1",
75
- "@semantic-release/github": "^12.0.6",
75
+ "@semantic-release/github": "^12.0.8",
76
76
  "@semantic-release/npm": "^13.1.5",
77
- "@semantic-release/release-notes-generator": "^14.1.0",
77
+ "@semantic-release/release-notes-generator": "^14.1.1",
78
78
  "@types/chai": "^5.2.2",
79
- "@types/node": "^25.5.0",
80
- "@types/sinon": "^17.0.4",
81
- "@vitest/coverage-v8": "^4.1.2",
79
+ "@types/node": "^25.6.2",
80
+ "@types/sinon": "^21.0.1",
81
+ "@vitest/coverage-v8": "^4.1.6",
82
82
  "chai": "^6.2.2",
83
83
  "conventional-changelog-conventionalcommits": "^9.3.0",
84
84
  "jsr": "^0.14.3",
85
- "oxfmt": "^0.44.0",
85
+ "oxfmt": "^0.46.0",
86
86
  "oxlint": "^1.61.0",
87
87
  "p-event": "^7.1.0",
88
88
  "semantic-release": "^25.0.3",
89
- "sinon": "^21.0.0",
90
- "sqs-producer": "^8.0.2",
89
+ "sinon": "^21.1.2",
90
+ "sqs-producer": "^8.0.9",
91
91
  "ts-node": "^10.9.2",
92
- "typedoc": "^0.28.17",
92
+ "typedoc": "^0.28.19",
93
93
  "typescript": "^5.9.3",
94
- "vitest": "^4.1.2"
94
+ "vitest": "^4.1.6"
95
95
  },
96
96
  "peerDependencies": {
97
- "@aws-sdk/client-sqs": "^3.1034.0"
97
+ "@aws-sdk/client-sqs": "^3.1036.0"
98
98
  },
99
99
  "overrides": {
100
100
  "cross-spawn": "^7.0.3"
@@ -35,13 +35,6 @@ module.exports = {
35
35
  ],
36
36
  "@semantic-release/changelog",
37
37
  "@semantic-release/npm",
38
- [
39
- "@semantic-release/git",
40
- {
41
- assets: ["package.json", "package-lock.json", "CHANGELOG.md"],
42
- message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
43
- },
44
- ],
45
38
  "@semantic-release/github",
46
39
  "@sebbo2002/semantic-release-jsr",
47
40
  ],
package/src/consumer.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  ReceiveMessageCommand,
8
8
  } from "@aws-sdk/client-sqs";
9
9
  import type {
10
+ BatchResultErrorEntry,
10
11
  Message,
11
12
  ChangeMessageVisibilityCommandInput,
12
13
  ChangeMessageVisibilityCommandOutput,
@@ -14,6 +15,7 @@ import type {
14
15
  ChangeMessageVisibilityBatchCommandOutput,
15
16
  DeleteMessageCommandInput,
16
17
  DeleteMessageBatchCommandInput,
18
+ DeleteMessageBatchCommandOutput,
17
19
  ReceiveMessageCommandInput,
18
20
  ReceiveMessageCommandOutput,
19
21
  QueueAttributeName,
@@ -93,12 +95,13 @@ export class Consumer extends TypedEventEmitter {
93
95
  this.alwaysAcknowledge = options.alwaysAcknowledge ?? false;
94
96
  this.extendedAWSErrors = options.extendedAWSErrors ?? false;
95
97
  this.strictReturn = options.strictReturn ?? false;
96
- this.sqs =
97
- options.sqs ||
98
- new SQSClient({
99
- useQueueUrlAsEndpoint: options.useQueueUrlAsEndpoint ?? true,
100
- region: options.region || process.env.AWS_REGION || "eu-west-1",
101
- });
98
+ if (options.sqs) {
99
+ this.sqs = options.sqs;
100
+ } else {
101
+ const useQueueUrlAsEndpoint = options.useQueueUrlAsEndpoint ?? true;
102
+ const region = options.region || process.env.AWS_REGION || "eu-west-1";
103
+ this.sqs = new SQSClient({ useQueueUrlAsEndpoint, region });
104
+ }
102
105
  }
103
106
 
104
107
  /**
@@ -131,10 +134,12 @@ export class Consumer extends TypedEventEmitter {
131
134
  * A reusable options object for sqs.send that's used to avoid duplication.
132
135
  */
133
136
  private get sqsSendOptions(): { abortSignal: AbortSignal } {
137
+ const abortSignal = this.abortController?.signal || new AbortController().signal;
138
+
134
139
  return {
135
140
  // return the current abortController signal or a fresh signal that has not been aborted.
136
141
  // This effectively defaults the signal sent to the AWS SDK to not aborted
137
- abortSignal: this.abortController?.signal || new AbortController().signal,
142
+ abortSignal,
138
143
  };
139
144
  }
140
145
 
@@ -261,13 +266,15 @@ export class Consumer extends TypedEventEmitter {
261
266
  try {
262
267
  this.emitError(err);
263
268
  } catch (listenerErr) {
264
- logger.warn(
265
- `An error event listener threw an error: ${listenerErr instanceof Error ? listenerErr.message : String(listenerErr)}`,
266
- );
269
+ const listenerErrorMessage =
270
+ listenerErr instanceof Error ? listenerErr.message : String(listenerErr);
271
+ logger.warn(`An error event listener threw an error: ${listenerErrorMessage}`);
267
272
  }
268
273
  if (isConnectionError(err)) {
274
+ const errorCode = err.code || "Unknown";
275
+
269
276
  logger.debug("authentication_error", {
270
- code: err.code || "Unknown",
277
+ code: errorCode,
271
278
  detail: "There was an authentication error. Pausing before retrying.",
272
279
  });
273
280
  currentPollingTimeout = this.authenticationErrorTimeout;
@@ -396,9 +403,9 @@ export class Consumer extends TypedEventEmitter {
396
403
  const ackedMessages: Message[] = await this.executeBatchHandler(messages);
397
404
 
398
405
  if (ackedMessages?.length > 0) {
399
- await this.deleteMessageBatch(ackedMessages);
406
+ const deletedMessages = await this.deleteMessageBatch(ackedMessages);
400
407
 
401
- ackedMessages.forEach((message: Message): void => {
408
+ deletedMessages.forEach((message: Message): void => {
402
409
  this.emit("message_processed", message);
403
410
  });
404
411
  }
@@ -483,10 +490,16 @@ export class Consumer extends TypedEventEmitter {
483
490
  })),
484
491
  };
485
492
  try {
486
- return await this.sqs.send(
493
+ const response = await this.sqs.send(
487
494
  new ChangeMessageVisibilityBatchCommand(params),
488
495
  this.sqsSendOptions,
489
496
  );
497
+
498
+ if (response) {
499
+ this.emitBatchFailures(response.Failed, messages);
500
+ }
501
+
502
+ return response;
490
503
  } catch (err) {
491
504
  this.emit(
492
505
  "error",
@@ -502,6 +515,23 @@ export class Consumer extends TypedEventEmitter {
502
515
  }
503
516
  }
504
517
 
518
+ private emitBatchFailures(
519
+ failed: BatchResultErrorEntry[] | undefined,
520
+ messages: Message[],
521
+ ): void {
522
+ if (!failed?.length) {
523
+ return;
524
+ }
525
+
526
+ const failedIds = failed.map((entry) => entry.Id).filter((id): id is string => Boolean(id));
527
+
528
+ this.emit(
529
+ "error",
530
+ new Error(`Batch operation failed for entries with Ids: ${failedIds.join(", ")}`),
531
+ messages,
532
+ );
533
+ }
534
+
505
535
  /**
506
536
  * Trigger the applications handleMessage function
507
537
  * @param message The message that was received from SQS
@@ -648,12 +678,12 @@ export class Consumer extends TypedEventEmitter {
648
678
  * Delete a batch of messages from the SQS queue.
649
679
  * @param messages The messages that should be deleted from SQS
650
680
  */
651
- private async deleteMessageBatch(messages: Message[]): Promise<void> {
681
+ private async deleteMessageBatch(messages: Message[]): Promise<Message[]> {
652
682
  if (!this.shouldDeleteMessages) {
653
683
  logger.debug("skipping_delete", {
654
684
  detail: "Skipping message delete since shouldDeleteMessages is set to false",
655
685
  });
656
- return;
686
+ return messages;
657
687
  }
658
688
  logger.debug("deleting_messages", {
659
689
  messageIds: messages.map((msg: Message) => msg.MessageId),
@@ -668,7 +698,18 @@ export class Consumer extends TypedEventEmitter {
668
698
  };
669
699
 
670
700
  try {
671
- await this.sqs.send(new DeleteMessageBatchCommand(deleteParams), this.sqsSendOptions);
701
+ const response = await this.sqs.send(
702
+ new DeleteMessageBatchCommand(deleteParams),
703
+ this.sqsSendOptions,
704
+ );
705
+
706
+ if (!response) {
707
+ return messages;
708
+ }
709
+
710
+ this.emitBatchFailures(response.Failed, messages);
711
+
712
+ return this.getSuccessfulBatchMessages(response, messages);
672
713
  } catch (err) {
673
714
  throw toSQSError(
674
715
  err,
@@ -679,4 +720,16 @@ export class Consumer extends TypedEventEmitter {
679
720
  );
680
721
  }
681
722
  }
723
+
724
+ private getSuccessfulBatchMessages(
725
+ response: DeleteMessageBatchCommandOutput,
726
+ messages: Message[],
727
+ ): Message[] {
728
+ if (!response.Successful) {
729
+ return [];
730
+ }
731
+
732
+ const successfulIds = new Set(response.Successful.map(({ Id }) => Id));
733
+ return messages.filter((message) => successfulIds.has(message.MessageId));
734
+ }
682
735
  }
package/src/errors.ts CHANGED
@@ -2,6 +2,9 @@ import type { Message } from "@aws-sdk/client-sqs";
2
2
 
3
3
  import type { AWSError } from "./types.js";
4
4
 
5
+ const DEFAULT_TIMEOUT_ERROR_MESSAGE = "Operation timed out.";
6
+ const DEFAULT_STANDARD_ERROR_MESSAGE = "An unexpected error occurred:";
7
+
5
8
  class SQSError extends Error {
6
9
  code: string;
7
10
  cause: AWSError;
@@ -26,9 +29,11 @@ class TimeoutError extends Error {
26
29
  cause: Error;
27
30
  time: Date;
28
31
 
29
- constructor(message = "Operation timed out.") {
30
- super(message);
31
- this.message = message;
32
+ constructor(message?: string) {
33
+ const errorMessage = message === undefined ? DEFAULT_TIMEOUT_ERROR_MESSAGE : message;
34
+
35
+ super(errorMessage);
36
+ this.message = errorMessage;
32
37
  this.name = "TimeoutError";
33
38
  this.messageIds = [];
34
39
  }
@@ -39,9 +44,11 @@ class StandardError extends Error {
39
44
  cause: Error;
40
45
  time: Date;
41
46
 
42
- constructor(message = "An unexpected error occurred:") {
43
- super(message);
44
- this.message = message;
47
+ constructor(message?: string) {
48
+ const errorMessage = message === undefined ? DEFAULT_STANDARD_ERROR_MESSAGE : message;
49
+
50
+ super(errorMessage);
51
+ this.message = errorMessage;
45
52
  this.name = "StandardError";
46
53
  this.messageIds = [];
47
54
  }