UNPKG

zurvan

Version:

A library for simulating time in Node.js

284 lines (252 loc) 8.37 kB
'use strict'; var TypeChecks = require('./utils/TypeChecks'); var TimeUnit = require('../TimeUnit'); var assert = require('assert'); function delayByCycling(schedule, cycleCount, f) { var cyclesExecuted = 0; (function cycle(f) { if (++cyclesExecuted < cycleCount) { schedule(cycle, f); } else { f(); } })(f); } function TimeForwarder( timeServer, timerInterceptor, immediateInterceptor, debugLogger ) { this.forwardingStartedSavedStack = undefined; this.timerInterceptor = timerInterceptor; this.timeServer = timeServer; this.immediateInterceptor = immediateInterceptor; this.debugLogger = debugLogger; } TimeForwarder.prototype.prepareTimeReport = function() { var currentTime = this.timeServer.currentTime.toMilliseconds(); var targetTime = this.timeServer.targetTime.toMilliseconds(); var timeReport = 'Cannot release timers during event expiration ' + 'current time: <<' + currentTime + '>> ms, target time: <<' + targetTime + '>> ms. '; if (targetTime === currentTime) { timeReport = timeReport + ' Target time reached, but queue not cleared yet. '; } return timeReport; }; TimeForwarder.prototype.stopForwarding = function() { var that = this; return new that.schedule.Promise(function(resolve, reject) { if (that.isExpiringEvents()) { return reject( new Error( that.prepareTimeReport() + 'Expiring events requested at: ' + that.forwardingStartedSavedStack ) ); } return resolve(); }); }; TimeForwarder.prototype.stopExpiringEvents = function() { this.forwardingStartedSavedStack = undefined; }; TimeForwarder.prototype.enable = function(config) { this.schedule = { Promise: config.promiseScheduler, EndOfQueue: this.immediateInterceptor.endOfQueueScheduler(), Internal: this.immediateInterceptor.internalScheduler() }; this.config = config; }; TimeForwarder.prototype.disable = function() { this.schedule = undefined; }; TimeForwarder.prototype.startExpiringEvents = function() { this.forwardingStartedSavedStack = new Error().stack; }; TimeForwarder.prototype.isExpiringEvents = function() { return this.forwardingStartedSavedStack !== undefined; }; TimeForwarder.prototype.advanceTime = function(timeToForward) { var advanceStep = new TimeUnit(timeToForward); var that = this; return new that.schedule.Promise(function(resolve, reject) { if (advanceStep.isShorterThan(TimeUnit.milliseconds(0))) { return reject( new Error( 'Zurvan cannot move back in time. Requested step: << ' + advanceStep.toMilliseconds() + 'ms >>' ) ); } if (that.isExpiringEvents()) { return reject( new Error( that.prepareTimeReport() + 'Forwarding requested from: ' + that.forwardingStartedSavedStack ) ); } that.timeServer.targetTime = that.timeServer.currentTime.extended( advanceStep ); that.debugLogger( 'advancing time to ' + that.timeServer.targetTime.toNanoseconds() + 'ns' ); that.startExpiringEvents(); // that's a workaround - in certain cases I believe this might not work (pathological chains of setImmediate/process.nextTick) // but I wasn't able to find out any such scenario, so I'm leaving it here for now - if you find one, file an issue on GitHub // or just increase the counter from configuration parameters delayByCycling( that.schedule.EndOfQueue, that.config.requestedCyclesAroundSetImmediateQueue, fireTimersOneByOne ); var executionErrors = []; var currentSetImmediateBatchSize = 0; function fireTimersOneByOne() { if (that.immediateInterceptor.areAwaiting()) { if ( ++currentSetImmediateBatchSize >= that.config.maxAllowedSetImmediateBatchSize ) { that.immediateInterceptor.startDroppingImmediates(); // full cycle is needed to drop the immediates causing infinite loop // original immediates used, because fake ones are already being dropped delayByCycling( that.schedule.Internal, that.config.requestedCyclesAroundSetImmediateQueue, function() { reject( new Error( 'Possible infinite setImmediate loop detected. ' + currentSetImmediateBatchSize + ' setImmediates in single batch occurred. Dropping all further immediates, global objects are in undefined state.' + ' Forwarding time requested from: ' + that.forwardingStartedSavedStack ) ); } ); return; } that.schedule.EndOfQueue(function() { fireTimersOneByOne(); }); return; } var closestTimer = that.timerInterceptor.nextTimer(); if ( closestTimer && !closestTimer.dueTime.isLongerThan(that.timeServer.targetTime) ) { closestTimer.clear(); that.timeServer.currentTime.setTo(closestTimer.dueTime); that.schedule.EndOfQueue(function() { try { closestTimer.expire(); } catch (err) { executionErrors.push({ failedAt: closestTimer.dueTime, error: err, timerCallDelay: closestTimer.callDelay.toMilliseconds() }); } that.schedule.EndOfQueue(function() { fireTimersOneByOne(); }); }); } else { that.timeServer.currentTime.setTo(that.timeServer.targetTime); that.stopExpiringEvents(); if (that.config.rejectOnCallbackFailure && executionErrors.length > 0) { reject(executionErrors); } else { resolve(executionErrors); } } } }); }; TimeForwarder.prototype.expireAllTimeouts = function() { var lastTimeout = this.timerInterceptor.lastTimeout(); if (lastTimeout) { var that = this; return this.advanceTime( lastTimeout.dueTime.shortened(that.timeServer.currentTime) ).then(function() { return that.expireAllTimeouts(); }); } return this.advanceTime(0); }; TimeForwarder.prototype.forwardTimeToNextTimer = function() { var closestTimer = this.timerInterceptor.nextTimer(); if (closestTimer) { return this.advanceTime( closestTimer.dueTime.shortened(this.timeServer.currentTime) ); } return this.advanceTime(0); }; TimeForwarder.prototype.fireAllOutdatedTimers = function() { var closestTimer = this.timerInterceptor.nextTimer(); while ( closestTimer && !closestTimer.dueTime.isLongerThan(this.timeServer.currentTime) ) { closestTimer.clear(); this.schedule.EndOfQueue(closestTimer.expire.bind(closestTimer)); closestTimer = this.timerInterceptor.nextTimer(); } }; function assertValidBlockStep(blockStep) { if (blockStep.isShorterThan(TimeUnit.milliseconds(0))) { throw new Error( 'Zurvan cannot move back in time. Requested step: << ' + blockStep.toMilliseconds() + 'ms >>' ); } } TimeForwarder.prototype.blockSystem = function(timeToBlock) { var blockStep = new TimeUnit(timeToBlock); assertValidBlockStep(blockStep); if (!this.isExpiringEvents()) { assert(this.timeServer.targetTime.isEqualTo(this.timeServer.currentTime)); this.timeServer.targetTime.add(blockStep); this.debugLogger( 'simulating blocking call until ' + this.timeServer.targetTime.toNanoseconds() + 'ns' ); } else if ( this.timeServer.targetTime.isShorterThan( this.timeServer.currentTime.extended(blockStep) ) ) { throw new Error( 'Cannot block system during advancing for longer than requested advance time. Currently at: << ' + this.timeServer.currentTime.toMilliseconds() + ' >> ms, target: << ' + this.timeServer.targetTime.toMilliseconds() + ' ms >>, requested step: << ' + blockStep.toMilliseconds() + ' ms >>. Forwarding requested from: ' + this.forwardingStartedSavedStack ); } this.timeServer.currentTime.add(blockStep); this.fireAllOutdatedTimers(); }; module.exports = TimeForwarder;