UNPKG

rvx

Version:

A signal based rendering library

469 lines (458 loc) 11.2 kB
/*! This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { Signal, isTracking, batch, $, watchUpdates } from './rvx.js'; class ProbeSignal extends Signal { #onDisposable; constructor(onDisposable, value) { super(value); this.#onDisposable = onDisposable; } notify() { super.notify(); if (!this.active) { this.#onDisposable(); } } } class ProbeMap { #probes = new Map(); #get; constructor(get) { this.#get = get; } access(key) { if (isTracking()) { let probe = this.#probes.get(key); if (probe === undefined) { probe = new ProbeSignal(() => this.#probes.delete(key), this.#get(key)); this.#probes.set(key, probe); } probe.access(); } } update(key, value) { const probe = this.#probes.get(key); if (probe !== undefined) { probe.value = value; } } fill(value) { for (const probe of this.#probes.values()) { probe.value = value; } } } function createReactiveArrayProxy(target, barrier) { const length = $(target.length); const indexProbes = new ProbeMap(i => target[i]); return new Proxy(target, { get(target, prop, recv) { if (prop === "length") { length.access(); return target.length; } const index = asCanonicalIndex(prop); if (index !== undefined) { indexProbes.access(index); return barrier.wrap(target[index]); } if (Object.hasOwn(replacements, prop)) { return replacements[prop]; } return Reflect.get(target, prop, recv); }, set(target, prop, value, recv) { if (prop === "length") { batch(() => { const previous = target.length; target.length = value; for (let i = previous; i >= target.length; i--) { indexProbes.update(i, undefined); } length.value = Number(value); }); return true; } const index = asCanonicalIndex(prop); if (index !== undefined) { batch(() => { value = barrier.unwrap(value); target[index] = value; indexProbes.update(index, value); }); return true; } return Reflect.set(target, prop, value, recv); }, has(target, prop) { const cIndex = asCanonicalIndex(prop); if (cIndex !== undefined) { indexProbes.access(cIndex); return cIndex in target; } return Reflect.has(target, prop); }, deleteProperty(target, prop) { const index = asCanonicalIndex(prop); if (index !== undefined) { batch(() => { delete target[index]; indexProbes.update(index, undefined); }); return true; } return Reflect.deleteProperty(target, prop); }, ownKeys(target) { if (isTracking()) { length.access(); for (let i = 0; i < target.length; i++) { indexProbes.access(i); } } return Reflect.ownKeys(target); }, }); } const replacements = Object.create(null); for (const key of [ "copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift", ]) { replacements[key] = function (...args) { return batch(() => { return Array.prototype[key].call(this, ...args); }); }; } function asCanonicalIndex(value) { if (typeof value === "symbol") { return undefined; } const index = Number(value); if (Number.isSafeInteger(index) && index >= 0 && index <= 0xFFFFFFFF) { return index; } return undefined; } class ReactiveMap extends Map { #target; #barrier; #size; #iterators; #getProbes; #hasProbes; constructor(target, barrier) { super(); this.#target = target; this.#barrier = barrier; this.#size = $(target.size); this.#iterators = $(); this.#getProbes = new ProbeMap(key => target.get(key)); this.#hasProbes = new ProbeMap(key => target.has(key)); } get size() { this.#size.access(); return this.#target.size; } get(key) { this.#getProbes.access(key); return this.#barrier.wrap(this.#target.get(key)); } has(key) { this.#hasProbes.access(key); return this.#target.has(key); } set(key, value) { batch(() => { value = this.#barrier.unwrap(value); this.#target.set(key, value); this.#size.value = this.#target.size; this.#iterators.notify(); this.#getProbes.update(key, value); this.#hasProbes.update(key, true); }); return this; } delete(key) { return batch(() => { const deleted = this.#target.delete(key); if (deleted) { this.#size.value = this.#target.size; this.#iterators.notify(); this.#getProbes.update(key, undefined); this.#hasProbes.update(key, false); } return deleted; }); } clear() { batch(() => { this.#target.clear(); this.#size.value = 0; this.#iterators.notify(); this.#getProbes.fill(undefined); this.#hasProbes.fill(false); }); } *entries() { this.#iterators.access(); for (const entry of this.#target.entries()) { yield [entry[0], this.#barrier.wrap(entry[1])]; } } keys() { this.#iterators.access(); return this.#target.keys(); } *values() { this.#iterators.access(); for (const entry of this.#target.values()) { yield this.#barrier.wrap(entry); } } forEach(callback, thisArg) { this.#iterators.access(); return this.#target.forEach((value, key) => callback.call(thisArg, this.#barrier.wrap(value), key, this)); } *[Symbol.iterator]() { this.#iterators.access(); for (const entry of this.#target.entries()) { yield [entry[0], this.#barrier.wrap(entry[1])]; } } get [Symbol.toStringTag]() { return this.#target[Symbol.toStringTag]; } } function createReactiveProxy(target, barrier) { const iterators = $(); const getProbes = new ProbeMap(key => target[key]); const hasProbes = new ProbeMap(key => key in target); const proto = Object.getPrototypeOf(target); function isReactive(prop) { if (proto !== null && prop in proto) { return false; } return true; } return new Proxy(target, { get(target, prop, recv) { if (isReactive(prop)) { getProbes.access(prop); } return barrier.wrap(Reflect.get(target, prop, recv)); }, has(target, prop) { if (isReactive(prop)) { hasProbes.access(prop); } return Reflect.has(target, prop); }, set(target, prop, value, recv) { value = barrier.unwrap(value); if (isReactive(prop)) { return batch(() => { const ok = Reflect.set(target, prop, value, recv); if (ok) { iterators.notify(); getProbes.update(prop, value); hasProbes.update(prop, true); } return ok; }); } return Reflect.set(target, prop, value, recv); }, deleteProperty(target, prop) { return batch(() => { const ok = Reflect.deleteProperty(target, prop); if (ok && isReactive(prop)) { iterators.notify(); getProbes.update(prop, undefined); hasProbes.update(prop, false); } return ok; }); }, ownKeys(target) { iterators.access(); return Reflect.ownKeys(target); }, }); } class ReactiveSet extends Set { #target; #barrier; #size; #iterators; #probes; constructor(target, barrier) { super(); this.#target = target; this.#barrier = barrier; this.#size = $(target.size); this.#iterators = $(); this.#probes = new ProbeMap(key => target.has(key)); } get size() { this.#size.access(); return this.#target.size; } has(value) { value = this.#barrier.unwrap(value); this.#probes.access(value); return this.#target.has(value); } add(value) { batch(() => { value = this.#barrier.unwrap(value); this.#target.add(value); this.#size.value = this.#target.size; this.#iterators.notify(); this.#probes.update(value, true); }); return this; } delete(value) { return batch(() => { value = this.#barrier.unwrap(value); const deleted = this.#target.delete(value); if (deleted) { this.#size.value = this.#target.size; this.#iterators.notify(); this.#probes.update(value, false); } return deleted; }); } clear() { batch(() => { this.#target.clear(); this.#size.value = 0; this.#iterators.notify(); this.#probes.fill(false); }); } *entries() { this.#iterators.access(); for (const entry of this.#target.entries()) { const value = this.#barrier.wrap(entry[0]); yield [value, value]; } } *keys() { this.#iterators.access(); for (const key of this.#target.keys()) { yield this.#barrier.wrap(key); } } *values() { this.#iterators.access(); for (const value of this.#target.values()) { yield this.#barrier.wrap(value); } } forEach(callback, thisArg) { this.#iterators.access(); return this.#target.forEach(value => { value = this.#barrier.wrap(value); callback.call(thisArg, value, value, this); }, thisArg); } *[Symbol.iterator]() { this.#iterators.access(); for (const value of this.#target) { yield this.#barrier.wrap(value); } } get [Symbol.toStringTag]() { return this.#target[Symbol.toStringTag]; } } const WRAP_INSTANCE = Symbol.for("rvx:store:wrap_instance"); const WRAPPERS = new WeakMap(); const TARGETS = new WeakMap(); const BARRIER = { wrap, unwrap }; function wrap(value) { if (value !== null && typeof value === "object") { if (TARGETS.has(value)) { return value; } let wrapper = WRAPPERS.get(value); if (wrapper !== undefined) { return wrapper; } const ctor = value.constructor; const wrapInstance = ctor[WRAP_INSTANCE]; if (wrapInstance) { wrapper = wrapInstance(value); } else { const proto = Object.getPrototypeOf(value); switch (proto) { case Object.prototype: wrapper = createReactiveProxy(value, BARRIER); break; case Array.prototype: wrapper = createReactiveArrayProxy(value, BARRIER); break; case Map.prototype: wrapper = new ReactiveMap(value, BARRIER); break; case Set.prototype: wrapper = new ReactiveSet(value, BARRIER); break; default: return value; } } WRAPPERS.set(value, wrapper); TARGETS.set(wrapper, value); return wrapper; } return value; } function unwrap(value) { if (value !== null && typeof value === "object") { const target = TARGETS.get(value); if (target !== undefined) { return target; } } return value; } function defaultWrapInstance(value) { return createReactiveProxy(value, BARRIER); } function wrapInstancesOf(targetClass, wrap) { Object.defineProperty(targetClass, WRAP_INSTANCE, { configurable: true, enumerable: false, writable: false, value: wrap ?? defaultWrapInstance, }); } function reflect(target, key) { const prop = $(watchUpdates(() => target[key], value => prop.value = value)); watchUpdates(prop, value => target[key] = value); return prop; } export { BARRIER, ProbeMap, ProbeSignal, ReactiveMap, ReactiveSet, createReactiveArrayProxy, createReactiveProxy, reflect, unwrap, wrap, wrapInstancesOf };