UNPKG

spica

Version:

Supervisor, Coroutine, Channel, select, AtomicPromise, Cancellation, Cache, List, Queue, Stack, and some utils.

490 lines (481 loc) 19.3 kB
import { DeepImmutable, DeepRequired } from './type'; import { isFinite, ObjectAssign } from './alias'; import { clock } from './chrono'; import { Coroutine, ICoroutine, isCoroutine } from './coroutine'; import { Observation, Observer, Publisher } from './observer'; import { AtomicPromise } from './promise'; import { AtomicFuture } from './future'; import { noop } from './function'; import { Ring } from './ring'; import { causeAsyncException } from './exception'; export interface SupervisorOptions { readonly name?: string; readonly capacity?: number; readonly timeout?: number; readonly destructor?: (reason: unknown) => void; readonly scheduler?: (cb: () => void) => void; readonly resource?: number; } export interface Supervisor<N extends string, P, R, S> extends Coroutine<undefined, undefined, undefined> { // #36218 constructor: typeof Supervisor & typeof Coroutine; } export abstract class Supervisor<N extends string, P = undefined, R = P, S = undefined> extends Coroutine<undefined, undefined, undefined> { private static $instances: Set<Supervisor<string, unknown, unknown, unknown>>; private static get instances() { return this.hasOwnProperty('$instances') ? this.$instances : this.$instances = new Set(); } private static $status: { readonly instances: number; readonly processes: number; }; public static get status() { if (this.hasOwnProperty('$status')) return this.$status; const { instances } = this; return this.$status = { get instances(): number { return instances.size; }, get processes(): number { return [...instances] .reduce((acc, sv) => acc + sv.workers.size , 0); }, } as const; } public static clear(reason?: unknown): void { while (this.instances.size > 0) { for (const sv of this.instances) { sv.terminate(reason); } } } protected static readonly standalone = new WeakSet<Supervisor.Process.Regular<unknown, unknown, unknown>>(); constructor(opts: SupervisorOptions = {}) { super(async function* (this: Supervisor<N, P, R, S>) { return await this.state; }, { delay: false }); ObjectAssign(this.settings, opts); this.name = this.settings.name; if (this.constructor === Supervisor) throw new Error(`Spica: Supervisor: <${this.name}>: Cannot instantiate abstract classes`); this.constructor.instances.add(this); } private readonly state = new AtomicFuture(); private destructor(reason: unknown): void { assert(this.alive === true); assert(this.available === true); this.available = false; this.clear(reason); assert(this.workers.size === 0); Object.freeze(this.workers); while (this.messages.length > 0) { const { 0: names, 1: param, 4: timer } = this.messages.shift()!; const name: N | undefined = names[Symbol.iterator]().next().value; timer && clearTimeout(timer); this.$events?.loss.emit([name], [name, param]); } assert(this.messages.length === 0); this.alive = false; // @ts-ignore #31251 this.constructor.instances.delete(this); Object.freeze(this); assert(this.alive === false); assert(this.available === false); this.settings.destructor(reason); this.state.bind(reason === undefined ? undefined : AtomicPromise.reject(reason)); } public readonly name: string; private readonly settings: DeepImmutable<DeepRequired<SupervisorOptions>> = { name: '', capacity: Infinity, timeout: Infinity, destructor: noop, scheduler: clock.next, resource: 10, }; private $events?: { readonly init: Observation<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>; readonly exit: Observation<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>; readonly loss: Observation<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>; }; public get events(): { readonly init: Observer<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>; readonly exit: Observer<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>; readonly loss: Observer<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>; } { return this.$events ??= { init: new Observation<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>(), exit: new Observation<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>(), loss: new Observation<[N | undefined], Supervisor.Event.Data.Loss<N, P>, unknown>(), }; } private readonly workers = new Map<N, Worker<N, P, R, S>>(); private alive = true; private available = true; private throwErrorIfNotAvailable(): void { if (!this.available) throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use terminated supervisors`); } // Workaround for #36053 public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.Function<P, R, S>, state?: S): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process.Function<P, R, S>, state: S): (reason?: unknown) => boolean; public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.GeneratorFunction<P, R, S>, state?: S): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process.GeneratorFunction<P, R, S>, state: S): (reason?: unknown) => boolean; public register(this: Supervisor<N, P, R, undefined>, name: N, process: Supervisor.Process.AsyncGeneratorFunction<P, R, S>, state?: S): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process.AsyncGeneratorFunction<P, R, S>, state: S): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process.Coroutine<P, R>, state?: never): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process<P, R, S>, state: S): (reason?: unknown) => boolean; public register(name: N, process: Supervisor.Process<P, R, S>, state?: S): (reason?: unknown) => boolean { state = state!; this.throwErrorIfNotAvailable(); if (isCoroutine(process)) { const port = process[process.constructor.port] as Coroutine<R, R, P>[typeof Coroutine.port]; const proc: Supervisor.Process.Regular<P, R, S> = { init: state => state, main: (param, state, kill) => port.ask(param) .then(({ value: reply, done }) => done && void kill() || [reply, state]), exit: reason => void process[process.constructor.terminate](reason), }; this.constructor.standalone.add(proc); const kill = this.register(name, proc, state); process.catch(kill); return kill; } if (isAsyncGeneratorFunction(process)) { let iter: AsyncGenerator<R, R, P>; return this.register( name, { init: (state, kill) => (iter = process(state, kill), iter.next().catch(kill), state), main: (param, state, kill) => AtomicPromise.resolve(iter.next(param)) .then(({ value: reply, done }) => done && void kill() || [reply, state]), exit: noop, }, state); } if (isGeneratorFunction(process)) { let iter: Generator<R, R, P>; return this.register( name, { init: (state, kill) => (iter = process(state, kill), iter.next(), state), main: (param, state, kill) => { const { value: reply, done } = iter.next(param); done && kill(); return [reply, state]; }, exit: noop, }, state); } if (typeof process === 'function') { return this.register( name, { init: state => state, main: process, exit: noop, }, state); } if (this.workers.has(name)) throw new Error(`Spica: Supervisor: <${this.name}/${name}>: Cannot register another process with tha same name`); this.schedule(); const worker: Worker<N, P, R, S> = new Worker( name, process, state, this, () => void this.schedule(), this.constructor.standalone.has(process), this.$events, () => { this.workers.get(name) === worker && void this.workers.delete(name); }); this.workers.set(name, worker); return worker.terminate; } public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, timeout?: number): AtomicPromise<R>; public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, callback: Supervisor.Callback<R>, timeout?: number): void; public call(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, callback?: Supervisor.Callback<R> | number, timeout = this.settings.timeout): AtomicPromise<R> | void { if (typeof callback !== 'function') return new AtomicPromise<R>((resolve, reject) => void this.call(name, param, (err, result) => err ? reject(err) : resolve(result), callback)); this.throwErrorIfNotAvailable(); this.messages.push( [typeof name === 'string' ? [name] : new NamePool(this.workers, name), param, callback, Date.now() + timeout, 0, ]); while (this.messages.length > (this.available ? this.settings.capacity : 0)) { const { 0: names, 1: param, 2: callback, 4: timer } = this.messages.shift()!; timer && clearTimeout(timer); const name: N | undefined = names[Symbol.iterator]().next().value; this.$events?.loss.emit([name], [name, param]); try { callback(new Error(`Spica: Supervisor: <${this.name}>: Message overflowed`), undefined); } catch (reason) { causeAsyncException(reason); } } if (this.messages.length === 0) return; this.throwErrorIfNotAvailable(); this.schedule(); if (timeout > 0 && timeout !== Infinity) { assert(this.messages.at(-1)![4] === 0); this.messages.at(-1)![4] = setTimeout(() => void this.schedule() , timeout + 3); } } public cast(name: N | ((names: Iterable<N>) => Iterable<N>), param: P, timeout = this.settings.timeout): AtomicPromise<R> | undefined { this.throwErrorIfNotAvailable(); const expire = Date.now() + timeout; let result: AtomicPromise<R> | undefined; for (name of typeof name === 'string' ? [name] : new NamePool(this.workers, name)) { if (result = this.workers.get(name)?.call([param, expire])) break; } if (result) return result; const n = typeof name === 'string' ? name : undefined; this.$events?.loss.emit([n], [n, param]); } public refs(name?: N): [N, Supervisor.Process.Regular<P, R, S>, S, (reason?: unknown) => boolean][] { assert(this.available || this.workers.size === 0); return name === undefined ? [...this.workers.values()].map(convert) : this.workers.has(name) ? [convert(this.workers.get(name)!)] : []; function convert(worker: Worker<N, P, R, S>): [N, Supervisor.Process.Regular<P, R, S>, S, (reason?: unknown) => boolean] { assert(worker instanceof Worker); return [ worker.name, worker.process, worker.state, worker.terminate, ]; } } public kill(name: N, reason?: unknown): boolean { if (!this.available) return false; assert(this.alive === true); return this.workers.has(name) ? this.workers.get(name)!.terminate(reason) : false; } public clear(reason?: unknown): void { while (this.workers.size > 0) { for (const worker of this.workers.values()) { worker.terminate(reason); } } } public terminate(reason?: unknown): boolean { if (!this.available) return false; assert(this.alive === true); this.destructor(reason); this[Coroutine.exit](undefined); return true; } public override [Coroutine.terminate](reason?: unknown): void { this.terminate(reason); } public override [Coroutine.port] = { ask: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); }, recv: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); }, send: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); }, connect: () => { throw new Error(`Spica: Supervisor: <${this.name}>: Cannot use coroutine port`); }, } as const; private scheduled = false; private schedule(): void { if (!this.available || this.scheduled || this.messages.length === 0) return; this.scheduled = true; const p = new AtomicFuture(false); p.finally(() => { this.scheduled = false; this.deliver(); }); this.settings.scheduler.call(undefined, p.bind); this.settings.scheduler === requestAnimationFrame && setTimeout(p.bind, 1000); } // Bug: Karma and TypeScript private readonly messages = new Ring<[Iterable<N>, P, Supervisor.Callback<R>, number, ReturnType<typeof setTimeout> | 0]>(); private deliver(): void { if (!this.available) return; assert(!this.scheduled); const since = Date.now(); for (let len = this.messages.length, i = 0; this.available && i < len; ++i) { if (this.settings.resource - (Date.now() - since) <= 0) return void this.schedule(); const { 0: names, 1: param, 2: callback, 3: expiration, 4: timer } = this.messages.at(i)!; let result: AtomicPromise<R> | undefined; let name: N | undefined; for (name of typeof names === 'string' ? [names] : names) { if (Date.now() > expiration) break; if (result = this.workers.get(name)?.call([param, expiration])) break; } if (!result && Date.now() < expiration) continue; this.messages.splice(i, 1); --i; --len; timer && clearTimeout(timer); if (result) { result.then( reply => void callback(undefined, reply), () => void callback(new Error(`Spica: Supervisor: <${this.name}>: Process failed`), undefined)); } else { this.$events?.loss.emit([name], [name, param]); try { callback(new Error(`Spica: Supervisor: <${this.name}>: Message expired`), undefined); } catch (reason) { causeAsyncException(reason); } } } } } export namespace Supervisor { export type Process<P, R = P, S = undefined> = | Process.Regular<P, R, S> | Process.Function<P, R, S> | Process.GeneratorFunction<P, R, S> | Process.AsyncGeneratorFunction<P, R, S> | Process.Coroutine<P, R>; export namespace Process { export type Regular<P, R, S> = { readonly init: (state: S, kill: (reason?: unknown) => void) => S; readonly main: Function<P, R, S>; readonly exit: (reason: unknown, state: S) => void; }; export type Function<P, R, S> = (param: P, state: S, kill: (reason?: unknown) => void) => Result<R, S> | PromiseLike<Result<R, S>>; export type GeneratorFunction<P, R, S> = (state: S, kill: (reason?: unknown) => void) => Generator<R, R, P>; export type AsyncGeneratorFunction<P, R, S> = (state: S, kill: (reason?: unknown) => void) => AsyncGenerator<R, R, P>; export type Coroutine<P, R> = ICoroutine<R, R, P>; export type Result<R, S> = readonly [R, S]; } export type Callback<R> = (...args: [error: undefined, reply: R] | [error: Error, reply: undefined]) => void; export namespace Event { export namespace Data { export type Init<N extends string, P, R, S> = readonly [N, Process.Regular<P, R, S>, S]; export type Exit<N extends string, P, R, S> = readonly [N, Process.Regular<P, R, S>, S, unknown]; export type Loss<N extends string, P> = readonly [N | undefined, P]; } } } function isAsyncGeneratorFunction<P, R, S>(process: Supervisor.Process<P, R, S>): process is Supervisor.Process.AsyncGeneratorFunction<P, R, S> { return process[Symbol.toStringTag] === 'AsyncGeneratorFunction'; } function isGeneratorFunction<P, R, S>(process: Supervisor.Process<P, R, S>): process is Supervisor.Process.GeneratorFunction<P, R, S> { return process[Symbol.toStringTag] === 'GeneratorFunction'; } class NamePool<N extends string> implements Iterable<N> { constructor( private readonly workers: ReadonlyMap<N, unknown>, private readonly selector: (names: Iterable<N>) => Iterable<N>, ) { } public [Symbol.iterator](): Iterator<N, undefined, undefined> { return this.selector(this.workers.keys())[Symbol.iterator](); } } class Worker<N extends string, P, R, S> { constructor( public readonly name: N, public readonly process: Supervisor.Process.Regular<P, R, S>, public state: S, private readonly sv: Supervisor<N, P, R, S>, private readonly schedule: () => void, initiated: boolean, private readonly events: { readonly init: Publisher<[N], Supervisor.Event.Data.Init<N, P, R, S>, unknown>; readonly exit: Publisher<[N], Supervisor.Event.Data.Exit<N, P, R, S>, unknown>; } | undefined, private readonly destructor_: () => void, ) { initiated && this.init(); } private destructor(reason: unknown): void { assert(this.alive === true); this.alive = false; this.available = false; Object.freeze(this); assert(this.alive === false); assert(this.available === false); try { this.destructor_(); } catch (reason) { causeAsyncException(reason); } if (this.initiated) { this.exit(reason); } } private alive = true; private available = true; private initiated = false; private init(): void { assert(!this.initiated); this.initiated = true; this.events?.init .emit([this.name], [this.name, this.process, this.state]); this.state = this.process.init(this.state, this.terminate); } private exit(reason: unknown): void { assert(this.initiated); try { this.process.exit(reason, this.state); this.events?.exit .emit([this.name], [this.name, this.process, this.state, reason]); } catch (reason_) { this.events?.exit .emit([this.name], [this.name, this.process, this.state, reason]); this.sv.terminate(reason_); } } public call([param, expiration]: [P, number]): AtomicPromise<R> | undefined { if (!this.available) return; return new AtomicPromise<Supervisor.Process.Result<R, S>>((resolve, reject) => { isFinite(expiration) && setTimeout(() => void reject(new Error()), expiration - Date.now()); assert(this.alive); assert(this.available); this.available = false; if (!this.initiated) { this.init(); if (!this.alive) return void reject(); } assert(this.alive); assert(!this.available); AtomicPromise.resolve(this.process.main(param, this.state, this.terminate)) .then(resolve, reject); }) .then(([reply, state]) => { if (this.alive) { this.schedule(); assert(!Object.isFrozen(this)); this.state = state; this.available = true; } return reply; }) .catch(reason => { this.schedule(); this.terminate(reason); throw reason; }); } public readonly terminate = (reason: unknown): boolean => { if (!this.alive) return false; this.destructor(reason); return true; }; }