qdone 2.1.1 → 2.2.1

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.
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.setCache = exports.getCache = exports.shutdownCache = exports.getCacheClient = void 0;
6
+ exports.getCacheClient = getCacheClient;
7
+ exports.shutdownCache = shutdownCache;
8
+ exports.getCache = getCache;
9
+ exports.setCache = setCache;
7
10
  const ioredis_1 = __importDefault(require("ioredis"));
8
11
  const url_1 = require("url");
9
12
  const debug_1 = __importDefault(require("debug"));
@@ -38,13 +41,11 @@ function getCacheClient(opt) {
38
41
  throw new UsageError('Caching requires the --cache-uri option');
39
42
  }
40
43
  }
41
- exports.getCacheClient = getCacheClient;
42
44
  function shutdownCache() {
43
45
  if (client)
44
46
  client.quit();
45
47
  client = undefined;
46
48
  }
47
- exports.shutdownCache = shutdownCache;
48
49
  /**
49
50
  * Returns a promise for the item. Resolves to false if cache is empty, object
50
51
  * if it is found.
@@ -57,7 +58,6 @@ async function getCache(key, opt) {
57
58
  debug({ action: 'getCache got', cacheKey, result });
58
59
  return result ? JSON.parse(result) : undefined;
59
60
  }
60
- exports.getCache = getCache;
61
61
  /**
62
62
  * Returns a promise for the status. Encodes object as JSON
63
63
  */
@@ -68,5 +68,4 @@ async function setCache(key, value, opt) {
68
68
  debug({ action: 'setCache', cacheKey, value });
69
69
  return client.setex(cacheKey, opt.cacheTtlSeconds, encoded);
70
70
  }
71
- exports.setCache = setCache;
72
71
  debug('loaded');
@@ -6,7 +6,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.putAggregateData = exports.setCloudWatchClient = exports.getCloudWatchClient = void 0;
9
+ exports.getCloudWatchClient = getCloudWatchClient;
10
+ exports.setCloudWatchClient = setCloudWatchClient;
11
+ exports.putAggregateData = putAggregateData;
10
12
  const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
11
13
  const debug_1 = __importDefault(require("debug"));
12
14
  const debug = (0, debug_1.default)('qdone:cloudWatch');
@@ -20,14 +22,12 @@ function getCloudWatchClient() {
20
22
  client = new client_cloudwatch_1.CloudWatchClient();
21
23
  return client;
22
24
  }
23
- exports.getCloudWatchClient = getCloudWatchClient;
24
25
  /**
25
26
  * Utility function to set the client explicitly, used in testing.
26
27
  */
27
28
  function setCloudWatchClient(explicitClient) {
28
29
  client = explicitClient;
29
30
  }
30
- exports.setCloudWatchClient = setCloudWatchClient;
31
31
  /**
32
32
  * Takes data in the form returned by getAggregageData() and pushes it to
33
33
  * CloudWatch metrics under the given queueName.
@@ -90,6 +90,16 @@ async function putAggregateData(total, timestamp) {
90
90
  Timestamp: now,
91
91
  Value: total.ApproximateNumberOfMessagesNotVisible || 0,
92
92
  Unit: 'Count'
93
+ },
94
+ {
95
+ MetricName: 'ApproximateAgeOfOldestMessage',
96
+ Dimensions: [{
97
+ Name: 'queueName',
98
+ Value: total.queueName
99
+ }],
100
+ Timestamp: now,
101
+ Value: total.ApproximateAgeOfOldestMessage || 0,
102
+ Unit: 'Seconds'
93
103
  }
94
104
  ]
95
105
  };
@@ -98,5 +108,4 @@ async function putAggregateData(total, timestamp) {
98
108
  const response = await client.send(command);
99
109
  debug({ response });
100
110
  }
101
- exports.putAggregateData = putAggregateData;
102
111
  debug('loaded');
@@ -6,7 +6,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.processMessages = exports.getMessages = exports.requestShutdown = void 0;
9
+ exports.requestShutdown = requestShutdown;
10
+ exports.getMessages = getMessages;
11
+ exports.processMessages = processMessages;
10
12
  const os_1 = require("os");
11
13
  const client_sqs_1 = require("@aws-sdk/client-sqs");
12
14
  const chalk_1 = __importDefault(require("chalk"));
@@ -30,7 +32,6 @@ async function requestShutdown() {
30
32
  }
31
33
  debug('requestShutdown done');
32
34
  }
33
- exports.requestShutdown = requestShutdown;
34
35
  async function getMessages(qrl, opt, maxMessages) {
35
36
  const params = {
36
37
  AttributeNames: ['All'],
@@ -44,7 +45,6 @@ async function getMessages(qrl, opt, maxMessages) {
44
45
  // debug('ReceiveMessage response', response)
45
46
  return response.Messages || [];
46
47
  }
47
- exports.getMessages = getMessages;
48
48
  //
49
49
  // Consumer
50
50
  //
@@ -170,4 +170,3 @@ async function processMessages(queues, callback, options) {
170
170
  }
171
171
  debug('after all');
172
172
  }
173
- exports.processMessages = processMessages;
@@ -3,7 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.dedupSuccessfullyProcessedMulti = exports.dedupSuccessfullyProcessed = exports.dedupShouldEnqueueMulti = exports.dedupShouldEnqueue = exports.statMaintenance = exports.updateStats = exports.addDedupParamsToMessage = exports.getCacheKey = exports.getDeduplicationId = void 0;
6
+ exports.getDeduplicationId = getDeduplicationId;
7
+ exports.getCacheKey = getCacheKey;
8
+ exports.addDedupParamsToMessage = addDedupParamsToMessage;
9
+ exports.updateStats = updateStats;
10
+ exports.statMaintenance = statMaintenance;
11
+ exports.dedupShouldEnqueue = dedupShouldEnqueue;
12
+ exports.dedupShouldEnqueueMulti = dedupShouldEnqueueMulti;
13
+ exports.dedupSuccessfullyProcessed = dedupSuccessfullyProcessed;
14
+ exports.dedupSuccessfullyProcessedMulti = dedupSuccessfullyProcessedMulti;
7
15
  const crypto_1 = require("crypto");
8
16
  const uuid_1 = require("uuid");
9
17
  const cache_js_1 = require("./cache.js");
@@ -30,7 +38,6 @@ function getDeduplicationId(dedupContent, opt) {
30
38
  const id = `sha1:{${hash}}:body:${truncated}`;
31
39
  return id;
32
40
  }
33
- exports.getDeduplicationId = getDeduplicationId;
34
41
  /**
35
42
  * Returns the cache key given a deduplication id.
36
43
  * @param {String} dedupId - a deduplication id returned from getDeduplicationId
@@ -42,7 +49,6 @@ function getCacheKey(dedupId, opt) {
42
49
  debug({ getCacheKey: { cacheKey } });
43
50
  return cacheKey;
44
51
  }
45
- exports.getCacheKey = getCacheKey;
46
52
  /**
47
53
  * Modifies a message (parameters to SendMessageCommand) to add the parameters
48
54
  * for whatever deduplication options the caller has set.
@@ -88,7 +94,6 @@ function addDedupParamsToMessage(message, opt, messageOptions) {
88
94
  }
89
95
  return message;
90
96
  }
91
- exports.addDedupParamsToMessage = addDedupParamsToMessage;
92
97
  /**
93
98
  * Updates statistics in redis, of which there are two:
94
99
  * 1. duplicateSet - a set who's members are cache keys and scores are the number of duplicate
@@ -113,7 +118,6 @@ async function updateStats(cacheKey, duplicates, expireAt, opt, pipeline) {
113
118
  await pipeline.exec();
114
119
  }
115
120
  }
116
- exports.updateStats = updateStats;
117
121
  /**
118
122
  * Removes expired items from stats.
119
123
  */
