rvx
Version:
A signal based rendering library
469 lines (458 loc) • 11.2 kB
JavaScript
/*!
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 };