wana
Version:
Easy observable state for React
779 lines (766 loc) • 19.7 kB
JavaScript
import { is } from '@alloc/is';
import { $O, globals, SIZE, $$, observe, hasOwn, nope, getDescriptor, getOwnDescriptor, setHidden, flop, emptyArray, $T, rethrowError, noop } from './815e560c.mjs';
import { noto, setDebug, getDebug, addDebugAction } from './6ab9b088.mjs';
import { isDev } from '@alloc/is-dev';
import queueMicrotask from '@alloc/queue-microtask';
import { batchedUpdates } from 'react-batched-updates';
let runQueue = [];
const renderQueue = [];
const batch = {
run(effect) {
runQueue.push(effect);
flushAsync();
},
render(depth, effect) {
let i = renderQueue.findIndex((other) => other.depth > depth);
if (i < 0)
i = renderQueue.length;
renderQueue.splice(i, 0, {depth, effect});
flushAsync();
}
};
let isQueued = false;
function flushAsync() {
if (!isQueued) {
isQueued = true;
queueMicrotask(() => {
isQueued = false;
if (flushSync()) {
flushAsync();
}
});
}
}
function flushSync() {
batchedUpdates(() => {
let runs = 0;
for (const effect of runQueue) {
if (++runs > 1e5)
break;
effect();
}
runQueue = runQueue.slice(runs);
renderQueue.forEach(({effect}) => effect());
renderQueue.length = 0;
});
return !!(runQueue.length || renderQueue.length);
}
const {get} = Map.prototype;
function onChange(observable, key, change) {
const observers = get.call(observable, key);
if (observers) {
observers.nonce++;
observers.onChange(change);
}
}
function emit(target, change) {
noto(() => {
const observable = target[$O];
if (globals.onChange) {
globals.onChange(change);
}
if (change.op !== "clear") {
onChange(observable, change.key, change);
} else if (is.map(change.oldValue)) {
change.oldValue.forEach((_, key) => onChange(observable, key, change));
}
if (change.key !== SIZE) {
onChange(observable, $O, change);
}
});
return true;
}
const emitAdd = (target, key, value) => emit(target, {
op: "add",
target,
key,
value
});
const emitRemove = (target, key, oldValue) => emit(target, {
op: "remove",
target,
key,
oldValue
});
const emitReplace = (target, key, value, oldValue) => emit(target, {
op: "replace",
target,
key,
value,
oldValue
});
const emitSplice = (target, key, value, oldValue) => emit(target, {
op: "splice",
target,
key,
value,
oldValue
});
const emitClear = (target, oldValue) => emit(target, {
op: "clear",
target,
oldValue
});
const emitDefine = (target, key, value, oldValue) => emit(target, {
op: "define",
target,
key,
value,
oldValue
});
const observeAll = [
Symbol.iterator,
"entries",
"every",
"filter",
"find",
"findIndex",
"flat",
"flatMap",
"forEach",
"includes",
"indexOf",
"join",
"keys",
"lastIndexOf",
"map",
"reduce",
"reduceRight",
"slice",
"some",
"values"
];
const wrapIterators = (ctr) => observeAll.reduce((methods, name) => (ctr.prototype[name] && (methods[name] = function(...args) {
const self = this[$$];
observe(self, $O);
return self[name](...args);
}), methods), {});
const ArrayIterators = wrapIterators(Array);
const MapIterators = wrapIterators(Map);
const SetIterators = wrapIterators(Set);
const ArrayOverrides = {
concat(...args) {
const self = this[$$];
observe(self, $O);
args.forEach((arg) => {
arg = arg && arg[$$];
if (is.array(arg)) {
observe(arg, $O);
}
});
return noto(() => self.concat(...args));
},
copyWithin: flop,
fill: flop,
pop() {
const self = this[$$];
const oldLength = self.length;
if (oldLength) {
const index = oldLength - 1;
const exists = index in self;
const value = self.pop();
if (exists)
emitRemove(self, index, value);
emitReplace(self, SIZE, index, oldLength);
return value;
}
},
push(...values) {
const self = this[$$];
const oldLength = self.length;
if (values.length) {
const length = self.push(...values);
emitSplice(self, oldLength, values, emptyArray);
emitReplace(self, SIZE, length, oldLength);
return length;
}
return oldLength;
},
reverse() {
const self = this[$$];
const oldValue = [...self];
self.reverse();
emitSplice(self, 0, [...self], oldValue);
return this;
},
shift() {
const self = this[$$];
const oldLength = self.length;
if (oldLength) {
const exists = 0 in self;
const value = self.shift();
if (exists)
emitRemove(self, 0, value);
emitReplace(self, SIZE, oldLength - 1, oldLength);
return value;
}
},
sort(compare) {
const self = this[$$];
const oldValue = [...self];
self.sort(compare);
emitSplice(self, 0, [...self], oldValue);
return this;
},
splice(start, removeCount = 0, ...values) {
const self = this[$$];
const oldLength = self.length;
if (start < 0) {
start = Math.max(0, start + oldLength);
} else if (start > oldLength) {
start = oldLength;
}
const removed = self.splice(start, removeCount, ...values);
removeCount = removed.length;
const addCount = values.length;
if (addCount || removeCount) {
const delta = addCount - removeCount;
emitSplice(self, start, values, [...removed]);
if (delta)
emitReplace(self, SIZE, oldLength + delta, oldLength);
}
return removed;
},
unshift(...values) {
const self = this[$$];
const oldLength = self.length;
if (values.length) {
const length = self.unshift(...values);
emitSplice(self, 0, values, emptyArray);
emitReplace(self, SIZE, length, oldLength);
return length;
}
return oldLength;
}
};
const ObjectTraps = {
has: (self, key) => (observe(self, key), Reflect.has(self, key)),
get(self, key, proxy) {
if (key === $$)
return self;
if (key !== $O) {
const desc = getDescriptor(self, key);
const get = desc && desc.get;
if (get) {
return get.call(proxy);
}
if (!desc || hasOwn(self, key)) {
observe(self, key);
}
}
return self[key];
},
set(self, key, value, proxy) {
const desc = getDescriptor(self, key);
return desc && desc.get ? Reflect.set(self, key, value, proxy) : setProperty(self, key, value);
},
defineProperty(self, key, desc) {
const prevDesc = getOwnDescriptor(self, key);
return Reflect.defineProperty(self, key, desc) && (desc.get || prevDesc && prevDesc.get) ? emitDefine(self, key, desc, prevDesc) : prevDesc ? desc.value === prevDesc.value || emitReplace(self, key, desc.value, prevDesc.value) : emitAdd(self, key, desc.value);
},
deleteProperty,
preventExtensions: nope
};
const ArrayTraps = {
has: ObjectTraps.has,
get(self, key) {
if (key === $$)
return self;
if (key === "length") {
observe(self, SIZE);
return self[key];
}
return ArrayOverrides[key] || globals.observe && ArrayIterators[key] || self[key];
},
set(self, key, value) {
if (key === "length") {
const oldLength = self.length;
if (value !== oldLength) {
const oldValues = self.slice(value);
self.length = value;
emitReplace(self, SIZE, value, oldLength);
if (value < oldLength) {
emitSplice(self, value, emptyArray, oldValues);
}
}
} else {
const oldLength = self.length;
setProperty(self, key, value);
if (oldLength !== self.length) {
emitReplace(self, SIZE, self.length, oldLength);
}
}
return true;
},
deleteProperty,
preventExtensions: nope
};
class ObservableMap extends Map {
constructor(source) {
super();
setHidden(this, $$, source);
}
get [($O)]() {
return this[$$][$O];
}
get size() {
observe(this[$$], SIZE);
return this[$$].size;
}
has(key) {
observe(this[$$], key);
return this[$$].has(key);
}
get(key) {
observe(this[$$], key);
return this[$$].get(key);
}
set(key, value) {
const exists = this[$$].has(key);
const oldValue = exists ? this[$$].get(key) : void 0;
if (!exists || value !== oldValue) {
this[$$].set(key, value);
if (exists) {
emitReplace(this[$$], key, value, oldValue);
} else {
const oldSize = this[$$].size;
emitAdd(this[$$], key, value);
emitReplace(this[$$], SIZE, oldSize + 1, oldSize);
}
}
return this;
}
delete(key) {
const oldSize = this[$$].size;
const oldValue = this[$$].get(key);
return this[$$].delete(key) && emitRemove(this[$$], key, oldValue) && emitReplace(this[$$], SIZE, oldSize - 1, oldSize);
}
clear() {
const oldSize = this[$$].size;
if (oldSize) {
const oldValues = new Map(this[$$]);
this[$$].clear();
emitClear(this[$$], oldValues);
emitReplace(this[$$], SIZE, 0, oldSize);
}
}
}
class ObservableSet extends Set {
constructor(source) {
super();
setHidden(this, $$, source);
}
get [($O)]() {
return this[$$][$O];
}
get size() {
observe(this, SIZE);
return this[$$].size;
}
has(value) {
observe(this[$$], SIZE);
return this[$$].has(value);
}
add(value) {
const oldSize = this[$$].size;
this[$$].add(value);
if (oldSize !== this[$$].size) {
emitAdd(this[$$], void 0, value);
emitReplace(this[$$], SIZE, oldSize + 1, oldSize);
}
return this;
}
delete(value) {
const oldSize = this[$$].size;
return this[$$].delete(value) && emitRemove(this[$$], void 0, value) && emitReplace(this[$$], SIZE, oldSize - 1, oldSize);
}
clear() {
const oldSize = this[$$].size;
if (oldSize) {
const oldValues = new Set(this[$$]);
this[$$].clear();
emitClear(this[$$], oldValues);
emitReplace(this[$$], SIZE, 0, oldSize);
}
}
}
defineMethods(ObservableMap, MapIterators);
defineMethods(ObservableSet, SetIterators);
function defineMethods(Class, overrides) {
Object.defineProperties(Class.prototype, Object.getOwnPropertyDescriptors(overrides));
}
function setProperty(self, key, value) {
const exists = hasOwn(self, key);
const oldValue = self[key];
self[key] = value;
return exists ? value === oldValue || emitReplace(self, key, value, oldValue) : emitAdd(self, key, value);
}
function deleteProperty(self, key) {
if (!hasOwn(self, key))
return true;
const oldValue = self[key];
delete self[key];
return emitRemove(self, key, oldValue);
}
const shallowChanges = (target, onChange) => target[$O].observe($O, onChange);
const canMakeObservable = (value) => is.object(value) && !is.date(value) && !is.regExp(value) && !is.promise(value) && !is.generator(value) && !is.generatorFunction(value) && !is.asyncFunction(value) && !is.weakMap(value) && !is.weakSet(value);
class Observer {
constructor() {
this.onChange = null;
}
get nonce() {
let nonce = 0;
this.observed.forEach((slot) => {
nonce += slot.nonce;
});
return nonce;
}
dispose() {
if (this.onChange) {
this.onChange = null;
this.observed.forEach((value) => value.delete(this));
}
}
}
class Observable extends Map {
constructor(source) {
super();
this.source = source;
if (isDev) {
setDebug(this, {
name: this.constructor.name || "Unknown"
});
}
if (source) {
this.proxy = is.map(source) ? new ObservableMap(source) : is.set(source) ? new ObservableSet(source) : new Proxy(source, source[$T] || (is.array(source) ? ArrayTraps : ObjectTraps));
}
}
has(key) {
const observers = super.get(key);
return !!(observers && observers.size);
}
get(key) {
let observers = super.get(key);
if (!observers) {
observers = new ObservedSlot(this, key);
this.set(key, observers);
}
return observers;
}
observe(key, onChange) {
const observers = this.get(key);
const observer = {
onChange,
dispose() {
observers.delete(observer);
}
};
observers.add(observer);
return observer;
}
}
class ObservedSlot extends Set {
constructor(owner, key) {
super();
this.owner = owner;
this.key = key;
this.nonce = 1;
}
onChange(change) {
if (this.size) {
for (const observer of Array.from(this)) {
if (observer.onChange) {
observer.onChange(change);
}
}
}
}
}
function auto(effect, config) {
const auto2 = new Auto(config);
auto2.run(effect);
return auto2;
}
class Auto {
constructor(config = {}) {
this.dirty = true;
this.nonce = 0;
this.observer = null;
this.sync = !!config.sync;
this.onDirty = config.onDirty || this._onDirty;
this.onError = config.onError || rethrowError;
this.onCommit = config.onCommit || noop;
this.onDispose = config.onDispose || noop;
}
run(compute) {
const observer = this.start(compute);
try {
const result = compute();
this.stop().commit(observer);
this.nonce = observer.nonce;
return result;
} catch (error) {
this.stop().onError(error);
}
}
rerun() {
const {observer} = this;
return observer && noto(() => this.run(observer.effect));
}
commit(observer, nonce) {
if (this.observer) {
this.observer.dispose();
}
if (nonce && nonce != observer.nonce) {
this.observer = null;
return false;
}
observer.onChange = this._onChange.bind(this);
observer.observed.forEach((observable) => observable.add(observer));
this.observer = observer;
this.onCommit(observer);
return true;
}
dispose() {
if (this.observer) {
this.observer.dispose();
this.observer = null;
this.onDispose();
}
}
start(effect) {
if (globals.observe) {
throw Error("Nested tracking is forbidden");
}
const observer = new AutoObserver(effect);
globals.auto = this;
globals.observe = (target, key) => {
observer.observed.add(target[$O].get(key));
};
return observer;
}
stop() {
if (globals.auto == this) {
globals.auto = globals.observe = null;
}
this.dirty = false;
return this;
}
clear() {
if (!this.dirty) {
this.dirty = true;
this.onDirty();
}
}
_onChange(change) {
if (isDev && getDebug(this)) {
addDebugAction(this, change);
}
if (this.sync && change.op == "clear" && is.function(change.target)) {
change.target();
}
this.clear();
}
_onDirty() {
const {observer, nonce} = this;
const maybeRerun = () => observer.nonce > nonce ? this.rerun() : this.dirty = false;
if (this.sync) {
maybeRerun();
} else {
batch.run(() => observer == this.observer && maybeRerun());
}
}
}
class AutoObserver extends Observer {
constructor(effect) {
super();
this.effect = effect;
this.observed = new Set();
}
}
function derive(run, discard) {
let memo;
const observable = new Observable();
const auto = new Auto({
onDirty() {
if (observable.has($O)) {
batch.run(derived);
observable.get($O).onChange({
op: "clear",
target: derived,
oldValue: memo
});
}
}
});
const derived = () => {
if (auto.dirty) {
noto(() => {
const oldMemo = memo;
const newMemo = run(auto);
if (newMemo !== oldMemo && !(discard && discard(newMemo, oldMemo))) {
emitReplace(derived, null, memo = newMemo, oldMemo);
}
});
}
observe(derived, $O);
return memo;
};
derived.auto = auto;
derived.dispose = () => auto.dispose();
setHidden(derived, $O, observable);
return derived;
}
function isDerived(value) {
return is.function(value) && !!value[$O];
}
function o(value) {
let state = value && value[$O];
if (!state || !getOwnDescriptor(value, $O)) {
if (!canMakeObservable(value)) {
return value;
}
if (is.function(value)) {
return derive((auto) => auto.run(value));
}
if (Object.isFrozen(value)) {
return value;
}
setHidden(value, $O, state = new Observable(value));
}
return state.proxy || value;
}
const when = (condition) => new Promise((resolve, reject) => {
const auto = new Auto({
onDirty() {
if (noto(() => this.run(condition))) {
this.dispose();
resolve();
}
},
onError(error) {
this.dispose();
reject(error);
}
});
auto.onDirty();
});
function watch(root, arg2, arg3) {
return arg3 ? new Watcher(root, arg3, arg2) : new Watcher(root, arg2);
}
class Watcher extends Observer {
constructor(root, onChange, key) {
super();
this.root = root;
this.key = key;
this.observed = new Set();
this.counts = new Map();
this.watch = (value, key, ctx) => {
if (canWatch(key) && is.map(ctx))
this._watch(key);
if (canWatch(value))
this._watch(value);
};
this.unwatch = (value, key, ctx) => {
if (canWatch(key) && is.map(ctx))
this._unwatch(key);
if (canWatch(value))
this._unwatch(value);
};
this.onChange = (change) => {
const {op, target, key: key2, value, oldValue} = change;
switch (op) {
case "replace":
if (canWatch(oldValue))
this._unwatch(oldValue);
case "add":
this.watch(value, key2, target);
break;
case "remove":
this.unwatch(value, key2, target);
break;
case "splice":
value.forEach(this.watch);
default:
oldValue.forEach(this.unwatch);
}
onChange(change);
};
if (key) {
this.counts.set(root, 1);
const observable = root[$O];
this.observed.add(observable.get(key).add(this));
this.watch(root[key], key, root);
} else {
this._watch(root);
}
}
dispose() {
if (this.onChange) {
super.dispose();
this.counts.clear();
}
}
_watch(target) {
const {counts} = this;
const count = counts.get(target) || 0;
counts.set(target, count + 1);
if (!count) {
if (target[$O]) {
this.observed.add(target[$O].get($O).add(this));
}
if (!target.forEach) {
target = Object.values(target);
}
target.forEach(this.watch);
}
}
_unwatch(target) {
const {counts} = this;
const count = counts.get(target);
if (is.undefined(count))
return;
if (count > 1) {
counts.set(target, count - 1);
} else {
counts.delete(target);
if (target[$O]) {
const value = target[$O].get($O);
this.observed.delete(value);
value.delete(this);
}
if (!target.forEach) {
target = Object.values(target);
}
target.forEach(this.unwatch);
}
}
}
const canWatch = (value) => value && typeof value == "object";
function mountAuto(auto) {
let mounted = false;
let observer;
let nonce = 0;
return (props) => {
if (props.observer) {
observer = props.observer;
if (!mounted) {
nonce = observer.nonce;
} else if (observer != auto.observer) {
auto.commit(observer);
}
} else {
mounted = props.mounted;
if (observer) {
if (!mounted) {
nonce = observer.nonce;
auto.dispose();
} else if (!auto.commit(observer, nonce)) {
auto.clear();
}
}
}
};
}
export { Auto, ObjectTraps, Observable, ObservedSlot, Watcher, auto, batch, derive, flushSync, isDerived, mountAuto, o, shallowChanges, watch, when };
//# sourceMappingURL=2f8394f9.mjs.map