/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { AnyObject } from "./types"; export type ProxyLazy = 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) & { $$vencordLazyFailed: () => boolean; }; export function makeLazy(factory: () => T, attempts = 5, { isIndirect = false }: { isIndirect?: boolean; } = {}): LazyFunction { let tries = 0; let cache: T; const getter = () => { if (!cache && attempts > tries) { tries++; cache = factory(); if (!cache && attempts === tries && !isIndirect) { console.error(`makeLazy factory failed:\n\n${factory}`); } } return cache; }; getter.$$vencordLazyFailed = () => tries === attempts; return getter; } // Proxies demand that these properties be unmodified, so proxyLazy // will always return the function default for them. const unconfigurable = ["arguments", "caller", "prototype"]; const handler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => [propName, (target: any, ...args: any[]) => Reflect[propName](target[SYM_LAZY_GET](), ...args)] )), set: (target, p, newValue) => { const lazyTarget = target[SYM_LAZY_GET](); return Reflect.set(lazyTarget, p, newValue, lazyTarget); }, ownKeys: target => { const keys = Reflect.ownKeys(target[SYM_LAZY_GET]()); 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); const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p); if (descriptor) Object.defineProperty(target, p, descriptor); return descriptor; } }; /** * 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 */ export function proxyLazy(factory: () => T, attempts = 5, isChild = false): ProxyLazy { const get = makeLazy(factory, attempts, { isIndirect: true }); let isSameTick = true; if (!isChild) setTimeout(() => isSameTick = false, 0); // Define the function in an object to preserve the name after minification const proxyDummy = ({ ProxyDummy() { } }).ProxyDummy; Object.assign(proxyDummy, { [SYM_LAZY_GET]() { if (!proxyDummy[SYM_LAZY_CACHED]) { if (!get.$$vencordLazyFailed()) { proxyDummy[SYM_LAZY_CACHED] = get(); } if (!proxyDummy[SYM_LAZY_CACHED]) { 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]); } } } return proxyDummy[SYM_LAZY_CACHED]; }, [SYM_LAZY_CACHED]: void 0 as T | undefined }); 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); } // 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 } = proxyLazy(() => ({ meow: [] }));` if (!isChild && isSameTick) { return proxyLazy( () => { const lazyTarget = target[SYM_LAZY_GET](); return Reflect.get(lazyTarget, p, lazyTarget); }, attempts, true ); } const lazyTarget = target[SYM_LAZY_GET](); if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { return Reflect.get(lazyTarget, p, lazyTarget); } 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; }