UNPKG

rxjs-autorun

Version:

Autorun expressions with RxJS Observables

226 lines (225 loc) 8.22 kB
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; } }; } });