@@ -135,7 +139,6 @@ async function statMaintenance(opt) {
135
139
  debug({ statMaintenance: { result } });
136
140
  }
137
141
  }
138
- exports.statMaintenance = statMaintenance;
139
142
  /**
140
143
  * Determines whether we should enqueue this message or whether it is a duplicate.
141
144
  * Returns true if enqueuing the message would not result in a duplicate.
@@ -160,7 +163,6 @@ async function dedupShouldEnqueue(message, opt) {
160
163
  }
161
164
  return false;
162
165
  }
163
- exports.dedupShouldEnqueue = dedupShouldEnqueue;
164
166
  /**
165
167
  * Determines which messages we should enqueue, returning only those that
166
168
  * would not be duplicates.
@@ -203,7 +205,6 @@ async function dedupShouldEnqueueMulti(messages, opt) {
203
205
  await statsPipeline.exec();
204
206
  return messagesToEnqueue;
205
207
  }
206
- exports.dedupShouldEnqueueMulti = dedupShouldEnqueueMulti;
207
208
  /**
208
209
  * Marks a message as processed so that subsequent calls to dedupShouldEnqueue
209
210
  * and dedupShouldEnqueueMulti will allow a message to be enqueued again
@@ -229,7 +230,6 @@ async function dedupSuccessfullyProcessed(message, opt) {
229
230
  }
230
231
  return 0;
231
232
  }
232
- exports.dedupSuccessfullyProcessed = dedupSuccessfullyProcessed;
233
233
  /**
234
234
  * Marks an array of messages as processed so that subsequent calls to
235
235
  * dedupShouldEnqueue and dedupShouldEnqueueMulti will allow a message to be
@@ -263,4 +263,3 @@ async function dedupSuccessfullyProcessedMulti(messages, opt) {
263
263
  }
264
264
  return 0;
265
265
  }
266
- exports.dedupSuccessfullyProcessedMulti = dedupSuccessfullyProcessedMulti;
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupVerbose = exports.setupAWS = exports.getOptionsWithDefaults = exports.validateMessageOptions = exports.validateQueueName = exports.validateInteger = exports.defaults = void 0;
3
+ exports.defaults = void 0;
4
+ exports.validateInteger = validateInteger;
5
+ exports.validateQueueName = validateQueueName;
6
+ exports.validateMessageOptions = validateMessageOptions;
7
+ exports.getOptionsWithDefaults = getOptionsWithDefaults;
8
+ exports.setupAWS = setupAWS;
9
+ exports.setupVerbose = setupVerbose;
4
10
  /**
5
11
  * Default options for qdone. Accepts a command line options object and
6
12
  * returns nicely-named options.
@@ -56,7 +62,6 @@ function validateInteger(opt, name) {
56
62
  throw new Error(`${name} needs to be an integer.`);
57
63
  return parsed;
58
64
  }
59
- exports.validateInteger = validateInteger;
60
65
  function validateQueueName(opt, name) {
61
66
  if (typeof name !== 'string')
62
67
  throw new Error(`${name} must be a string.`);
@@ -64,9 +69,8 @@ function validateQueueName(opt, name) {
64
69
  throw new Error(`${name} can contain only numbers, letters, hypens and underscores.`);
65
70
  return name;
66
71
  }
67
- exports.validateQueueName = validateQueueName;
68
72
  function validateMessageOptions(messageOptions) {
69
- const validKeys = ['deduplicationId', 'groupId'];
73
+ const validKeys = ['deduplicationId', 'groupId', 'delay'];
70
74
  if (typeof messageOptions === 'object' &&
71
75
  !Array.isArray(messageOptions) &&
72
76
  messageOptions !== null) {
@@ -78,7 +82,6 @@ function validateMessageOptions(messageOptions) {
78
82
  }
79
83
  return {};
80
84
  }
81
- exports.validateMessageOptions = validateMessageOptions;
82
85
  /**
83
86
  * This function should be called by each exposed API entry point on the
84
87
  * options passed in from the caller. It supports options named in camelCase
@@ -137,7 +140,9 @@ function getOptionsWithDefaults(options) {
137
140
  delete: options.delete || process.env.QDONE_DELETE === 'true' || exports.defaults.delete,
138
141
  // Check
139
142
  create: options.create || process.env.QDONE_CREATE === 'true' || exports.defaults.create,
140
- overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || exports.defaults.overwrite
143
+ overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || exports.defaults.overwrite,
144
+ // Dependency injection
145
+ Redis: options.Redis
141
146
  };
142
147
  // Setting this env here means we don't have to in AWS SDK constructors
143
148
  process.env.AWS_REGION = opt.region;
@@ -167,16 +172,13 @@ function getOptionsWithDefaults(options) {
167
172
  throw new Error('Use either --deduplication-id or --dedup-id-per-message but not both');
168
173
  return opt;
169
174
  }
170
- exports.getOptionsWithDefaults = getOptionsWithDefaults;
171
175
  function setupAWS(options) {
172
176
  const opt = getOptionsWithDefaults(options);
173
177
  process.env.AWS_REGION = opt.region;
174
178
  }
175
- exports.setupAWS = setupAWS;
176
179
  function setupVerbose(options) {
177
180
  const verbose = options.verbose || (process.stderr.isTTY && !options.quiet);
178
181
  const quiet = options.quiet || (!process.stderr.isTTY && !options.verbose);
179
182
  options.verbose = verbose;
180
183
  options.quiet = quiet;
181
184
  }
182
- exports.setupVerbose = setupVerbose;
@@ -3,7 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.enqueueBatch = exports.enqueue = exports.addMessage = exports.flushMessages = exports.sendMessageBatch = exports.sendMessage = exports.formatMessage = exports.getQueueAttributes = exports.getOrCreateQueue = exports.getQueueParams = exports.getOrCreateFailQueue = exports.getFailParams = exports.getOrCreateDLQ = exports.getDLQParams = void 0;
6
+ exports.getDLQParams = getDLQParams;
7
+ exports.getOrCreateDLQ = getOrCreateDLQ;
8
+ exports.getFailParams = getFailParams;
9
+ exports.getOrCreateFailQueue = getOrCreateFailQueue;
10
+ exports.getQueueParams = getQueueParams;
11
+ exports.getOrCreateQueue = getOrCreateQueue;
12
+ exports.getQueueAttributes = getQueueAttributes;
13
+ exports.formatMessage = formatMessage;
14
+ exports.sendMessage = sendMessage;
15
+ exports.sendMessageBatch = sendMessageBatch;
16
+ exports.flushMessages = flushMessages;
17
+ exports.addMessage = addMessage;
18
+ exports.enqueue = enqueue;
19
+ exports.enqueueBatch = enqueueBatch;
7
20
  const node_1 = require("@sentry/node");
8
21
  const uuid_1 = require("uuid");
9
22
  const chalk_1 = __importDefault(require("chalk"));
@@ -27,7 +40,6 @@ function getDLQParams(queue, opt) {
27
40
  params.Attributes.FifoQueue = 'true';
28
41
  return { dqname, params };
29
42
  }
30
- exports.getDLQParams = getDLQParams;
31
43
  async function getOrCreateDLQ(queue, opt) {
32
44
  debug('getOrCreateDLQ(', queue, ')');
33
45
  const { dqname, params } = getDLQParams(queue, opt);
@@ -51,7 +63,6 @@ async function getOrCreateDLQ(queue, opt) {
51
63
  return dqrl;
52
64
  }
53
65
  }
54
- exports.getOrCreateDLQ = getOrCreateDLQ;
55
66
  /**
56
67
  * Returns the parameters needed for creating a failed queue. If DLQ options
57
68
  * are set, it makes an API call to get this DLQ's ARN.
@@ -81,7 +92,6 @@ async function getFailParams(queue, opt) {
81
92
  params.Attributes.FifoQueue = 'true';
82
93
  return params;
83
94
  }
84
- exports.getFailParams = getFailParams;
85
95
  /**
86
96
  * Returns the qrl for the failed queue for the given queue. Creates the queue
87
97
  * if it does not exist.
@@ -124,7 +134,6 @@ async function getOrCreateFailQueue(queue, opt, doesNotExist) {
124
134
  return fqrl;
125
135
  }
126
136
  }
127
- exports.getOrCreateFailQueue = getOrCreateFailQueue;
128
137
  /**
129
138
  * Returns the parameters needed for creating a queue. If fail options
130
139
  * are set, it makes an API call to get the fail queue's ARN.
@@ -150,7 +159,6 @@ async function getQueueParams(queue, opt) {
150
159
  params.Attributes.FifoQueue = 'true';
151
160
  return params;
152
161
  }
153
- exports.getQueueParams = getQueueParams;
154
162
  /**
155
163
  * Returns a qrl for a queue that either exists or does not
156
164
  */
