sqs-consumer 6.0.0 → 6.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/dist/bind.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function autoBind(obj: object): void;
package/dist/bind.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoBind = void 0;
4
+ function isMethod(propertyName, value) {
5
+ return propertyName !== 'constructor' && typeof value === 'function';
6
+ }
7
+ function autoBind(obj) {
8
+ const propertyNames = Object.getOwnPropertyNames(obj.constructor.prototype);
9
+ propertyNames.forEach((propertyName) => {
10
+ const value = obj[propertyName];
11
+ if (isMethod(propertyName, value)) {
12
+ obj[propertyName] = value.bind(obj);
13
+ }
14
+ });
15
+ }
16
+ exports.autoBind = autoBind;
@@ -0,0 +1,72 @@
1
+ /// <reference types="node" />
2
+ import { SQSClient, Message } from '@aws-sdk/client-sqs';
3
+ import { EventEmitter } from 'events';
4
+ export interface ConsumerOptions {
5
+ queueUrl?: string;
6
+ attributeNames?: string[];
7
+ messageAttributeNames?: string[];
8
+ stopped?: boolean;
9
+ batchSize?: number;
10
+ visibilityTimeout?: number;
11
+ waitTimeSeconds?: number;
12
+ authenticationErrorTimeout?: number;
13
+ pollingWaitTimeMs?: number;
14
+ terminateVisibilityTimeout?: boolean;
15
+ heartbeatInterval?: number;
16
+ sqs?: SQSClient;
17
+ region?: string;
18
+ handleMessageTimeout?: number;
19
+ shouldDeleteMessages?: boolean;
20
+ handleMessage?(message: Message): Promise<void>;
21
+ handleMessageBatch?(messages: Message[]): Promise<void>;
22
+ }
23
+ interface Events {
24
+ response_processed: [];
25
+ empty: [];
26
+ message_received: [Message];
27
+ message_processed: [Message];
28
+ error: [Error, void | Message | Message[]];
29
+ timeout_error: [Error, Message];
30
+ processing_error: [Error, Message];
31
+ stopped: [];
32
+ }
33
+ export declare class Consumer extends EventEmitter {
34
+ private queueUrl;
35
+ private handleMessage;
36
+ private handleMessageBatch;
37
+ private handleMessageTimeout;
38
+ private attributeNames;
39
+ private messageAttributeNames;
40
+ private stopped;
41
+ private batchSize;
42
+ private visibilityTimeout;
43
+ private waitTimeSeconds;
44
+ private authenticationErrorTimeout;
45
+ private pollingWaitTimeMs;
46
+ private terminateVisibilityTimeout;
47
+ private heartbeatInterval;
48
+ private sqs;
49
+ private shouldDeleteMessages;
50
+ constructor(options: ConsumerOptions);
51
+ emit<T extends keyof Events>(event: T, ...args: Events[T]): boolean;
52
+ on<T extends keyof Events>(event: T, listener: (...args: Events[T]) => void): this;
53
+ once<T extends keyof Events>(event: T, listener: (...args: Events[T]) => void): this;
54
+ get isRunning(): boolean;
55
+ static create(options: ConsumerOptions): Consumer;
56
+ start(): void;
57
+ stop(): void;
58
+ private handleSqsResponse;
59
+ private processMessage;
60
+ private receiveMessage;
61
+ private deleteMessage;
62
+ private executeHandler;
63
+ private changeVisibilityTimeout;
64
+ private emitError;
65
+ private poll;
66
+ private processMessageBatch;
67
+ private deleteMessageBatch;
68
+ private executeBatchHandler;
69
+ private changeVisibilityTimeoutBatch;
70
+ private startHeartbeat;
71
+ }
72
+ export {};
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Consumer = void 0;
4
+ const client_sqs_1 = require("@aws-sdk/client-sqs");
5
+ const debug_1 = require("debug");
6
+ const events_1 = require("events");
7
+ const bind_1 = require("./bind");
8
+ const errors_1 = require("./errors");
9
+ const debug = (0, debug_1.default)('sqs-consumer');
10
+ const requiredOptions = [
11
+ 'queueUrl',
12
+ // only one of handleMessage / handleMessagesBatch is required
13
+ 'handleMessage|handleMessageBatch'
14
+ ];
15
+ function createTimeout(duration) {
16
+ let timeout;
17
+ const pending = new Promise((_, reject) => {
18
+ timeout = setTimeout(() => {
19
+ reject(new errors_1.TimeoutError());
20
+ }, duration);
21
+ });
22
+ return [timeout, pending];
23
+ }
24
+ function assertOptions(options) {
25
+ requiredOptions.forEach((option) => {
26
+ const possibilities = option.split('|');
27
+ if (!possibilities.find((p) => options[p])) {
28
+ throw new Error(`Missing SQS consumer option [ ${possibilities.join(' or ')} ].`);
29
+ }
30
+ });
31
+ if (options.batchSize > 10 || options.batchSize < 1) {
32
+ throw new Error('SQS batchSize option must be between 1 and 10.');
33
+ }
34
+ if (options.heartbeatInterval &&
35
+ !(options.heartbeatInterval < options.visibilityTimeout)) {
36
+ throw new Error('heartbeatInterval must be less than visibilityTimeout.');
37
+ }
38
+ }
39
+ function isConnectionError(err) {
40
+ if (err instanceof errors_1.SQSError) {
41
+ return (err.statusCode === 403 ||
42
+ err.code === 'CredentialsError' ||
43
+ err.code === 'UnknownEndpoint');
44
+ }
45
+ return false;
46
+ }
47
+ function toSQSError(err, message) {
48
+ var _a, _b;
49
+ const sqsError = new errors_1.SQSError(message);
50
+ sqsError.code = err.name;
51
+ sqsError.statusCode = (_a = err.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode;
52
+ sqsError.retryable = (_b = err.$retryable) === null || _b === void 0 ? void 0 : _b.throttling;
53
+ sqsError.service = err.$service;
54
+ sqsError.fault = err.$fault;
55
+ sqsError.time = new Date();
56
+ return sqsError;
57
+ }
58
+ function hasMessages(response) {
59
+ return response.Messages && response.Messages.length > 0;
60
+ }
61
+ class Consumer extends events_1.EventEmitter {
62
+ constructor(options) {
63
+ var _a, _b, _c, _d;
64
+ super();
65
+ assertOptions(options);
66
+ this.queueUrl = options.queueUrl;
67
+ this.handleMessage = options.handleMessage;
68
+ this.handleMessageBatch = options.handleMessageBatch;
69
+ this.handleMessageTimeout = options.handleMessageTimeout;
70
+ this.attributeNames = options.attributeNames || [];
71
+ this.messageAttributeNames = options.messageAttributeNames || [];
72
+ this.stopped = true;
73
+ this.batchSize = options.batchSize || 1;
74
+ this.visibilityTimeout = options.visibilityTimeout;
75
+ this.terminateVisibilityTimeout =
76
+ options.terminateVisibilityTimeout || false;
77
+ this.heartbeatInterval = options.heartbeatInterval;
78
+ this.waitTimeSeconds = (_a = options.waitTimeSeconds) !== null && _a !== void 0 ? _a : 20;
79
+ this.authenticationErrorTimeout =
80
+ (_b = options.authenticationErrorTimeout) !== null && _b !== void 0 ? _b : 10000;
81
+ this.pollingWaitTimeMs = (_c = options.pollingWaitTimeMs) !== null && _c !== void 0 ? _c : 0;
82
+ this.shouldDeleteMessages = (_d = options.shouldDeleteMessages) !== null && _d !== void 0 ? _d : true;
83
+ this.sqs =
84
+ options.sqs ||
85
+ new client_sqs_1.SQSClient({
86
+ region: options.region || process.env.AWS_REGION || 'eu-west-1'
87
+ });
88
+ (0, bind_1.autoBind)(this);
89
+ }
90
+ emit(event, ...args) {
91
+ return super.emit(event, ...args);
92
+ }
93
+ on(event, listener) {
94
+ return super.on(event, listener);
95
+ }
96
+ once(event, listener) {
97
+ return super.once(event, listener);
98
+ }
99
+ get isRunning() {
100
+ return !this.stopped;
101
+ }
102
+ static create(options) {
103
+ return new Consumer(options);
104
+ }
105
+ start() {
106
+ if (this.stopped) {
107
+ debug('Starting consumer');
108
+ this.stopped = false;
109
+ this.poll();
110
+ }
111
+ }
112
+ stop() {
113
+ debug('Stopping consumer');
114
+ this.stopped = true;
115
+ }
116
+ async handleSqsResponse(response) {
117
+ debug('Received SQS response');
118
+ debug(response);
119
+ if (response) {
120
+ if (hasMessages(response)) {
121
+ if (this.handleMessageBatch) {
122
+ // prefer handling messages in batch when available
123
+ await this.processMessageBatch(response.Messages);
124
+ }
125
+ else {
126
+ await Promise.all(response.Messages.map(this.processMessage));
127
+ }
128
+ this.emit('response_processed');
129
+ }
130
+ else {
131
+ this.emit('empty');
132
+ }
133
+ }
134
+ }
135
+ async processMessage(message) {
136
+ this.emit('message_received', message);
137
+ let heartbeat;
138
+ try {
139
+ if (this.heartbeatInterval) {
140
+ heartbeat = this.startHeartbeat(async () => {
141
+ return this.changeVisibilityTimeout(message, this.visibilityTimeout);
142
+ });
143
+ }
144
+ await this.executeHandler(message);
145
+ await this.deleteMessage(message);
146
+ this.emit('message_processed', message);
147
+ }
148
+ catch (err) {
149
+ this.emitError(err, message);
150
+ if (this.terminateVisibilityTimeout) {
151
+ await this.changeVisibilityTimeout(message, 0);
152
+ }
153
+ }
154
+ finally {
155
+ clearInterval(heartbeat);
156
+ }
157
+ }
158
+ async receiveMessage(params) {
159
+ try {
160
+ return await this.sqs.send(new client_sqs_1.ReceiveMessageCommand(params));
161
+ }
162
+ catch (err) {
163
+ throw toSQSError(err, `SQS receive message failed: ${err.message}`);
164
+ }
165
+ }
166
+ async deleteMessage(message) {
167
+ if (!this.shouldDeleteMessages) {
168
+ debug('Skipping message delete since shouldDeleteMessages is set to false');
169
+ return;
170
+ }
171
+ debug('Deleting message %s', message.MessageId);
172
+ const deleteParams = {
173
+ QueueUrl: this.queueUrl,
174
+ ReceiptHandle: message.ReceiptHandle
175
+ };
176
+ try {
177
+ await this.sqs.send(new client_sqs_1.DeleteMessageCommand(deleteParams));
178
+ }
179
+ catch (err) {
180
+ throw toSQSError(err, `SQS delete message failed: ${err.message}`);
181
+ }
182
+ }
183
+ async executeHandler(message) {
184
+ let timeout;
185
+ let pending;
186
+ try {
187
+ if (this.handleMessageTimeout) {
188
+ [timeout, pending] = createTimeout(this.handleMessageTimeout);
189
+ await Promise.race([this.handleMessage(message), pending]);
190
+ }
191
+ else {
192
+ await this.handleMessage(message);
193
+ }
194
+ }
195
+ catch (err) {
196
+ if (err instanceof errors_1.TimeoutError) {
197
+ err.message = `Message handler timed out after ${this.handleMessageTimeout}ms: Operation timed out.`;
198
+ }
199
+ else if (err instanceof Error) {
200
+ err.message = `Unexpected message handler failure: ${err.message}`;
201
+ }
202
+ throw err;
203
+ }
204
+ finally {
205
+ clearTimeout(timeout);
206
+ }
207
+ }
208
+ async changeVisibilityTimeout(message, timeout) {
209
+ try {
210
+ const input = {
211
+ QueueUrl: this.queueUrl,
212
+ ReceiptHandle: message.ReceiptHandle,
213
+ VisibilityTimeout: timeout
214
+ };
215
+ return await this.sqs.send(new client_sqs_1.ChangeMessageVisibilityCommand(input));
216
+ }
217
+ catch (err) {
218
+ this.emit('error', toSQSError(err, `Error changing visibility timeout: ${err.message}`), message);
219
+ }
220
+ }
221
+ emitError(err, message) {
222
+ if (err.name === errors_1.SQSError.name) {
223
+ this.emit('error', err, message);
224
+ }
225
+ else if (err instanceof errors_1.TimeoutError) {
226
+ this.emit('timeout_error', err, message);
227
+ }
228
+ else {
229
+ this.emit('processing_error', err, message);
230
+ }
231
+ }
232
+ poll() {
233
+ if (this.stopped) {
234
+ this.emit('stopped');
235
+ return;
236
+ }
237
+ debug('Polling for messages');
238
+ const receiveParams = {
239
+ QueueUrl: this.queueUrl,
240
+ AttributeNames: this.attributeNames,
241
+ MessageAttributeNames: this.messageAttributeNames,
242
+ MaxNumberOfMessages: this.batchSize,
243
+ WaitTimeSeconds: this.waitTimeSeconds,
244
+ VisibilityTimeout: this.visibilityTimeout
245
+ };
246
+ let currentPollingTimeout = this.pollingWaitTimeMs;
247
+ this.receiveMessage(receiveParams)
248
+ .then(this.handleSqsResponse)
249
+ .catch((err) => {
250
+ this.emit('error', err);
251
+ if (isConnectionError(err)) {
252
+ debug('There was an authentication error. Pausing before retrying.');
253
+ currentPollingTimeout = this.authenticationErrorTimeout;
254
+ }
255
+ return;
256
+ })
257
+ .then(() => {
258
+ setTimeout(this.poll, currentPollingTimeout);
259
+ })
260
+ .catch((err) => {
261
+ this.emit('error', err);
262
+ });
263
+ }
264
+ async processMessageBatch(messages) {
265
+ messages.forEach((message) => {
266
+ this.emit('message_received', message);
267
+ });
268
+ let heartbeat;
269
+ try {
270
+ if (this.heartbeatInterval) {
271
+ heartbeat = this.startHeartbeat(async () => {
272
+ return this.changeVisibilityTimeoutBatch(messages, this.visibilityTimeout);
273
+ });
274
+ }
275
+ await this.executeBatchHandler(messages);
276
+ await this.deleteMessageBatch(messages);
277
+ messages.forEach((message) => {
278
+ this.emit('message_processed', message);
279
+ });
280
+ }
281
+ catch (err) {
282
+ this.emit('error', err, messages);
283
+ if (this.terminateVisibilityTimeout) {
284
+ await this.changeVisibilityTimeoutBatch(messages, 0);
285
+ }
286
+ }
287
+ finally {
288
+ clearInterval(heartbeat);
289
+ }
290
+ }
291
+ async deleteMessageBatch(messages) {
292
+ if (!this.shouldDeleteMessages) {
293
+ debug('Skipping message delete since shouldDeleteMessages is set to false');
294
+ return;
295
+ }
296
+ debug('Deleting messages %s', messages.map((msg) => msg.MessageId).join(' ,'));
297
+ const deleteParams = {
298
+ QueueUrl: this.queueUrl,
299
+ Entries: messages.map((message) => ({
300
+ Id: message.MessageId,
301
+ ReceiptHandle: message.ReceiptHandle
302
+ }))
303
+ };
304
+ try {
305
+ await this.sqs.send(new client_sqs_1.DeleteMessageBatchCommand(deleteParams));
306
+ }
307
+ catch (err) {
308
+ throw toSQSError(err, `SQS delete message failed: ${err.message}`);
309
+ }
310
+ }
311
+ async executeBatchHandler(messages) {
312
+ try {
313
+ await this.handleMessageBatch(messages);
314
+ }
315
+ catch (err) {
316
+ err.message = `Unexpected message handler failure: ${err.message}`;
317
+ throw err;
318
+ }
319
+ }
320
+ async changeVisibilityTimeoutBatch(messages, timeout) {
321
+ const params = {
322
+ QueueUrl: this.queueUrl,
323
+ Entries: messages.map((message) => ({
324
+ Id: message.MessageId,
325
+ ReceiptHandle: message.ReceiptHandle,
326
+ VisibilityTimeout: timeout
327
+ }))
328
+ };
329
+ try {
330
+ return await this.sqs.send(new client_sqs_1.ChangeMessageVisibilityBatchCommand(params));
331
+ }
332
+ catch (err) {
333
+ this.emit('error', toSQSError(err, `Error changing visibility timeout: ${err.message}`), messages);
334
+ }
335
+ }
336
+ startHeartbeat(heartbeatFn) {
337
+ return setInterval(() => {
338
+ heartbeatFn();
339
+ }, this.heartbeatInterval * 1000);
340
+ }
341
+ }
342
+ exports.Consumer = Consumer;
@@ -0,0 +1,13 @@
1
+ declare class SQSError extends Error {
2
+ code: string;
3
+ statusCode: number;
4
+ service: string;
5
+ time: Date;
6
+ retryable: boolean;
7
+ fault: 'client' | 'server';
8
+ constructor(message: string);
9
+ }
10
+ declare class TimeoutError extends Error {
11
+ constructor(message?: string);
12
+ }
13
+ export { SQSError, TimeoutError };
package/dist/errors.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimeoutError = exports.SQSError = void 0;
4
+ class SQSError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = this.constructor.name;
8
+ }
9
+ }
10
+ exports.SQSError = SQSError;
11
+ class TimeoutError extends Error {
12
+ constructor(message = 'Operation timed out.') {
13
+ super(message);
14
+ this.message = message;
15
+ this.name = 'TimeoutError';
16
+ }
17
+ }
18
+ exports.TimeoutError = TimeoutError;
@@ -0,0 +1 @@
1
+ export { Consumer, ConsumerOptions } from './consumer';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Consumer = void 0;
4
+ var consumer_1 = require("./consumer");
5
+ Object.defineProperty(exports, "Consumer", { enumerable: true, get: function () { return consumer_1.Consumer; } });
@@ -0,0 +1,59 @@
1
+ export type AWSError = {
2
+ /**
3
+ * Name, eg. ConditionalCheckFailedException
4
+ */
5
+ name: string;
6
+ /**
7
+ * Human-readable error response message
8
+ */
9
+ message: string;
10
+ /**
11
+ * Non-standard stacktrace
12
+ */
13
+ stack?: string;
14
+ /**
15
+ * Whether the client or server are at fault.
16
+ */
17
+ readonly $fault?: 'client' | 'server';
18
+ /**
19
+ * The service that encountered the exception.
20
+ */
21
+ readonly $service?: string;
22
+ /**
23
+ * Indicates that an error MAY be retried by the client.
24
+ */
25
+ readonly $retryable?: {
26
+ /**
27
+ * Indicates that the error is a retryable throttling error.
28
+ */
29
+ readonly throttling?: boolean;
30
+ };
31
+ $metadata?: {
32
+ /**
33
+ * The status code of the last HTTP response received for this operation.
34
+ */
35
+ httpStatusCode?: number;
36
+ /**
37
+ * A unique identifier for the last request sent for this operation. Often
38
+ * requested by AWS service teams to aid in debugging.
39
+ */
40
+ requestId?: string;
41
+ /**
42
+ * A secondary identifier for the last request sent. Used for debugging.
43
+ */
44
+ extendedRequestId?: string;
45
+ /**
46
+ * A tertiary identifier for the last request sent. Used for debugging.
47
+ */
48
+ cfId?: string;
49
+ /**
50
+ * The number of times this operation was attempted.
51
+ */
52
+ attempts?: number;
53
+ /**
54
+ * The total amount of time (in milliseconds) that was spent waiting between
55
+ * retry attempts.
56
+ */
57
+ totalRetryDelay?: number;
58
+ };
59
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqs-consumer",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "description": "Build SQS-based Node applications without the boilerplate",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "build": "npm run clean && tsc",
9
9
  "watch": "tsc --watch",
10
10
  "clean": "rm -fr dist/*",
11
- "prepublish": "npm run build",
11
+ "prepare": "npm run build",
12
12
  "pretest": "npm run build",
13
13
  "test": "mocha --recursive --full-trace --exit",
14
14
  "coverage": "nyc mocha && nyc report --reporter=html && nyc report --reporter=json-summary",