@fgiova/sqs-consumer 3.3.0 → 3.5.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 +21 -2
- package/dist/cjs/hooks.d.ts +5 -1
- package/dist/cjs/hooks.js +6 -0
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +27 -3
- package/dist/esm/hooks.d.mts +5 -1
- package/dist/esm/hooks.mjs +6 -0
- package/dist/esm/index.d.mts +2 -1
- package/dist/esm/index.mjs +27 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,9 +86,10 @@ const consumer = new SQSConsumer({
|
|
|
86
86
|
| messageAttributeNames | string[] | [] | The message attribute names to be included in the response |
|
|
87
87
|
| attributeNames | string[] | [] | The attribute names to be included in the response |
|
|
88
88
|
| errorDelaySeconds | number | 0 | Seconds a failed message stays invisible before it can be received again |
|
|
89
|
-
| errorDelayBackoff | (attempt: number) => number |
|
|
89
|
+
| errorDelayBackoff | (attempt: number, message: Message) => number | | Returns the delay in seconds for a failed message, given its `ApproximateReceiveCount` and the message itself. Takes precedence over `errorDelaySeconds` |
|
|
90
|
+
| maxReceiveCount | number / (attempt: number, message: Message) => number | | Receive count after which a failed message triggers `onMaxReceiveCountExceeded`. A function receives the `ApproximateReceiveCount` and the message, and returns the threshold |
|
|
90
91
|
|
|
91
|
-
Delays are truncated to an integer and clamped to the SQS range 0–43200 seconds. When `errorDelayBackoff` is set, `ApproximateReceiveCount` is added to `attributeNames` automatically. If `errorDelayBackoff` throws, the error is reported to `onSQSError` and `errorDelaySeconds` is used instead.
|
|
92
|
+
Delays are truncated to an integer and clamped to the SQS range 0–43200 seconds. When `errorDelayBackoff` is set, `ApproximateReceiveCount` is added to `attributeNames` automatically. If `errorDelayBackoff` throws, the error is reported to `onSQSError` together with the message and `errorDelaySeconds` is used instead.
|
|
92
93
|
|
|
93
94
|
> **Note:** SQS counts the 12-hour visibility limit from when the message was received, not from when the delay is set. A delay that exceeds the remaining time (roughly `43200 - executionTimeout - visibility extensions`) is silently rejected by SQS and the message is retried after its current visibility timeout. Keep delays well below 12 hours, and keep their sum below the queue's `MessageRetentionPeriod`, or the message expires without reaching the DLQ.
|
|
94
95
|
>
|
|
@@ -105,6 +106,23 @@ const consumer = new SQSConsumer({
|
|
|
105
106
|
});
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
When a message fails and its `ApproximateReceiveCount` reaches `maxReceiveCount`, the `onMaxReceiveCountExceeded` hook is called with the message and its receive count. **The message is deleted unless a hook returns `false`**, including when no hook is registered: return `false` to release it with the usual error delay instead. When `maxReceiveCount` is set, `ApproximateReceiveCount` is added to `attributeNames` automatically. If the `maxReceiveCount` function throws, the error is reported to `onSQSError` together with the message and the message is released.
|
|
110
|
+
|
|
111
|
+
> **Note:** if the queue has a redrive policy, set `maxReceiveCount` below the queue's own `maxReceiveCount`, or SQS moves the message to the DLQ before the hook is called.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const consumer = new SQSConsumer({
|
|
115
|
+
queueARN: "arn:aws:sqs:eu-central-1:000000000000:test",
|
|
116
|
+
handler: async (message) => { /* ... */ },
|
|
117
|
+
consumerOptions: {
|
|
118
|
+
maxReceiveCount: 5,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
consumer.addHook("onMaxReceiveCountExceeded", async (message, count) => {
|
|
122
|
+
await archive(message); // keep a copy, then let the consumer delete it
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
108
126
|
### Hooks
|
|
109
127
|
| Option | Type | Description |
|
|
110
128
|
|------------------|-----------------------------------------------------------------------|---------------------------------------------------------------------|
|
|
@@ -113,6 +131,7 @@ const consumer = new SQSConsumer({
|
|
|
113
131
|
| onHandlerSuccess | (message: Message) => Promise<Message> | Called when the consumer handles a message successfully |
|
|
114
132
|
| onHandlerTimeout | (message: Message) => Promise<boolean> | Called when the consumer handler execution exceed executionTimeout |
|
|
115
133
|
| onHandlerError | (message: Message, error: Error) => Promise<boolean> | Called when the consumer handler execution throws an error |
|
|
134
|
+
| onMaxReceiveCountExceeded | (message: Message, count: number) => Promise<boolean> | Called when a failed message reaches `maxReceiveCount`. Return `false` to keep the message, otherwise it is deleted |
|
|
116
135
|
| onSuccess | (message: Message) => Promise<boolean> | Called when the consumer handler execution finishes successfully |
|
|
117
136
|
| onError | (hook: HookName, message: Message, error: Error) => Promise<boolean> | Called when the consumer handler execution throws an uncaught error |
|
|
118
137
|
| onSQSError | (error: Error, message?: Message) => Promise<void> | Called when the consumer receives an error from the SQS service |
|
package/dist/cjs/hooks.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/complexity/noBannedTypes: Function required as generic */
|
|
2
2
|
import type { Message } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Logger } from "./logger";
|
|
4
|
-
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
|
|
4
|
+
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onMaxReceiveCountExceeded" | "onSuccess" | "onError" | "onSQSError";
|
|
5
5
|
export type HookSignatureMap = {
|
|
6
6
|
onStart: {
|
|
7
7
|
args: [sqsConsumer: unknown];
|
|
@@ -31,6 +31,10 @@ export type HookSignatureMap = {
|
|
|
31
31
|
args: [message: Message, error: Error];
|
|
32
32
|
return: boolean;
|
|
33
33
|
};
|
|
34
|
+
onMaxReceiveCountExceeded: {
|
|
35
|
+
args: [message: Message, count: number];
|
|
36
|
+
return: boolean;
|
|
37
|
+
};
|
|
34
38
|
onSuccess: {
|
|
35
39
|
args: [message: Message];
|
|
36
40
|
return: boolean;
|
package/dist/cjs/hooks.js
CHANGED
|
@@ -39,6 +39,11 @@ class Hooks {
|
|
|
39
39
|
throwable: false,
|
|
40
40
|
type: "boolean-return",
|
|
41
41
|
},
|
|
42
|
+
onMaxReceiveCountExceeded: {
|
|
43
|
+
S: Symbol("maxReceiveCountExceeded"),
|
|
44
|
+
throwable: false,
|
|
45
|
+
type: "boolean-return",
|
|
46
|
+
},
|
|
42
47
|
onSuccess: {
|
|
43
48
|
S: Symbol("success"),
|
|
44
49
|
throwable: false,
|
|
@@ -64,6 +69,7 @@ class Hooks {
|
|
|
64
69
|
[this.hookSymbols.onHandlerSuccess.S]: [],
|
|
65
70
|
[this.hookSymbols.onHandlerTimeout.S]: [],
|
|
66
71
|
[this.hookSymbols.onHandlerError.S]: [],
|
|
72
|
+
[this.hookSymbols.onMaxReceiveCountExceeded.S]: [],
|
|
67
73
|
[this.hookSymbols.onSuccess.S]: [],
|
|
68
74
|
[this.hookSymbols.onError.S]: [],
|
|
69
75
|
[this.hookSymbols.onSQSError.S]: [],
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -24,7 +24,8 @@ type ConsumerOptions = {
|
|
|
24
24
|
messageAttributeNames?: string[];
|
|
25
25
|
attributeNames?: string[];
|
|
26
26
|
errorDelaySeconds?: number;
|
|
27
|
-
errorDelayBackoff?: (attempt: number) => number;
|
|
27
|
+
errorDelayBackoff?: (attempt: number, message: Message) => number;
|
|
28
|
+
maxReceiveCount?: number | ((attempt: number, message: Message) => number);
|
|
28
29
|
};
|
|
29
30
|
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
30
31
|
export type HooksOptions = {
|
package/dist/cjs/index.js
CHANGED
|
@@ -45,13 +45,15 @@ class SQSConsumer {
|
|
|
45
45
|
attributeNames: [
|
|
46
46
|
...new Set([
|
|
47
47
|
...(options.consumerOptions?.attributeNames ?? []),
|
|
48
|
-
...(options.consumerOptions?.errorDelayBackoff
|
|
48
|
+
...(options.consumerOptions?.errorDelayBackoff ||
|
|
49
|
+
options.consumerOptions?.maxReceiveCount !== undefined
|
|
49
50
|
? ["ApproximateReceiveCount"]
|
|
50
51
|
: []),
|
|
51
52
|
]),
|
|
52
53
|
],
|
|
53
54
|
errorDelaySeconds: options.consumerOptions?.errorDelaySeconds ?? 0,
|
|
54
55
|
errorDelayBackoff: options.consumerOptions?.errorDelayBackoff,
|
|
56
|
+
maxReceiveCount: options.consumerOptions?.maxReceiveCount,
|
|
55
57
|
};
|
|
56
58
|
this.sqsClient =
|
|
57
59
|
options.clientOptions?.sqsClient ??
|
|
@@ -150,14 +152,36 @@ class SQSConsumer {
|
|
|
150
152
|
for (const result of messagesResults) {
|
|
151
153
|
if (result.error) {
|
|
152
154
|
let delaySeconds = this.consumerOptions.errorDelaySeconds;
|
|
155
|
+
const maxReceiveCount = this.consumerOptions.maxReceiveCount;
|
|
156
|
+
if (maxReceiveCount !== undefined) {
|
|
157
|
+
try {
|
|
158
|
+
const receiveCount = Number(
|
|
159
|
+
/* c8 ignore next 1 */
|
|
160
|
+
result.message.Attributes?.ApproximateReceiveCount ?? 0);
|
|
161
|
+
if (receiveCount >=
|
|
162
|
+
(typeof maxReceiveCount === "function"
|
|
163
|
+
? maxReceiveCount(receiveCount, result.message)
|
|
164
|
+
: maxReceiveCount)) {
|
|
165
|
+
const toDelete = await this.hooks.runHook("onMaxReceiveCountExceeded", result.message, receiveCount);
|
|
166
|
+
if (toDelete) {
|
|
167
|
+
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
168
|
+
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
await this.hooks.runHook("onSQSError", e, result.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
153
177
|
if (this.consumerOptions.errorDelayBackoff) {
|
|
154
178
|
try {
|
|
155
179
|
delaySeconds = this.consumerOptions.errorDelayBackoff(
|
|
156
180
|
/* c8 ignore next 1 */
|
|
157
|
-
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0));
|
|
181
|
+
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0), result.message);
|
|
158
182
|
}
|
|
159
183
|
catch (e) {
|
|
160
|
-
await this.hooks.runHook("onSQSError", e);
|
|
184
|
+
await this.hooks.runHook("onSQSError", e, result.message);
|
|
161
185
|
}
|
|
162
186
|
}
|
|
163
187
|
candidateToRelease.push({
|
package/dist/esm/hooks.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/complexity/noBannedTypes: Function required as generic */
|
|
2
2
|
import type { Message } from "@fgiova/mini-sqs-client";
|
|
3
3
|
import type { Logger } from "./logger.mjs";
|
|
4
|
-
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onSuccess" | "onError" | "onSQSError";
|
|
4
|
+
export type HookName = "onStart" | "onStop" | "onPoll" | "onMessage" | "onHandlerSuccess" | "onHandlerTimeout" | "onHandlerError" | "onMaxReceiveCountExceeded" | "onSuccess" | "onError" | "onSQSError";
|
|
5
5
|
export type HookSignatureMap = {
|
|
6
6
|
onStart: {
|
|
7
7
|
args: [sqsConsumer: unknown];
|
|
@@ -31,6 +31,10 @@ export type HookSignatureMap = {
|
|
|
31
31
|
args: [message: Message, error: Error];
|
|
32
32
|
return: boolean;
|
|
33
33
|
};
|
|
34
|
+
onMaxReceiveCountExceeded: {
|
|
35
|
+
args: [message: Message, count: number];
|
|
36
|
+
return: boolean;
|
|
37
|
+
};
|
|
34
38
|
onSuccess: {
|
|
35
39
|
args: [message: Message];
|
|
36
40
|
return: boolean;
|
package/dist/esm/hooks.mjs
CHANGED
|
@@ -36,6 +36,11 @@ export class Hooks {
|
|
|
36
36
|
throwable: false,
|
|
37
37
|
type: "boolean-return",
|
|
38
38
|
},
|
|
39
|
+
onMaxReceiveCountExceeded: {
|
|
40
|
+
S: Symbol("maxReceiveCountExceeded"),
|
|
41
|
+
throwable: false,
|
|
42
|
+
type: "boolean-return",
|
|
43
|
+
},
|
|
39
44
|
onSuccess: {
|
|
40
45
|
S: Symbol("success"),
|
|
41
46
|
throwable: false,
|
|
@@ -61,6 +66,7 @@ export class Hooks {
|
|
|
61
66
|
[this.hookSymbols.onHandlerSuccess.S]: [],
|
|
62
67
|
[this.hookSymbols.onHandlerTimeout.S]: [],
|
|
63
68
|
[this.hookSymbols.onHandlerError.S]: [],
|
|
69
|
+
[this.hookSymbols.onMaxReceiveCountExceeded.S]: [],
|
|
64
70
|
[this.hookSymbols.onSuccess.S]: [],
|
|
65
71
|
[this.hookSymbols.onError.S]: [],
|
|
66
72
|
[this.hookSymbols.onSQSError.S]: [],
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -24,7 +24,8 @@ type ConsumerOptions = {
|
|
|
24
24
|
messageAttributeNames?: string[];
|
|
25
25
|
attributeNames?: string[];
|
|
26
26
|
errorDelaySeconds?: number;
|
|
27
|
-
errorDelayBackoff?: (attempt: number) => number;
|
|
27
|
+
errorDelayBackoff?: (attempt: number, message: Message) => number;
|
|
28
|
+
maxReceiveCount?: number | ((attempt: number, message: Message) => number);
|
|
28
29
|
};
|
|
29
30
|
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
30
31
|
export type HooksOptions = {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -38,13 +38,15 @@ export class SQSConsumer {
|
|
|
38
38
|
attributeNames: [
|
|
39
39
|
...new Set([
|
|
40
40
|
...(options.consumerOptions?.attributeNames ?? []),
|
|
41
|
-
...(options.consumerOptions?.errorDelayBackoff
|
|
41
|
+
...(options.consumerOptions?.errorDelayBackoff ||
|
|
42
|
+
options.consumerOptions?.maxReceiveCount !== undefined
|
|
42
43
|
? ["ApproximateReceiveCount"]
|
|
43
44
|
: []),
|
|
44
45
|
]),
|
|
45
46
|
],
|
|
46
47
|
errorDelaySeconds: options.consumerOptions?.errorDelaySeconds ?? 0,
|
|
47
48
|
errorDelayBackoff: options.consumerOptions?.errorDelayBackoff,
|
|
49
|
+
maxReceiveCount: options.consumerOptions?.maxReceiveCount,
|
|
48
50
|
};
|
|
49
51
|
this.sqsClient =
|
|
50
52
|
options.clientOptions?.sqsClient ??
|
|
@@ -143,14 +145,36 @@ export class SQSConsumer {
|
|
|
143
145
|
for (const result of messagesResults) {
|
|
144
146
|
if (result.error) {
|
|
145
147
|
let delaySeconds = this.consumerOptions.errorDelaySeconds;
|
|
148
|
+
const maxReceiveCount = this.consumerOptions.maxReceiveCount;
|
|
149
|
+
if (maxReceiveCount !== undefined) {
|
|
150
|
+
try {
|
|
151
|
+
const receiveCount = Number(
|
|
152
|
+
/* c8 ignore next 1 */
|
|
153
|
+
result.message.Attributes?.ApproximateReceiveCount ?? 0);
|
|
154
|
+
if (receiveCount >=
|
|
155
|
+
(typeof maxReceiveCount === "function"
|
|
156
|
+
? maxReceiveCount(receiveCount, result.message)
|
|
157
|
+
: maxReceiveCount)) {
|
|
158
|
+
const toDelete = await this.hooks.runHook("onMaxReceiveCountExceeded", result.message, receiveCount);
|
|
159
|
+
if (toDelete) {
|
|
160
|
+
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
161
|
+
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
await this.hooks.runHook("onSQSError", e, result.message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
146
170
|
if (this.consumerOptions.errorDelayBackoff) {
|
|
147
171
|
try {
|
|
148
172
|
delaySeconds = this.consumerOptions.errorDelayBackoff(
|
|
149
173
|
/* c8 ignore next 1 */
|
|
150
|
-
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0));
|
|
174
|
+
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0), result.message);
|
|
151
175
|
}
|
|
152
176
|
catch (e) {
|
|
153
|
-
await this.hooks.runHook("onSQSError", e);
|
|
177
|
+
await this.hooks.runHook("onSQSError", e, result.message);
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
180
|
candidateToRelease.push({
|