UNPKG

gate-executor

Version:

A work queue that can be gated, stopping to wait for sub-queues to complete.

375 lines (302 loc) 11.7 kB
/* Copyright (c) 2014-2021 Richard Rodger, MIT License */ type Work = { id: string // Identifier ge: string // GateExecutor identifier tm: number // Timeout dn: string // Description fn: (callback: () => void) => void start: number end: number gate?: any callback: () => void ctxt: { [key: string]: any } } // 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?: any) { 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(this: any, options: any, instance_counter: number) { let self: any = {} self.id = ++instance_counter self.options = options // Work queue. let q: Work[] = [] // Work-in-progress set. let progress: any = { // 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: any[] = [] // Internal state. let s: any = { // 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: any) { 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: any) { // 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: any) { 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: 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: any = GateExecutor(options, instance_counter) let fn = function gate(done: any) { // 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: any = [] // 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 export default MakeGateExecutor if (undefined != typeof (module.exports)) { module.exports = MakeGateExecutor }