UNPKG

promisesds

Version:

ES6 Promises data structures and utils

289 lines (276 loc) 11 kB
module.exports = function() { "use strict"; /** * Abstracts a sequence of a asynchronous actions, the order of execution of the * different actions is enforced using deferred objects. * The successful completion of an action will trigger the start of the next one. * If an action fails the following actions will fail too until an action with * fallback is found in the queue (the fallback action will be called then). * * Actions consist of a function that receives a Deferred object as its first * parameter and the result of the previous action as the following parameters. * * A Deferred object consists of a resolve and reject methods that manage the underlying * promise * * Actions are pushed using the available methods or using an array when * the sequence is created. * For every push feature there is an object syntax using properties and a * method and parameters syntax. Additional features include pushing promises, * setting timeouts for the sequence to reach a point and executing actions * when the queue is empty. * * @param {array[Object|function]} actions An array with the inital actions to * execute in the secuence using * object syntax: * Function: action to execute. The sequence will continue when it resolves * its Deferred object. * {action, fallback}: action and fallback in case of failure of the * previous action. * {promise}: promise that will stop the secuence untils it's completed * {synchronous}: action executed synchronously without the need to resolve * the deferred object. * {timeout, duration}: action to execute if the Sequence has not * reached that point after duration. * {whenEmpty, fallback}: action to execute when the sequence has no * pending actionsto execute. */ var Sequence = function (actions) { var self = this; this.lastPromise = Promise.resolve(); if (Array.isArray(actions)) { actions.forEach(function (action) { self.pushObject(action); }); } else if (actions !== undefined) { throw new Error('actions (if passed) must be an array'); } }; /** * Aux function to emulate jQuery deferred functionality * @return {Object} Object with resolve and reject methods and a promise property */ var deferred = function() { var dfr; var promise = new Promise(function(resolve, reject) { dfr = { resolve: resolve, reject: reject }; }); dfr.promise = promise; return dfr; }; /** * Adds an action with object syntax @see Sequence(actions) * @param {Object} obj action or feature to add to the sequence * @return {Sequence} current instance to allow chaining */ Sequence.prototype.pushObject = function (obj) { if (obj && obj.call) { this.push(obj); } else if (obj.action) { this.push(obj.action, obj.fallback); } else if (obj.timeout) { this.setTimeout(obj.timeout, obj.duration); } else if (obj.whenEmpty) { this.whenEmpty(obj.whenEmpty, obj.fallback); } else if (obj.promise) { this.pushPromise(obj.promise); } else if (obj.synchronous) { this.pushSynchronous(obj.synchronous, obj.fallback); } else { var err = new Error('action not recognized ' + obj); err.action = obj; throw err; } return this; }; /** * [private] Pipes the resolveWith and rejectWith of two promises so when the origin * is completed the target completes too. Checks if origin is a promise * @param {Promise} origin promise to attach the callbacks to pipe the completion * @param {Deferred} target deferred linked to the origin promise */ var pipeResolve = function (origin, target) { if (origin && origin.then && origin.then.call) { origin.then(function (value) { target.resolve(value); }, function (value) { target.reject(value); }); } }; /** * Main method to add actions to the sequence pushes actions at the end of * the sequence that will be executed when all the previous ones are resolved. * The fallback method is called if the previous action failed. * @param {Function} action Action to execute * (action(deferred, [args,]) : result) * deferred: Deferred object that will trigger the next action if * completed succesfully or call the next fallback if rejected. The * arguments passed when resolved will be passed to the next action or * fallback. * args: optional arguments sent by the previous action. * result: optional Deferred that will resolve the action instead of the * parameter deferred * @param {Function} fallback Action to execute if the last action failed * (fallback(deferred, [args,]) : result) * deferred: Deferred object that will trigger the next action if * completed succesfully or call the next fallback if rejected. The * arguments passed when resolved will be passed to the next action or * fallback. * args: optional arguments sent by the previous action. * result: optional Deferred that will resolve the action instead of the * parameter deferred * @return {Sequence} current instance to allow chaining */ Sequence.prototype.push = function (action, fallback) { var nextDeferred = deferred(); var oldPromise = this.lastPromise; this.lastPromise = nextDeferred.promise; delete nextDeferred.promise; oldPromise.then(function (value) { var result = action(nextDeferred, value); pipeResolve(result, nextDeferred); }); if (fallback) { oldPromise.then(null, function (value) { var result = fallback(nextDeferred, value); pipeResolve(result, nextDeferred); }); } else { oldPromise.then(null, function (value) { nextDeferred.reject(value); }); } return this; }; /** * Pushes a promise into the sequence, the sequence cannot control * the start of the action but guarantees that the next action will not * be executed until all the previous ones and the promise completes * @param {Promise} promise Promise to introduce in the sequence * @return {Sequence} current instance to allow chaining */ Sequence.prototype.pushPromise = function (promise) { var oldPromise = this.lastPromise; this.push(function (deferred) { oldPromise.then(function () { promise.then(function (value) { deferred.resolve(value); }, function (value) { deferred.reject(value); }); }, function (value) { promise.always(function () { deferred.reject(value); }); }); }); return this; }; /** * Adds an action and a fallback to be executed synchronously. * The function don't receive a deferred object and they will execute * when the previous action completes and the next action will be triggered * as soon as the function exits. * Synchronous actions cannot stop the execution of the next action. * @param {Function} action Action to execute if the previous one completes * successfully. * (action([args,]) : result) * args: optional arguments sent by the previous action. * result: optional return value sent to the next action * @param {Function} fallback Action to execute if the previous one fails. * (fallback([args,]) : result) * args: optional arguments sent by the previous actions. * result: optional return value sent to the next action * @return {Sequence} current instance to allow chaining */ Sequence.prototype.pushSynchronous = function (action, fallback) { this.push(function (deferred, value) { var result = action(value); deferred.resolve(result); }, function (deferred, value) { var result = fallback(value); deferred.resolve(result); }); return this; }; /** * Sets a timeout at the current position in the sequence if the timeout * expires before the previous action completes the handler will be fired * as if it were a regular action. If the previous action completes before * the timeout expires the next action will be executed and the timeout * handler will never be called. * @param {Function} handler Action to execute if the timeout expires * (handler(deferred) : result) * @see Sequence.push() action parameter * @param {Int} duration Milliseconds to wait before triggering * the timeout handler * @return {Sequence} current instance to allow chaining */ Sequence.prototype.setTimeout = function (handler, duration) { var timeoutDfr = deferred(); var timeoutFired = false; var id = setTimeout(function () { timeoutFired = true; var result = handler(timeoutDfr); pipeResolve(result, timeoutDfr); }, duration); var oldPromise = this.lastPromise; this.lastPromise = timeoutDfr.promise; var pipeDfr = function (method) { return function (value) { if (!timeoutFired) { clearTimeout(id); method(value); } }; }; oldPromise.then(pipeDfr(timeoutDfr.resolve), pipeDfr(timeoutDfr.reject)); return this; }; /** * Adds an action that will be executed when the sequence has no * more actions to execute. If actions are added after whenEmpty action * is added but before it is executed they will be executed before. * whenEmpty action will be executed at most once (if actions keep being * added or are not resolved it can starve) * @param {Function} action Action to execute if the last action succeed * (action(deferred, [args,]) : result) * @see Sequence.push() action parameter * @param {Function} fallback Action to execute if the last action failed * (fallback(deferred, [args,]) : result) * @see Sequence.push() fallback parameter * @return {Sequence} current instance to allow chaining */ Sequence.prototype.whenEmpty = function (action, fallback) { var currentPromise = this.lastPromise; var self = this; var pipeActions = function (func) { return function (value) { if (self.lastPromise === currentPromise) { var nextDeferred = deferred(); self.lastPromise = nextDeferred.promise; var result = func(nextDeferred, value); pipeResolve(result, nextDeferred); } else { currentPromise = self.lastPromise; currentPromise.then(pipeActions(action), pipeActions(fallback)); } }; }; currentPromise.then(pipeActions(action), pipeActions(fallback)); return this; }; /** * Returns the promise that will be resolved by the last action currently * in the sequence. * @return {Promise} promise of the last action in the sequence */ Sequence.prototype.promise = function () { return this.lastPromise; }; return Sequence; }();