@@ -190,7 +198,6 @@ async function getOrCreateQueue(queue, opt) {
190
198
  return qrl;
191
199
  }
192
200
  }
193
- exports.getOrCreateQueue = getOrCreateQueue;
194
201
  async function getQueueAttributes(qrl) {
195
202
  debug('getQueueAttributes(', qrl, ')');
196
203
  const client = (0, sqs_js_1.getSQSClient)();
@@ -201,7 +208,6 @@ async function getQueueAttributes(qrl) {
201
208
  debug('GetQueueAttributes returned', data);
202
209
  return data;
203
210
  }
204
- exports.getQueueAttributes = getQueueAttributes;
205
211
  function formatMessage(body, id, opt, messageOptions) {
206
212
  const message = { MessageBody: body };
207
213
  if (typeof id !== 'undefined')
@@ -216,7 +222,6 @@ function formatMessage(body, id, opt, messageOptions) {
216
222
  message.DelaySeconds = messageOptions.delay;
217
223
  return message;
218
224
  }
219
- exports.formatMessage = formatMessage;
220
225
  // Retry happens within the context of the send functions
221
226
  const retryableExceptions = [
222
227
  client_sqs_1.RequestThrottled,
@@ -272,7 +277,6 @@ async function sendMessage(qrl, queue, command, opt, messageOptions) {
272
277
  debug({ sendMessageResult: result });
273
278
  return result;
274
279
  }
275
- exports.sendMessage = sendMessage;
276
280
  async function sendMessageBatch(qrl, queue, messages, opt) {
277
281
  debug('sendMessageBatch(', qrl, messages.map(e => Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' })), ')');
278
282
  const params = { Entries: messages, QueueUrl: qrl };
@@ -350,7 +354,6 @@ async function sendMessageBatch(qrl, queue, messages, opt) {
350
354
  };
351
355
  return backoff.run(send, shouldRetry);
352
356
  }
353
- exports.sendMessageBatch = sendMessageBatch;
354
357
  let requestCount = 0;
355
358
  //
356
359
  // Flushes the internal message buffer for qrl.
@@ -421,7 +424,6 @@ async function flushMessages(qrl, queue, opt, sendBuffer) {
421
424
  }
422
425
  return whileNotEmpty();
423
426
  }
424
- exports.flushMessages = flushMessages;
425
427
  //
426
428
  // Adds a message to the inernal message buffer for the given qrl.
427
429
  // Automaticaly flushes if queue has >= 10 messages.
@@ -438,7 +440,6 @@ async function addMessage(qrl, queue, command, messageIndex, opt, sendBuffer, me
438
440
  }
439
441
  return { numFlushed: 0, results: [] };
440
442
  }
441
- exports.addMessage = addMessage;
442
443
  //
443
444
  // Enqueue a single command
444
445
  // Returns a promise for the SQS API response.
@@ -458,7 +459,6 @@ async function enqueue(queue, command, options) {
458
459
  throw e;
459
460
  }
460
461
  }
461
- exports.enqueue = enqueue;
462
462
  //
463
463
  // Enqueue many commands formatted as an array of {queue: ..., command: ...} pairs.
464
464
  // Returns a promise for the total number of messages enqueued.
@@ -517,5 +517,4 @@ async function enqueueBatch(pairs, options) {
517
517
  throw e;
518
518
  }
519
519
  }
