diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index c3d328044..4a71c8f6e 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -14,7 +14,7 @@ import pLimit from "p-limit"; function getWebpackChunkMap() { const sym = Symbol(); - let chunksMap: unknown; + let chunksMap: unknown = null; Object.defineProperty(Object.prototype, sym, { get() { @@ -30,9 +30,38 @@ function getWebpackChunkMap() { return chunksMap as Record | null; } +let chunksAlreadyLoaded = false; + export async function loadLazyChunks() { const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); + + if (chunksAlreadyLoaded) { + LazyChunkLoaderLogger.log("Lazy chunks have already been loaded"); + return; + } + const queue = pLimit(50); + const workerAssetCache = new Map>(); + const WORKER_ASSET_REGEX = /importScripts\(|self\.postMessage/; + + async function isWorkerAsset(url: string, useQueue: boolean = true): Promise { + if (workerAssetCache.has(url)) { + return workerAssetCache.get(url)!; + } + + const doFetch = () => { + return fetch(url) + .then(r => r.text()) + .then(t => WORKER_ASSET_REGEX.test(t)); + }; + + const res = useQueue + ? queue(doFetch) + : doFetch(); + + workerAssetCache.set(url, res); + return res; + } try { LazyChunkLoaderLogger.log("Loading all chunks..."); @@ -41,12 +70,12 @@ export async function loadLazyChunks() { const invalidChunks = new Set(); const deferredRequires = new Set(); - const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers(); + const { promise: chunkSearchingDone, resolve: chunkSearchingDoneResolve } = Promise.withResolvers(); - // True if resolved, false otherwise - const chunksSearchPromises = [] as Array<() => boolean>; + // True if searching promise for that chunk is resolved, false otherwise + let chunkSearchResolvedGetters = [] as Array<() => boolean>; - // This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those + // This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesn't include those const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g); const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g); @@ -56,7 +85,7 @@ export async function loadLazyChunks() { // Workaround to avoid loading the CSS debugging chunk which turns the app pink // const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&")); - // Disabled for now since this causes lots of chunks concatenated into the same module get marked as invalid, and thus not loaded. + // Disabled for now since this causes lots of chunks concatenated into the same module to get marked as invalid, and thus not loaded. const hasCssDebuggingLoad = foundCssDebuggingLoad = false; const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex); @@ -93,13 +122,7 @@ export async function loadLazyChunks() { if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; - const isWorkerAsset = await queue(() => - fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => /importScripts\(|self\.postMessage/.test(t)) - ); - - if (isWorkerAsset) { + if (await isWorkerAsset(wreq.p + wreq.u(id))) { invalidChunks.add(id); invalidChunkGroup = true; continue; @@ -140,35 +163,27 @@ export async function loadLazyChunks() { } } - // setImmediate to only check if all chunks were loaded after this function resolves - // We check if all chunks were loaded every time a factory is loaded - // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved - // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + + // Filter out resolved chunk search promises. If the array length is 0 that means all the pending searches and loadings are done + // and our regex has finished scanning all modules. Once this happens, the artificial "natural" loading of chunks has been completed + // and we can continue with the rest of the code. + // setImmediate here is needed so the filtering also includes the promises created from searching the modules loaded by the current invokation. + // Otherwise, it could resolve before the async code being put at the end of the event loop has a chance to run and add the promise to the array. + // The async code mentioned is the invokation of the current function from the factoryListener. setTimeout(() => { - let allResolved = true; - - for (let i = 0; i < chunksSearchPromises.length; i++) { - const isResolved = chunksSearchPromises[i](); - - if (isResolved) { - // Remove finished promises to avoid having to iterate through a huge array everytime - chunksSearchPromises.splice(i--, 1); - } else { - allResolved = false; - } + chunkSearchResolvedGetters = chunkSearchResolvedGetters.filter(isResolvedGetter => !isResolvedGetter()); + if (chunkSearchResolvedGetters.length === 0) { + chunkSearchingDoneResolve(); } - - if (allResolved) chunksSearchingResolve(); }, 0); } function factoryListener(factory: AnyModuleFactory | ModuleFactory) { let isResolved = false; searchAndLoadLazyChunks(String(factory)) - .then(() => isResolved = true) - .catch(() => isResolved = true); + .finally(() => isResolved = true); - chunksSearchPromises.push(() => isResolved); + chunkSearchResolvedGetters.push(() => isResolved); } Webpack.factoryListeners.add(factoryListener); @@ -176,7 +191,7 @@ export async function loadLazyChunks() { factoryListener(wreq.m[moduleId]); } - await chunksSearchingDone; + await chunkSearchingDone; Webpack.factoryListeners.delete(factoryListener); // Require deferred entry points @@ -198,17 +213,16 @@ export async function loadLazyChunks() { }); await Promise.all(chunksLeft.map(async id => queue(async () => { - const isWorkerAsset = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => /importScripts\(|self\.postMessage/.test(t)); + // We will deadlock if we use the queue inside a queue func + const isWorkerFile = await isWorkerAsset(wreq.p + wreq.u(id), false); - // Loads the chunk. Currently this only happens with the language packs which are loaded differently - if (!isWorkerAsset) { + if (!isWorkerFile) { await wreq.e(id); } }))); LazyChunkLoaderLogger.log("Finished loading all chunks!"); + chunksAlreadyLoaded = true; } catch (e) { LazyChunkLoaderLogger.log("A fatal error occurred:", e); } diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 2be235dfa..982113aac 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -17,7 +17,7 @@ async function runReporter() { try { ReporterLogger.log("Starting test..."); - const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers(); + const { promise: loadLazyChunksDone, resolve: loadLazyChunksDoneResolve } = Promise.withResolvers(); // The main patch for starting the reporter chunk loading addPatch({ @@ -28,11 +28,10 @@ async function runReporter() { } }, "Vencord Reporter"); + // initReporter is called in the patched entry point of Discord // @ts-expect-error Vencord.Webpack._initReporter = function () { - // initReporter is called in the patched entry point of Discord - // setImmediate to only start searching for lazy chunks after Discord initialized the app - setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0); + loadLazyChunks().then(loadLazyChunksDoneResolve); }; await loadLazyChunksDone;