a new take on reactivity package

pull/11774/head
godzylinux 2 years ago
parent 0128df33da
commit 22fc8869ef

@ -1,158 +1,16 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { map } from './utils.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
var inited = false;
/**
* @template T
* @extends {Set<T>}
*/
export class ReactiveSet extends Set {
/** @type {Map<T, import('#client').Source<boolean>>} */
#sources = new Map();
#version = source(0);
#size = source(0);
/**
* @param {Iterable<T> | null | undefined} [value]
*/
constructor(value) {
super();
// If the value is invalid then the native exception will fire here
if (DEV) new Set(value);
if (value) {
var sources = this.#sources;
for (var element of value) {
sources.set(element, source(true));
}
this.#size.v = sources.size;
import { make_reactive } from './utils.js';
export const ReactiveSet = make_reactive(Set, {
mutation_properties: /** @type {const} */ (['add', 'clear', 'delete']),
interceptors: {
add: (value, property, ...params) => {
return !value.has(params[0]);
},
clear: (value, property, ...params) => {
return value.size !== 0;
},
delete: (value, property, ...params) => {
return !value.has(params[0]);
}
if (!inited) this.#init();
}
// We init as part of the first instance so that we can treeshake this class
#init() {
inited = true;
var proto = ReactiveSet.prototype;
var set_proto = Set.prototype;
for (const method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// We don't populate the underlying Set, so we need to create a clone using
// our internal values and then pass that to the method.
var clone = new Set(this.values());
// @ts-ignore
return set_proto[method].apply(clone, v);
};
}
for (const method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// We don't populate the underlying Set, so we need to create a clone using
// our internal values and then pass that to the method.
var clone = new Set(this.values());
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(clone, v));
return new ReactiveSet(set);
};
}
}
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {T} value */
has(value) {
var s = this.#sources.get(value);
if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return false;
}
return get(s);
}
/** @param {T} value */
add(value) {
var sources = this.#sources;
if (!sources.has(value)) {
sources.set(value, source(true));
set(this.#size, sources.size);
this.#increment_version();
}
return this;
}
/** @param {T} value */
delete(value) {
var sources = this.#sources;
var s = sources.get(value);
if (s !== undefined) {
var removed = sources.delete(value);
set(this.#size, sources.size);
set(s, false);
this.#increment_version();
return removed;
}
return false;
}
clear() {
var sources = this.#sources;
if (sources.size !== 0) {
set(this.#size, 0);
for (var s of sources.values()) {
set(s, false);
}
this.#increment_version();
}
sources.clear();
}
keys() {
get(this.#version);
return map(this.#sources.keys(), (key) => key, 'Set Iterator');
}
values() {
return this.keys();
}
entries() {
return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]), 'Set Iterator');
}
[Symbol.iterator]() {
return this.keys();
}
get size() {
return get(this.#size);
}
}
});

@ -1,3 +1,6 @@
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
/**
* @template T
* @template U
@ -27,3 +30,84 @@ export function map(iterable, fn, name) {
function get_this() {
return this;
}
/**
* @template TEntityInstance
* @template {(keyof TEntityInstance)[]} TMutationProperties
* @typedef {Record<TMutationProperties[number], (value: TEntityInstance, property: TMutationProperties[number], ...params: any[])=>boolean>} Interceptors - return false if you want to prevent reactivity for this call/get
*/
/**
* @template TEntityInstance
* @template {(keyof TEntityInstance)[]} TMutationProperties
* @typedef {object} Options
* @prop {TMutationProperties} mutation_properties - an array of property names on `TEntityInstance`. when calling a property on `TEntityInstance`, if the property name exists in this array, then mentioned property is made reactive.
* @prop {Interceptors<TEntityInstance, TMutationProperties>} [interceptors={}] - if the property names in `mutation_properties` shouldn't be always reactive, such calling `set.add(2)` twice, you can prevent the reactivity by returning false from these interceptors
*/
/**
* @template {new (...args: any) => any} TEntity - the entity we want to make reactive
* @template {(keyof InstanceType<TEntity>)[]} TMutationProperties
* @param {TEntity} Entity - the class/function we want to make reactive
* @param {Options<InstanceType<TEntity>, TMutationProperties>} options - configurations for how reactivity works for this entity
* @returns {TEntity}
*/
export const make_reactive = (Entity, options) => {
/**
* @template {InstanceType<TEntity>} TEntityInstance
* @template {keyof TEntityInstance} TProperty
* @param {import('#client').Source<number>} target
* @param {TProperty} property
* @param {TEntityInstance} entity_instance
* @param {TEntityInstance[TProperty] extends (...args: any)=>any ? Parameters<TEntityInstance[TProperty]>: never} params
*/
function notify_if_required(target, property, entity_instance, ...params) {
if (options.interceptors?.[property]?.(entity_instance, property, ...params) === false) {
// if interceptor said to not make this call reactive then bailout
return;
}
if (options.mutation_properties.some((v) => v === property)) {
set(target, target.v + 1);
} else {
get(target);
}
}
/**
* @template {ConstructorParameters<TEntity>} TParams
*/
// we return a class so that the caller can call it with new
// @ts-ignore
return class {
/**
* @param {TParams} params
* @returns
*/
constructor(...params) {
const sig = source(0);
return new Proxy(new Entity(...params), {
get(target, property) {
const orig_property = target[property];
let result;
if (typeof orig_property === 'function') {
// Bind functions directly to the Set
result = ((/** @type {any} */ ...params) => {
notify_if_required(sig, property, target, ...params);
return orig_property.bind(target)(...params);
}).bind(target);
} else {
// Properly handle getters
result = Reflect.get(target, property, target);
notify_if_required(sig, property, target);
}
return result;
}
});
}
};
};

Loading…
Cancel
Save