520
- exports.enqueueBatch = enqueueBatch;
521
520
  debug('loaded');
@@ -3,7 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.idleQueues = exports.stripSuffixes = exports.processQueueSet = exports.deleteQueue = exports.checkIdle = exports.getMetric = exports.cheapIdleCheck = exports._cheapIdleCheck = exports.attributeNames = void 0;
6
+ exports.attributeNames = void 0;
7
+ exports._cheapIdleCheck = _cheapIdleCheck;
8
+ exports.cheapIdleCheck = cheapIdleCheck;
9
+ exports.getMetric = getMetric;
10
+ exports.checkIdle = checkIdle;
11
+ exports.deleteQueue = deleteQueue;
12
+ exports.processQueueSet = processQueueSet;
13
+ exports.stripSuffixes = stripSuffixes;
14
+ exports.idleQueues = idleQueues;
7
15
  /**
8
16
  * Implementation of checks and caching of checks to determine if queues are idle.
9
17
  */
@@ -63,7 +71,6 @@ async function _cheapIdleCheck(qname, qrl, opt) {
63
71
  }
64
72
  }
65
73
  }
66
- exports._cheapIdleCheck = _cheapIdleCheck;
67
74
  /**
68
75
  * Gets queue attributes from the SQS api and assesses whether queue is idle
69
76
  * at this immediate moment.
@@ -91,7 +98,6 @@ async function cheapIdleCheck(qname, qrl, opt) {
91
98
  return { result, SQS };
92
99
  }
93
100
  }
94
- exports.cheapIdleCheck = cheapIdleCheck;
95
101
  /**
96
102
  * Gets a single metric from the CloudWatch api.
97
103
  */
@@ -116,7 +122,6 @@ async function getMetric(qname, qrl, metricName, opt) {
116
122
  [metricName]: data.Datapoints.map(d => d.Sum).reduce((a, b) => a + b, 0)
117
123
  };
118
124
  }
119
- exports.getMetric = getMetric;
120
125
  /**
121
126
  * Checks if a single queue is idle. First queries the cheap ($0.40/1M) SQS API and
122
127
  * continues to the expensive ($10/1M) CloudWatch API, checking to make sure there
@@ -171,7 +176,6 @@ async function checkIdle(qname, qrl, opt) {
171
176
  debug('checkIdle stats', stats);
172
177
  return stats;
173
178
  }
174
- exports.checkIdle = checkIdle;
175
179
  /**
176
180
  * Just deletes a queue.
177
181
  */
@@ -186,7 +190,6 @@ async function deleteQueue(qname, qrl, opt) {
186
190
  apiCalls: { SQS: 1, CloudWatch: 0 }
187
191
  };
188
192
  }
189
- exports.deleteQueue = deleteQueue;
190
193
  /**
191
194
  * Processes a queue and its fail and delete queue, treating them as a unit.
192
195
  */
@@ -280,7 +283,6 @@ async function processQueueSet(qname, qrl, opt) {
280
283
  }
281
284
  return result;
282
285
  }
283
- exports.processQueueSet = processQueueSet;
284
286
  //
