UNPKG

loop-controller

Version:
205 lines (204 loc) 7.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LoopController = void 0; const _yapu_1 = require("yapu"); const _jab_1 = require("@jawis/jab"); const _state_waiter_1 = require("state-waiter"); /** * Controls serial execution of promises made from elements of an array. * * - Takes an array and a function that makes a promise for each element in the array. * - Ensures that it's possible to use the following operations: * - pause * - resume * * Terminology * - loop-promise: Will resolve when all elements have been processed. See `this.getPromise` * - pause-promise: Will resolve when a pause operation succeeds. I.e. when the current iteration is done, and execution stops. * * Features * - Pause-promise can resolve with either "paused" or "cancelled". * - It resolves with paused if the current iteration is finised before a new resume operation arrives * - It resolves with cancelled if a resume operation arrives before the current iteration finishes. * - If an iteration throws an error the execution will stop. * - Both loop and pause promise will be rejected in case of error. * * impl * - When pausing at last iteration, the state will become "paused", and the loop-promise will not resolve, * until resume is called. Maybe that should change? */ class LoopController { /** * */ constructor(deps) { this.deps = deps; /** * */ this.isRunning = () => this.waiter.is("running") || this.waiter.is("pausing"); /** * Return the loop-promise. * * note * this isn't really compatible with: setArray and prependArray. Some semantic needs to be defined. */ this.getPromise = () => this.loopProm.promise; /** * Replace the underlying array. * * - Calling this, will not start execution. Call resume afterwards to do that. * - If executing, the new array will be used after the current iteration is done. * - A new loop promise is created, because the exsisting elements are dropped. Their loop will never resolve. */ this.setArray = (arr) => { this.curIdx = -1; this.arr = arr; //go into paused, so it's evident, that there's more elements, and a loop promise to think about. if (this.waiter.is("done")) { this.waiter.set("paused"); } }; /** * Prepend an array in front of the remaining elements. * * - The new array will be executed before the remaining elements from the old array. * The then the old array will continue executing, as originally planed. */ this.prependArray = (arr) => { const remaining = this.arr.slice(this.curIdx + 1); this.arr = arr.concat(remaining); //set after remaining is calculated. this.curIdx = -1; //go into paused. if (this.waiter.is("done")) { this.waiter.set("paused"); } }; /** * Pause execution * * - It's okay to pause, when already pausing, it's a no-op. * - If already paused, a promise is returned, that resolve to "paused" next tick. * - A promise is returned, that resolves to "paused" when pause state is reached. Or "cancelled" if a resume operation * cancels the pause before current iteration is done. * - Pausing again will return the same promise as before, if pause state hasn't been reached since last pause operation. */ this.pause = () => { const state = this.waiter.getState(); switch (state) { case "paused": case "done": return Promise.resolve("paused"); case "running": this.waiter.set("pausing"); this.pauseProm = (0, _yapu_1.getPromise)(); return (0, _jab_1.def)(this.pauseProm).promise; case "pausing": return (0, _jab_1.def)(this.pauseProm).promise; default: return (0, _jab_1.assertNever)(state); } }; /** * Resume execution * * - It's okay to resume, when already running, it's a no-op. * - Resume resolves the pause-promise with: "cancelled". * - If the loop isn't running at all, e.g. wasn't auto-started, resume will simply start the loop. */ this.resume = () => { const state = this.waiter.getState(); switch (state) { case "pausing": (0, _jab_1.def)(this.pauseProm).resolve("cancelled"); this.waiter.set("running"); return; case "paused": this.waiter.set("running"); this.deps.onStart && this.deps.onStart(); this.tryRunNextTest(); return; case "done": case "running": //nothing to do return; default: return (0, _jab_1.assertNever)(state); } }; /** * * impl * important that `this.arr` and `curIdx` is used in one tick only. Because it can change at any time. */ this.tryRunNextTest = () => { if (this.curIdx + 1 < this.arr.length) { this.curIdx++; this.deps .makePromise(this.arr[this.curIdx]) .then(this.onIterationDone, this.onIterationError); } else { //we're done this.waiter.set("done"); this.loopProm.resolve(); this.deps.onStop && this.deps.onStop(); } }; /** * */ this.onIterationError = (error) => { const state = this.waiter.getState(); switch (state) { case "pausing": (0, _jab_1.def)(this.pauseProm).reject(error); //fall through case "running": this.waiter.set("done"); this.loopProm.reject(error); this.deps.onStop && this.deps.onStop(); return; case "paused": case "done": throw new Error("Impossible"); default: return (0, _jab_1.assertNever)(state); } }; /** * */ this.onIterationDone = () => { this.waiter.event("iteration-done"); const state = this.waiter.getState(); switch (state) { case "pausing": this.waiter.set("paused"); (0, _jab_1.def)(this.pauseProm).resolve("paused"); this.deps.onStop && this.deps.onStop(); return; case "running": this.tryRunNextTest(); return; case "paused": case "done": throw new Error("Impossible: " + state); default: return (0, _jab_1.assertNever)(state); } }; this.waiter = new _state_waiter_1.Waiter({ onError: this.deps.onError || console.log, startState: "paused", }); this.curIdx = -1; this.arr = deps.initialArray; this.loopProm = (0, _yapu_1.getPromise)(); if (deps.autoStart === undefined || deps.autoStart === true) { this.resume(); } } } exports.LoopController = LoopController;