@difizen/mana-observable
Version:
241 lines (227 loc) • 7.14 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getPropertyDescriptor, isPlainObject } from '@difizen/mana-common';
import { ObservableSymbol } from './core';
import { Notifiable } from './notifiable';
import { Notifier } from './notifier';
import { observable } from './observable';
import { Observability } from './utils';
type Act = (...args: any) => void;
export function tryInvokeGetter(getter: () => any, proxy: any, origin: any) {
try {
return getter.call(proxy);
} catch (e: any) {
if (e instanceof TypeError && e.message.includes('Illegal invocation')) {
return getter.call(origin);
}
throw e;
}
}
export type Trackable = {
[ObservableSymbol.Tracker]: Record<string, any>;
[Trackable.activator]?: (() => void) | undefined;
};
export namespace Trackable {
export const activator = Symbol('activator');
export function is(target: any): target is Trackable {
return (
Observability.canBeObservable(target) && (target as any)[ObservableSymbol.Tracker]
);
}
export function getOrigin(target: Trackable): any {
return target[ObservableSymbol.Tracker];
}
export function tryGetOrigin(target: any): any {
if (!is(target)) {
return target;
}
return getOrigin(target);
}
}
export namespace Tracker {
export function set<T extends Record<any, any> = any>(target: T, act: Act, proxy: T) {
Reflect.defineMetadata(act, proxy, target, ObservableSymbol.Tracker);
}
export function get<T extends Record<any, any> = any>(
target: T,
act: Act,
): (T & Trackable) | undefined {
return Reflect.getMetadata(act, target, ObservableSymbol.Tracker);
}
export function has<T extends Record<any, any> = any>(target: T, act: Act) {
return Reflect.hasOwnMetadata(act, target, ObservableSymbol.Tracker);
}
export function tramsform(toTrack: any, act: Act, activator?: () => void) {
if (toTrack instanceof Array) {
return transformArray(toTrack, act, activator);
}
if (toTrack instanceof Map) {
return transformMap(toTrack, act, activator);
}
if (isPlainObject(toTrack)) {
return transformPlainObject(toTrack, act, activator);
}
return toTrack;
}
export function transformArray(toTrack: any[], act: Act, activator?: () => void) {
return new Proxy(toTrack, {
get(target: any, property: string | symbol): any {
const value = target[property];
if (property === ObservableSymbol.Self) {
return value;
}
if (Object.isFrozen(target)) {
return value;
}
if (property === Trackable.activator) {
return activator;
}
return track(value, act);
},
});
}
export function transformPlainObject(toTrack: any, act: Act, activator?: () => void) {
return new Proxy(toTrack, {
get(target: any, property: string | symbol): any {
const value = target[property];
if (property === ObservableSymbol.Self) {
return value;
}
if (Object.isFrozen(target)) {
return value;
}
if (property === Trackable.activator) {
return activator;
}
return track(value, act);
},
});
}
export function transformMap(
toTrack: Map<any, any>,
act: Act,
activator?: () => void,
) {
return new Proxy(toTrack, {
get(target: any, property: string | symbol): any {
const value = target[property];
if (property === ObservableSymbol.Self) {
return value;
}
if (Object.isFrozen(target)) {
return value;
}
if (property === Trackable.activator) {
return activator;
}
if (property === 'get' && typeof value === 'function') {
return function (...args: any[]) {
const innerValue = value.apply(target, args);
return track(innerValue, act);
};
}
return value;
},
});
}
export function toInstanceTracker<T extends Record<any, any>>(origin: T, act: Act) {
// try make observable
if (!Observability.marked(origin)) {
observable(origin);
}
const proxy = new Proxy(origin, {
get(target: any, property: string | symbol): any {
if (property === ObservableSymbol.Tracker) {
return target;
}
if (property === ObservableSymbol.Self) {
return target;
}
let notifier;
let value;
if (typeof property === 'string') {
if (Observability.shouldKeepOrigin(target, property)) {
return target[property];
}
const descriptor = getPropertyDescriptor(target, property);
if (descriptor?.configurable === false && descriptor?.writable === false) {
// non-configurable and non-writable property should return the actual value
return target[property];
}
if (Observability.marked(target, property)) {
notifier = Notifier.getOrCreate(target, property);
Notifier.once(notifier, act, () => {
act({
key: property as keyof T,
value: target[property],
});
});
value = target[property];
return track(value, act, true);
}
if (descriptor?.get) {
return tryInvokeGetter(descriptor.get, proxy, target);
}
}
value = target[property];
return track(value, act, true);
},
});
set(origin, act, proxy);
return proxy;
}
export function toNotifiableTracker<T extends Record<any, any>>(
origin: T,
act: Act,
asProperty: boolean,
) {
let maybeNotifiable: T = Notifiable.get(origin);
if (!maybeNotifiable) {
maybeNotifiable = Notifiable.transform(origin);
}
// set reactable listener
let activator;
if (!asProperty && Notifiable.is(maybeNotifiable)) {
const notifier = Notifiable.getNotifier(maybeNotifiable);
activator = () => {
Notifier.once(notifier, act, () => {
act(origin);
});
};
activator();
}
const proxy = tramsform(maybeNotifiable, act, activator);
set(origin, act, proxy);
return proxy;
}
export function track<T extends Record<any, any>>(
object: T,
act: Act,
asProperty = false,
): T {
if (!Observability.canBeObservable(object)) {
return object;
}
// get origin
let origin = object;
if (Trackable.is(object)) {
origin = Trackable.getOrigin(object);
}
origin = Observability.getOrigin(origin);
// already has tracker
let exist: (T & Trackable) | undefined = undefined;
if (has(origin, act)) {
exist = get(origin, act);
if (exist) {
if (exist[Trackable.activator]) {
exist[Trackable.activator]();
}
return exist;
}
}
if (Notifiable.canBeNotifiable(origin)) {
return toNotifiableTracker(origin, act, asProperty);
} else {
return toInstanceTracker(origin, act);
}
}
}