Vencord/src/utils/lazy.ts

140 lines
4.9 KiB
TypeScript
Raw Normal View History

2022-10-21 23:17:06 +00:00
/*
2024-05-03 02:18:12 +00:00
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
2022-10-21 23:17:06 +00:00
import { AnyObject } from "./types";
export type ProxyLazy<T = AnyObject> = T & {
[SYM_LAZY_GET]: () => T;
[SYM_LAZY_CACHED]: T | undefined;
};
export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get");
export const SYM_LAZY_CACHED = Symbol.for("vencord.lazy.cached");
export type LazyFunction<T> = (() => T) & {
$$vencordLazyFailed: () => boolean;
};
export function makeLazy<T>(factory: () => T, attempts = 5, { isIndirect = false }: { isIndirect?: boolean; } = {}): LazyFunction<T> {
2023-09-23 01:25:19 +00:00
let tries = 0;
2023-05-05 23:36:00 +00:00
let cache: T;
2024-05-04 23:56:32 +00:00
const getter = () => {
if (!cache && attempts > tries) {
tries++;
2023-09-23 01:25:19 +00:00
cache = factory();
if (!cache && attempts === tries && !isIndirect) {
console.error(`makeLazy factory failed:\n\n${factory}`);
2024-05-03 02:18:12 +00:00
}
2023-09-23 01:25:19 +00:00
}
2023-09-23 01:25:19 +00:00
return cache;
};
2024-05-04 23:56:32 +00:00
getter.$$vencordLazyFailed = () => tries === attempts;
2024-05-04 23:56:32 +00:00
return getter;
2023-05-05 23:36:00 +00:00
}
2022-11-06 17:00:59 +00:00
// Proxies demand that these properties be unmodified, so proxyLazy
// will always return the function default for them.
const unconfigurable = ["arguments", "caller", "prototype"];
2022-10-14 19:34:35 +00:00
2024-05-03 02:18:12 +00:00
const handler: ProxyHandler<any> = {
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
[propName, (target: any, ...args: any[]) => Reflect[propName](target[SYM_LAZY_GET](), ...args)]
2024-05-03 02:18:12 +00:00
)),
2024-05-24 01:52:32 +00:00
set: (target, p, newValue) => {
const lazyTarget = target[SYM_LAZY_GET]();
2024-05-24 01:52:32 +00:00
return Reflect.set(lazyTarget, p, newValue, lazyTarget);
},
2024-05-03 02:18:12 +00:00
ownKeys: target => {
const keys = Reflect.ownKeys(target[SYM_LAZY_GET]());
2024-05-03 02:18:12 +00:00
for (const key of unconfigurable) {
if (!keys.includes(key)) keys.push(key);
}
return keys;
},
getOwnPropertyDescriptor: (target, p) => {
if (typeof p === "string" && unconfigurable.includes(p))
return Reflect.getOwnPropertyDescriptor(target, p);
2022-11-06 17:00:59 +00:00
const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p);
2024-05-03 02:18:12 +00:00
if (descriptor) Object.defineProperty(target, p, descriptor);
return descriptor;
2022-11-06 17:00:59 +00:00
}
};
2022-10-14 19:34:35 +00:00
/**
2024-05-03 02:18:12 +00:00
* Wraps the result of factory in a Proxy you can consume as if it wasn't lazy.
2024-05-26 23:41:28 +00:00
* On first property access, the factory is evaluated.
2024-05-03 02:18:12 +00:00
* @param factory Factory returning the result
* @param attempts How many times to try to evaluate the factory before giving up
* @returns Result of factory function
2022-10-14 19:34:35 +00:00
*/
export function proxyLazy<T = AnyObject>(factory: () => T, attempts = 5, isChild = false): ProxyLazy<T> {
2024-05-16 02:07:35 +00:00
const get = makeLazy(factory, attempts, { isIndirect: true });
2024-05-03 02:18:12 +00:00
let isSameTick = true;
2024-05-03 02:18:12 +00:00
if (!isChild) setTimeout(() => isSameTick = false, 0);
// Define the function in an object to preserve the name after minification
2024-05-24 02:20:46 +00:00
const proxyDummy = ({ ProxyDummy() { } }).ProxyDummy;
Object.assign(proxyDummy, {
[SYM_LAZY_GET]() {
if (!proxyDummy[SYM_LAZY_CACHED]) {
2024-05-04 23:56:32 +00:00
if (!get.$$vencordLazyFailed()) {
proxyDummy[SYM_LAZY_CACHED] = get();
2024-05-04 23:56:32 +00:00
}
2024-05-03 02:18:12 +00:00
if (!proxyDummy[SYM_LAZY_CACHED]) {
2024-05-04 23:56:32 +00:00
throw new Error(`proxyLazy factory failed:\n\n${factory}`);
} else {
if (typeof proxyDummy[SYM_LAZY_CACHED] === "function") {
proxy.toString = proxyDummy[SYM_LAZY_CACHED].toString.bind(proxyDummy[SYM_LAZY_CACHED]);
}
2024-05-03 02:18:12 +00:00
}
2023-07-05 22:53:43 +00:00
}
2024-05-03 02:18:12 +00:00
return proxyDummy[SYM_LAZY_CACHED];
2024-05-03 02:18:12 +00:00
},
[SYM_LAZY_CACHED]: void 0 as T | undefined
});
2022-11-06 17:00:59 +00:00
const proxy = new Proxy(proxyDummy, {
...handler,
get(target, p, receiver) {
if (p === SYM_LAZY_GET || p === SYM_LAZY_CACHED) {
return Reflect.get(target, p, receiver);
}
2024-05-03 02:18:12 +00:00
// If we're still in the same tick, it means the lazy was immediately used.
// thus, we lazy proxy the get access to make things like destructuring work as expected
// meow here will also be a lazy
2024-05-08 03:25:34 +00:00
// `const { meow } = proxyLazy(() => ({ meow: [] }));`
2024-05-03 02:18:12 +00:00
if (!isChild && isSameTick) {
return proxyLazy(
() => {
const lazyTarget = target[SYM_LAZY_GET]();
return Reflect.get(lazyTarget, p, lazyTarget);
},
attempts,
true
);
2024-05-03 02:18:12 +00:00
}
const lazyTarget = target[SYM_LAZY_GET]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
2024-05-24 01:51:23 +00:00
return Reflect.get(lazyTarget, p, lazyTarget);
}
2024-05-03 02:18:12 +00:00
throw new Error("proxyLazy called on a primitive value. This can happen if you try to destructure a primitive at the same tick as the proxy was created.");
}
});
return proxy;
2022-10-14 19:34:35 +00:00
}