UNPKG

rxjs-autorun

Version:

Autorun expressions with RxJS Observables

251 lines (250 loc) 10.1 kB
var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; import { Observable, Subscription } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; // an error to make mid-flight interruptions // when a value is still not available var HALT_ERROR = Object.create(null); // error if tracker is used out of autorun/computed context export var TrackerError = new Error('$ or _ can only be called within computed or autorun context'); var errorTracker = function () { throw TrackerError; }; errorTracker.weak = errorTracker; errorTracker.normal = errorTracker; errorTracker.strong = errorTracker; var context = { _: errorTracker, $: errorTracker }; export var forwardTracker = function (tracker) { var r = (function (o) { return context[tracker](o); }); r.weak = function (o) { return context[tracker].weak(o); }; r.normal = function (o) { return context[tracker].normal(o); }; r.strong = function (o) { return context[tracker].strong(o); }; return r; }; export var runner = function (fn, distinct) { if (distinct === void 0) { distinct = false; } return new Observable(function (observer) { var deps = new Map(); // context to be used for running expression var newCtx = { $: createTrackers(true), _: createTrackers(false) }; // on unsubscribe/complete we destroy all subscriptions var sub = new Subscription(function () { deps.forEach(function (dep) { dep.subscription.unsubscribe(); }); }); // flag that indicates that current run might've affected completion status // we'll check completion after the first run var shouldCheckCompletion = true; // initial run runFn(); return sub; function runFn() { // Mark all deps as untracked and unused, lowering normal to weak var loweredStrengthDeps = []; deps.forEach(function (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); } }); var prevCtxt = context; context = newCtx; try { var 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(function (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() { var e_1, _a; // reset the flag shouldCheckCompletion = false; try { // any dep is still running for (var _b = __values(deps.values()), _c = _b.next(); !_c.done; _c = _b.next()) { var dep = _c.value; if (dep.track && !dep.completed) { // one of the $-tracked deps is still running return; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } // All $-tracked deps completed observer.complete(); } function removeUnusedDeps(ofStrength) { deps.forEach(function (dep, key) { if (dep.used || dep.strength > ofStrength) { return; } dep.subscription.unsubscribe(); deps.delete(key); }); } function createTrackers(track) { var 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)) { var v_1 = deps.get(o); v_1.used = true; if (track && !v_1.track) { // Previously tracked with _, but now also with $. // So completed state becomes relevant now. // Happens in case of e.g. computed(() => _(o) + $(o)) v_1.track = true; } if (strength > v_1.strength) { // Previous tracking strength was weaker than it currently // is. So temporarily use the stronger version. v_1.strength = strength; } if (v_1.hasValue) { return v_1.value; } else { throw HALT_ERROR; } } var v = { hasValue: false, value: void 0, // Eagerly create subscription that can be destroyed. subscription: new Subscription(), strength: 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 var isAsync = false; var hasSyncError = false; var syncError = void 0; v.subscription.add((distinct ? o.pipe(distinctUntilChanged()) : o) .subscribe({ next: function (value) { var hadValue = v.hasValue; v.hasValue = true; v.value = value; var 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: function (err) { if (isAsync) { observer.error(err); } else { syncError = err; hasSyncError = true; } }, complete: function () { 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; } }; } }); };