detail/TimeForwarder.js
'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;