Vencord/src/utils/lazy.ts

103 lines
3.6 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
2023-09-23 01:25:19 +00:00
export function makeLazy<T>(factory: () => T, attempts = 5): () => T {
let tries = 0;
2023-05-05 23:36:00 +00:00
let cache: T;
2023-09-23 01:25:19 +00:00
return () => {
if (!cache && attempts > tries++) {
cache = factory();
2024-05-03 02:18:12 +00:00
if (!cache && attempts === tries) {
console.error(`Lazy factory failed:\n${factory}`);
}
2023-09-23 01:25:19 +00:00
}
return cache;
};
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[proxyLazyGet](), ...args)]
)),
ownKeys: target => {
const keys = Reflect.ownKeys(target[proxyLazyGet]());
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
2024-05-03 02:18:12 +00:00
const descriptor = Reflect.getOwnPropertyDescriptor(target[proxyLazyGet](), p);
if (descriptor) Object.defineProperty(target, p, descriptor);
return descriptor;
2022-11-06 17:00:59 +00:00
}
};
2024-05-03 02:18:12 +00:00
const proxyLazyGet = Symbol.for("vencord.lazy.get");
const proxyLazyCache = Symbol.for("vencord.lazy.cached");
2022-10-14 20:38:36 +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.
* On first property access, the factory is evaluated
* @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
*/
2024-05-03 02:18:12 +00:00
export function proxyLazy<T = any>(factory: () => T, attempts = 5, isChild = false): T {
const get = makeLazy(factory, attempts);
let isSameTick = true;
2024-05-03 02:18:12 +00:00
if (!isChild) setTimeout(() => isSameTick = false, 0);
2024-05-03 02:18:12 +00:00
let failed = false;
2023-07-05 22:53:43 +00:00
const proxyDummy = Object.assign(function () { }, {
2024-05-03 02:18:12 +00:00
[proxyLazyGet]() {
if (!proxyDummy[proxyLazyCache] && !failed) {
proxyDummy[proxyLazyCache] = get();
if (!proxyDummy[proxyLazyCache]) {
failed = true;
throw new Error(`proxyLazy factory failed:\n${factory}`);
}
2023-07-05 22:53:43 +00:00
}
2024-05-03 02:18:12 +00:00
return proxyDummy[proxyLazyCache];
},
[proxyLazyCache]: void 0 as T | undefined
});
2022-11-06 17:00:59 +00:00
return new Proxy(proxyDummy, {
...handler,
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
// `const { meow } = findByPropsLazy("meow");`
2024-05-03 02:18:12 +00:00
if (!isChild && isSameTick) {
return proxyLazy(
2024-05-03 02:18:12 +00:00
() => Reflect.get(target[proxyLazyGet](), p, receiver),
attempts,
true
);
2024-05-03 02:18:12 +00:00
}
const lazyTarget = target[proxyLazyGet]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
return Reflect.get(lazyTarget, p, receiver);
}
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.");
}
2024-05-03 02:18:12 +00:00
}) as T;
2022-10-14 19:34:35 +00:00
}