value-ref
Version:
Value reference with reactivity
134 lines (107 loc) • 4.15 kB
JavaScript
// lil subscriby (v-less)
Symbol.observable||=Symbol('observable');
// is target observable
const observable = arg => arg && !!(
arg[Symbol.observable] || arg[Symbol.asyncIterator] ||
arg.call && arg.set ||
arg.subscribe || arg.then
// || arg.mutation && arg._state != null
);
// cleanup subscriptions
// ref: https://v8.dev/features/weak-references
// FIXME: maybe there's smarter way to unsubscribe in weakref, like, wrapping target in weakref?
const registry$1 = new FinalizationRegistry(unsub => unsub.call?.()),
// this thingy must lose target out of context to let gc hit
unsubr = sub => sub && (() => sub.unsubscribe?.());
var sube = (target, next, error, complete, stop, unsub) => target && (
unsub = unsubr((target[Symbol.observable]?.() || target).subscribe?.( next, error, complete )) ||
target.set && target.call?.(stop, next) || // observ
(
target.then?.(v => (!stop && next(v), complete?.()), error) ||
(async v => {
try {
// FIXME: possible drawback: it will catch error happened in next, not only in iterator
for await (v of target) { if (stop) return; next(v); }
complete?.();
} catch (err) { error?.(err); }
})()
) && (_ => stop=1),
// register autocleanup
registry$1.register(target, unsub),
unsub
);
const ref = (...init) => new Ref(...init);
const NEXT=0, ERROR=1, UNSUB=3, TEARDOWN=4;
const unsubscribe = obs => obs?.map?.(sub => sub[UNSUB]()),
registry = new FinalizationRegistry(unsubscribe);
class Ref {
#observers=[]
#value=[]
// NOTE: on finalization strategy
// we unsubscribe only by losing source, not by losing subscriptions
// safe is to let event handlers sit there as far as source is available
// it can generate events, dereferencing listeners would be incorrect
constructor(...args) {
this.#value = args;
registry.register(this, this.#observers);
}
get value() { return this.#value[0] }
set value(val) { this.#value[0] = val, this.set(...this.#value);}
set(...values) {
this.#value = values;
for (let sub of this.#observers)
(sub[TEARDOWN]?.call?.(), sub[TEARDOWN] = sub[NEXT](...this.#value));
}
valueOf() {return this.value}
toString() {return this.value}
toJSON() {return this.value}
[Symbol.toPrimitive](hint) {return this.value}
*[Symbol.iterator]() { for (let value of this.#value) yield value; }
subscribe(next, error, complete) {
next = next?.next || next;
error = next?.error || error;
complete = next?.complete || complete;
const observers = this.#observers,
unsubscribe = () => (
subscription[TEARDOWN]?.call?.(),
observers.splice(observers.indexOf(subscription) >>> 0, 1)
),
subscription = [
next,
error,
complete,
unsubscribe,
this.#value.length ? next(...this.#value) : null // teardown
];
observers.push(subscription);
return unsubscribe.unsubscribe = unsubscribe
}
// FIXME: it never gets called
error(e) {this.#observers.map(sub => sub[ERROR]?.(e));}
[Symbol.observable||=Symbol.for('observable')](){return this}
async *[Symbol.asyncIterator]() {
let resolve, buf = [], p = new Promise(r => resolve = r),
unsub = this.subscribe(v => ( buf.push(v), resolve(), p = new Promise(r => resolve = r) ));
try { while (1) yield* buf.splice(0), await p; }
catch {}
unsub();
}
[Symbol.dispose||=Symbol('dispose')]() {
unsubscribe(this.#observers);
this.#value = null;
this.#observers = null;
}
}
// create new ref from [possibly multiple] sources
const from = Ref.from = ref.from = (...args) => {
let map, values, ref;
if (args[args.length-1]?.call) map = args.pop();
values = [];
args.map(
(arg,i) => observable(arg) ?
sube(arg, v => (values[i]=v, ref && (map ? ref.value=map(...values) : ref.set(...values)))) :
(values[i] = arg)
);
return ref = map ? new Ref(map(...values)) : new Ref(...values)
};
export { Ref, ref as default, from };