@thi.ng/rstream
Version:
Reactive streams & subscription primitives for constructing dataflow graphs / pipelines
224 lines (223 loc) • 5.88 kB
JavaScript
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
};