UNPKG

@thi.ng/fibers

Version:

Process hierarchies & operators for cooperative multitasking

402 lines (401 loc) 12.2 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; import { INotifyMixin } from "@thi.ng/api/mixins/inotify"; import { isFunction } from "@thi.ng/checks/is-function"; import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; import { illegalState } from "@thi.ng/errors/illegal-state"; import { monotonic, prefixed } from "@thi.ng/idgen"; import { EVENT_FIBER_CANCELED, EVENT_FIBER_DONE, EVENT_FIBER_ERROR, STATE_ACTIVE, STATE_CANCELED, STATE_DONE, STATE_ERROR, STATE_NEW } from "./api.js"; let DEFAULT_ID_GEN = prefixed("fib-", monotonic()); const setDefaultIDGen = (gen) => DEFAULT_ID_GEN = gen; const NO_RESULT = { done: false, value: void 0 }; let Fiber = class { /** * This fiber's user provided or generated ID. */ id; /** * This fiber's parent. */ parent; gen; idgen; state = STATE_NEW; autoTerminate = false; children = []; value = void 0; error; logger; user; _promise; constructor(gen, opts) { if (opts) { this.autoTerminate = !!opts.terminate; this.logger = opts.logger; this.parent = opts.parent; this.user = { init: opts.init, deinit: opts.deinit, catch: opts.catch }; if (opts.id) { this.id = opts.id; } else { this.idgen = opts.idgen || DEFAULT_ID_GEN; } } else { this.idgen = DEFAULT_ID_GEN; } if (this.idgen) this.id = this.idgen.next(); this.gen = isFunction(gen) ? gen(this) : gen; } /** * Co-routine which blocks whilst this fiber (incl. its children) is active. * Then return this fiber's value. */ *[Symbol.iterator]() { while (this.state <= STATE_ACTIVE && this.next() <= STATE_ACTIVE) yield; return this.value; } /** * Returns a promise which only resolves when the fiber is not active * anymore. If there was an unhandled error during the fiber execution the * promise will reject, else if the fiber (incl. children) completed on its * own or was cancelled, the promise resolves with the fiber's final * (possibly `undefined`) value. * * @remarks * The promise assumes the fiber either already has been (or will be) * scheduled & executed via other means. This promise only repeatedly checks * for any state changes of this fiber (at a configurable * frequency/interval), but does *NOT* trigger fiber execution! * * @param delay */ promise(delay = 1) { return this._promise || (this._promise = new Promise((resolve, reject) => { const timerID = setInterval(() => { if (this.state > STATE_ACTIVE) { clearInterval(timerID); this.state < STATE_ERROR ? resolve(this.value) : reject(this.error); } }, delay); })); } /** * Returns this fiber's result value (if any). Only available if the fiber * completed successfully and produced a value (either by returning a value * from the fiber's generator or externally via {@link Fiber.done}). */ deref() { return this.value; } /** * Returns true if this fiber is still in new or active state (i.e. still * can be processed). */ isActive() { return this.state <= STATE_ACTIVE; } /** * Returns child fiber for given `id`. * * @param id */ childForID(id) { return this.children.find((f) => f.id === id); } /** * Adds given `body` as child process to this fiber. If not already a * {@link Fiber} instance, it will be wrapped as such incl. with given * options. `opts` are only used for this latter case and will inherit this * (parent) fiber's {@link FiberOpts.logger} and {@link FiberOpts.idgen} as * defaults. Returns child fiber. * * @remarks * Child fibers are only processed when the parent is processed (e.g. via * {@link Fiber.run} or via `yield* fiber`). Also see {@link Fiber.join} to * wait for all child processes to finish. * * Non-active child process (i.e. finished, cancelled or errored) are * automatically removed from the parent. If the child fiber is needed for * future inspection, the return value of `fork()` should be stored by the * user. Whilst still active, child fibers can also be looked up via * {@link Fiber.childForID}. * * @example * ```ts tangle:../export/fork.ts * import { fiber, wait } from "@thi.ng/fibers"; * * fiber(function* (ctx) { * console.log("main start") * // create 2 child processes * ctx.fork(function* () { * console.log("child1 start"); * yield* wait(500); * console.log("child1 end"); * }); * // second process will take longer than first * ctx.fork(function* () { * console.log("child2 start"); * yield* wait(1000); * console.log("child2 end"); * }); * // wait for children to complete * yield* ctx.join(); * console.log("main end") * }).run(); * * // main start * // child1 start * // child2 start * // child1 end * // child2 end * // main end * ``` * * @param body * @param opts */ fork(body, opts) { if (!this.isActive()) illegalState(`fiber (id: ${this.id}) not active`); let $fiber; if (body instanceof Fiber) { if (body.parent) illegalArgs(`fiber id: ${body.id} already has a parent`); $fiber = body; } else { $fiber = fiber(body, { parent: this, logger: this.logger, idgen: this.idgen, ...opts }); } this.children.push($fiber); this.logger?.debug("forking", $fiber.id); return $fiber; } /** * Calls {@link Fiber.fork} for all given fibers and returns them as array. * * @remarks * Also see {@link Fiber.join} to wait for all child processes to complete. * * @param fibers */ forkAll(...fibers) { return fibers.map((f) => this.fork(f)); } /** * Waits for all child processes to complete/terminate. Use as `yield* * fiber.join()`. * * @remarks * See {@link Fiber.fork}, {@link Fiber.forkAll}. * */ *join() { this.logger?.debug("waiting for children..."); while (this.children.length) yield; } /** * Processes a single iteration of this fiber and any of its children. Does * nothing if the fiber is not active anymore. Returns fiber's state. * * @remarks * New, ininitialized fibers are first initialized via {@link Fiber.init}. * Likewise, when fibers are terminated (for whatever reason), they will be * de-initialized via {@link Fiber.deinit}. For all of these cases * (init/deinit), hooks for user customization are provided via * {@link FiberOpts.init}, {@link FiberOpts.deinit} and * {@link FiberOpts.catch}. */ next() { switch (this.state) { case STATE_NEW: this.init(); // explicit fallthrough case STATE_ACTIVE: try { const { children } = this; if (children.length) { for (let i = 0, n = children.length; i < n; ) { const child = children[i]; if (child.state > STATE_ACTIVE || child.next() > STATE_ACTIVE) { children.splice(i, 1); n--; } else i++; } } else if (this.autoTerminate) { this.cancel(); break; } const res = this.gen ? this.gen.next(this) : NO_RESULT; if (res.done) this.done(res.value); } catch (e) { this.catch(e); } break; default: } return this.state; } init() { this.logger?.debug("init", this.id); this.user?.init?.(this); this.state = STATE_ACTIVE; } deinit() { this.logger?.debug("deinit", this.id); this.user?.deinit?.(this); this.children.length = 0; this.gen = null; } /** * Cancels further processing of this fiber and its children (if any). Calls * {@link Fiber.deinit} and emits {@link EVENT_FIBER_CANCELED} event. * * @remarks * Function is a no-op if the fiber is not active anymore. */ cancel() { if (!this.isActive()) return; this.logger?.debug("cancel", this.id); for (let child of this.children) child.cancel(); this.deinit(); this.state = STATE_CANCELED; this.idgen?.free(this.id); this.notify({ id: EVENT_FIBER_CANCELED, target: this }); } /** * Stops further processing of this fiber and its children (if any) and sets * this fiber's value to given `value`. Calls {@link Fiber.deinit} and emits * {@link EVENT_FIBER_DONE} event. * * @remarks * Function is a no-op if the fiber is not active anymore. * * @param value */ done(value) { if (!this.isActive()) return; this.logger?.debug("done", this.id, value); this.value = value; for (let child of this.children) child.done(); this.deinit(); this.state = STATE_DONE; this.idgen?.free(this.id); this.notify({ id: EVENT_FIBER_DONE, target: this, value }); } /** * Stops further processing of this fiber, cancels all child processes (if * any) and sets this fiber's {@link Fiber.error} value to given `error`. * Calls {@link Fiber.deinit} and emits {@link EVENT_FIBER_ERROR} event. * * @remarks * Function is a no-op if the fiber already is in an error state. See * {@link FiberOpts.catch} for details about user provided error handling * and interception logic. * * @param err */ catch(err) { if (this.state >= STATE_ERROR || this.user?.catch?.(this, err)) return; this.logger ? this.logger.severe(`error ${this.id}:`, err) : console.warn(`error ${this.id}:`, err); for (let child of this.children) child.cancel(); this.state = STATE_ERROR; this.error = err; this.deinit(); this.idgen?.free(this.id); this.notify({ id: EVENT_FIBER_ERROR, target: this, value: err }); } // @ts-ignore mixin // prettier-ignore addListener(id, fn, scope) { } // @ts-ignore mixin // prettier-ignore removeListener(id, fn, scope) { } // @ts-ignore mixin notify(event) { } /** * Calls {@link Fiber.runWith} using default loop handlers. * * @remarks * Current default handlers: * * - `requestAnimationFrame()` in browsers * - `setImmediate()` in NodeJs * - `setTimeout(fn, 1)` otherwise */ run() { return this.runWith( typeof requestAnimationFrame === "function" ? requestAnimationFrame : typeof setImmediate === "function" ? setImmediate : (fn) => setTimeout(fn, 1) ); } /** * Starts fiber execution using the provided higher-order loop/interval * `handler` (e.g. see {@link Fiber.run}). * * @remarks * That given `handler` is used to repeatedly schedule the next execution of * {@link Fiber.next} (indirectly, via a zero-arg helper function passed to * the `handler`). * * Note: **Do not use `setInterval` instead of `setTimeout`**. The given * `handler` must only manage a single execution step, not multiple. * * @example * ```ts tangle:../export/run-with.ts * import { fiber } from "@thi.ng/fibers"; * * // start with custom higher frequency handler * fiber(function*() { * while(true) { * console.log("hello"); * yield; * } * }).runWith(setImmediate); * ``` * * @param handler */ runWith(handler) { this.logger?.debug(`running ${this.id}...`); const loop = () => { if (this.state <= STATE_ACTIVE && this.next() <= STATE_ACTIVE) handler(loop); }; loop(); return this; } }; Fiber = __decorateClass([ INotifyMixin ], Fiber); const fiber = (fiber2, opts) => fiber2 != null && fiber2 instanceof Fiber ? fiber2 : new Fiber(fiber2, opts); export { Fiber, fiber, setDefaultIDGen };