@thi.ng/fibers
Version:
Process hierarchies & operators for cooperative multitasking
402 lines (401 loc) • 12.2 kB
JavaScript
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
};