obsidian-visualiser/shared/reactive.ts

304 lines
12 KiB
TypeScript

export type Reactive<T> = T | (() => T);
export const isString = (val: unknown): val is string => typeof val === 'string';
const isIntegerKey = (key: unknown): boolean => isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key;
let defered = false, _deferSet = new Set<() => void>();
export const _defer = (fn: () => void) => {
if(!defered)
{
defered = true;
queueMicrotask(() => {
_deferSet.forEach(e => e());
_deferSet.clear();
defered = false;
});
}
_deferSet.add(fn);
}
let activeEffect: (() => void) | null = null, _isTracking = true;
const SYMBOLS = {
PROXY: Symbol('is a proxy'),
ITERATE: Symbol('iterating'),
RAW: Symbol('raw value'),
} as const;
function reactiveReadArray<T>(array: T[]): T[]
{
const _raw = raw(array)
if (_raw === array) return _raw;
track(_raw, SYMBOLS.ITERATE);
return _raw.map(wrapReactive);
}
function shallowReadArray<T>(arr: T[]): T[]
{
track((arr = raw(arr)), SYMBOLS.ITERATE);
return arr;
}
function iterator(self: unknown[], method: keyof Array<unknown>, wrapValue: (value: any) => unknown)
{
const arr = shallowReadArray(self);
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
_next: IterableIterator<unknown>['next']
};
if (arr !== self && !isShallow(self))
{
iter._next = iter.next;
iter.next = () => {
const result = iter._next();
if (!result.done) result.value = wrapValue(result.value);
return result;
}
}
return iter;
}
function wrapReactive(obj: any): any
{
return obj && typeof obj === 'object' ? reactive(obj as Proxy<object>) : obj;
}
const arrayProto = Array.prototype
function apply(self: unknown[], method: keyof Array<any>, fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, wrappedRetFn?: (result: any) => unknown, args?: IArguments)
{
const arr = shallowReadArray(self);
const needsWrap = arr !== self;
const methodFn = arr[method] as Function;
if (methodFn !== arrayProto[method as any])
{
const result = methodFn.apply(self, args);
return needsWrap ? toReactive(result) : result;
}
let wrappedFn = fn;
if (arr !== self)
{
if (needsWrap)
{
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, wrapReactive(item), index, self);
};
}
else if (fn.length > 2)
{
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, item, index, self);
};
}
}
const result = methodFn.call(arr, wrappedFn, thisArg);
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;
}
function reduce(self: unknown[], method: keyof Array<any>, fn: (acc: unknown, item: unknown, index: number, array: unknown[]) => unknown, args: unknown[])
{
const arr = shallowReadArray(self);
let wrappedFn = fn;
if (arr !== self && fn.length > 3)
{
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) };
}
else
{
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) ;}
}
return (arr[method] as any)(wrappedFn, ...args);
}
function searchProxy(self: unknown[], method: keyof Array<any>, args: unknown[])
{
const arr = raw(self) as any;
track(arr, SYMBOLS.ITERATE);
const res = arr[method](...args);
if ((res === -1 || res === false) && isProxy(args[0]))
{
args[0] = raw(args[0]);
return arr[method](...args);
}
return res;
}
function noTracking(self: unknown[], method: keyof Array<any>, args: unknown[] = [])
{
_isTracking = false;
const res = (raw(self) as any)[method].apply(self, args);
_isTracking = true;
return res;
}
const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ without getting an error
__proto__: null, // <-- Required to remove the object prototype removing the object default functions from the substitution
[Symbol.iterator]() { return iterator(this, Symbol.iterator, item => wrapReactive(item)) },
concat(...args: unknown[]) { return reactiveReadArray(this).concat(...args.map(x => (Array.isArray(x) ? reactiveReadArray(x) : x))) },
entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = wrapReactive(value[1]); return value; }) },
every(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'every', fn, thisArg, undefined, arguments) },
filter(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply( this, 'filter', fn, thisArg, v => v.map((item: unknown) => wrapReactive(item)), arguments, ) },
find(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'find', fn, thisArg, item => wrapReactive(item), arguments, ) },
findIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findIndex', fn, thisArg, undefined, arguments) },
findLast(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'findLast', fn, thisArg, item => wrapReactive(item), arguments) },
findLastIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments) },
forEach(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'forEach', fn, thisArg, undefined, arguments) },
includes(...args: unknown[]) { return searchProxy(this, 'includes', args) },
indexOf(...args: unknown[]) { return searchProxy(this, 'indexOf', args) },
join(separator?: string) { return reactiveReadArray(this).join(separator) },
lastIndexOf(...args: unknown[]) { return searchProxy(this, 'lastIndexOf', args) },
map(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'map', fn, thisArg, undefined, arguments) },
pop() { return noTracking(this, 'pop') },
push(...args: unknown[]) { return noTracking(this, 'push', args) },
reduce(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduce', fn, args) },
reduceRight(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduceRight', fn, args) },
shift() { return noTracking(this, 'shift') },
some(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'some', fn, thisArg, undefined, arguments) },
splice(...args: unknown[]) { return noTracking(this, 'splice', args) },
toReversed() { return reactiveReadArray(this).toReversed() },
toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) },
toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) },
unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) },
values() { return iterator(this, 'values', item => wrapReactive(item)) }, /* */
};
// Store object to proxy correspondance
const _reactiveCache = new WeakMap();
// Store a Weak map of all the tracked object.
// For each object, we have a map of its properties, allowing us to effectively listen to absolutely everything on the object
// For a given property, we have a set of "effect" (function called on value update)
type Dependency = Set<() => void>;
const _tracker = new WeakMap<object, Map<string | symbol | null, Dependency>>();
function trigger(target: object, key?: string | symbol | null, value?: unknown)
{
const dependencies = _tracker.get(target);
if(!dependencies) return;
const run = (dep?: Dependency) => {
dep?.forEach(_defer);
};
const isArray = Array.isArray(target);
const arrayIndex = isIntegerKey(key);
//When the array length is modified, call not only the length and ITERATE dependencies but also the added/removed items dependencies
if(isArray && key === 'length')
{
// Run for 'length' key, SYMBOL.ITERATE and any index key after the new length (for reduction)
dependencies.forEach((v, k: any) => (k === 'length' || k === SYMBOLS.ITERATE || (isIntegerKey(k) && k >= (value as number))) && run(v));
}
else
{
key !== undefined && run(dependencies.get(key));
arrayIndex && run(dependencies.get(SYMBOLS.ITERATE));
}
}
function track(target: object, key: string | symbol | null)
{
if(!activeEffect || !_isTracking) return;
let dependencies = _tracker.get(target);
if(!dependencies)
{
dependencies = new Map();
_tracker.set(target, dependencies);
}
let set = dependencies.get(key);
if(!set)
{
set = new Set();
dependencies.set(key, set);
}
set.add(activeEffect);
//if(set) console.log('Tracking %o with key "%s"', target, key, set.size);
}
export type Proxy<T> = T & {
[SYMBOLS.PROXY]?: boolean;
[SYMBOLS.RAW]?: T;
};
export function isProxy(target: Proxy<any>): boolean
{
return target[SYMBOLS.PROXY];
}
export function reactive<T extends object>(obj: T | Proxy<T>): T | Proxy<T>
{
if((obj as Proxy<T>)[SYMBOLS.PROXY])
return obj;
if(_reactiveCache.has(obj))
return _reactiveCache.get(obj)!;
const prototype = Object.getPrototypeOf(obj);
const isArray = Array.isArray(obj);
const proxy = new Proxy<T>(obj, {
get: (target, key, receiver) => {
if(key === SYMBOLS.PROXY)
return true;
else if(key === SYMBOLS.RAW)
return obj;
if(key in arraySubstitute)
return arraySubstitute[key]!;
const value = Reflect.get(target, key, receiver);
track(target, key);
//If the value is an object, mark it as reactive dynamically
if(value && typeof value === 'object')
return reactive(value as Proxy<object>);
return value;
},
set: (target, key, value, receiver) => {
if(key === SYMBOLS.PROXY || key === SYMBOLS.RAW)
return false;
const result = Reflect.set(target, key, raw(value), receiver);
trigger(target, key, value);
return result;
},
deleteProperty: (target, key) => {
const has = key in target;
const result = Reflect.deleteProperty(target, key);
if(result && has) trigger(target, key);
return result;
},
has: (target, key) => {
const result = Reflect.has(target, key);
track(target, key);
return result;
},
ownKeys: (target) => {
const result = Reflect.ownKeys(target);
track(target, SYMBOLS.ITERATE);
return result;
}
}) as Proxy<T>;
_reactiveCache.set(obj, proxy);
return proxy;
}
export function raw<T>(obj: T): T
{
return typeof obj === 'object' ? ((obj as Proxy<T>)[SYMBOLS.RAW] as Proxy<T> | undefined) ?? obj : obj;
}
export function reactivity<T>(reactiveProperty: Reactive<T>, effect: (processed: T) => void)
{
// Function wrapping to keep the context safe and secured.
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
const secureEffect = () => effect(typeof reactiveProperty === 'function' ? (reactiveProperty as () => T)() : reactiveProperty);
const secureContext = () => {
activeEffect = secureContext;
try {
return secureEffect();
} finally {
activeEffect = null;
}
};
return secureContext();
}