@fgiova/sqs-consumer 3.2.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 +26 -7
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +35 -10
- package/dist/esm/index.d.mts +2 -0
- package/dist/esm/index.mjs +35 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -78,13 +78,32 @@ const consumer = new SQSConsumer({
|
|
|
78
78
|
| destroySigner | boolean | true | Whether to destroy the signer when the consumer is destroyed. |
|
|
79
79
|
|
|
80
80
|
### ConsumerOptions
|
|
81
|
-
| Option | Type
|
|
82
|
-
|
|
83
|
-
| visibilityTimeout | number
|
|
84
|
-
| waitTimeSeconds | number
|
|
85
|
-
| itemsPerRequest | number
|
|
86
|
-
| messageAttributeNames | string[]
|
|
87
|
-
| attributeNames | string[]
|
|
81
|
+
| Option | Type | Default | Description |
|
|
82
|
+
|-----------------------|-----------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
|
83
|
+
| visibilityTimeout | number | 30 | The visibility timeout for the messages in seconds |
|
|
84
|
+
| waitTimeSeconds | number | 20 | The wait time for the receiveMessage call in seconds |
|
|
85
|
+
| itemsPerRequest | number | 10 | The maximum number of messages to be received at once |
|
|
86
|
+
| messageAttributeNames | string[] | [] | The message attribute names to be included in the response |
|
|
87
|
+
| attributeNames | string[] | [] | The attribute names to be included in the response |
|
|
88
|
+
| errorDelaySeconds | number | 0 | Seconds a failed message stays invisible before it can be received again |
|
|
89
|
+
| errorDelayBackoff | (attempt: number) => number | | Returns the delay in seconds for a failed message, given its `ApproximateReceiveCount`. Takes precedence over `errorDelaySeconds` |
|
|
90
|
+
|
|
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
|
+
|
|
93
|
+
> **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
|
+
> Every retry increments `ApproximateReceiveCount`, so with a redrive policy the message moves to the DLQ after `maxReceiveCount` attempts: size the backoff accordingly. On FIFO queues a delayed message blocks its whole message group until the delay expires.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const consumer = new SQSConsumer({
|
|
99
|
+
queueARN: "arn:aws:sqs:eu-central-1:000000000000:test",
|
|
100
|
+
handler: async (message) => { /* ... */ },
|
|
101
|
+
consumerOptions: {
|
|
102
|
+
// 1st failure: 2s, 2nd: 4s, 3rd: 8s ... capped at 15 minutes
|
|
103
|
+
errorDelayBackoff: (attempt) => Math.min(2 ** attempt, 900),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
88
107
|
|
|
89
108
|
### Hooks
|
|
90
109
|
| Option | Type | Description |
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ type ConsumerOptions = {
|
|
|
23
23
|
itemsPerRequest?: number;
|
|
24
24
|
messageAttributeNames?: string[];
|
|
25
25
|
attributeNames?: string[];
|
|
26
|
+
errorDelaySeconds?: number;
|
|
27
|
+
errorDelayBackoff?: (attempt: number) => number;
|
|
26
28
|
};
|
|
27
29
|
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
28
30
|
export type HooksOptions = {
|
package/dist/cjs/index.js
CHANGED
|
@@ -42,7 +42,16 @@ class SQSConsumer {
|
|
|
42
42
|
waitTimeSeconds: options.consumerOptions?.waitTimeSeconds ?? 20,
|
|
43
43
|
itemsPerRequest: options.consumerOptions?.itemsPerRequest ?? 10,
|
|
44
44
|
messageAttributeNames: options.consumerOptions?.messageAttributeNames ?? [],
|
|
45
|
-
attributeNames:
|
|
45
|
+
attributeNames: [
|
|
46
|
+
...new Set([
|
|
47
|
+
...(options.consumerOptions?.attributeNames ?? []),
|
|
48
|
+
...(options.consumerOptions?.errorDelayBackoff
|
|
49
|
+
? ["ApproximateReceiveCount"]
|
|
50
|
+
: []),
|
|
51
|
+
]),
|
|
52
|
+
],
|
|
53
|
+
errorDelaySeconds: options.consumerOptions?.errorDelaySeconds ?? 0,
|
|
54
|
+
errorDelayBackoff: options.consumerOptions?.errorDelayBackoff,
|
|
46
55
|
};
|
|
47
56
|
this.sqsClient =
|
|
48
57
|
options.clientOptions?.sqsClient ??
|
|
@@ -120,9 +129,11 @@ class SQSConsumer {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
haExtendVisibilityTimeout(pendingReceipts, visibilityTimeout) {
|
|
123
|
-
if (this.handlerOptions.extendVisibilityTimeout
|
|
124
|
-
visibilityTimeout > 0) {
|
|
132
|
+
if (this.handlerOptions.extendVisibilityTimeout && visibilityTimeout > 0) {
|
|
125
133
|
return setInterval(async () => {
|
|
134
|
+
/* c8 ignore next 1 */
|
|
135
|
+
if (!pendingReceipts.size)
|
|
136
|
+
return;
|
|
126
137
|
try {
|
|
127
138
|
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, [...pendingReceipts], visibilityTimeout);
|
|
128
139
|
} /* c8 ignore next 3 */
|
|
@@ -138,10 +149,24 @@ class SQSConsumer {
|
|
|
138
149
|
const candidateToRelease = [];
|
|
139
150
|
for (const result of messagesResults) {
|
|
140
151
|
if (result.error) {
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
let delaySeconds = this.consumerOptions.errorDelaySeconds;
|
|
153
|
+
if (this.consumerOptions.errorDelayBackoff) {
|
|
154
|
+
try {
|
|
155
|
+
delaySeconds = this.consumerOptions.errorDelayBackoff(
|
|
156
|
+
/* c8 ignore next 1 */
|
|
157
|
+
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0));
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
await this.hooks.runHook("onSQSError", e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
candidateToRelease.push({
|
|
164
|
+
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
165
|
+
receiptHandle: result.message.ReceiptHandle,
|
|
166
|
+
visibilityTimeout: Math.min(43200, Math.max(0, Math.trunc(delaySeconds) || 0)),
|
|
167
|
+
});
|
|
143
168
|
}
|
|
144
|
-
else if (this.handlerOptions.deleteMessage
|
|
169
|
+
else if (this.handlerOptions.deleteMessage) {
|
|
145
170
|
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
146
171
|
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
147
172
|
}
|
|
@@ -150,15 +175,14 @@ class SQSConsumer {
|
|
|
150
175
|
await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
|
|
151
176
|
}
|
|
152
177
|
if (candidateToRelease.length) {
|
|
153
|
-
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease
|
|
178
|
+
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease);
|
|
154
179
|
}
|
|
155
180
|
}
|
|
156
181
|
async pollMessages() {
|
|
157
182
|
let consecutiveErrors = 0;
|
|
158
183
|
while (this.running) {
|
|
159
184
|
try {
|
|
160
|
-
|
|
161
|
-
const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
|
|
185
|
+
const visibilityTimeout = this.consumerOptions.visibilityTimeout;
|
|
162
186
|
const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
|
|
163
187
|
WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
|
|
164
188
|
MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
|
|
@@ -175,13 +199,14 @@ class SQSConsumer {
|
|
|
175
199
|
messages.map((message) => message.ReceiptHandle));
|
|
176
200
|
const haTimeout = this.haExtendVisibilityTimeout(pendingReceipts, visibilityTimeout);
|
|
177
201
|
try {
|
|
178
|
-
if (this.handlerOptions.parallelExecution
|
|
202
|
+
if (this.handlerOptions.parallelExecution) {
|
|
179
203
|
const results = await (0, p_map_1.default)(messages, async (message) => {
|
|
180
204
|
return await this.runOnMessageHook(message);
|
|
181
205
|
}, {
|
|
182
206
|
concurrency: 10,
|
|
183
207
|
stopOnError: false,
|
|
184
208
|
});
|
|
209
|
+
pendingReceipts.clear();
|
|
185
210
|
await this.deleteMessages(results);
|
|
186
211
|
}
|
|
187
212
|
else {
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -23,6 +23,8 @@ type ConsumerOptions = {
|
|
|
23
23
|
itemsPerRequest?: number;
|
|
24
24
|
messageAttributeNames?: string[];
|
|
25
25
|
attributeNames?: string[];
|
|
26
|
+
errorDelaySeconds?: number;
|
|
27
|
+
errorDelayBackoff?: (attempt: number) => number;
|
|
26
28
|
};
|
|
27
29
|
type ConsumerHookName = Exclude<HookName, "onStart" | "onStop">;
|
|
28
30
|
export type HooksOptions = {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -35,7 +35,16 @@ export class SQSConsumer {
|
|
|
35
35
|
waitTimeSeconds: options.consumerOptions?.waitTimeSeconds ?? 20,
|
|
36
36
|
itemsPerRequest: options.consumerOptions?.itemsPerRequest ?? 10,
|
|
37
37
|
messageAttributeNames: options.consumerOptions?.messageAttributeNames ?? [],
|
|
38
|
-
attributeNames:
|
|
38
|
+
attributeNames: [
|
|
39
|
+
...new Set([
|
|
40
|
+
...(options.consumerOptions?.attributeNames ?? []),
|
|
41
|
+
...(options.consumerOptions?.errorDelayBackoff
|
|
42
|
+
? ["ApproximateReceiveCount"]
|
|
43
|
+
: []),
|
|
44
|
+
]),
|
|
45
|
+
],
|
|
46
|
+
errorDelaySeconds: options.consumerOptions?.errorDelaySeconds ?? 0,
|
|
47
|
+
errorDelayBackoff: options.consumerOptions?.errorDelayBackoff,
|
|
39
48
|
};
|
|
40
49
|
this.sqsClient =
|
|
41
50
|
options.clientOptions?.sqsClient ??
|
|
@@ -113,9 +122,11 @@ export class SQSConsumer {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
haExtendVisibilityTimeout(pendingReceipts, visibilityTimeout) {
|
|
116
|
-
if (this.handlerOptions.extendVisibilityTimeout
|
|
117
|
-
visibilityTimeout > 0) {
|
|
125
|
+
if (this.handlerOptions.extendVisibilityTimeout && visibilityTimeout > 0) {
|
|
118
126
|
return setInterval(async () => {
|
|
127
|
+
/* c8 ignore next 1 */
|
|
128
|
+
if (!pendingReceipts.size)
|
|
129
|
+
return;
|
|
119
130
|
try {
|
|
120
131
|
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, [...pendingReceipts], visibilityTimeout);
|
|
121
132
|
} /* c8 ignore next 3 */
|
|
@@ -131,10 +142,24 @@ export class SQSConsumer {
|
|
|
131
142
|
const candidateToRelease = [];
|
|
132
143
|
for (const result of messagesResults) {
|
|
133
144
|
if (result.error) {
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
let delaySeconds = this.consumerOptions.errorDelaySeconds;
|
|
146
|
+
if (this.consumerOptions.errorDelayBackoff) {
|
|
147
|
+
try {
|
|
148
|
+
delaySeconds = this.consumerOptions.errorDelayBackoff(
|
|
149
|
+
/* c8 ignore next 1 */
|
|
150
|
+
Number(result.message.Attributes?.ApproximateReceiveCount ?? 0));
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
await this.hooks.runHook("onSQSError", e);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
candidateToRelease.push({
|
|
157
|
+
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
158
|
+
receiptHandle: result.message.ReceiptHandle,
|
|
159
|
+
visibilityTimeout: Math.min(43200, Math.max(0, Math.trunc(delaySeconds) || 0)),
|
|
160
|
+
});
|
|
136
161
|
}
|
|
137
|
-
else if (this.handlerOptions.deleteMessage
|
|
162
|
+
else if (this.handlerOptions.deleteMessage) {
|
|
138
163
|
// biome-ignore lint/style/noNonNullAssertion: ReceiptHandle must be present here
|
|
139
164
|
candidatesToDelete.push(result.message.ReceiptHandle);
|
|
140
165
|
}
|
|
@@ -143,15 +168,14 @@ export class SQSConsumer {
|
|
|
143
168
|
await this.sqsClient.deleteMessageBatch(this.queueARN, candidatesToDelete);
|
|
144
169
|
}
|
|
145
170
|
if (candidateToRelease.length) {
|
|
146
|
-
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease
|
|
171
|
+
await this.sqsClient.changeMessageVisibilityBatch(this.queueARN, candidateToRelease);
|
|
147
172
|
}
|
|
148
173
|
}
|
|
149
174
|
async pollMessages() {
|
|
150
175
|
let consecutiveErrors = 0;
|
|
151
176
|
while (this.running) {
|
|
152
177
|
try {
|
|
153
|
-
|
|
154
|
-
const visibilityTimeout = this.consumerOptions.visibilityTimeout ?? 30;
|
|
178
|
+
const visibilityTimeout = this.consumerOptions.visibilityTimeout;
|
|
155
179
|
const messagesResult = await this.sqsClient.receiveMessage(this.queueARN, {
|
|
156
180
|
WaitTimeSeconds: this.consumerOptions.waitTimeSeconds,
|
|
157
181
|
MaxNumberOfMessages: this.consumerOptions.itemsPerRequest,
|
|
@@ -168,13 +192,14 @@ export class SQSConsumer {
|
|
|
168
192
|
messages.map((message) => message.ReceiptHandle));
|
|
169
193
|
const haTimeout = this.haExtendVisibilityTimeout(pendingReceipts, visibilityTimeout);
|
|
170
194
|
try {
|
|
171
|
-
if (this.handlerOptions.parallelExecution
|
|
195
|
+
if (this.handlerOptions.parallelExecution) {
|
|
172
196
|
const results = await pMap(messages, async (message) => {
|
|
173
197
|
return await this.runOnMessageHook(message);
|
|
174
198
|
}, {
|
|
175
199
|
concurrency: 10,
|
|
176
200
|
stopOnError: false,
|
|
177
201
|
});
|
|
202
|
+
pendingReceipts.clear();
|
|
178
203
|
await this.deleteMessages(results);
|
|
179
204
|
}
|
|
180
205
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgiova/sqs-consumer",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"main": "./dist/cjs/index.js",
|
|
5
5
|
"types": "./dist/cjs/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@fgiova/mini-sqs-client": "^5.
|
|
59
|
+
"@fgiova/mini-sqs-client": "^5.2.0",
|
|
60
60
|
"@watchable/unpromise": "^1.0.2",
|
|
61
61
|
"p-map": "^7.0.3"
|
|
62
62
|
},
|