sqs-consumer 6.1.0 → 6.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/.github/workflows/coverage.yml +1 -1
- package/.github/workflows/docs.yml +55 -0
- package/.prettierignore +2 -1
- package/README.md +18 -42
- package/dist/bind.d.ts +4 -0
- package/dist/bind.js +9 -0
- package/dist/consumer.d.ts +90 -21
- package/dist/consumer.js +244 -217
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +32 -1
- package/dist/types.d.ts +134 -1
- package/dist/types.js +28 -0
- package/dist/validation.d.ts +13 -0
- package/dist/validation.js +36 -0
- package/package.json +13 -13
- package/src/bind.ts +9 -0
- package/src/consumer.ts +289 -279
- package/src/errors.ts +36 -1
- package/src/types.ts +146 -1
- package/src/validation.ts +45 -0
- package/typedoc.json +13 -0
package/src/consumer.ts
CHANGED
|
@@ -16,102 +16,42 @@ import {
|
|
|
16
16
|
ReceiveMessageCommandOutput
|
|
17
17
|
} from '@aws-sdk/client-sqs';
|
|
18
18
|
import Debug from 'debug';
|
|
19
|
-
import { EventEmitter } from 'events';
|
|
20
19
|
|
|
21
|
-
import {
|
|
20
|
+
import { ConsumerOptions, TypedEventEmitter } from './types';
|
|
22
21
|
import { autoBind } from './bind';
|
|
23
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
SQSError,
|
|
24
|
+
TimeoutError,
|
|
25
|
+
toSQSError,
|
|
26
|
+
isConnectionError
|
|
27
|
+
} from './errors';
|
|
28
|
+
import { assertOptions, hasMessages } from './validation';
|
|
24
29
|
|
|
25
30
|
const debug = Debug('sqs-consumer');
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pending: Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createTimeout(duration: number): TimeoutResponse[] {
|
|
39
|
-
let timeout;
|
|
40
|
-
const pending = new Promise((_, reject) => {
|
|
41
|
-
timeout = setTimeout((): void => {
|
|
42
|
-
reject(new TimeoutError());
|
|
43
|
-
}, duration);
|
|
44
|
-
});
|
|
45
|
-
return [timeout, pending];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function assertOptions(options: ConsumerOptions): void {
|
|
49
|
-
requiredOptions.forEach((option) => {
|
|
50
|
-
const possibilities = option.split('|');
|
|
51
|
-
if (!possibilities.find((p) => options[p])) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Missing SQS consumer option [ ${possibilities.join(' or ')} ].`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (options.batchSize > 10 || options.batchSize < 1) {
|
|
59
|
-
throw new Error('SQS batchSize option must be between 1 and 10.');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
options.heartbeatInterval &&
|
|
64
|
-
!(options.heartbeatInterval < options.visibilityTimeout)
|
|
65
|
-
) {
|
|
66
|
-
throw new Error('heartbeatInterval must be less than visibilityTimeout.');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isConnectionError(err: Error): boolean {
|
|
71
|
-
if (err instanceof SQSError) {
|
|
72
|
-
return (
|
|
73
|
-
err.statusCode === 403 ||
|
|
74
|
-
err.code === 'CredentialsError' ||
|
|
75
|
-
err.code === 'UnknownEndpoint' ||
|
|
76
|
-
err.code === 'AWS.SimpleQueueService.NonExistentQueue'
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function toSQSError(err: AWSError, message: string): SQSError {
|
|
83
|
-
const sqsError = new SQSError(message);
|
|
84
|
-
sqsError.code = err.name;
|
|
85
|
-
sqsError.statusCode = err.$metadata?.httpStatusCode;
|
|
86
|
-
sqsError.retryable = err.$retryable?.throttling;
|
|
87
|
-
sqsError.service = err.$service;
|
|
88
|
-
sqsError.fault = err.$fault;
|
|
89
|
-
sqsError.time = new Date();
|
|
90
|
-
|
|
91
|
-
return sqsError;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function hasMessages(response: ReceiveMessageCommandOutput): boolean {
|
|
95
|
-
return response.Messages && response.Messages.length > 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export class Consumer extends EventEmitter {
|
|
32
|
+
/**
|
|
33
|
+
* [Usage](https://bbc.github.io/sqs-consumer/index.html#usage)
|
|
34
|
+
*/
|
|
35
|
+
export class Consumer extends TypedEventEmitter {
|
|
36
|
+
private pollingTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
37
|
+
private heartbeatTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
38
|
+
private handleMessageTimeoutId: NodeJS.Timeout | undefined = undefined;
|
|
39
|
+
private stopped = true;
|
|
99
40
|
private queueUrl: string;
|
|
100
|
-
private handleMessage: (message: Message) => Promise<void>;
|
|
41
|
+
private handleMessage: (message: Message) => Promise<Message | void>;
|
|
101
42
|
private handleMessageBatch: (message: Message[]) => Promise<Message[] | void>;
|
|
43
|
+
private sqs: SQSClient;
|
|
102
44
|
private handleMessageTimeout: number;
|
|
103
45
|
private attributeNames: string[];
|
|
104
46
|
private messageAttributeNames: string[];
|
|
105
|
-
private
|
|
47
|
+
private shouldDeleteMessages: boolean;
|
|
106
48
|
private batchSize: number;
|
|
107
49
|
private visibilityTimeout: number;
|
|
50
|
+
private terminateVisibilityTimeout: boolean;
|
|
108
51
|
private waitTimeSeconds: number;
|
|
109
52
|
private authenticationErrorTimeout: number;
|
|
110
53
|
private pollingWaitTimeMs: number;
|
|
111
|
-
private terminateVisibilityTimeout: boolean;
|
|
112
54
|
private heartbeatInterval: number;
|
|
113
|
-
private sqs: SQSClient;
|
|
114
|
-
private shouldDeleteMessages: boolean;
|
|
115
55
|
|
|
116
56
|
constructor(options: ConsumerOptions) {
|
|
117
57
|
super();
|
|
@@ -122,7 +62,6 @@ export class Consumer extends EventEmitter {
|
|
|
122
62
|
this.handleMessageTimeout = options.handleMessageTimeout;
|
|
123
63
|
this.attributeNames = options.attributeNames || [];
|
|
124
64
|
this.messageAttributeNames = options.messageAttributeNames || [];
|
|
125
|
-
this.stopped = true;
|
|
126
65
|
this.batchSize = options.batchSize || 1;
|
|
127
66
|
this.visibilityTimeout = options.visibilityTimeout;
|
|
128
67
|
this.terminateVisibilityTimeout =
|
|
@@ -133,89 +72,171 @@ export class Consumer extends EventEmitter {
|
|
|
133
72
|
options.authenticationErrorTimeout ?? 10000;
|
|
134
73
|
this.pollingWaitTimeMs = options.pollingWaitTimeMs ?? 0;
|
|
135
74
|
this.shouldDeleteMessages = options.shouldDeleteMessages ?? true;
|
|
136
|
-
|
|
137
75
|
this.sqs =
|
|
138
76
|
options.sqs ||
|
|
139
77
|
new SQSClient({
|
|
140
78
|
region: options.region || process.env.AWS_REGION || 'eu-west-1'
|
|
141
79
|
});
|
|
142
|
-
|
|
143
80
|
autoBind(this);
|
|
144
81
|
}
|
|
145
82
|
|
|
146
|
-
|
|
147
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Creates a new SQS consumer.
|
|
85
|
+
*/
|
|
86
|
+
public static create(options: ConsumerOptions): Consumer {
|
|
87
|
+
return new Consumer(options);
|
|
148
88
|
}
|
|
149
89
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
):
|
|
154
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Start polling the queue for messages.
|
|
92
|
+
*/
|
|
93
|
+
public start(): void {
|
|
94
|
+
if (this.stopped) {
|
|
95
|
+
debug('Starting consumer');
|
|
96
|
+
this.stopped = false;
|
|
97
|
+
this.poll();
|
|
98
|
+
}
|
|
155
99
|
}
|
|
156
100
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
):
|
|
161
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Stop polling the queue for messages (pre existing requests will still be made until concluded).
|
|
103
|
+
*/
|
|
104
|
+
public stop(): void {
|
|
105
|
+
if (this.stopped) {
|
|
106
|
+
debug('Consumer was already stopped');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
debug('Stopping consumer');
|
|
111
|
+
this.stopped = true;
|
|
112
|
+
|
|
113
|
+
if (this.pollingTimeoutId) {
|
|
114
|
+
clearTimeout(this.pollingTimeoutId);
|
|
115
|
+
this.pollingTimeoutId = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.emit('stopped');
|
|
162
119
|
}
|
|
163
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Returns the current polling state of the consumer: `true` if it is actively polling, `false` if it is not.
|
|
123
|
+
*/
|
|
164
124
|
public get isRunning(): boolean {
|
|
165
125
|
return !this.stopped;
|
|
166
126
|
}
|
|
167
127
|
|
|
168
|
-
|
|
169
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Emit one of the consumer's error events depending on the error received.
|
|
130
|
+
* @param err The error object to forward on
|
|
131
|
+
* @param message The message that the error occurred on
|
|
132
|
+
*/
|
|
133
|
+
private emitError(err: Error, message?: Message): void {
|
|
134
|
+
if (!message) {
|
|
135
|
+
this.emit('error', err);
|
|
136
|
+
} else if (err.name === SQSError.name) {
|
|
137
|
+
this.emit('error', err, message);
|
|
138
|
+
} else if (err instanceof TimeoutError) {
|
|
139
|
+
this.emit('timeout_error', err, message);
|
|
140
|
+
} else {
|
|
141
|
+
this.emit('processing_error', err, message);
|
|
142
|
+
}
|
|
170
143
|
}
|
|
171
144
|
|
|
172
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Poll for new messages from SQS
|
|
147
|
+
*/
|
|
148
|
+
private poll(): void {
|
|
173
149
|
if (this.stopped) {
|
|
174
|
-
debug('
|
|
175
|
-
|
|
176
|
-
this.poll();
|
|
150
|
+
debug('Poll was called while consumer was stopped, cancelling poll...');
|
|
151
|
+
return;
|
|
177
152
|
}
|
|
153
|
+
|
|
154
|
+
debug('Polling for messages');
|
|
155
|
+
|
|
156
|
+
let currentPollingTimeout = this.pollingWaitTimeMs;
|
|
157
|
+
this.receiveMessage({
|
|
158
|
+
QueueUrl: this.queueUrl,
|
|
159
|
+
AttributeNames: this.attributeNames,
|
|
160
|
+
MessageAttributeNames: this.messageAttributeNames,
|
|
161
|
+
MaxNumberOfMessages: this.batchSize,
|
|
162
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
163
|
+
VisibilityTimeout: this.visibilityTimeout
|
|
164
|
+
})
|
|
165
|
+
.then(this.handleSqsResponse)
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
this.emitError(err);
|
|
168
|
+
if (isConnectionError(err)) {
|
|
169
|
+
debug('There was an authentication error. Pausing before retrying.');
|
|
170
|
+
currentPollingTimeout = this.authenticationErrorTimeout;
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
})
|
|
174
|
+
.then(() => {
|
|
175
|
+
if (this.pollingTimeoutId) {
|
|
176
|
+
clearTimeout(this.pollingTimeoutId);
|
|
177
|
+
}
|
|
178
|
+
this.pollingTimeoutId = setTimeout(this.poll, currentPollingTimeout);
|
|
179
|
+
})
|
|
180
|
+
.catch((err) => {
|
|
181
|
+
this.emitError(err);
|
|
182
|
+
});
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Send a request to SQS to retrieve messages
|
|
187
|
+
* @param params The required params to receive messages from SQS
|
|
188
|
+
*/
|
|
189
|
+
private async receiveMessage(
|
|
190
|
+
params: ReceiveMessageCommandInput
|
|
191
|
+
): Promise<ReceiveMessageCommandOutput> {
|
|
192
|
+
try {
|
|
193
|
+
return await this.sqs.send(new ReceiveMessageCommand(params));
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw toSQSError(err, `SQS receive message failed: ${err.message}`);
|
|
196
|
+
}
|
|
183
197
|
}
|
|
184
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Handles the response from AWS SQS, determining if we should proceed to
|
|
201
|
+
* the message handler.
|
|
202
|
+
* @param response The output from AWS SQS
|
|
203
|
+
*/
|
|
185
204
|
private async handleSqsResponse(
|
|
186
205
|
response: ReceiveMessageCommandOutput
|
|
187
206
|
): Promise<void> {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (response) {
|
|
192
|
-
if (hasMessages(response)) {
|
|
193
|
-
if (this.handleMessageBatch) {
|
|
194
|
-
// prefer handling messages in batch when available
|
|
195
|
-
await this.processMessageBatch(response.Messages);
|
|
196
|
-
} else {
|
|
197
|
-
await Promise.all(response.Messages.map(this.processMessage));
|
|
198
|
-
}
|
|
199
|
-
this.emit('response_processed');
|
|
207
|
+
if (hasMessages(response)) {
|
|
208
|
+
if (this.handleMessageBatch) {
|
|
209
|
+
await this.processMessageBatch(response.Messages);
|
|
200
210
|
} else {
|
|
201
|
-
this.
|
|
211
|
+
await Promise.all(response.Messages.map(this.processMessage));
|
|
202
212
|
}
|
|
213
|
+
|
|
214
|
+
this.emit('response_processed');
|
|
215
|
+
} else if (response) {
|
|
216
|
+
this.emit('empty');
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Process a message that has been received from SQS. This will execute the message
|
|
222
|
+
* handler and delete the message once complete.
|
|
223
|
+
* @param message The message that was delivered from SQS
|
|
224
|
+
*/
|
|
206
225
|
private async processMessage(message: Message): Promise<void> {
|
|
207
|
-
this.emit('message_received', message);
|
|
208
|
-
|
|
209
|
-
let heartbeat;
|
|
210
226
|
try {
|
|
227
|
+
this.emit('message_received', message);
|
|
228
|
+
|
|
211
229
|
if (this.heartbeatInterval) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
this.heartbeatTimeoutId = this.startHeartbeat(message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ackedMessage = await this.executeHandler(message);
|
|
234
|
+
|
|
235
|
+
if (ackedMessage?.MessageId === message.MessageId) {
|
|
236
|
+
await this.deleteMessage(message);
|
|
237
|
+
|
|
238
|
+
this.emit('message_processed', message);
|
|
215
239
|
}
|
|
216
|
-
await this.executeHandler(message);
|
|
217
|
-
await this.deleteMessage(message);
|
|
218
|
-
this.emit('message_processed', message);
|
|
219
240
|
} catch (err) {
|
|
220
241
|
this.emitError(err, message);
|
|
221
242
|
|
|
@@ -223,63 +244,71 @@ export class Consumer extends EventEmitter {
|
|
|
223
244
|
await this.changeVisibilityTimeout(message, 0);
|
|
224
245
|
}
|
|
225
246
|
} finally {
|
|
226
|
-
clearInterval(
|
|
247
|
+
clearInterval(this.heartbeatTimeoutId);
|
|
248
|
+
this.heartbeatTimeoutId = undefined;
|
|
227
249
|
}
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Process a batch of messages from the SQS queue.
|
|
254
|
+
* @param messages The messages that were delivered from SQS
|
|
255
|
+
*/
|
|
256
|
+
private async processMessageBatch(messages: Message[]): Promise<void> {
|
|
233
257
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
}
|
|
258
|
+
messages.forEach((message) => {
|
|
259
|
+
this.emit('message_received', message);
|
|
260
|
+
});
|
|
239
261
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
'Skipping message delete since shouldDeleteMessages is set to false'
|
|
244
|
-
);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
debug('Deleting message %s', message.MessageId);
|
|
262
|
+
if (this.heartbeatInterval) {
|
|
263
|
+
this.heartbeatTimeoutId = this.startHeartbeat(null, messages);
|
|
264
|
+
}
|
|
248
265
|
|
|
249
|
-
|
|
250
|
-
QueueUrl: this.queueUrl,
|
|
251
|
-
ReceiptHandle: message.ReceiptHandle
|
|
252
|
-
};
|
|
266
|
+
const ackedMessages = await this.executeBatchHandler(messages);
|
|
253
267
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} catch (err) {
|
|
257
|
-
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
268
|
+
if (ackedMessages?.length > 0) {
|
|
269
|
+
await this.deleteMessageBatch(ackedMessages);
|
|
260
270
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
if (this.handleMessageTimeout) {
|
|
266
|
-
[timeout, pending] = createTimeout(this.handleMessageTimeout);
|
|
267
|
-
await Promise.race([this.handleMessage(message), pending]);
|
|
268
|
-
} else {
|
|
269
|
-
await this.handleMessage(message);
|
|
271
|
+
ackedMessages.forEach((message) => {
|
|
272
|
+
this.emit('message_processed', message);
|
|
273
|
+
});
|
|
270
274
|
}
|
|
271
275
|
} catch (err) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
this.emit('error', err, messages);
|
|
277
|
+
|
|
278
|
+
if (this.terminateVisibilityTimeout) {
|
|
279
|
+
await this.changeVisibilityTimeoutBatch(messages, 0);
|
|
276
280
|
}
|
|
277
|
-
throw err;
|
|
278
281
|
} finally {
|
|
279
|
-
|
|
282
|
+
clearInterval(this.heartbeatTimeoutId);
|
|
283
|
+
this.heartbeatTimeoutId = undefined;
|
|
280
284
|
}
|
|
281
285
|
}
|
|
282
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Trigger a function on a set interval
|
|
289
|
+
* @param heartbeatFn The function that should be triggered
|
|
290
|
+
*/
|
|
291
|
+
private startHeartbeat(
|
|
292
|
+
message?: Message,
|
|
293
|
+
messages?: Message[]
|
|
294
|
+
): NodeJS.Timeout {
|
|
295
|
+
return setInterval(() => {
|
|
296
|
+
if (this.handleMessageBatch) {
|
|
297
|
+
return this.changeVisibilityTimeoutBatch(
|
|
298
|
+
messages,
|
|
299
|
+
this.visibilityTimeout
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
return this.changeVisibilityTimeout(message, this.visibilityTimeout);
|
|
303
|
+
}
|
|
304
|
+
}, this.heartbeatInterval * 1000);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Change the visibility timeout on a message
|
|
309
|
+
* @param message The message to change the value of
|
|
310
|
+
* @param timeout The new timeout that should be set
|
|
311
|
+
*/
|
|
283
312
|
private async changeVisibilityTimeout(
|
|
284
313
|
message: Message,
|
|
285
314
|
timeout: number
|
|
@@ -300,156 +329,137 @@ export class Consumer extends EventEmitter {
|
|
|
300
329
|
}
|
|
301
330
|
}
|
|
302
331
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
private poll(): void {
|
|
314
|
-
if (this.stopped) {
|
|
315
|
-
this.emit('stopped');
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
debug('Polling for messages');
|
|
320
|
-
const receiveParams: ReceiveMessageCommandInput = {
|
|
332
|
+
/**
|
|
333
|
+
* Change the visibility timeout on a batch of messages
|
|
334
|
+
* @param messages The messages to change the value of
|
|
335
|
+
* @param timeout The new timeout that should be set
|
|
336
|
+
*/
|
|
337
|
+
private async changeVisibilityTimeoutBatch(
|
|
338
|
+
messages: Message[],
|
|
339
|
+
timeout: number
|
|
340
|
+
): Promise<ChangeMessageVisibilityBatchCommandOutput> {
|
|
341
|
+
const params: ChangeMessageVisibilityBatchCommandInput = {
|
|
321
342
|
QueueUrl: this.queueUrl,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
343
|
+
Entries: messages.map((message) => ({
|
|
344
|
+
Id: message.MessageId,
|
|
345
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
346
|
+
VisibilityTimeout: timeout
|
|
347
|
+
}))
|
|
327
348
|
};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
})
|
|
340
|
-
.then(() => {
|
|
341
|
-
setTimeout(this.poll, currentPollingTimeout);
|
|
342
|
-
})
|
|
343
|
-
.catch((err) => {
|
|
344
|
-
this.emit('error', err);
|
|
345
|
-
});
|
|
349
|
+
try {
|
|
350
|
+
return await this.sqs.send(
|
|
351
|
+
new ChangeMessageVisibilityBatchCommand(params)
|
|
352
|
+
);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this.emit(
|
|
355
|
+
'error',
|
|
356
|
+
toSQSError(err, `Error changing visibility timeout: ${err.message}`),
|
|
357
|
+
messages
|
|
358
|
+
);
|
|
359
|
+
}
|
|
346
360
|
}
|
|
347
361
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
let heartbeat;
|
|
362
|
+
/**
|
|
363
|
+
* Trigger the applications handleMessage function
|
|
364
|
+
* @param message The message that was received from SQS
|
|
365
|
+
*/
|
|
366
|
+
private async executeHandler(message: Message): Promise<Message> {
|
|
354
367
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
let result;
|
|
369
|
+
|
|
370
|
+
if (this.handleMessageTimeout) {
|
|
371
|
+
const pending = new Promise((_, reject) => {
|
|
372
|
+
this.handleMessageTimeoutId = setTimeout((): void => {
|
|
373
|
+
reject(new TimeoutError());
|
|
374
|
+
}, this.handleMessageTimeout);
|
|
361
375
|
});
|
|
376
|
+
result = await Promise.race([this.handleMessage(message), pending]);
|
|
377
|
+
} else {
|
|
378
|
+
result = await this.handleMessage(message);
|
|
362
379
|
}
|
|
363
|
-
const ackedMessages = await this.executeBatchHandler(messages);
|
|
364
380
|
|
|
365
|
-
|
|
366
|
-
|
|
381
|
+
return result instanceof Object ? result : message;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
err.message =
|
|
384
|
+
err instanceof TimeoutError
|
|
385
|
+
? `Message handler timed out after ${this.handleMessageTimeout}ms: Operation timed out.`
|
|
386
|
+
: `Unexpected message handler failure: ${err.message}`;
|
|
387
|
+
throw err;
|
|
388
|
+
} finally {
|
|
389
|
+
if (this.handleMessageTimeoutId) {
|
|
390
|
+
clearTimeout(this.handleMessageTimeoutId);
|
|
367
391
|
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
368
394
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Execute the application's message batch handler
|
|
397
|
+
* @param messages The messages that should be forwarded from the SQS queue
|
|
398
|
+
*/
|
|
399
|
+
private async executeBatchHandler(messages: Message[]): Promise<Message[]> {
|
|
400
|
+
try {
|
|
401
|
+
const result = await this.handleMessageBatch(messages);
|
|
374
402
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
clearInterval(heartbeat);
|
|
403
|
+
return result instanceof Object ? result : messages;
|
|
404
|
+
} catch (err) {
|
|
405
|
+
err.message = `Unexpected message handler failure: ${err.message}`;
|
|
406
|
+
throw err;
|
|
380
407
|
}
|
|
381
408
|
}
|
|
382
409
|
|
|
383
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Delete a single message from SQS
|
|
412
|
+
* @param message The message to delete from the SQS queue
|
|
413
|
+
*/
|
|
414
|
+
private async deleteMessage(message: Message): Promise<void> {
|
|
384
415
|
if (!this.shouldDeleteMessages) {
|
|
385
416
|
debug(
|
|
386
417
|
'Skipping message delete since shouldDeleteMessages is set to false'
|
|
387
418
|
);
|
|
388
419
|
return;
|
|
389
420
|
}
|
|
390
|
-
debug(
|
|
391
|
-
'Deleting messages %s',
|
|
392
|
-
messages.map((msg) => msg.MessageId).join(' ,')
|
|
393
|
-
);
|
|
421
|
+
debug('Deleting message %s', message.MessageId);
|
|
394
422
|
|
|
395
|
-
const deleteParams:
|
|
423
|
+
const deleteParams: DeleteMessageCommandInput = {
|
|
396
424
|
QueueUrl: this.queueUrl,
|
|
397
|
-
|
|
398
|
-
Id: message.MessageId,
|
|
399
|
-
ReceiptHandle: message.ReceiptHandle
|
|
400
|
-
}))
|
|
425
|
+
ReceiptHandle: message.ReceiptHandle
|
|
401
426
|
};
|
|
402
427
|
|
|
403
428
|
try {
|
|
404
|
-
await this.sqs.send(new
|
|
429
|
+
await this.sqs.send(new DeleteMessageCommand(deleteParams));
|
|
405
430
|
} catch (err) {
|
|
406
431
|
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
407
432
|
}
|
|
408
433
|
}
|
|
409
434
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
err.message = `Unexpected message handler failure: ${err.message}`;
|
|
421
|
-
throw err;
|
|
435
|
+
/**
|
|
436
|
+
* Delete a batch of messages from the SQS queue.
|
|
437
|
+
* @param messages The messages that should be deleted from SQS
|
|
438
|
+
*/
|
|
439
|
+
private async deleteMessageBatch(messages: Message[]): Promise<void> {
|
|
440
|
+
if (!this.shouldDeleteMessages) {
|
|
441
|
+
debug(
|
|
442
|
+
'Skipping message delete since shouldDeleteMessages is set to false'
|
|
443
|
+
);
|
|
444
|
+
return;
|
|
422
445
|
}
|
|
423
|
-
|
|
446
|
+
debug(
|
|
447
|
+
'Deleting messages %s',
|
|
448
|
+
messages.map((msg) => msg.MessageId).join(' ,')
|
|
449
|
+
);
|
|
424
450
|
|
|
425
|
-
|
|
426
|
-
messages: Message[],
|
|
427
|
-
timeout: number
|
|
428
|
-
): Promise<ChangeMessageVisibilityBatchCommandOutput> {
|
|
429
|
-
const params: ChangeMessageVisibilityBatchCommandInput = {
|
|
451
|
+
const deleteParams: DeleteMessageBatchCommandInput = {
|
|
430
452
|
QueueUrl: this.queueUrl,
|
|
431
453
|
Entries: messages.map((message) => ({
|
|
432
454
|
Id: message.MessageId,
|
|
433
|
-
ReceiptHandle: message.ReceiptHandle
|
|
434
|
-
VisibilityTimeout: timeout
|
|
455
|
+
ReceiptHandle: message.ReceiptHandle
|
|
435
456
|
}))
|
|
436
457
|
};
|
|
458
|
+
|
|
437
459
|
try {
|
|
438
|
-
|
|
439
|
-
new ChangeMessageVisibilityBatchCommand(params)
|
|
440
|
-
);
|
|
460
|
+
await this.sqs.send(new DeleteMessageBatchCommand(deleteParams));
|
|
441
461
|
} catch (err) {
|
|
442
|
-
|
|
443
|
-
'error',
|
|
444
|
-
toSQSError(err, `Error changing visibility timeout: ${err.message}`),
|
|
445
|
-
messages
|
|
446
|
-
);
|
|
462
|
+
throw toSQSError(err, `SQS delete message failed: ${err.message}`);
|
|
447
463
|
}
|
|
448
464
|
}
|
|
449
|
-
|
|
450
|
-
private startHeartbeat(heartbeatFn: () => void): NodeJS.Timeout {
|
|
451
|
-
return setInterval(() => {
|
|
452
|
-
heartbeatFn();
|
|
453
|
-
}, this.heartbeatInterval * 1000);
|
|
454
|
-
}
|
|
455
465
|
}
|