UNPKG

zurvan

Version:

A library for simulating time in Node.js

293 lines (254 loc) 8.62 kB
'use strict'; var Dependencies = require('./detail/Dependencies'); var missingStartupDependencies = Dependencies.missingAtStartup(); if (missingStartupDependencies) { throw new Error(missingStartupDependencies); } var ImmediateInterceptor = require('./detail/ImmediateInterceptor'); var AllTimersInterceptor = require('./detail/AllTimersInterceptor'); var ProcessTimerInterceptor = require('./detail/ProcessTimerInterceptor'); var DateInterceptor = require('./detail/DateInterceptor'); var TimeForwarder = require('./detail/TimeForwarder'); var TimeServer = require('./detail/TimeServer'); var APICreator = require('./detail/utils/APICreator'); var Configuration = require('./detail/Configuration'); function rejectPromiseWithError(errorMessage, promiseScheduler) { return function() { return promiseScheduler.reject(new Error(errorMessage)); }; } function enterRejectingState(actor) { actor.timeForwarder.disable(); actor.advanceTime = rejectPromiseWithError( 'Cannot advance time if timers are not intercepted by this instance of zurvan', actor.config.promiseScheduler ); actor.blockSystem = function() { throw new Error( 'Cannot block system if timers are not intercepted by this instance of zurvan' ); }; actor.expireAllTimeouts = rejectPromiseWithError( 'Cannot expire timeouts if timers are not intercepted by this instance of zurvan', actor.config.promiseScheduler ); actor.forwardTimeToNextTimer = rejectPromiseWithError( 'Cannot forward time if timers are not intercepted by this instance of zurvan', actor.config.promiseScheduler ); } function sequentialScenario(zurvan, scenarioSteps) { return scenarioSteps.reduce(function(prevStep, currStep) { return prevStep.then(function(args) { return currStep(zurvan, args); }); }, zurvan.config.promiseScheduler.resolve()); } function enterForwardingState(actor) { actor.advanceTime = function(timeToForward) { return this.timeForwarder.advanceTime(timeToForward); }; actor.blockSystem = function(timeToBlock) { return this.timeForwarder.blockSystem(timeToBlock); }; actor.expireAllTimeouts = function() { return this.timeForwarder.expireAllTimeouts(); }; actor.forwardTimeToNextTimer = function() { return this.timeForwarder.forwardTimeToNextTimer(); }; actor.timeForwarder.enable(actor.config); } // me sad, but timeouts are global stuff :( // it may be modified in future, but I doubt it var zurvanActiveInstance; function Zurvan(config) { this.globalConfig = config; this.config = this.globalConfig; this.resetSubcomponents(); enterRejectingState(this); } Zurvan.prototype.resetSubcomponents = function() { this.timeServer = new TimeServer(); this.immediateInterceptor = new ImmediateInterceptor(); this.allTimersInterceptor = new AllTimersInterceptor(this.timeServer); this.timeForwarder = new TimeForwarder( this.timeServer, this.allTimersInterceptor, this.immediateInterceptor, function(message) { this.config.debugLogger(message); } ); this.processTimerInterceptor = new ProcessTimerInterceptor(this.timeServer); this.dateInterceptor = new DateInterceptor(this.timeServer); }; Zurvan.prototype.interceptTimers = function(config) { var newConfig = Configuration.merge(config, this.globalConfig); newConfig.debugLogger('intercepting timers'); // this error has to be synchronous, since we do not know yet whether the system supports Promises var missingRuntimeDependencies = Dependencies.missingAtIntercept(newConfig); if (missingRuntimeDependencies) { throw new Error(missingStartupDependencies); } // also synchronous, to keep configuration check before going into the loop var validationResult = Configuration.validate(newConfig); if (validationResult.length > 0) { throw new Error(validationResult.join('\n')); } var interceptionStack = new Error().stack; var that = this; return new newConfig.promiseScheduler(function(resolve, reject) { if (zurvanActiveInstance) { return reject( new Error( 'Cannot intercept timers that are already intercepted by another instance of zurvan. Intercepted: ' + zurvanActiveInstance.interceptionStack ) ); } return resolve(); }).then(function() { return new newConfig.promiseScheduler(function(resolve) { that.config = newConfig; that.interceptionStack = interceptionStack; zurvanActiveInstance = that; that.timeServer.setupTime( that.config.timeSinceStartup, that.config.systemTime ); if (!that.config.ignoreDate) { that.dateInterceptor.intercept(); } that.immediateInterceptor.intercept(that.config); if (!that.config.ignoreProcessTimers) { that.processTimerInterceptor.intercept(); } that.allTimersInterceptor.intercept(that.config); enterForwardingState(that); resolve(); }) .then(function() { return that.waitForEmptyQueue(); }) .catch(function(err) { if (!that.config.ignoreProcessTimers) { that.processTimerInterceptor.release(); } if (!that.config.ignoreDate) { that.dateInterceptor.release(); } that.immediateInterceptor.release(); that.allTimersInterceptor.release(); zurvanActiveInstance = undefined; enterRejectingState(that); return newConfig.promiseScheduler.reject(err); }); }); }; function validateReleasingTimers(zurvan) { return new zurvan.config.promiseScheduler(function(resolve, reject) { if (zurvanActiveInstance !== zurvan) { if (!zurvanActiveInstance) { return reject( new Error( 'Cannot release timers that were not intercepted by zurvan at all' ) ); } return reject( new Error( 'Cannot release timers that were intercepted by different instance of zurvan. Intercepted: ' + zurvanActiveInstance.interceptionStack ) ); } return resolve(); }); } function gatherLeftovers(zurvan) { var leftovers = {}; leftovers.immediates = zurvan.immediateInterceptor.release(); if (!zurvan.config.ignoreProcessTimers) { leftovers.processTime = zurvan.processTimerInterceptor.release(); } if (!zurvan.config.ignoreDate) { leftovers.date = zurvan.dateInterceptor.release(); } var toTimerAPI = function(timer) { return { dueTime: timer.dueTime, callDelay: timer.callDelay, callback: function() { return timer.callback.call(); } }; }; var timers = zurvan.allTimersInterceptor.release(); leftovers.timeouts = timers.timeouts.map(toTimerAPI); leftovers.intervals = timers.intervals.map(toTimerAPI); leftovers.currentTime = zurvan.timeServer.currentTime.copy(); zurvan.interceptionStack = undefined; zurvanActiveInstance = undefined; enterRejectingState(zurvan); return leftovers; } var releaseSteps = [ validateReleasingTimers, function(zurvan) { return zurvan.timeForwarder.stopForwarding(); }, function(zurvan) { return zurvan.waitForEmptyQueue(); }, gatherLeftovers ]; var forcedReleaseSteps = [ validateReleasingTimers, gatherLeftovers, function(zurvan, leftovers) { zurvan.resetSubcomponents(); return leftovers; } ]; Zurvan.prototype.releaseTimers = function() { this.config.debugLogger('releasing timers'); return sequentialScenario(this, releaseSteps); }; Zurvan.prototype.forcedReleaseTimers = function() { this.config.debugLogger('releasing timers (forced)'); return sequentialScenario(this, forcedReleaseSteps); }; Zurvan.prototype.setSystemTime = function(newSystemTime) { return this.timeServer.setSystemTime(newSystemTime); }; Zurvan.prototype.waitForEmptyQueue = function() { return this.advanceTime(0); }; var apiFunctions = [ 'releaseTimers', 'interceptTimers', 'setSystemTime', 'advanceTime', 'blockSystem', 'expireAllTimeouts', 'forwardTimeToNextTimer', 'waitForEmptyQueue', 'forcedReleaseTimers' ]; function createZurvanAPI(newDefaultConfig) { var configuration = Configuration.merge( newDefaultConfig, Configuration.defaultConfiguration() ); var api = APICreator.createAPI(new Zurvan(configuration), apiFunctions); api.withDefaultConfiguration = function(config) { return createZurvanAPI( config ? Configuration.merge(config, configuration) : Configuration.defaultConfiguration() ); }; return api; } module.exports = createZurvanAPI();