285
287
  // Strips failed and dlq suffix from a queue name or URL
286
288
  //
@@ -288,7 +290,6 @@ function stripSuffixes(queueName, opt) {
288
290
  const suffixFinder = new RegExp(`(${opt.dlqSuffix}|${opt.failSuffix}){1}(|${qrlCache_js_1.fifoSuffix})$`);
289
291
  return queueName.replace(suffixFinder, '$2');
290
292
  }
291
- exports.stripSuffixes = stripSuffixes;
292
293
  //
293
294
  // Resolve queues for listening loop listen
294
295
  //
@@ -329,5 +330,4 @@ async function idleQueues(queues, options) {
329
330
  // Otherwise, let caller know
330
331
  return 'noQueues';
331
332
  }
332
- exports.idleQueues = idleQueues;
333
333
  debug('loaded');
@@ -6,9 +6,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.getAggregateData = exports.interpretWildcard = exports.monitor = void 0;
9
+ exports.monitor = monitor;
10
+ exports.interpretWildcard = interpretWildcard;
11
+ exports.getQueueAge = getQueueAge;
12
+ exports.getAggregateData = getAggregateData;
10
13
  const sqs_js_1 = require("./sqs.js");
11
14
  const cloudWatch_js_1 = require("./cloudWatch.js");
15
+ const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
12
16
  const defaults_js_1 = require("./defaults.js");
13
17
  const qrlCache_js_1 = require("./qrlCache.js");
14
18
  const debug_1 = __importDefault(require("debug"));
@@ -32,7 +36,6 @@ async function monitor(queue, save, options) {
32
36
  process.stderr.write('done\n');
33
37
  }
34
38
  }
35
- exports.monitor = monitor;
36
39
  /**
37
40
  * Splits a queue name with a single wildcard into prefix and suffix regex.
38
41
  */
@@ -44,13 +47,43 @@ function interpretWildcard(queueName) {
44
47
  // debug({ prefix, suffix, safeSuffix, suffixRegex })
45
48
  return { prefix, suffix, safeSuffix, suffixRegex };
46
49
  }
47
- exports.interpretWildcard = interpretWildcard;
50
+ /**
51
+ * Gets ApproximateAgeOfOldestMessage for a single queue from CloudWatch.
52
+ * This metric is not available via the SQS GetQueueAttributes API.
53
+ */
54
+ async function getQueueAge(queueName) {
55
+ const now = new Date();
56
+ const params = {
57
+ StartTime: new Date(now.getTime() - 1000 * 60 * 5),
58
+ EndTime: now,
59
+ MetricName: 'ApproximateAgeOfOldestMessage',
60
+ Namespace: 'AWS/SQS',
61
+ Period: 300,
62
+ Dimensions: [{ Name: 'QueueName', Value: queueName }],
63
+ Statistics: ['Maximum']
64
+ };
65
+ const client = (0, cloudWatch_js_1.getCloudWatchClient)();
66
+ const cmd = new client_cloudwatch_1.GetMetricStatisticsCommand(params);
67
+ try {
68
+ const data = await client.send(cmd);
69
+ debug('getQueueAge', queueName, data);
70
+ if (!data.Datapoints || data.Datapoints.length === 0)
71
+ return 0;
72
+ return Math.max(...data.Datapoints.map(d => d.Maximum));
73
+ }
74
+ catch (e) {
75
+ debug('getQueueAge error', queueName, e);
76
+ return 0;
77
+ }
78
+ }
48
79
  /**
49
80
  * Aggregates inmportant attributes across queues and reports a summary.
50
- * Attributes:
81
+ * Attributes (from SQS GetQueueAttributes):
51
82
  * - ApproximateNumberOfMessages: Sum
52
83
  * - ApproximateNumberOfMessagesDelayed: Sum
53
84
  * - ApproximateNumberOfMessagesNotVisible: Sum
85
+ * Metrics (from CloudWatch):
86
+ * - ApproximateAgeOfOldestMessage: Max
54
87
  */
55
88
  async function getAggregateData(queueName) {
56
89
  const { prefix, suffixRegex } = interpretWildcard(queueName);
@@ -70,11 +103,14 @@ async function getAggregateData(queueName) {
70
103
  }
71
104
  }
72
105
  }
106
+ // Fetch ApproximateAgeOfOldestMessage from CloudWatch (not available via SQS API)
107
+ // Only query queues with messages to minimize CloudWatch API costs
108
+ const ageResults = await Promise.all([...total.contributingQueueNames].map(queue => getQueueAge(queue)));
109
+ total.ApproximateAgeOfOldestMessage = Math.max(0, ...ageResults);
73
110
  // debug({ total })
74
111
  // convert set to array
75
112
  total.contributingQueueNames = [...total.contributingQueueNames.values()];
76
113
  total.queueName = queueName;
77
114
  return total;
78
115
  }
79
- exports.getAggregateData = getAggregateData;
80
116
  debug('loaded');
@@ -7,7 +7,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.getQnameUrlPairs = exports.getMatchingQueues = exports.ingestQRLs = exports.qrlCacheInvalidate = exports.qrlCacheSet = exports.qrlCacheGet = exports.qrlCacheClear = exports.normalizeDLQName = exports.normalizeFailQueueName = exports.normalizeQueueName = exports.fifoSuffix = void 0;
10
+ exports.fifoSuffix = void 0;
11
+ exports.normalizeQueueName = normalizeQueueName;
12
+ exports.normalizeFailQueueName = normalizeFailQueueName;
13
+ exports.normalizeDLQName = normalizeDLQName;
14
+ exports.qrlCacheClear = qrlCacheClear;
15
+ exports.qrlCacheGet = qrlCacheGet;
16
+ exports.qrlCacheSet = qrlCacheSet;
17
+ exports.qrlCacheInvalidate = qrlCacheInvalidate;
18
+ exports.ingestQRLs = ingestQRLs;
19
+ exports.getMatchingQueues = getMatchingQueues;
20
+ exports.getQnameUrlPairs = getQnameUrlPairs;
11
21
  const url_1 = require("url");
