@oat-sa/tao-core-sdk
Version:
Core libraries of TAO
483 lines (427 loc) • 13.9 kB
JavaScript
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2015-2019 (original work) Open Assessment Technologies SA ;
*/
/**
* Defines a polling manager with flow control: schedules action to run periodically.
* Ensures each polling step is done before scheduling the next one, even if the action is asynchronous.
* Handles synchronous as wells as asynchronous actions.
* The scheduling can be paused/resumed any time, or the next schedule can be forced to occurs immediately.
*
* @example <caption>Simple synchronous polling</caption>
* // direct assignment
* var poll = polling({
* action: function() {
* // something to do at interval
*
* // you can stop the polling immediately at this point is needed, using:
* // this.stop();
* },
* interval: 50, // each action will occur 50 ms after the last step
* autoStart: true // start immediately
* });
*
* // explicit assignment
* var poll = polling();
*
* poll.setAction(function() {
* // something to do at interval
* });
*
* // each action will occur 50 ms after the last step
* poll.setInterval(50);
*
* // start the polling
* poll.start();
*
* // stop after a period of time
* setTimeout(function() {
* poll.stop();
* }, 1000);
*
* @example <caption>Asynchronous polling</caption>
* // direct assignment
* var poll = polling({
* action: function() {
* // get into asynchronous mode
* var async = this.async();
*
* // defer the next schedule
* setTimeout(function() {
* if (needToContinue) {
* // continue the polling
* async.resolve();
* } else {
* // stop immediately the polling
* async.reject();
* }
* }, 100);
* },
* interval: 50, // each action will occur 50 ms after the last step
* autoStart: true // start immediately
* });
*
* // explicit assignment
* var poll = polling();
*
* // you can also change the 'this' of each action
* var anObject = { foo: 'bar' };
* poll.setContext(anObject);
*
* poll.setAction(function(p) {
* // get into asynchronous mode,
* // but as the context is not the polling manager
* // you need to use the argument
* var async = p.async();
*
* // defer the next schedule
* setTimeout(function() {
* if (needToContinue) {
* // continue the polling
* async.resolve();
* } else {
* // stop immediately the polling
* async.reject();
* }
* }, 100);
* });
*
* // each action will occur 50 ms after the last step
* poll.setInterval(50);
*
* // start the polling
* poll.start();
*
* // stop after a period of time
* setTimeout(function() {
* poll.stop();
* }, 1000);
*
* @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
*/
import _ from 'lodash';
import Promise from 'core/promise';
import eventifier from 'core/eventifier';
/**
* The default value of the polling interval
* @type {Number}
* @private
*/
const _defaultInterval = 60 * 1000;
/**
* Create a polling manager for a particular action
* @param {Object|Function} [config] - A config object, or the action called on each iteration
* @param {Function} [config.action] - The callback action called on each iteration, the polling instance is provided as first argument
* @param {Number|String} [config.interval] - The minimal time between two iterations
* @param {Number|String} [config.max] - Set a max number of iterations, after what the polling is stopped.
* @param {Boolean} [config.autoStart] - Whether or not the polling should start immediately
* @param {Object} [config.context] - An optional context to apply on each action call
* @param {number} pollingInterval - The minimal time between two iterations (to be set when the first parameter is a function)
* @returns {polling}
*/
function pollingFactory(config, pollingInterval = _defaultInterval) {
let timer, promise, interval, max, iter, action, context, autoStart;
const state = {};
/**
* Defines the polling manager
* @type {Object}
*/
const polling = {
/**
* Gets the current action into asynchronous mode.
* The next iteration won't be executed until the resolve method has been called.
* However if the reject method is called, the polling is then stopped!
* @returns {Object} Returns a promise resolver that provides resolve() and reject() methods
*/
async() {
const resolver = {};
// create a promise and extract the control callbacks
promise = new Promise(function (resolve, reject) {
resolver.resolve = resolve;
resolver.reject = reject;
});
// directly install the pending actions
promise
.then(function () {
promise = null;
state.processing = false;
// next iteration only if allowed
if (!state.stopped) {
startTimer();
}
/**
* Notifies the polling continues
* @event polling#resolved
*/
polling.trigger('resolved');
})
.catch(function () {
promise = null;
state.processing = false;
// breaks the polling
polling.stop();
/**
* Notifies the polling has been halted
* @event polling#rejected
*/
polling.trigger('rejected');
});
/**
* Notifies the current action is asynchronous
* @event polling#async
* @param {Object} resolver
* @param {Function} resolver.resolve
* @param {Function} resolver.reject
*/
polling.trigger('async', resolver);
return resolver;
},
/**
* Forces the next iteration to be executed now, unless it is already running.
* If the polling has been stopped, start it again.
* @returns {polling}
*/
next() {
var _next;
// reset the counter if the polling is stopped
if (state.stopped) {
iter = 0;
}
// ensure the scheduling if off
stopTimer();
// prevent more iterations than needed to be ran
if (max && iter >= max) {
return this;
}
// the next() method can be called either to force a next iteration or to start immediately the action
// so we need to ensure the schedule is not blocked
state.stopped = false;
if (!promise) {
/**
* Notifies the action
* @event polling#next
* @param {polling} polling
*/
this.trigger('next');
iteration();
} else {
// as a promise is still pending, ensure a call to next() will be processed after
_next = this.next.bind(this);
promise.then(_next).catch(_next);
}
return this;
},
/**
* Starts the polling if it is not currently running
* @returns {polling}
*/
start() {
if (!timer) {
iter = 0;
startTimer();
/**
* Notifies the start
* @event polling#start
*/
this.trigger('start');
}
return this;
},
/**
* Stops the polling if it is currently running
* @returns {polling}
*/
stop() {
stopTimer();
/**
* Notifies the stop
* @event polling#stop
*/
this.trigger('stop');
return this;
},
/**
* Sets the minimum time interval between two actions
* @param {Number|String} value
* @returns {polling}
*/
setInterval(value) {
interval = Math.abs(parseInt(value, 10) || _defaultInterval);
/**
* Notifies the interval change
* @event polling#setinterval
* @param {Number} interval
*/
this.trigger('setinterval', interval);
return this;
},
/**
* Gets the minimum time interval between two actions
* @returns {Number}
*/
getInterval() {
return interval;
},
/**
* Sets the polling action
* @param {Function} fn
* @returns {polling}
*/
setAction(fn) {
action = fn;
/**
* Notifies the action change
* @event polling#setaction
* @param {Function} action
*/
this.trigger('setaction', action);
return this;
},
/**
* Gets the polling action
* @returns {Function}
*/
getAction() {
return action;
},
/**
* Sets the context applied on each action call
* @param {Object} ctx
* @returns {polling}
*/
setContext(ctx) {
context = ctx || this;
/**
* Notifies the context change
* @event polling#setcontext
* @param {Object} context
*/
this.trigger('setcontext', ctx);
return this;
},
/**
* Gets the context applied on each action call
* @returns {Object}
*/
getContext() {
return context;
},
/**
* Sets the max number of polling occurrences
* @param {Number} value
* @returns {polling}
*/
setMax(value) {
max = Math.abs(parseInt(value, 10) || 0);
return this;
},
/**
* Gets the max number of polling occurrences
* @returns {Number}
*/
getMax() {
return max;
},
/**
* Gets the number of ran iterations
* @returns {Number}
*/
getIteration() {
return iter || 0;
},
/**
* Checks if the manager is in a particular state
* @param {String} stateName The name of the state to check. Possible values are:
* - stopped: the polling manager is stopped, and won't process action until restart
* - pending: the polling manager has scheduled an action an is waiting for it processing
* - processing: the polling manager is currently processing an action and wait for its completion
* @returns {Boolean}
*/
is: function is(stateName) {
return !!state[stateName];
}
};
/**
* Fires a new timer
*/
function startTimer() {
timer = setTimeout(iteration, interval);
state.stopped = false;
state.pending = true;
}
/**
* Stops the current timer
*/
function stopTimer() {
clearTimeout(timer);
timer = null;
state.stopped = true;
state.pending = false;
}
/**
* Runs an iteration of the polling loop
*/
function iteration() {
// prevent more iterations than needed to be ran
if (max && iter >= max) {
// breaks the polling
polling.stop();
return;
}
// count the iteration
iter = (iter || 0) + 1;
state.processing = true;
state.pending = false;
/**
* Notifies the action is about to be called
* @event polling#call
*/
polling.trigger('call');
// process the action in the right context
action.call(context, polling);
// next iteration in synchronous mode
if (!promise && !state.stopped) {
state.processing = false;
startTimer();
}
}
eventifier(polling);
// some defaults
interval = _defaultInterval;
context = polling;
action = null;
state.stopped = true;
autoStart = false;
iter = 0;
// maybe only the action is provided
if (_.isFunction(config)) {
polling.setAction(config);
config = null;
}
// loads the config
if (_.isObject(config)) {
polling.setAction(config.action);
polling.setInterval(config.interval || pollingInterval);
polling.setContext(config.context);
polling.setMax(config.max);
autoStart = !!config.autoStart;
}
if (autoStart) {
polling.start();
}
return polling;
}
export default pollingFactory;