loop-controller
Version:
Controls serial execution of promises.
205 lines (204 loc) • 7.96 kB
JavaScript
"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;