rxjs-autorun
Version:
Autorun expressions with RxJS Observables
226 lines (225 loc) • 8.22 kB
JavaScript
import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
// an error to make mid-flight interruptions
// when a value is still not available
const HALT_ERROR = Object.create(null);
// error if tracker is used out of autorun/computed context
export const TrackerError = new Error('$ or _ can only be called within computed or autorun context');
const errorTracker = () => { throw TrackerError; };
errorTracker.weak = errorTracker;
errorTracker.normal = errorTracker;
errorTracker.strong = errorTracker;
let context = {
_: errorTracker,
$: errorTracker
};
export const forwardTracker = (tracker) => {
const r = ((o) => context[tracker](o));
r.weak = o => context[tracker].weak(o);
r.normal = o => context[tracker].normal(o);
r.strong = o => context[tracker].strong(o);
return r;
};
export const runner = (fn, distinct = false) => new Observable(observer => {
const deps = new Map();
// context to be used for running expression
const newCtx = {
$: createTrackers(true),
_: createTrackers(false)
};
// on unsubscribe/complete we destroy all subscriptions
const sub = new Subscription(() => {
deps.forEach(dep => {
dep.subscription.unsubscribe();
});
});
// flag that indicates that current run might've affected completion status
// we'll check completion after the first run
let shouldCheckCompletion = true;
// initial run
runFn();
return sub;
function runFn() {
// Mark all deps as untracked and unused, lowering normal to weak
const loweredStrengthDeps = [];
deps.forEach(dep => {
dep.track = false;
dep.used = false;
// setting normal strength to weak, so that if previously `a` was
// tracked as normal and in the latest run we only see `weak(a)` -
// we can mark it as weak. this is restored if halted
if (dep.strength === 1 /* Normal */) {
dep.strength = 0 /* Weak */;
loweredStrengthDeps.push(dep);
}
});
const prevCtxt = context;
context = newCtx;
try {
const result = fn();
removeUnusedDeps(1 /* Normal */);
observer.next(result);
}
catch (e) {
// handling mid-flight interruption error
// NOTE: check requires === strict equality
if (e === HALT_ERROR) {
// restore lowered strength
loweredStrengthDeps.forEach(dep => {
dep.strength = 1 /* Normal */;
});
// clean-up weak subscriptions
removeUnusedDeps(0 /* Weak */);
}
else {
// rethrow original errors
observer.error(e);
// we're errored, no need to check completion
shouldCheckCompletion = false;
}
}
finally {
context = prevCtxt;
// if this run was flagged as potentially completing
if (shouldCheckCompletion) {
checkCompletion();
}
}
}
function checkCompletion() {
// reset the flag
shouldCheckCompletion = false;
// any dep is still running
for (let dep of deps.values()) {
if (dep.track && !dep.completed) {
// one of the $-tracked deps is still running
return;
}
}
// All $-tracked deps completed
observer.complete();
}
function removeUnusedDeps(ofStrength) {
deps.forEach((dep, key) => {
if (dep.used || dep.strength > ofStrength) {
return;
}
dep.subscription.unsubscribe();
deps.delete(key);
});
}
function createTrackers(track) {
const r = createTracker(track, 1 /* Normal */);
r.weak = createTracker(track, 0 /* Weak */);
r.normal = createTracker(track, 1 /* Normal */);
r.strong = createTracker(track, 2 /* Strong */);
return r;
}
function createTracker(track, strength) {
return function tracker(o) {
if (deps.has(o)) {
const v = deps.get(o);
v.used = true;
if (track && !v.track) {
// Previously tracked with _, but now also with $.
// So completed state becomes relevant now.
// Happens in case of e.g. computed(() => _(o) + $(o))
v.track = true;
}
if (strength > v.strength) {
// Previous tracking strength was weaker than it currently
// is. So temporarily use the stronger version.
v.strength = strength;
}
if (v.hasValue) {
return v.value;
}
else {
throw HALT_ERROR;
}
}
const v = {
hasValue: false,
value: void 0,
// Eagerly create subscription that can be destroyed.
subscription: new Subscription(),
strength,
track: true,
used: true,
completed: false
};
deps.set(o, v);
// Sync Code Section {{{
// NOTE: we will synchronously (immediately) evaluate observables
// that can synchronously emit a value. Such observables as:
// - of(…)
// - o.pipe( startWith(…) )
// - BehaviorSubject
// - ReplaySubject
// - etc
let isAsync = false;
let hasSyncError = false;
let syncError = void 0;
v.subscription.add((distinct
? o.pipe(distinctUntilChanged())
: o)
.subscribe({
next(value) {
const hadValue = v.hasValue;
v.hasValue = true;
v.value = value;
const isUntrackFirstValue = !hadValue && !track;
// It could be that all tracked deps already completed.
// So signal that completion state might have changed.
if (isUntrackFirstValue) {
shouldCheckCompletion = true;
}
if (isAsync && v.track) {
runFn();
}
if (isUntrackFirstValue) {
// Untracked dep now has it's first value. So really untrack it.
v.track = false;
}
},
error(err) {
if (isAsync) {
observer.error(err);
}
else {
syncError = err;
hasSyncError = true;
}
},
complete() {
v.completed = true;
// if we don't have a value — we interrupt evaluation
// and complete output. See issue #22
if (!v.hasValue) {
observer.complete();
// immediately halt the computation
if (!isAsync) {
hasSyncError = true;
syncError = HALT_ERROR;
}
}
if (isAsync && v.track) {
checkCompletion();
}
}
}));
if (hasSyncError) {
throw syncError;
}
isAsync = true;
// }}} End Of Sync Section
if (v.hasValue) {
// Must have value because v.hasValue is true
return v.value;
}
else {
throw HALT_ERROR;
}
};
}
});