diff --git a/lib/agenda.js b/lib/agenda.js index b87b91fa5..f79028165 100644 --- a/lib/agenda.js +++ b/lib/agenda.js @@ -3,8 +3,22 @@ * - Refactor remaining deprecated MongoDB Native Driver methods: findAndModify() */ +/** + * Debug Notes: + * Use debug('some text') to print debugging information + * It uses printf style formatting and supports the following: + * %O Pretty-print an Object on multiple lines. + * %o Pretty-print an Object all on a single line. + * %s String. + * %d Number (both integer and float). + * %j JSON. Replaced with the string '[Circular]' if the argument contains circular references. + * %% Single percent sign ('%'). This does not consume an argument. + * Example: debug('job started with attrs: %j', job.toJSON()) + * To view output, run using command "DEBUG=agenda:* node index.js" + */ + var Job = require('./job.js'), humanInterval = require('human-interval'), utils = require('util'), - Emitter = require('events').EventEmitter; + Emitter = require('events').EventEmitter, debug = require('debug')('agenda:worker'); var MongoClient = require('mongodb').MongoClient, Db = require('mongodb').Db; @@ -75,15 +89,15 @@ Agenda.prototype.database = function(url, collection, options, cb) { MongoClient.connect(url, options, function( error, db ){ if (error) { + debug('error connecting to MongoDB using collection: [%s]', collection); if (cb) { cb(error, null); } else { throw error; } - return; } - + debug('successful connection to MongoDB using collection: [%s]', collection); self._mdb = db; self.db_init( collection, cb ); }); @@ -97,8 +111,10 @@ Agenda.prototype.database = function(url, collection, options, cb) { * @returns {undefined} */ Agenda.prototype.db_init = function( collection, cb ){ + debug('init database collection using name [%s]', collection); this._collection = this._mdb.collection(collection || 'agendaJobs'); var self = this; + debug('attempting index creation'); this._collection.createIndexes([{ "key": {"name" : 1, "priority" : -1, "lockedAt" : 1, "nextRunAt" : 1, "disabled" : 1}, "name": "findAndLockNextJobIndex1" @@ -106,6 +122,11 @@ Agenda.prototype.db_init = function( collection, cb ){ "key": {"name" : 1, "lockedAt" : 1, "priority" : -1, "nextRunAt" : 1, "disabled" : 1}, "name": "findAndLockNextJobIndex2" }], function( err, result ){ + if (err) { + debug('index creation failed, attempting legacy index creation next'); + } else { + debug('index creation success'); + } handleLegacyCreateIndex(err, result, self, cb) }); }; @@ -120,6 +141,7 @@ Agenda.prototype.db_init = function( collection, cb ){ */ function handleLegacyCreateIndex(err, result, self, cb){ if (err && err.message !== 'no such cmd: createIndexes'){ + debug('not attempting legacy index, emitting error'); self.emit('error', err); } else { // Looks like a mongo.version < 2.4.x @@ -145,6 +167,7 @@ function handleLegacyCreateIndex(err, result, self, cb){ * @returns {exports} agenda instance */ Agenda.prototype.name = function(name) { + debug('Agenda.name(%s)', name); this._name = name; return this; }; @@ -155,6 +178,7 @@ Agenda.prototype.name = function(name) { * @returns {exports} agenda instance */ Agenda.prototype.processEvery = function(time) { + debug('Agenda.processEvery(%d)', time); this._processEvery = humanInterval(time); return this; }; @@ -165,6 +189,7 @@ Agenda.prototype.processEvery = function(time) { * @returns {exports} agenda instance */ Agenda.prototype.maxConcurrency = function(num) { + debug('Agenda.maxConcurrency(%d)', num); this._maxConcurrency = num; return this; }; @@ -175,6 +200,7 @@ Agenda.prototype.maxConcurrency = function(num) { * @returns {exports} agenda instance */ Agenda.prototype.defaultConcurrency = function(num) { + debug('Agenda.defaultConcurrency(%d)', num); this._defaultConcurrency = num; return this; }; @@ -186,6 +212,7 @@ Agenda.prototype.defaultConcurrency = function(num) { * @returns {exports} agenda instance */ Agenda.prototype.lockLimit = function(num) { + debug('Agenda.lockLimit(%d)', num); this._lockLimit = num; return this; }; @@ -196,6 +223,7 @@ Agenda.prototype.lockLimit = function(num) { * @returns {exports} agenda instance */ Agenda.prototype.defaultLockLimit = function(num) { + debug('Agenda.defaultLockLimit(%d)', num); this._defaultLockLimit = num; return this; }; @@ -207,6 +235,7 @@ Agenda.prototype.defaultLockLimit = function(num) { * @returns {exports} agenda instance */ Agenda.prototype.defaultLockLifetime = function(ms){ + debug('Agenda.defaultLockLifetime(%d)', ms); this._defaultLockLifetime = ms; return this; }; @@ -219,6 +248,7 @@ Agenda.prototype.defaultLockLifetime = function(ms){ * @returns {module.Job} instance of new job */ Agenda.prototype.create = function(name, data) { + debug('Agenda.create(%s, [Object])', name); var priority = this._definitions[name] ? this._definitions[name].priority : 0; var job = new Job({name: name, data: data, type: 'normal', priority: priority, agenda: this}); return job; @@ -250,6 +280,7 @@ Agenda.prototype.jobs = function( query, cb ){ */ Agenda.prototype.purge = function(cb) { var definedNames = Object.keys(this._definitions); + debug('Agenda.purge(%o)'); this.cancel( {name: {$not: {$in: definedNames}}}, cb ); }; @@ -275,6 +306,7 @@ Agenda.prototype.define = function(name, options, processor) { running: 0, locked: 0 }; + debug('job [%s] defined with following options: \n%O', name, this._definitions[name]); }; /** @@ -298,8 +330,10 @@ Agenda.prototype.every = function(interval, names, data, options, cb) { } if (typeof names === 'string' || names instanceof String) { + debug('Agenda.every(%s, %O, [Object], %O, cb)', interval, names, options); return createJob(interval, names, data, options, cb); } else if (Array.isArray(names)) { + debug('Agenda.every(%s, %s, [Object], %O, cb)', interval, names, options); return createJobs(interval, names, data, options, cb); } @@ -342,7 +376,12 @@ Agenda.prototype.every = function(interval, names, data, options, cb) { return; } results[i] = result; - if (--pending === 0 && cb) cb(null, results); + if (--pending === 0 && cb) { + debug('every() -> all jobs created successfully'); + cb(null, results); + } else { + debug('every() -> error creating one or more of the jobs'); + } }); }); @@ -366,8 +405,10 @@ Agenda.prototype.schedule = function(when, names, data, cb) { } if (typeof names === 'string' || names instanceof String) { + debug('Agenda.schedule(%s, %O, [Object], cb)', when, names); return createJob(when, names, data, cb); } else if (Array.isArray(names)) { + debug('Agenda.schedule(%s, %O, [Object], cb)', when, names); return createJobs(when, names, data, cb); } @@ -406,7 +447,12 @@ Agenda.prototype.schedule = function(when, names, data, cb) { return; } results[i] = result; - if (--pending === 0 && cb) cb(null, results); + if (--pending === 0 && cb) { + debug('Agenda.schedule()::createJobs() -> all jobs created successfully'); + cb(null, results); + } else { + debug('Agenda.schedule()::createJobs() -> error creating one or more of the jobs'); + } }); }); } @@ -424,6 +470,7 @@ Agenda.prototype.now = function(name, data, cb) { cb = data; data = undefined; } + debug('Agenda.now(%s, [Object])', name); var job = this.create(name, data); job.schedule(new Date()); job.save(cb); @@ -439,8 +486,14 @@ Agenda.prototype.now = function(name, data, cb) { * @returns {undefined} */ Agenda.prototype.cancel = function(query, cb) { + debug('attempting to cancel all Agenda jobs', query); this._collection.deleteMany( query, function( error, result ){ if (cb) { + if (error) { + debug('error trying to delete jobs from MongoDB'); + } else { + debug('jobs cancelled'); + } cb( error, result && result.result ? result.result.n : undefined ); } }); @@ -454,6 +507,8 @@ Agenda.prototype.cancel = function(query, cb) { */ Agenda.prototype.saveJob = function(job, cb) { + debug('attempting to save a job into Agenda instance'); + // Grab information needed to save job but that we don't want to persist in MongoDB var fn = cb, self = this; var id = job.attrs._id; @@ -468,25 +523,30 @@ Agenda.prototype.saveJob = function(job, cb) { // Store name of agenda queue as last modifier in job data props.lastModifiedBy = this._name; + debug('set job props: \n%O', props); // Grab current time and set default query options for MongoDB var now = new Date(), protect = {}, update = { $set: props }; + debug('current time stored as %s', now.toISOString()); // If the job already had an ID, then update the properties of the job // i.e, who last modified it, etc if (id) { - // Update the job and process the resulting data + // Update the job and process the resulting data' + debug('job already has _id, calling findOneAndUpdate() using _id as query'); this._collection.findOneAndUpdate({ _id: id }, update, { returnOriginal: false }, processDbResult); } else if (props.type === 'single') { // Job type set to 'single' so... // NOTE: Again, not sure about difference between 'single' here and 'once' in job.js + debug('job with type of "single" found'); // If the nextRunAt time is older than the current time, "protect" that property, meaning, don't change // a scheduled job's next run time! if (props.nextRunAt && props.nextRunAt <= now) { + debug('job has a scheduled nextRunAt time, protecting that field from upsert'); protect.nextRunAt = props.nextRunAt; delete props.nextRunAt; } @@ -498,6 +558,7 @@ Agenda.prototype.saveJob = function(job, cb) { // Try an upsert // NOTE: 'single' again, not exactly sure what it means + debug('calling findOneAndUpdate() with job name and type of "single" as query'); this._collection.findOneAndUpdate( { name: props.name, type: 'single' }, update, @@ -515,11 +576,13 @@ Agenda.prototype.saveJob = function(job, cb) { } // Use the 'unique' query object to find an existing job or create a new one + debug('calling findOneAndUpdate() with unique object as query: \n%O', query); this._collection.findOneAndUpdate(query, update, { upsert: true, returnOriginal: false }, processDbResult); } else { // If all else fails, the job does not exist yet so we just insert it into MongoDB + debug('using default behavior, inserting new job via insertOne() with props that were set: \n%O', props); this._collection.insertOne(props, processDbResult); } @@ -536,6 +599,7 @@ Agenda.prototype.saveJob = function(job, cb) { // Check if there is an error and either cb(error) or throw if there is no callback if (err) { + debug('processDbResult() received an error, job was not updated/created'); if (fn) { return fn(err); } else { @@ -543,6 +607,8 @@ Agenda.prototype.saveJob = function(job, cb) { } } else if (result) { + debug('processDbResult() called with success, checking whether to process job immediately or not'); + // We have a result from the above calls // findAndModify() returns different results than insertOne() so check for that var res = result.ops ? result.ops : result.value; @@ -559,6 +625,7 @@ Agenda.prototype.saveJob = function(job, cb) { // If the current job would have been processed in an older scan, process the job immediately if (job.attrs.nextRunAt && job.attrs.nextRunAt < self._nextScanAt) { + debug('[%s:%s] job would have ran by nextScanAt, processing the job immediately', job.attrs.name, res._id); processJobs.call(self, job); } } @@ -579,8 +646,11 @@ Agenda.prototype.saveJob = function(job, cb) { */ Agenda.prototype.start = function() { if (!this._processInterval) { + debug('Agenda.start called, creating interval to call processJobs every [%dms]', this._processEvery); this._processInterval = setInterval(processJobs.bind(this), this._processEvery); process.nextTick(processJobs.bind(this)); + } else { + debug('Agenda.start was already called, ignoring'); } }; @@ -590,6 +660,7 @@ Agenda.prototype.start = function() { * @returns {undefined} */ Agenda.prototype.stop = function(cb) { + debug('Agenda.stop called, clearing interval for processJobs()'); cb = cb || function() {}; clearInterval(this._processInterval); this._processInterval = undefined; @@ -608,11 +679,13 @@ Agenda.prototype.stop = function(cb) { Agenda.prototype._findAndLockNextJob = function(jobName, definition, cb) { var self = this, now = new Date(), lockDeadline = new Date(Date.now().valueOf() - definition.lockLifetime); + debug('_findAndLockNextJob(%s, [Function], cb)', jobName); // Don't try and access MongoDB if we've lost connection to it. // Trying to resolve crash on Dev PC when it resumes from sleep. NOTE: Does this still happen? var s = this._mdb.s || this._mdb.db.s; if (s.topology.connections().length === 0) { + debug('missing MongoDB connection, not attempting to find and lock a job'); cb(new Error('No MongoDB Connection')); } else { @@ -649,8 +722,12 @@ Agenda.prototype._findAndLockNextJob = function(jobName, definition, cb) { function(err, result) { var job; if (!err && result.value) { + debug('found a job available to lock, creating a new job on Agenda with id [%s]', result.value._id); job = createJob(self, result.value); } + if (err) { + debug('error occurred when running query to find and lock job'); + } cb(err, job); } ); @@ -678,9 +755,11 @@ function createJob(agenda, jobData) { * @returns {undefined} */ Agenda.prototype._unlockJobs = function(done) { + debug('Agenda._unlockJobs()'); var jobIds = this._lockedJobs.map(function(job) { return job.attrs._id; }); + debug('about to unlock jobs with ids: %O', jobIds); this._collection.updateMany({_id: { $in: jobIds } }, { $set: { lockedAt: null } }, done); }; @@ -694,6 +773,7 @@ function processJobs(extraJob) { // Make sure an interval has actually been set // Prevents race condition with 'Agenda.stop' and already scheduled run if (!this._processInterval) { + debug('no _processInterval set when calling processJobs, returning'); return; } @@ -705,6 +785,7 @@ function processJobs(extraJob) { // Go through each jobName set in 'Agenda.process' and fill the queue with the next jobs for (jobName in definitions) { if ({}.hasOwnProperty.call(definitions, jobName)) { + debug('queuing up job to process: [%s]', jobName); jobQueueFilling(jobName); } } @@ -712,6 +793,7 @@ function processJobs(extraJob) { } else if (definitions[extraJob.attrs.name]) { // Add the job to list of jobs to lock and then lock it immediately! + debug('job was passed directly to processJobs(), locking and running immediately'); self._jobsToLock.push(extraJob); lockOnTheFly(); @@ -733,6 +815,7 @@ function processJobs(extraJob) { if (jobDefinition.lockLimit && jobDefinition.lockLimit <= jobDefinition.locked) { shouldLock = false; } + debug('job [%s] lock status: shouldLock = %s', name, shouldLock); return shouldLock; } @@ -790,11 +873,13 @@ function processJobs(extraJob) { // Already running this? Return if (self._isLockingOnTheFly) { + debug('lockOnTheFly() already running, returning'); return; } // Don't have any jobs to run? Return if (!self._jobsToLock.length) { + debug('no jobs to current lock on the fly, returning'); self._isLockingOnTheFly = false; return; } @@ -810,6 +895,7 @@ function processJobs(extraJob) { // Jobs that were waiting to be locked will be picked up during a // future locking interval. if (!shouldLock(job.attrs.name)) { + debug('lock limit hit for: [%s]', job.attrs.name); self._jobsToLock = []; self._isLockingOnTheFly = false; return; @@ -832,6 +918,7 @@ function processJobs(extraJob) { // Did the "job" get locked? Create a job object and run if (resp.value) { + debug('found a job that can be locked on the fly in MongoDB'); var job = createJob(self, resp.value); self._lockedJobs.push(job); definitions[job.attrs.name].locked++; @@ -857,6 +944,7 @@ function processJobs(extraJob) { // Don't lock because of a limit we have set (lockLimit, etc) if (!shouldLock(name)) { + debug('lock limit reached in queue filling for [%s]', name); return; } @@ -864,10 +952,14 @@ function processJobs(extraJob) { var now = new Date(); self._nextScanAt = new Date(now.valueOf() + self._processEvery); + // NOTE: This may print a lot based on _processInterval + debug('_nextScanAt set to [%s]', self._nextScanAt.toISOString()); + // For this job name, find the next job to run and lock it! self._findAndLockNextJob(name, definitions[name], function(err, job) { if (err) { + debug('[%s] job lock failed while filling queue', name); throw err; } @@ -877,6 +969,7 @@ function processJobs(extraJob) { // 3. Queue the job to actually be run now that it is locked // 4. Recursively run this same method we are in to check for more available jobs of same type! if (job) { + debug('[%s:%s] job locked while filling queue', name, job.attrs._id); self._lockedJobs.push(job); definitions[job.attrs.name].locked++; enqueueJobs(job); @@ -910,13 +1003,17 @@ function processJobs(extraJob) { // We now have the job we are going to process and its definition var job = jobQueue.splice(next, 1)[0], jobDefinition = definitions[job.attrs.name]; + debug('[%s:%s] about to process job', job.attrs.name, job.attrs._id); // If the 'nextRunAt' time is older than the current time, run the job // Otherwise, setTimeout that gets called at the time of 'nextRunAt' if (job.attrs.nextRunAt < now) { + debug('[%s:%s] nextRunAt is in the past, run the job immediately', job.attrs.name, job.attrs._id); runOrRetry(); } else { - setTimeout(runOrRetry, job.attrs.nextRunAt - now); + var runIn = job.attrs.nextRunAt - now; + debug('[%s:%s] nextRunAt is in the future, calling setTimeout(%d)', job.attrs.name, job.attrs._id, runIn); + setTimeout(runOrRetry, runIn); } /** @@ -935,6 +1032,7 @@ function processJobs(extraJob) { // Remove from local lock // NOTE: Shouldn't we update the 'lockedAt' value in MongoDB so it can be picked up on restart? if (job.attrs.lockedAt < lockDeadline) { + debug('[%s:%s] job lock has expired, freeing it up', job.attrs.name, job.attrs._id); self._lockedJobs.splice(self._lockedJobs.indexOf(job), 1); jobDefinition.locked--; jobProcessing(); @@ -946,6 +1044,7 @@ function processJobs(extraJob) { jobDefinition.running++; // CALL THE ACTUAL METHOD TO PROCESS THE JOB!!! + debug('[%s:%s] processing job', job.attrs.name, job.attrs._id); job.run(processJobResult); // Re-run the loop to check for more jobs to process (locally) @@ -954,6 +1053,7 @@ function processJobs(extraJob) { } else { // Run the job immediately by putting it on the top of the queue + debug('[%s:%s] concurrency preventing immediate run, pushing job to top of queue', job.attrs.name, job.attrs._id); enqueueJobs(job, true); } @@ -973,7 +1073,10 @@ function processJobs(extraJob) { var name = job.attrs.name; // Job isn't in running jobs so throw an error - if (self._runningJobs.indexOf(job) === -1) throw ("callback already called - job " + name + " already marked complete"); + if (self._runningJobs.indexOf(job) === -1) { + debug('[%s] callback was called, job must have been marked as complete already', job.attrs._id); + throw ("callback already called - job " + name + " already marked complete"); + } // Remove the job from the running queue self._runningJobs.splice(self._runningJobs.indexOf(job), 1); diff --git a/lib/job.js b/lib/job.js index 7d3cab37c..f71923d9b 100644 --- a/lib/job.js +++ b/lib/job.js @@ -4,7 +4,7 @@ */ var humanInterval = require('human-interval'), CronTime = require('cron').CronTime, date = require('date.js'), - moment = require('moment-timezone'); + moment = require('moment-timezone'), debug = require('debug')('agenda:job'); var Job = module.exports = function Job(args) { args = args || {}; @@ -87,6 +87,8 @@ Job.prototype.computeNextRunAt = function() { * @returns {undefined} */ function computeFromInterval() { + + debug('[%s:%s] computing next run via interval [%s]', this.attrs.name, this.attrs._id, interval); var lastRun = this.attrs.lastRunAt || new Date(); lastRun = dateForTimezone(lastRun); try { @@ -97,18 +99,22 @@ Job.prototype.computeNextRunAt = function() { nextDate = cronTime._getNextDateFrom(dateForTimezone(new Date(lastRun.valueOf() + 1000))); } this.attrs.nextRunAt = nextDate; + debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString()); } catch (e) { // Nope, humanInterval then! try { if (!this.attrs.lastRunAt && humanInterval(interval)) { this.attrs.nextRunAt = lastRun.valueOf(); + debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString()); } else { this.attrs.nextRunAt = lastRun.valueOf() + humanInterval(interval); + debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString()); } } catch (e) {} } finally { if (isNaN(this.attrs.nextRunAt)) { this.attrs.nextRunAt = undefined; + debug('[%s:%s] failed to calculate nextRunAt due to invalid repeat interval', this.attrs.name, this.attrs._id); this.fail('failed to calculate nextRunAt due to invalid repeat interval'); } } @@ -125,11 +131,14 @@ Job.prototype.computeNextRunAt = function() { var offset = Date.now(); // if you do not specify offset date for below test it will fail for ms if (offset === date(repeatAt,offset).valueOf()) { this.attrs.nextRunAt = undefined; + debug('[%s:%s] failed to calculate repeatAt due to invalid format', this.attrs.name, this.attrs._id); this.fail('failed to calculate repeatAt time due to invalid format'); } else if (nextDate.valueOf() === lastRun.valueOf()) { this.attrs.nextRunAt = date('tomorrow at ', repeatAt); + debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString()); } else { this.attrs.nextRunAt = date(repeatAt); + debug('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString()); } } }; @@ -221,6 +230,7 @@ Job.prototype.fail = function(reason) { var now = new Date(); this.attrs.failedAt = now; this.attrs.lastFinishedAt = now; + debug('[%s:%s] fail() called [%d] times so far', this.attrs.name, this.attrs._id, this.attrs.failCount); return this; }; @@ -237,6 +247,7 @@ Job.prototype.run = function(cb) { var setImmediate = setImmediate || process.nextTick; // eslint-disable-line no-use-before-define setImmediate(function() { self.attrs.lastRunAt = new Date(); + debug('[%s:%s] setting lastRunAt to: %s', self.attrs.name, self.attrs._id, self.attrs.lastRunAt.toISOString()); self.computeNextRunAt(); self.save(function() { var jobCallback = function(err) { @@ -246,33 +257,43 @@ Job.prototype.run = function(cb) { if (!err) self.attrs.lastFinishedAt = new Date(); self.attrs.lockedAt = null; + debug('[%s:%s] job finished at [%s] and was unlocked', self.attrs.name, self.attrs._id, self.attrs.lastFinishedAt); + self.save(function(saveErr, job) { cb && cb(err || saveErr, job); if (err) { agenda.emit('fail', err, self); agenda.emit('fail:' + self.attrs.name, err, self); + debug('[%s:%s] failed to be saved to MongoDB', self.attrs.name, self.attrs._id); } else { agenda.emit('success', self); agenda.emit('success:' + self.attrs.name, self); + debug('[%s:%s] was saved successfully to MongoDB', self.attrs.name, self.attrs._id); } agenda.emit('complete', self); agenda.emit('complete:' + self.attrs.name, self); + debug('[%s:%s] job has finished', self.attrs.name, self.attrs._id); }); }; try { agenda.emit('start', self); agenda.emit('start:' + self.attrs.name, self); + debug('[%s:%s] starting job', self.attrs.name, self.attrs._id); if (!definition) { + debug('[%s:%s] has no definition, can not run', self.attrs.name, self.attrs._id); throw new Error('Undefined job'); } if (definition.fn.length === 2) { + debug('[%s:%s] process function being called', self.attrs.name, self.attrs._id); definition.fn(self, jobCallback); } else { + debug('[%s:%s] process function being called', self.attrs.name, self.attrs._id); definition.fn(self); jobCallback(); } } catch (e) { + debug('[%s:%s] unknown error occurred', self.attrs.name, self.attrs._id); jobCallback(e); } }); diff --git a/package.json b/package.json index 02695ba28..dd6ec3302 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "url": "https://github.com/agenda/agenda/issues" }, "dependencies": { + "debug": "^2.6.8", "cron": "~1.1.0", "date.js": "~0.3.1", "human-interval": "~0.1.3", diff --git a/yarn.lock b/yarn.lock index bfc53a692..79d4707ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1071,7 +1071,7 @@ qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" -readable-stream@2.2.7: +readable-stream@2.2.7, readable-stream@^2.2.2: version "2.2.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" dependencies: