rxjs-autorun
Version:
Autorun expressions with RxJS Observables
251 lines (250 loc) • 10.1 kB
JavaScript
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;
}
};
}
});
};