gate-executor
Version:
A work queue that can be gated, stopping to wait for sub-queues to complete.
294 lines • 12.7 kB
JavaScript
/* Copyright (c) 2014-2021 Richard Rodger, MIT License */
Object.defineProperty(exports, "__esModule", { value: true });
// Create root instance. Exported as module.
// * `options` (object): instance options as key-value pairs.
//
// The options are:
// * `interval` (integer): millisecond interval for timeout checks. Default: 111.
// * `timeout` (integer): common millisecond timeout.
// Can be overridden by work item options. Default: 2222.
function MakeGateExecutor(options) {
options = options || {};
options.interval = null == options.interval ? 111 : options.interval;
options.timeout = null == options.timeout ? 2222 : options.timeout;
return GateExecutor(options, 0);
}
// Create a new instance.
// * `options` (object): instance options as key-value pairs.
// * `instance_counter` (integer): count number of instances created;
// used as identifier.
function GateExecutor(options, instance_counter) {
let self = {};
self.id = ++instance_counter;
self.options = options;
// Work queue.
let q = [];
// Work-in-progress set.
let progress = {
// Lookup work by id.
lookup: {},
// Work history - a list of work items in the order executed.
history: [],
};
// List of work items to check for timeouts.
let timeout_checklist = [];
// Internal state.
let s = {
// Count of work items added to this instance. Used as generated work identifier.
work_counter: 0,
// When `true`, the instance is in a gated state, and work cannot proceed
// until the gated in-progress work item is completed.
gate: false,
// When `true`, the instance processes work items as they arrive.
// When `false`, no processing happens, and the instance must be started by
// calling the `start` method.
running: false,
// A function called when the work queue and work-in-progress set
// are empty. Set by calling the `clear` method. Will be called
// each time the instance empty.
clear: null,
// A function called once only when the work queue and
// work-in-progress set are first emptied after each start. Set as
// an optional argument to the `start` method.
firstclear: null,
// Timeout interval reference value returned by `setInterval`.
// Timeouts are not checked using `setTimeout`, as it is more
// efficient, and more than sufficient, to check timeouts periodically.
tm_in: null,
hw_tmc: 0,
hw_hst: 0,
};
// Process the next work item.
function processor() {
// If not running, don't process any work items.
if (!s.running) {
return;
}
// The timeout interval check is stopped and started only as needed.
if (!self.isclear() && !s.tm_in) {
s.tm_in = setInterval(timeout_check, options.interval);
}
// Process the next work item, returning `true` if there was one.
let next = false;
do {
next = false;
let work = null;
// Remove next work item from the front of the work queue.
if (!s.gate) {
work = q.shift();
}
if (work) {
// Add work item to the work-in-progress set.
progress.lookup[work.id] = work;
progress.history.push(work);
s.hw_hst =
progress.history.length > s.hw_hst ? progress.history.length : s.hw_hst;
// If work item is a gate, set the state of the instance as
// gated. This work item will need to complete before later
// work items in the queue can be processed.
s.gate = work.gate;
// Call the work item function (which does the real work),
// passing a callback. This callback has no arguments
// (including no error!). It is called only to indicate
// completion of the work item. Work items must handle their
// own errors and results.
work.start = Date.now();
work.callback = make_work_fn_callback(work);
timeout_checklist.push(work);
s.hw_tmc =
timeout_checklist.length > s.hw_tmc ? timeout_checklist.length : s.hw_tmc;
work.fn(work.callback);
next = true;
}
} while (next);
// Keep processing work items until none are left or a gate is reached.
}
// Create the callback for the work function
function make_work_fn_callback(work) {
return function work_fn_callback() {
if (work.done) {
return;
}
work.end = Date.now();
// Remove the work item from the work-in-progress set. As
// work items may complete out of order, prune the history
// from the front until the first incomplete work
// item. Later complete work items will eventually be
// reached on another processing round.
work.done = true;
delete progress.lookup[work.id];
while (progress.history[0] && progress.history[0].done) {
progress.history.shift();
}
while (timeout_checklist[0] && timeout_checklist[0].done) {
timeout_checklist.shift();
}
// If the work item was a gate, it is now complete, and the
// instance can be ungated, allowing later work items in the
// queue to be processed.
if (work.gate) {
s.gate = false;
}
// If work queue and work-in-progress set are empty, then
// call the registered clear functions.
if (0 === q.length && 0 === progress.history.length) {
clearInterval(s.tm_in);
s.tm_in = null;
if (s.firstclear) {
let fc = s.firstclear;
s.firstclear = null;
fc();
}
if (s.clear) {
s.clear();
}
}
// Process each work item on next tick to avoid lockups.
setImmediate(processor);
};
}
// To be run periodically via setInterval. For timed out work items,
// calls the done callback to allow work queue to proceed, and marks
// the work item as finished. Work items can receive notification of
// timeouts by providing an `ontm` callback property in the
// work definition object. Work items must handle timeout errors
// themselves, gate-executor cares only for the fact that a timeout
// happened, so it can continue processing.
function timeout_check() {
let now = Date.now();
let work = null;
for (let i = 0; i < timeout_checklist.length; ++i) {
work = timeout_checklist[i];
if (!work.gate && !work.done && work.tm < now - work.start) {
if (work.ontm) {
work.ontm(work.tm, work.start, now);
}
work.callback();
}
}
}
// Start processing work items. Must be called to start processing.
// Can be called at anytime, interspersed with calls to other
// methods, including `add`. Takes a function as argument, which is
// called only once on the next time the queues are clear.
self.start = function (firstclear) {
// Allow API chaining by not starting in current execution path.
setImmediate(function () {
s.running = true;
if (firstclear) {
s.firstclear = firstclear;
}
processor();
});
return self;
};
// Pause the processing of work items. Newly added items, and items
// not yet started, will not proceed, but items already in progress
// will complete, and the clear function will be called once all in
// progress items finish.
self.pause = function () {
s.running = false;
};
// Submit a function that will be called each time there are no more
// work items to process. Multiple calls to this method will replace
// the previously registered clear function.
self.clear = function (done) {
s.clear = done;
return self;
};
// Returns `true` when there are no more work items to process.
self.isclear = function () {
return 0 === q.length && 0 === progress.history.length;
};
// Add a work item. This is an object with fields:
// * `fn` (function): the function that performs the work. Takes a
// single argument, the callback function to call when the work is
// complete. THis callback does **not** accept errors or
// results. It's only purpose is to indicate that the work is
// complete (whether failed or not). The work function itself must
// handle callbacks to the application. Required.
// * `id` (string): identifier for the work item. Optional.
// * `tm` (integer): millisecond timeout specific to this work item,
// overrides general timeout. Optional.
// * `ontm` (function): callback to indicate work item timeout. Optional.
// * `dn` (string): description of the work item, used in the
// state description. Optional.
self.add = function (work) {
s.work_counter += 1;
work.id = work.id || '' + s.work_counter;
work.ge = self.id;
work.tm = null == work.tm ? options.timeout : work.tm;
work.dn = work.dn || work.fn.name || '' + Date.now();
// Used by calling code to store additional context.
work.ctxt = {};
q.push(work);
if (s.running) {
// Work items are **not** processed in the current execution path!
// This prevents lockup, and avoids false positives in unit tests.
// Work items are assumed to be inherently asynchronous.
setImmediate(processor);
}
return self;
};
// Create a new gate. Returns a new `GateExecutor` instance. All
// work items added to the new instance must complete before the
// gate is cleared, and work items in the queue can be processed. A
// gate is cleared when the new instance is **first** cleared. Work
// items subsequently added to the new instance are not considered
// part of the gate. Gates can extend to any depth and form a tree
// structure that requires breadth-first traversal in terms of the
// work item queue. Gates do not have timeouts, and can only be
// cleared when all added work items complete.
self.gate = function () {
let ge = GateExecutor(options, instance_counter);
let fn = function gate(done) {
// This is the work function of the gate, which starts the new
// instance, and considers the gate work item complete when the
// work queue clears for the first time.
ge.start(done);
};
self.add({ gate: ge, fn: fn });
return ge;
};
// Return a data structure describing the current state of the work
// queues, and organised as a tree structure indicating the gating
// relationships.
self.state = function () {
let out = [];
// First list any in-progress work items.
for (let hI = 0; hI < progress.history.length; ++hI) {
let pe = progress.history[hI];
if (!pe.done) {
out.push({ s: 'a', ge: pe.ge, dn: pe.dn, id: pe.id });
}
}
// Then list any waiting work items.
for (let qI = 0; qI < q.length; ++qI) {
let qe = q[qI];
if (qe.gate) {
// Go down a level when there's a gate.
out.push(qe.gate.state());
}
else {
out.push({ s: 'w', ge: qe.ge, dn: qe.dn, id: qe.id });
}
}
out.internal = {
qlen: q.length,
hlen: progress.history.length,
klen: Object.keys(progress.lookup).length,
tlen: timeout_check.length,
hw_hst: s.hw_hst,
hw_tmc: s.hw_tmc,
};
return out;
};
return self;
}
// The module function
exports.default = MakeGateExecutor;
if (undefined != typeof (module.exports)) {
module.exports = MakeGateExecutor;
}
//# sourceMappingURL=gate-executor.js.map
;