Lewerow/Zurvan

View on GitHub
zurvan.js

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
'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();