UNPKG

@thi.ng/rstream

Version:

Reactive streams & subscription primitives for constructing dataflow graphs / pipelines

224 lines (223 loc) 5.88 kB
import { SEMAPHORE } from "@thi.ng/api/api"; import { peek } from "@thi.ng/arrays/peek"; import { isPlainObject } from "@thi.ng/checks/is-plain-object"; import { assert } from "@thi.ng/errors/assert"; import { illegalState } from "@thi.ng/errors/illegal-state"; import { NULL_LOGGER } from "@thi.ng/logger/null"; import { ROOT } from "@thi.ng/logger/root"; import { comp } from "@thi.ng/transducers/comp"; import { map } from "@thi.ng/transducers/map"; import { push } from "@thi.ng/transducers/push"; import { isReduced, unreduced } from "@thi.ng/transducers/reduced"; import { State } from "./api.js"; import { __optsWithID } from "./idgen.js"; import { LOGGER } from "./logger.js"; const subscription = (sub, opts) => new Subscription(sub, opts); class Subscription { constructor(wrapped, opts) { this.wrapped = wrapped; opts = __optsWithID(`sub`, { closeIn: "last", closeOut: "last", cache: true, ...opts }); this.parent = opts.parent; this.id = opts.id; this.closeIn = opts.closeIn; this.closeOut = opts.closeOut; this.cacheLast = opts.cache; opts.xform && (this.xform = opts.xform(push())); } wrapped; id; closeIn; closeOut; parent; __owner; xform; cacheLast; last = SEMAPHORE; state = State.IDLE; subs = []; deref() { return this.last !== SEMAPHORE ? this.last : void 0; } getState() { return this.state; } setState(state) { this.state = state; } subscribe(sub, opts = {}) { this.ensureState(); let $sub; if (sub instanceof Subscription && !opts.xform) { sub.ensureState(); assert(!sub.parent, `sub '${sub.id}' already has a parent`); sub.parent = this; $sub = sub; } else { $sub = new Subscription(sub, { ...opts, parent: this }); } this.subs.push($sub); this.setState(State.ACTIVE); $sub.setState(State.ACTIVE); this.last != SEMAPHORE && $sub.next(this.last); return $sub; } transform(...args) { let sub; let opts; if (isPlainObject(peek(args))) { opts = args.pop(); sub = { error: opts.error }; } return this.subscribe( sub, __optsWithID( "xform", args.length > 0 ? { ...opts, // @ts-ignore xform: comp(...args) } : opts ) ); } /** * Syntax sugar for {@link Subscription.transform} when using a single * [`map`](https://docs.thi.ng/umbrella/transducers/functions/map.html) * transducer only. The given function `fn` is used as `map`'s * transformation fn. * * @param fn - * @param opts - */ map(fn, opts) { return this.transform(map(fn), opts || {}); } unsubscribe(sub) { return sub ? this.unsubscribeChild(sub) : this.unsubscribeSelf(); } unsubscribeSelf() { LOGGER.debug(this.id, "unsub self"); this.parent?.unsubscribe(this); this.state < State.UNSUBSCRIBED && (this.state = State.UNSUBSCRIBED); this.release(); return true; } unsubscribeChild(sub) { LOGGER.debug(this.id, "unsub child", sub.id); const idx = this.subs.indexOf(sub); if (idx >= 0) { this.subs.splice(idx, 1); if (this.closeOut === "first" || !this.subs.length && this.closeOut !== "never") { this.unsubscribe(); } return true; } return false; } next(x) { if (this.state >= State.DONE) return; this.xform ? this.dispatchXform(x) : this.dispatch(x); } done() { LOGGER.debug(this.id, "entering done()"); if (this.state >= State.DONE) return; if (this.xform) { if (!this.dispatchXformDone()) return; } this.state = State.DONE; if (this.dispatchTo("done")) { this.state < State.UNSUBSCRIBED && this.unsubscribe(); } LOGGER.debug(this.id, "exiting done()"); } error(e) { const sub = this.wrapped; const errorHandler = sub?.error; errorHandler && LOGGER.debug(this.id, "attempting wrapped error handler"); return errorHandler?.(e) || this.unhandledError(e); } unhandledError(e) { const isNullLogger = LOGGER.parent === NULL_LOGGER || LOGGER.parent === ROOT && ROOT.parent === NULL_LOGGER; (isNullLogger ? console : LOGGER).warn(this.id, "unhandled error:", e); this.unsubscribe(); this.state = State.ERROR; return false; } dispatchTo(type, x) { let s = this.wrapped; if (s) { try { s[type] && s[type](x); } catch (e) { if (!this.error(e)) return false; } } const subs = type === "next" ? this.subs : [...this.subs]; for (let i = subs.length; i-- > 0; ) { s = subs[i]; try { s[type] && s[type](x); } catch (e) { if (type === "error" || !s.error || !s.error(e)) { return this.unhandledError(e); } } } return true; } dispatch(x) { LOGGER.debug(this.id, "dispatch", x); this.cacheLast && (this.last = x); this.dispatchTo("next", x); } dispatchXform(x) { let acc; try { acc = this.xform[2]([], x); } catch (e) { this.error(e); return; } if (this.dispatchXformVals(acc)) { isReduced(acc) && this.done(); } } dispatchXformDone() { let acc; try { acc = this.xform[1]([]); } catch (e) { return this.error(e); } return this.dispatchXformVals(acc); } dispatchXformVals(acc) { const uacc = unreduced(acc); for (let i = 0, n = uacc.length; i < n && this.state < State.DONE; i++) { this.dispatch(uacc[i]); } return this.state < State.ERROR; } ensureState() { if (this.state >= State.DONE) { illegalState(`operation not allowed in state ${State[this.state]}`); } } release() { this.subs.length = 0; delete this.parent; delete this.xform; delete this.last; } } export { Subscription, subscription };