12
22
  const path_1 = require("path");
13
23
  const client_sqs_1 = require("@aws-sdk/client-sqs");
@@ -27,7 +37,6 @@ function normalizeQueueName(qname, opt) {
27
37
  const base = hasFifo ? qname.slice(0, -exports.fifoSuffix.length) : qname;
28
38
  return (needsPrefix ? opt.prefix : '') + base + (needsFifo ? exports.fifoSuffix : '');
29
39
  }
30
- exports.normalizeQueueName = normalizeQueueName;
31
40
  //
32
41
  // Normalizes fail queue name, appending both --fail-suffix and .fifo depending on opt
33
42
  //
@@ -38,7 +47,6 @@ function normalizeFailQueueName(fqname, opt) {
38
47
  const needsFail = !base.endsWith(opt.failSuffix);
39
48
  return base + (needsFail ? opt.failSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
40
49
  }
41
- exports.normalizeFailQueueName = normalizeFailQueueName;
42
50
  //
43
51
  // Normalizes dlq queue name, appending both --dlq-suffix and .fifo depending on opt
44
52
  //
@@ -48,14 +56,12 @@ function normalizeDLQName(dqname, opt) {
48
56
  const needsDead = !base.endsWith(opt.dlqSuffix);
49
57
  return base + (needsDead ? opt.dlqSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
50
58
  }
51
- exports.normalizeDLQName = normalizeDLQName;
52
59
  //
53
60
  // Clear cache
54
61
  //
55
62
  function qrlCacheClear() {
56
63
  qcache.clear();
57
64
  }
58
- exports.qrlCacheClear = qrlCacheClear;
59
65
  //
60
66
  // Get QRL (Queue URL)
61
67
  // Returns a promise for the queue name
@@ -80,7 +86,6 @@ async function qrlCacheGet(qname) {
80
86
  // debug('qcache', Object.keys(qcache), 'get', qname, ' => ', qcache[qname])
81
87
  return qrl;
82
88
  }
83
- exports.qrlCacheGet = qrlCacheGet;
84
89
  //
85
90
  // Set QRL (Queue URL)
86
91
  // Immediately updates the cache
@@ -90,7 +95,6 @@ function qrlCacheSet(qname, qrl) {
90
95
  qcache.set(qname, qrl);
91
96
  // debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
92
97
  }
93
- exports.qrlCacheSet = qrlCacheSet;
94
98
  //
95
99
  // Invalidate cache for given qname
96
100
  //
@@ -98,7 +102,6 @@ function qrlCacheInvalidate(qname) {
98
102
  // debug('qcache', Object.keys(qcache), 'delete', qname, ' (was ', qcache[qname], ')')
99
103
  qcache.delete(qname);
100
104
  }
101
- exports.qrlCacheInvalidate = qrlCacheInvalidate;
102
105
  //
103
106
  // Ingets multiple QRLs
104
107
  // Extracts queue names from an array of QRLs and immediately updates the cache.
@@ -114,7 +117,6 @@ function ingestQRLs(qrls) {
114
117
  }
115
118
  return pairs;
116
119
  }
117
- exports.ingestQRLs = ingestQRLs;
118
120
  /**
119
121
  * Returns qrls for queues matching the given prefix and regex.
120
122
  */
@@ -133,7 +135,6 @@ async function getMatchingQueues(prefix, nextToken) {
133
135
  (qrls || []).push(...await getMatchingQueues(prefix, keepGoing));
134
136
  return qrls || [];
135
137
  }
136
- exports.getMatchingQueues = getMatchingQueues;
137
138
  //
138
139
  // Resolves into a flattened aray of {qname: ..., qrl: ...} objects.
139
140
  //
@@ -168,5 +169,4 @@ async function getQnameUrlPairs(qnames, opt) {
168
169
  const results = await Promise.all(promises);
169
170
  return ([].concat.apply([], results)).filter(r => r);
170
171
  }
171
- exports.getQnameUrlPairs = getQnameUrlPairs;
172
172
  debug('loaded');
@@ -311,7 +311,15 @@ class JobExecutor {
311
311
  this.stats.runningJobs++;
312
312
  this.stats.waitingJobs--;
313
313
  const queue = job.qname.slice(this.opt.prefix.length);
314
- const result = await job.callback(queue, job.payload);
314
+ const attributes = {
315
+ queueName: job.qname,
316
+ messageId: job.message.MessageId || '',
317
+ receiveCount: job.message.Attributes?.ApproximateReceiveCount || '1',
318
+ sentTimestamp: job.message.Attributes?.SentTimestamp || '',
319
+ firstReceiveTimestamp: job.message.Attributes?.ApproximateFirstReceiveTimestamp || '',
320
+ messageGroupId: job.message.Attributes?.MessageGroupId || ''
321
+ };
322
+ const result = await job.callback(queue, job.payload, attributes);
315
323
  debug('executeJob callback finished', { payload: job.payload, result });
316
324
  if (this.opt.verbose) {
317
325
  console.error(chalk_1.default.green('SUCCESS'), job.payload);
@@ -369,7 +377,7 @@ class JobExecutor {
369
377
  for (const [job, i] of jobs.map((job, i) => [job, i])) {
370
378
  // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
371
379
  const nextJob = jobs[i + 1];
372
- const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId;
380
+ const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId;
373
381
  // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
374
382
  // Execute serial or parallel
375
383
  if (nextJobIsSerial)
@@ -6,7 +6,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.getQueueAttributes = exports.getMatchingQueues = exports.setSQSClient = exports.getSQSClient = void 0;
9
+ exports.getSQSClient = getSQSClient;
10
+ exports.setSQSClient = setSQSClient;
11
+ exports.getMatchingQueues = getMatchingQueues;
12
+ exports.getQueueAttributes = getQueueAttributes;
10
13
  const client_sqs_1 = require("@aws-sdk/client-sqs");
11
14
  const path_1 = require("path");
12
15
  const debug_1 = __importDefault(require("debug"));
@@ -21,14 +24,12 @@ function getSQSClient() {
21
24
  client = new client_sqs_1.SQSClient();
22
25
  return client;
23
26
  }
24
- exports.getSQSClient = getSQSClient;
25
27
  /**
26
28
  * Utility function to set the client explicitly, used in testing.
27
29
  */
28
30
  function setSQSClient(explicitClient) {
29
31
  client = explicitClient;
30
32
  }
31
- exports.setSQSClient = setSQSClient;
32
33
  /**
33
34
  * Returns qrls for queues matching the given prefix and regex.
34
35
  */
@@ -48,7 +49,6 @@ async function getMatchingQueues(prefix, regex) {
48
49
  }
49
50
  return processQueues();
50
51
  }
51
- exports.getMatchingQueues = getMatchingQueues;
52
52
  /**
53
53
  * Gets attributes on every queue in parallel.
54
54
  */
@@ -93,5 +93,4 @@ async function getQueueAttributes(qrls) {
93
93
  }
94
94
  return Promise.all(promises);
95
95
  }
96
- exports.getQueueAttributes = getQueueAttributes;
97
96
  debug('loaded');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "A distributed scheduler for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -14,15 +14,13 @@
14
14
  "dependencies": {
15
15
  "@aws-sdk/client-cloudwatch": "3.465.0",
16
16
  "@aws-sdk/client-sqs": "3.465.0",
17
- "@sentry/node": "^7.87.0",
17
+ "@sentry/node": "^7.120.4",
18
18
  "chalk": "^4.1.2",
19
19
  "command-line-args": "^5.2.1",
20
20
  "command-line-commands": "^3.0.2",
21
- "command-line-usage": "^7.0.1",
22
- "debug": "^4.3.4",
23
- "ioredis": "^5.3.2",
24
- "ioredis-mock": "^8.9.0",
25
- "standard": "^17.1.0",
21
+ "command-line-usage": "^7.0.3",
22
+ "debug": "^4.4.3",
23
+ "ioredis": "^5.9.3",
26
24
  "tree-kill": "^1.2.2",
27
25
  "uuid": "^9.0.1"
28
26
  },
@@ -36,10 +34,12 @@
36
34
  }
37
35
  },
38
36
  "devDependencies": {
39
- "aws-sdk-client-mock": "^3.0.0",
40
- "aws-sdk-client-mock-jest": "^3.0.0",
37
+ "aws-sdk-client-mock": "^3.1.0",
38
+ "aws-sdk-client-mock-jest": "^3.1.0",
39
+ "ioredis-mock": "^8.13.1",
41
40
  "jest": "^29.7.0",
42
- "typescript": "^5.3.3"
41
+ "standard": "^17.1.2",
42
+ "typescript": "^5.9.3"
43
43
  },
44
44
  "preferGlobal": true,
45
45
  "engines": {
package/src/cloudWatch.js CHANGED
@@ -85,6 +85,16 @@ export async function putAggregateData (total, timestamp) {
85
85
  Timestamp: now,
86
86
  Value: total.ApproximateNumberOfMessagesNotVisible || 0,
87
87
  Unit: 'Count'
88
+ },
89
+ {
90
+ MetricName: 'ApproximateAgeOfOldestMessage',
91
+ Dimensions: [{
92
+ Name: 'queueName',
93
+ Value: total.queueName
94
+ }],
95
+ Timestamp: now,
96
+ Value: total.ApproximateAgeOfOldestMessage || 0,
97
+ Unit: 'Seconds'
88
98
  }
89
99
  ]
90
100
  }
