@xeplr/utils 1.0.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/lib/email.js ADDED
@@ -0,0 +1,303 @@
1
+ const nodemailer = require('nodemailer');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ let _config = null;
6
+ let _queue = null;
7
+ let _deadLetterQueue = null;
8
+
9
+ /**
10
+ * Configure the email service.
11
+ * @param {object} config
12
+ * @param {string} config.provider - 'smtp' | 'aws' | 'azure' | 'brevo'
13
+ * @param {object} [config.smtp] - { host, port, user, pass, from }
14
+ * @param {object} [config.aws] - { region, accessKeyId, secretAccessKey, from }
15
+ * @param {object} [config.azure] - { connectionString, from }
16
+ * @param {object} [config.brevo] - { apiKey, fromEmail, fromName }
17
+ * @param {boolean} [config.useQueue=false] - Queue emails with retry instead of sending immediately
18
+ * @param {number} [config.maxRetries=3] - Max retry attempts per email
19
+ * @param {number} [config.retryIntervalInSeconds=60] - Seconds between retry attempts
20
+ * @param {string} [config.store='memory'] - Queue store: 'memory' or 'redis'
21
+ * @param {string} [config.redisKey='xeplr:queue:email'] - Redis key for email queue
22
+ */
23
+ function configureEmail(config) {
24
+ _config = config;
25
+
26
+ if (config.useQueue) {
27
+ var Queue = require('./queue');
28
+ var maxRetries = config.maxRetries || 3;
29
+ var storeType = config.store || 'memory';
30
+
31
+ _deadLetterQueue = new Queue({
32
+ store: storeType,
33
+ redisKey: config.deadLetterKey || 'xeplr:queue:email:dead',
34
+ autoIntervalInSeconds: 0,
35
+ maxEmptyTicks: 0
36
+ });
37
+
38
+ _queue = new Queue({
39
+ store: storeType,
40
+ redisKey: config.redisKey || 'xeplr:queue:email',
41
+ autoIntervalInSeconds: config.retryIntervalInSeconds || 60,
42
+ maxEmptyTicks: 0,
43
+ action: async function(item) {
44
+ try {
45
+ await _sendDirect(item.to, item.subject, item.html, item.cc, item.attachments);
46
+ } catch (err) {
47
+ var attempt = (item._retries || 0) + 1;
48
+ if (attempt < maxRetries) {
49
+ console.error('[xeplr-email] send failed (attempt ' + attempt + '/' + maxRetries + '): ' + err.message + ' — will retry');
50
+ _queue.addToQueue({ to: item.to, subject: item.subject, html: item.html, cc: item.cc, attachments: item.attachments, _retries: attempt });
51
+ } else {
52
+ console.error('[xeplr-email] send failed after ' + maxRetries + ' attempts, moved to dead letter queue');
53
+ _deadLetterQueue.addToQueue({ to: item.to, subject: item.subject, html: item.html, cc: item.cc, attachments: item.attachments, error: err.message, failedAt: new Date().toISOString() });
54
+ }
55
+ }
56
+ }
57
+ });
58
+ }
59
+ }
60
+
61
+ function getConfig() {
62
+ if (_config) return _config;
63
+
64
+ // Fall back to environment variables
65
+ const provider = (process.env.EMAIL_PROVIDER || 'smtp').toLowerCase();
66
+ return {
67
+ provider,
68
+ smtp: {
69
+ host: process.env.SMTP_HOST,
70
+ port: parseInt(process.env.SMTP_PORT) || 587,
71
+ user: process.env.SMTP_USER,
72
+ pass: process.env.SMTP_PASS,
73
+ from: process.env.SMTP_FROM
74
+ },
75
+ aws: {
76
+ region: process.env.AWS_REGION,
77
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
78
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
79
+ from: process.env.AWS_SES_FROM
80
+ },
81
+ azure: {
82
+ connectionString: process.env.AZURE_COMMUNICATION_CONNECTION_STRING,
83
+ from: process.env.AZURE_EMAIL_FROM
84
+ },
85
+ brevo: {
86
+ apiKey: process.env.BREVO_API_KEY,
87
+ fromEmail: process.env.BREVO_FROM_EMAIL,
88
+ fromName: process.env.BREVO_FROM_NAME || 'App'
89
+ }
90
+ };
91
+ }
92
+
93
+ function buildAttachments(attachments) {
94
+ if (!attachments || !attachments.length) return [];
95
+ return attachments.map(function(filePath) {
96
+ return {
97
+ filename: path.basename(filePath),
98
+ content: fs.readFileSync(filePath)
99
+ };
100
+ });
101
+ }
102
+
103
+ async function sendViaSMTP(to, subject, html, cc, attachments) {
104
+ var cfg = getConfig().smtp;
105
+ var transporter = nodemailer.createTransport({
106
+ host: cfg.host,
107
+ port: cfg.port,
108
+ secure: false,
109
+ auth: { user: cfg.user, pass: cfg.pass }
110
+ });
111
+
112
+ var mailOptions = { from: cfg.from, to, subject, html };
113
+ if (cc && cc.length) mailOptions.cc = cc;
114
+ if (attachments && attachments.length) mailOptions.attachments = buildAttachments(attachments);
115
+
116
+ await transporter.sendMail(mailOptions);
117
+ }
118
+
119
+ async function sendViaAWS(to, subject, html, cc, attachments) {
120
+ var cfg = getConfig().aws;
121
+
122
+ // If there are attachments or cc, use SESv2 with raw email via nodemailer
123
+ if ((attachments && attachments.length) || (cc && cc.length)) {
124
+ var { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');
125
+ var transporter = nodemailer.createTransport({ streamTransport: true });
126
+
127
+ var mailOptions = { from: cfg.from, to, subject, html };
128
+ if (cc && cc.length) mailOptions.cc = cc;
129
+ if (attachments && attachments.length) mailOptions.attachments = buildAttachments(attachments);
130
+
131
+ var info = await transporter.sendMail(mailOptions);
132
+ var rawMessage = await streamToBuffer(info.message);
133
+
134
+ var client = new SESv2Client({
135
+ region: cfg.region,
136
+ credentials: { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey }
137
+ });
138
+
139
+ await client.send(new SendEmailCommand({
140
+ Content: { Raw: { Data: rawMessage } }
141
+ }));
142
+ return;
143
+ }
144
+
145
+ var { SESClient, SendEmailCommand: SimpleSendCommand } = require('@aws-sdk/client-ses');
146
+
147
+ var client = new SESClient({
148
+ region: cfg.region,
149
+ credentials: { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey }
150
+ });
151
+
152
+ var command = new SimpleSendCommand({
153
+ Source: cfg.from,
154
+ Destination: { ToAddresses: to },
155
+ Message: {
156
+ Subject: { Data: subject },
157
+ Body: { Html: { Data: html } }
158
+ }
159
+ });
160
+
161
+ await client.send(command);
162
+ }
163
+
164
+ async function sendViaAzure(to, subject, html, cc, attachments) {
165
+ var { EmailClient } = require('@azure/communication-email');
166
+ var cfg = getConfig().azure;
167
+
168
+ var message = {
169
+ senderAddress: cfg.from,
170
+ content: { subject, html },
171
+ recipients: {
172
+ to: to.map(function(addr) { return { address: addr }; })
173
+ }
174
+ };
175
+
176
+ if (cc && cc.length) {
177
+ message.recipients.cc = cc.map(function(addr) { return { address: addr }; });
178
+ }
179
+
180
+ if (attachments && attachments.length) {
181
+ message.attachments = attachments.map(function(filePath) {
182
+ return {
183
+ name: path.basename(filePath),
184
+ contentType: 'application/octet-stream',
185
+ contentInBase64: fs.readFileSync(filePath).toString('base64')
186
+ };
187
+ });
188
+ }
189
+
190
+ var client = new EmailClient(cfg.connectionString);
191
+ var poller = await client.beginSend(message);
192
+ await poller.pollUntilDone();
193
+ }
194
+
195
+ async function sendViaBrevo(to, subject, html, cc, attachments) {
196
+ var cfg = getConfig().brevo;
197
+
198
+ var payload = {
199
+ sender: { name: cfg.fromName, email: cfg.fromEmail },
200
+ to: to.map(function(addr) { return { email: addr }; }),
201
+ subject,
202
+ htmlContent: html
203
+ };
204
+
205
+ if (cc && cc.length) {
206
+ payload.cc = cc.map(function(addr) { return { email: addr }; });
207
+ }
208
+
209
+ if (attachments && attachments.length) {
210
+ payload.attachment = attachments.map(function(filePath) {
211
+ return {
212
+ name: path.basename(filePath),
213
+ content: fs.readFileSync(filePath).toString('base64')
214
+ };
215
+ });
216
+ }
217
+
218
+ var res = await fetch('https://api.brevo.com/v3/smtp/email', {
219
+ method: 'POST',
220
+ headers: {
221
+ 'accept': 'application/json',
222
+ 'content-type': 'application/json',
223
+ 'api-key': cfg.apiKey
224
+ },
225
+ body: JSON.stringify(payload)
226
+ });
227
+
228
+ if (!res.ok) {
229
+ var err = await res.json();
230
+ throw new Error('Brevo email failed: ' + (err.message || JSON.stringify(err)));
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Send an email directly (no queue).
236
+ */
237
+ async function _sendDirect(to, subject, html, cc, attachments) {
238
+ to = Array.isArray(to) ? to : [to];
239
+ var provider = getConfig().provider;
240
+
241
+ switch (provider) {
242
+ case 'smtp':
243
+ return sendViaSMTP(to, subject, html, cc, attachments);
244
+ case 'aws':
245
+ return sendViaAWS(to, subject, html, cc, attachments);
246
+ case 'azure':
247
+ return sendViaAzure(to, subject, html, cc, attachments);
248
+ case 'brevo':
249
+ return sendViaBrevo(to, subject, html, cc, attachments);
250
+ default:
251
+ throw new Error('Unknown email provider: ' + provider);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Send an email. If useQueue is enabled, queues with retry. Otherwise sends directly.
257
+ * @param {string|string[]} to - Recipient emails
258
+ * @param {string} subject - Email subject
259
+ * @param {string} html - HTML body
260
+ * @param {string[]} [cc] - CC recipients
261
+ * @param {string[]} [attachments] - File paths to attach
262
+ */
263
+ async function sendEmail(to, subject, html, cc, attachments) {
264
+ if (_queue) {
265
+ _queue.addToQueue({ to: Array.isArray(to) ? to : [to], subject, html, cc, attachments, _retries: 0 });
266
+ return;
267
+ }
268
+ return _sendDirect(to, subject, html, cc, attachments);
269
+ }
270
+
271
+ function streamToBuffer(stream) {
272
+ return new Promise(function(resolve, reject) {
273
+ var chunks = [];
274
+ stream.on('data', function(chunk) { chunks.push(chunk); });
275
+ stream.on('end', function() { resolve(Buffer.concat(chunks)); });
276
+ stream.on('error', reject);
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Get the dead letter queue (failed emails after max retries).
282
+ * Returns null if queue is not enabled.
283
+ */
284
+ function getDeadLetterQueue() {
285
+ return _deadLetterQueue;
286
+ }
287
+
288
+ /**
289
+ * Replay all dead letter emails back through the main queue.
290
+ * Drains the dead letter queue and re-sends each item.
291
+ * @returns {Promise<number>} Number of emails replayed
292
+ */
293
+ async function replayDeadLetters() {
294
+ if (!_deadLetterQueue) return 0;
295
+ var items = await _deadLetterQueue.drain();
296
+ for (var i = 0; i < items.length; i++) {
297
+ var item = items[i];
298
+ await sendEmail(item.to, item.subject, item.html, item.cc, item.attachments);
299
+ }
300
+ return items.length;
301
+ }
302
+
303
+ module.exports = { sendEmail, configureEmail, getDeadLetterQueue, replayDeadLetters };
@@ -0,0 +1,135 @@
1
+ const multer = require('multer');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { generateId } = require('./helpers');
5
+
6
+ class FileUploader {
7
+ /**
8
+ * @param {object} options
9
+ * @param {function} options.auth - Auth function: receives (req), must return truthy or throw
10
+ * @param {string[]} [options.allowedTypes] - MIME types: ['image/png', 'image/*', '*']
11
+ * @param {number} [options.maxSize] - Max file size in bytes (default: 5MB)
12
+ * @param {string} [options.destination] - Upload directory (default: './uploads')
13
+ */
14
+ constructor(options = {}) {
15
+ this._auth = options.auth || null;
16
+ this._allowedTypes = options.allowedTypes || ['*'];
17
+ this._maxSize = options.maxSize || 5 * 1024 * 1024;
18
+ this._destination = options.destination || process.env.UPLOAD_DIR || './uploads';
19
+
20
+ if (!fs.existsSync(this._destination)) {
21
+ fs.mkdirSync(this._destination, { recursive: true });
22
+ }
23
+
24
+ this._multer = multer({
25
+ storage: multer.diskStorage({
26
+ destination: (req, file, cb) => {
27
+ cb(null, this._destination);
28
+ },
29
+ filename: (req, file, cb) => {
30
+ var ext = path.extname(file.originalname);
31
+ var name = generateId() + ext;
32
+ cb(null, name);
33
+ }
34
+ }),
35
+ limits: {
36
+ fileSize: this._maxSize
37
+ },
38
+ fileFilter: (req, file, cb) => {
39
+ if (this._isTypeAllowed(file.mimetype)) {
40
+ cb(null, true);
41
+ } else {
42
+ cb(new Error('File type not allowed: ' + file.mimetype));
43
+ }
44
+ }
45
+ });
46
+ }
47
+
48
+ _isTypeAllowed(mimetype) {
49
+ for (var i = 0; i < this._allowedTypes.length; i++) {
50
+ var allowed = this._allowedTypes[i];
51
+ if (allowed === '*') return true;
52
+ if (allowed.endsWith('/*')) {
53
+ var category = allowed.split('/')[0];
54
+ if (mimetype.startsWith(category + '/')) return true;
55
+ }
56
+ if (allowed === mimetype) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ _authGuard() {
62
+ var auth = this._auth;
63
+ return async function(req, res, next) {
64
+ if (!auth) return next();
65
+ try {
66
+ var result = await auth(req);
67
+ if (!result) {
68
+ res.writeHead ? _jsonResponse(res, 401, { error: 'Unauthorized' })
69
+ : res.status(401).json({ error: 'Unauthorized' });
70
+ return;
71
+ }
72
+ next();
73
+ } catch (err) {
74
+ res.writeHead ? _jsonResponse(res, 401, { error: err.message || 'Unauthorized' })
75
+ : res.status(401).json({ error: err.message || 'Unauthorized' });
76
+ }
77
+ };
78
+ }
79
+
80
+ _errorHandler() {
81
+ return function(err, req, res, next) {
82
+ if (err.code === 'LIMIT_FILE_SIZE') {
83
+ return res.status(413).json({ error: 'File too large' });
84
+ }
85
+ if (err.message && err.message.startsWith('File type not allowed')) {
86
+ return res.status(400).json({ error: err.message });
87
+ }
88
+ return res.status(500).json({ error: err.message || 'Upload failed' });
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Accept a single file upload.
94
+ * @param {string} fieldName - Form field name
95
+ * @returns {function[]} Express middleware chain
96
+ */
97
+ single(fieldName) {
98
+ var guard = this._authGuard();
99
+ var upload = this._multer.single(fieldName);
100
+ var errorHandler = this._errorHandler();
101
+ return [guard, upload, errorHandler];
102
+ }
103
+
104
+ /**
105
+ * Accept multiple file uploads on a single field.
106
+ * @param {string} fieldName - Form field name
107
+ * @param {number} [maxCount=10] - Maximum number of files
108
+ * @returns {function[]} Express middleware chain
109
+ */
110
+ array(fieldName, maxCount) {
111
+ var guard = this._authGuard();
112
+ var upload = this._multer.array(fieldName, maxCount || 10);
113
+ var errorHandler = this._errorHandler();
114
+ return [guard, upload, errorHandler];
115
+ }
116
+
117
+ /**
118
+ * Accept file uploads on multiple fields.
119
+ * @param {Array<{name: string, maxCount: number}>} fields
120
+ * @returns {function[]} Express middleware chain
121
+ */
122
+ fields(fields) {
123
+ var guard = this._authGuard();
124
+ var upload = this._multer.fields(fields);
125
+ var errorHandler = this._errorHandler();
126
+ return [guard, upload, errorHandler];
127
+ }
128
+ }
129
+
130
+ function _jsonResponse(res, statusCode, data) {
131
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
132
+ res.end(JSON.stringify(data));
133
+ }
134
+
135
+ module.exports = FileUploader;
package/lib/helpers.js ADDED
@@ -0,0 +1,18 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Generate a random ID string (25 chars hex).
5
+ */
6
+ function generateId() {
7
+ return crypto.randomBytes(12).toString('hex').substring(0, 25);
8
+ }
9
+
10
+ /**
11
+ * Format a Date as ISO datetime string for database storage.
12
+ */
13
+ function formatDbDateTime(date) {
14
+ return (date || new Date()).toISOString();
15
+ }
16
+
17
+ // Keep alias for backward compat
18
+ module.exports = { generateId, formatDbDateTime, mysqlDateTime: formatDbDateTime };
package/lib/logger.js ADDED
@@ -0,0 +1,51 @@
1
+ let _baseUrl = null;
2
+
3
+ /**
4
+ * Configure the logger client.
5
+ * @param {object} config
6
+ * @param {string} config.url - Logs service base URL (e.g. http://localhost:19005)
7
+ */
8
+ function configureLogger(config) {
9
+ _baseUrl = config.url;
10
+ }
11
+
12
+ function getBaseUrl() {
13
+ return _baseUrl || process.env.LOGS_URL || 'http://localhost:19005';
14
+ }
15
+
16
+ async function createSession({ source, action }) {
17
+ var res = await fetch(getBaseUrl() + '/internal/sessions', {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body: JSON.stringify({ source, action })
21
+ });
22
+ if (!res.ok) throw new Error('Failed to create log session: ' + res.statusText);
23
+ return res.json();
24
+ }
25
+
26
+ async function log(sessionId, level, message, metadata) {
27
+ var res = await fetch(getBaseUrl() + '/internal/logs', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ sessionId, level, message, metadata })
31
+ });
32
+ if (!res.ok) throw new Error('Failed to log: ' + res.statusText);
33
+ return res.json();
34
+ }
35
+
36
+ async function closeSession(sessionId) {
37
+ var res = await fetch(getBaseUrl() + '/internal/sessions/' + sessionId + '/close', {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' }
40
+ });
41
+ if (!res.ok) throw new Error('Failed to close session: ' + res.statusText);
42
+ return res.json();
43
+ }
44
+
45
+ async function getSessionLogs(sessionId) {
46
+ var res = await fetch(getBaseUrl() + '/internal/sessions/' + sessionId);
47
+ if (!res.ok) throw new Error('Failed to get logs: ' + res.statusText);
48
+ return res.json();
49
+ }
50
+
51
+ module.exports = { configureLogger, createSession, log, closeSession, getSessionLogs };
package/lib/queue.js ADDED
@@ -0,0 +1,160 @@
1
+ var MemoryStore = {
2
+ create: function() {
3
+ var items = [];
4
+ return {
5
+ push: function(item) { items.push(item); },
6
+ shift: async function() { return items.shift(); },
7
+ length: async function() { return items.length; },
8
+ list: async function() { return items.slice(); },
9
+ clear: async function() { items.length = 0; }
10
+ };
11
+ }
12
+ };
13
+
14
+ var RedisStore = {
15
+ create: function(options) {
16
+ var cache = require('./cache');
17
+ var client = cache.getClient();
18
+ var key = options.redisKey || 'xeplr:queue:default';
19
+
20
+ return {
21
+ push: function(item) {
22
+ client.rpush(key, JSON.stringify(item));
23
+ },
24
+ shift: async function() {
25
+ var raw = await client.lpop(key);
26
+ return raw ? JSON.parse(raw) : undefined;
27
+ },
28
+ length: async function() {
29
+ return await client.llen(key);
30
+ },
31
+ list: async function() {
32
+ var rawItems = await client.lrange(key, 0, -1);
33
+ return rawItems.map(function(raw) { return JSON.parse(raw); });
34
+ },
35
+ clear: async function() {
36
+ await client.del(key);
37
+ }
38
+ };
39
+ }
40
+ };
41
+
42
+ class Queue {
43
+ constructor(options = {}) {
44
+ this.action = options.action || null;
45
+ this.autoIntervalInSeconds = options.autoIntervalInSeconds || 0;
46
+ this.maxEmptyTicks = options.maxEmptyTicks || 0;
47
+ this._processing = false;
48
+ this._paused = false;
49
+ this._stopped = false;
50
+ this._emptyTicks = 0;
51
+
52
+ var storeType = options.store || 'memory';
53
+ if (storeType === 'redis') {
54
+ this._store = RedisStore.create(options);
55
+ } else {
56
+ this._store = MemoryStore.create();
57
+ }
58
+
59
+ if (this.autoIntervalInSeconds > 0) {
60
+ this._scheduleNext();
61
+ }
62
+ }
63
+
64
+ addToQueue(item) {
65
+ this._store.push(item);
66
+ if (this._paused && !this._stopped) {
67
+ this.resume();
68
+ }
69
+ }
70
+
71
+ async flushQueue() {
72
+ if (this._processing || this._paused) return;
73
+ var len = await this._store.length();
74
+ if (len === 0) return;
75
+ this._processing = true;
76
+ try {
77
+ while (true) {
78
+ if (this._paused) break;
79
+ var item = await this._store.shift();
80
+ if (item === undefined) break;
81
+ if (this.action) {
82
+ await this.action(item);
83
+ }
84
+ }
85
+ } finally {
86
+ this._processing = false;
87
+ }
88
+ }
89
+
90
+ pause() {
91
+ this._paused = true;
92
+ }
93
+
94
+ resume() {
95
+ if (!this._paused) return;
96
+ this._paused = false;
97
+ this._emptyTicks = 0;
98
+ if (this.autoIntervalInSeconds > 0 && !this._stopped) {
99
+ this._scheduleNext();
100
+ }
101
+ }
102
+
103
+ stop() {
104
+ this._stopped = true;
105
+ this._paused = true;
106
+ }
107
+
108
+ /**
109
+ * List all items in the queue without removing them.
110
+ */
111
+ async list() {
112
+ return this._store.list();
113
+ }
114
+
115
+ /**
116
+ * Clear all items from the queue.
117
+ */
118
+ async clear() {
119
+ return this._store.clear();
120
+ }
121
+
122
+ /**
123
+ * Drain all items from the queue and return them.
124
+ * Items are removed from the queue.
125
+ */
126
+ async drain() {
127
+ var result = [];
128
+ while (true) {
129
+ var item = await this._store.shift();
130
+ if (item === undefined) break;
131
+ result.push(item);
132
+ }
133
+ return result;
134
+ }
135
+
136
+ _scheduleNext() {
137
+ if (this._stopped || this._paused) return;
138
+ var self = this;
139
+ var timer = setTimeout(async function() {
140
+ var len = await self._store.length();
141
+ if (len === 0) {
142
+ self._emptyTicks++;
143
+ if (self.maxEmptyTicks > 0 && self._emptyTicks >= self.maxEmptyTicks) {
144
+ self._paused = true;
145
+ return;
146
+ }
147
+ } else {
148
+ self._emptyTicks = 0;
149
+ await self.flushQueue();
150
+ }
151
+ self._scheduleNext();
152
+ }, self.autoIntervalInSeconds * 1000);
153
+
154
+ if (timer && timer.unref) {
155
+ timer.unref();
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = Queue;