166 lines
5.2 KiB
TypeScript
166 lines
5.2 KiB
TypeScript
/* eslint-disable func-call-spacing */
|
|
|
|
type ExemptedEntries = Record<symbol | string, unknown>;
|
|
|
|
interface LazyOptions<E extends ExemptedEntries> {
|
|
hint?: "function" | "object";
|
|
exemptedEntries?: E;
|
|
}
|
|
|
|
interface ContextHolder {
|
|
options: LazyOptions<any>;
|
|
factory: any;
|
|
}
|
|
|
|
const unconfigurable = new Set(["arguments", "caller", "prototype"]);
|
|
const isUnconfigurable = (key: PropertyKey) =>
|
|
typeof key === "string" && unconfigurable.has(key);
|
|
|
|
const factories = new WeakMap<any, () => any>();
|
|
const proxyContextHolder = new WeakMap<any, ContextHolder>();
|
|
|
|
const lazyHandler: ProxyHandler<any> = {
|
|
...Object.fromEntries(
|
|
Object.getOwnPropertyNames(Reflect).map((fnName) => {
|
|
return [
|
|
fnName,
|
|
(target: any, ...args: any[]) => {
|
|
const contextHolder = proxyContextHolder.get(target);
|
|
const resolved = contextHolder?.factory();
|
|
if (!resolved)
|
|
throw new Error(
|
|
`Trying to Reflect.${fnName} of ${typeof resolved}`,
|
|
);
|
|
// @ts-expect-error
|
|
return Reflect[fnName](resolved, ...args);
|
|
},
|
|
];
|
|
}),
|
|
),
|
|
has(target, p) {
|
|
const contextHolder = proxyContextHolder.get(target);
|
|
|
|
if (contextHolder?.options) {
|
|
const { exemptedEntries: isolatedEntries } = contextHolder.options;
|
|
if (isolatedEntries && p in isolatedEntries) return true;
|
|
}
|
|
|
|
const resolved = contextHolder?.factory();
|
|
if (!resolved)
|
|
throw new Error(`Trying to Reflect.has of ${typeof resolved}`);
|
|
return Reflect.has(resolved, p);
|
|
},
|
|
get(target, p, receiver) {
|
|
if (__DEV__ && p === "__IS_BUNNY_LAZY_PROXY__") return true;
|
|
|
|
const contextHolder = proxyContextHolder.get(target);
|
|
|
|
if (contextHolder?.options) {
|
|
const { exemptedEntries: isolatedEntries } = contextHolder.options;
|
|
if (isolatedEntries?.[p]) return isolatedEntries[p];
|
|
}
|
|
|
|
const resolved = contextHolder?.factory();
|
|
if (!resolved)
|
|
throw new Error(`Trying to Reflect.get of ${typeof resolved}`);
|
|
return Reflect.get(resolved, p, receiver);
|
|
},
|
|
ownKeys: (target) => {
|
|
const contextHolder = proxyContextHolder.get(target);
|
|
const resolved = contextHolder?.factory();
|
|
if (!resolved)
|
|
throw new Error(`Trying to Reflect.ownKeys of ${typeof resolved}`);
|
|
|
|
const cacheKeys = Reflect.ownKeys(resolved);
|
|
unconfigurable.forEach(
|
|
(key) => !cacheKeys.includes(key) && cacheKeys.push(key),
|
|
);
|
|
return cacheKeys;
|
|
},
|
|
getOwnPropertyDescriptor: (target, p) => {
|
|
if (isUnconfigurable(p)) return Reflect.getOwnPropertyDescriptor(target, p);
|
|
|
|
const contextHolder = proxyContextHolder.get(target);
|
|
const resolved = contextHolder?.factory();
|
|
if (!resolved)
|
|
throw new Error(
|
|
`Trying to getOwnPropertyDescriptor of ${typeof resolved}`,
|
|
);
|
|
|
|
const descriptor = Reflect.getOwnPropertyDescriptor(resolved, p);
|
|
if (descriptor) Object.defineProperty(target, p, descriptor);
|
|
return descriptor;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Lazy proxy that will only call the factory function when needed (when a property is accessed)
|
|
* @param factory Factory function to create the object
|
|
* @param asFunction Mock the proxy as a function
|
|
* @returns A proxy that will call the factory function only when needed
|
|
* @example const ChannelStore = proxyLazy(() => findByProps("getChannelId"));
|
|
*/
|
|
export function proxyLazy<T, I extends ExemptedEntries>(
|
|
factory: () => T,
|
|
opts: LazyOptions<I> = {},
|
|
): T {
|
|
let cache: T;
|
|
|
|
const dummy = opts.hint !== "object" ? function () {} : {};
|
|
const proxyFactory = () => (cache ??= factory());
|
|
|
|
const proxy = new Proxy(dummy, lazyHandler) as T & I;
|
|
factories.set(proxy, proxyFactory);
|
|
// Use the proxyFactory in the context holder so the lazy handler calls the cached
|
|
// resolver instead of the raw factory. This avoids attempting to Reflect.* on
|
|
// an unresolved/undefined value when the original factory hasn't produced the object yet.
|
|
proxyContextHolder.set(dummy, {
|
|
factory: proxyFactory,
|
|
options: opts,
|
|
});
|
|
|
|
return proxy;
|
|
}
|
|
|
|
/**
|
|
* Lazily destructure an object with all the properties being lazified. This assumes all the properties are either an object or a function
|
|
* @param factory Factory function which resolves to the object (and caches it)
|
|
* @param asFunction Mock the proxy as a function
|
|
* @example
|
|
*
|
|
* const { uuid4 } = lazyDestructure(() => findByProps("uuid4"))
|
|
* uuid4; // <- is a lazy proxy!
|
|
*/
|
|
export function lazyDestructure<
|
|
T extends Record<PropertyKey, unknown>,
|
|
I extends ExemptedEntries,
|
|
>(factory: () => T, opts: LazyOptions<I> = {}): T {
|
|
const proxiedObject = proxyLazy(factory);
|
|
|
|
return new Proxy(
|
|
{},
|
|
{
|
|
get(_, property) {
|
|
if (property === Symbol.iterator) {
|
|
return function* () {
|
|
yield proxiedObject;
|
|
yield new Proxy(
|
|
{},
|
|
{
|
|
get: (_, p) => proxyLazy(() => proxiedObject[p], opts),
|
|
},
|
|
);
|
|
throw new Error(
|
|
"This is not a real iterator, this is likely used incorrectly",
|
|
);
|
|
};
|
|
}
|
|
return proxyLazy(() => proxiedObject[property], opts);
|
|
},
|
|
},
|
|
) as T;
|
|
}
|
|
|
|
export function getProxyFactory<T>(obj: T): (() => T) | void {
|
|
return factories.get(obj) as (() => T) | void;
|
|
}
|