package/src/defaults.js CHANGED
@@ -66,7 +66,7 @@ export function validateQueueName (opt, name) {
66
66
  }
67
67
 
68
68
  export function validateMessageOptions (messageOptions) {
69
- const validKeys = ['deduplicationId', 'groupId']
69
+ const validKeys = ['deduplicationId', 'groupId', 'delay']
70
70
  if (typeof messageOptions === 'object' &&
71
71
  !Array.isArray(messageOptions) &&
72
72
  messageOptions !== null) {
@@ -142,7 +142,10 @@ export function getOptionsWithDefaults (options) {
142
142
 
143
143
  // Check
144
144
  create: options.create || process.env.QDONE_CREATE === 'true' || defaults.create,
145
- overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || defaults.overwrite
145
+ overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || defaults.overwrite,
146
+
147
+ // Dependency injection
148
+ Redis: options.Redis
146
149
  }
147
150
 
148
151
  // Setting this env here means we don't have to in AWS SDK constructors
package/src/idleQueues.js CHANGED
@@ -7,7 +7,7 @@ import { getCloudWatchClient } from './cloudWatch.js'
7
7
  import { getOptionsWithDefaults } from './defaults.js'
8
8
  import { GetQueueAttributesCommand, DeleteQueueCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
9
9
  import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
10
- import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix, qrlCacheSet } from './qrlCache.js'
10
+ import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix } from './qrlCache.js'
11
11
  import { getCache, setCache } from './cache.js'
12
12
  // const AWS = require('aws-sdk')
13
13
 
package/src/monitor.js CHANGED
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import { getMatchingQueues, getQueueAttributes } from './sqs.js'
6
- import { putAggregateData } from './cloudWatch.js'
6
+ import { putAggregateData, getCloudWatchClient } from './cloudWatch.js'
7
+ import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
7
8
  import { getOptionsWithDefaults } from './defaults.js'
8
9
  import { normalizeQueueName } from './qrlCache.js'
9
10
  import Debug from 'debug'
@@ -38,12 +39,42 @@ export function interpretWildcard (queueName) {
38
39
  return { prefix, suffix, safeSuffix, suffixRegex }
39
40
  }
40
41
 
42
+ /**
43
+ * Gets ApproximateAgeOfOldestMessage for a single queue from CloudWatch.
44
+ * This metric is not available via the SQS GetQueueAttributes API.
45
+ */
46
+ export async function getQueueAge (queueName) {
47
+ const now = new Date()
48
+ const params = {
49
+ StartTime: new Date(now.getTime() - 1000 * 60 * 5),
50
+ EndTime: now,
51
+ MetricName: 'ApproximateAgeOfOldestMessage',
52
+ Namespace: 'AWS/SQS',
53
+ Period: 300,
54
+ Dimensions: [{ Name: 'QueueName', Value: queueName }],
55
+ Statistics: ['Maximum']
56
+ }
57
+ const client = getCloudWatchClient()
58
+ const cmd = new GetMetricStatisticsCommand(params)
59
+ try {
60
+ const data = await client.send(cmd)
61
+ debug('getQueueAge', queueName, data)
62
+ if (!data.Datapoints || data.Datapoints.length === 0) return 0
63
+ return Math.max(...data.Datapoints.map(d => d.Maximum))
64
+ } catch (e) {
65
+ debug('getQueueAge error', queueName, e)
66
+ return 0
67
+ }
68
+ }
69
+
41
70
  /**
42
71
  * Aggregates inmportant attributes across queues and reports a summary.
43
- * Attributes:
72
+ * Attributes (from SQS GetQueueAttributes):
44
73
  * - ApproximateNumberOfMessages: Sum
45
74
  * - ApproximateNumberOfMessagesDelayed: Sum
46
75
  * - ApproximateNumberOfMessagesNotVisible: Sum
76
+ * Metrics (from CloudWatch):
77
+ * - ApproximateAgeOfOldestMessage: Max
47
78
  */
48
79
  export async function getAggregateData (queueName) {
49
80
  const { prefix, suffixRegex } = interpretWildcard(queueName)
@@ -63,6 +94,14 @@ export async function getAggregateData (queueName) {
63
94
  }
64
95
  }
65
96
  }
97
+
98
+ // Fetch ApproximateAgeOfOldestMessage from CloudWatch (not available via SQS API)
99
+ // Only query queues with messages to minimize CloudWatch API costs
100
+ const ageResults = await Promise.all(
101
+ [...total.contributingQueueNames].map(queue => getQueueAge(queue))
102
+ )
103
+ total.ApproximateAgeOfOldestMessage = Math.max(0, ...ageResults)
104
+
66
105
  // debug({ total })
67
106
  // convert set to array
68
107
  total.contributingQueueNames = [...total.contributingQueueNames.values()]
@@ -322,7 +322,15 @@ export class JobExecutor {
322
322
  this.stats.runningJobs++
323
323
  this.stats.waitingJobs--
324
324
  const queue = job.qname.slice(this.opt.prefix.length)
325
- const result = await job.callback(queue, job.payload)
325
+ const attributes = {
326
+ queueName: job.qname,
327
+ messageId: job.message.MessageId || '',
328
+ receiveCount: job.message.Attributes?.ApproximateReceiveCount || '1',
329
+ sentTimestamp: job.message.Attributes?.SentTimestamp || '',
330
+ firstReceiveTimestamp: job.message.Attributes?.ApproximateFirstReceiveTimestamp || '',
331
+ messageGroupId: job.message.Attributes?.MessageGroupId || ''
332
+ }
333
+ const result = await job.callback(queue, job.payload, attributes)
326
334
  debug('executeJob callback finished', { payload: job.payload, result })
327
335
  if (this.opt.verbose) {
328
336
  console.error(chalk.green('SUCCESS'), job.payload)
@@ -381,7 +389,7 @@ export class JobExecutor {
381
389
  for (const [job, i] of jobs.map((job, i) => [job, i])) {
382
390
  // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
383
391
  const nextJob = jobs[i + 1]
384
- const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId
392
+ const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId
385
393
 
386
394
  // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
387
395
  // Execute serial or parallel
package/src/worker.js CHANGED
@@ -104,10 +104,21 @@ export async function executeJob (job, qname, qrl, opt) {
104
104
  const treeKiller = setTimeout(killTree, opt.killAfter * 1000)
105
105
  debug({ treeKiller: opt.killAfter * 1000, date: Date.now() })
106
106
 
107
+ // Build environment with SQS message attributes for child process
108
+ const env = {
109
+ ...process.env,
110
+ QDONE_QUEUE_NAME: qname,
111
+ SQS_MESSAGE_ID: job.MessageId || '',
112
+ SQS_RECEIVE_COUNT: job.Attributes?.ApproximateReceiveCount || '1',
113
+ SQS_SENT_TIMESTAMP: job.Attributes?.SentTimestamp || '',
114
+ SQS_FIRST_RECEIVE_TIMESTAMP: job.Attributes?.ApproximateFirstReceiveTimestamp || '',
115
+ SQS_MESSAGE_GROUP_ID: job.Attributes?.MessageGroupId || ''
116
+ }
117
+
107
118
  try {
108
119
  // Success path for job execution
109
120
  const { stdout, stderr } = await new Promise(function (resolve, reject) {
110
- child = exec(cmd, function (err, stdout, stderr) {
121
+ child = exec(cmd, { env }, function (err, stdout, stderr) {
111
122
  if (err) {
112
123
  err.stdout = stdout
113
124
  err.stderr = stderr