From 6745ca251436678301be7f019e2584483d183c3e Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:55:57 +0200 Subject: [PATCH] Speed up UI loading, and usage of the app Start enabled Vendetta plugins in small staggered batches and defer network fetches to the background. Register core plugins at init so they appear in the UI immediately. Remove BUNNY_PROXY_PREFIX usage and the unproxied-install confirmation. Restore Updater settings entry and bump build version to 1.1.6-2. Apply various Metro internals cleanup and robustness fixes (import tracking, export/blacklist handling, error guards) --- scripts/build.mjs | 2 +- src/core/ui/settings/index.ts | 14 +- src/core/ui/settings/pages/Plugins/index.tsx | 42 +-- src/core/vendetta/plugins.ts | 79 +++-- src/index.ts | 11 - src/lib/addons/plugins/index.ts | 57 ++++ src/lib/utils/constants.ts | 3 - src/metro/internals/modules.ts | 312 +++++++++++-------- 8 files changed, 311 insertions(+), 209 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index c4d344a..0fd5b07 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -77,7 +77,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.1.6-1"`, + version: `"1.1.6-2"`, }, }, }, diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index e25ab5c..cad430b 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -20,13 +20,6 @@ export default function initSettings() { render: () => import("@core/ui/settings/pages/General"), useTrailing: () => `(${version})`, }, - { - key: "SHIGGYCORD_UPDATER", - title: () => "Updater", - icon: findAssetId("BellIcon"), - render: () => import("@core/ui/settings/pages/Updater"), - }, - { key: "CORE_PLUGINS", title: () => Strings.PLUGINS, @@ -56,7 +49,12 @@ export default function initSettings() { render: () => import("@core/ui/settings/pages/Fonts"), usePredicate: () => isFontSupported(), }, - + { + key: "SHIGGYCORD_UPDATER", + title: () => "Updater", + icon: findAssetId("BellIcon"), + render: () => import("@core/ui/settings/pages/Updater"), + }, { key: "BUNNY_DEVELOPER", title: () => Strings.DEVELOPER, diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 7634947..31a1e0a 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -26,7 +26,7 @@ import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; import { useObservable } from "@lib/api/storage"; import { showToast } from "@lib/ui/toasts"; -import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; +import { VD_PROXY_PREFIX } from "@lib/utils/constants"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; import { ComponentProps } from "react"; @@ -186,39 +186,6 @@ function InstallButton({ } }; - const continueInstallation = async () => { - if ( - !addon.installUrl.startsWith(VD_PROXY_PREFIX) && - !addon.installUrl.startsWith(BUNNY_PROXY_PREFIX) && - !settings.developerSettings - ) { - openAlert( - "bunny-plugin-unproxied-confirmation", - - {addon.installUrl} - - } - actions={ - - - - - } - />, - ); - } else { - performInstall(); - } - }; - const handleUninstall = () => { VdPluginManager.removePlugin(pluginId); setRefreshTick((prev: number) => prev + 1); @@ -628,12 +595,13 @@ export default function Plugins() { // Keep core plugins registration available if needed elsewhere const corePlugins = [...corePluginInstances.keys()] - .map((id) => registeredPlugins.get(id)!) + .map((id) => registeredPlugins.get(id)) + .filter((m): m is NonNullable => Boolean(m)) .map(unifyBunnyPlugin); // Merge lists: show Vendetta-managed plugins first (recent first), - // then Bunny-installed externals (recent first). - return [...vdPlugins, ...bnPlugins]; + // include core plugins, then Bunny-installed externals (recent first). + return [...vdPlugins, ...corePlugins, ...bnPlugins]; }} ListHeaderComponent={() => null} installAction={{ diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index 174b7dc..76fab8e 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -8,7 +8,7 @@ import { import { Author } from "@lib/addons/types"; import { settings } from "@lib/api/settings"; import { safeFetch } from "@lib/utils"; -import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; +import { VD_PROXY_PREFIX } from "@lib/utils/constants"; import { logger, LoggerClass } from "@lib/utils/logger"; type EvaledPlugin = { @@ -192,24 +192,65 @@ export const VdPluginManager = { const allIds = Object.keys(plugins); if (!settings.safeMode?.enabled) { - // Loop over any plugin that is enabled, update it if allowed, then start it. - await Promise.allSettled( - allIds - .filter((pl) => plugins[pl].enabled) - .map( - async (pl) => ( - plugins[pl].update && - (await this.fetchPlugin(pl).catch((e: Error) => - logger.error(e.message), - )), - await this.startPlugin(pl) - ), - ), - ); - // Wait for the above to finish, then update all disabled plugins that are allowed to. - allIds - .filter((pl) => !plugins[pl].enabled && plugins[pl].update) - .forEach((pl) => this.fetchPlugin(pl)); + // Staggered startup: start enabled plugins in small batches to avoid + // CPU / I/O spikes during app launch. Network fetches (which may be slow) + // are deferred to background so the UI can become interactive faster. + const enabledIds = allIds.filter((pl) => plugins[pl].enabled); + const batchSize = 2; + const staggerInterval = 500; + + if (enabledIds.length > 0) { + const sleep = (ms: number) => + new Promise((res) => setTimeout(res, ms)); + + for (let i = 0; i < enabledIds.length; i += batchSize) { + const batch = enabledIds.slice(i, i + batchSize); + + // Start the batch in parallel but avoid letting a single failing plugin + // abort the whole process. + await Promise.allSettled( + batch.map(async (id) => { + try { + if (!plugins[id]) return; + await this.startPlugin(id).catch((e) => + logger.error(`Vendetta plugin ${id} failed to start:`, e), + ); + } catch (err) { + logger.error( + `Unexpected error while starting vendetta plugin ${id}:`, + err, + ); + } + }), + ); + + if (i + batchSize < enabledIds.length) { + await sleep(staggerInterval); + } + } + } + + // Defer network fetches to the background so startup isn't delayed. + // We fetch updates for plugins (both enabled and disabled) but do this + // asynchronously and with a small delay between requests to reduce contention. + (async () => { + const toFetch = allIds.filter((pl) => plugins[pl]?.update); + for (const pl of toFetch) { + try { + // Fire fetch; fetchPlugin may update plugin JS/manifest. Errors are logged. + await this.fetchPlugin(pl).catch((e: Error) => { + logger.error(e.message); + }); + } catch (e) { + logger.error( + `Failed background fetch for vendetta plugin ${pl}:`, + e, + ); + } + // Small pause between network requests to avoid spikes + await new Promise((res) => setTimeout(res, 200)); + } + })(); } return () => this.stopAllPlugins(); diff --git a/src/index.ts b/src/index.ts index 402ff52..b51122d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,17 +119,6 @@ export default async () => { setTimeout(runDeferred, 200); } - // Schedule a delayed plugin repository refresh (5 minutes) if the earlier deferred schedule didn't run. - // This ensures we refresh plugin metadata after the app is fully launched but avoid startup contention. - setTimeout( - () => { - updatePlugins().catch((e) => - logger.log("Scheduled updatePlugins (5min) failed:", e), - ); - }, - 5 * 60 * 1000, - ); - // Periodic bundle check: every 3 hours, check GitHub releases (ignore prereleases). // We use the GitHub Releases API to find the latest non-prerelease release and compare its tag. const checkBundle = async () => { diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 917ae45..a407059 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -33,6 +33,53 @@ export const apiObjects = new Map< ReturnType >(); +/** + * Register core plugins into the runtime registries so they appear in the UI + * as soon as this module is imported. This function is intentionally synchronous + * and idempotent so it is safe to call at module-load time or from initialization + * code. + */ +export function registerCorePlugins() { + try { + const core = getCorePlugins(); + for (const id in core) { + try { + const { default: instance, preenabled } = core[id]; + + // Ensure manifest is registered for UI listing + registeredPlugins.set(id, instance.manifest); + + // Keep a reference to the core plugin instance so isCorePlugin checks work + corePluginInstances.set(id, instance); + + // Ensure pluginSettings has an entry for the core plugin so the UI can + // reflect enabled/disabled state. Do not overwrite existing user settings. + try { + // `pluginSettings` is a storage wrapper; ensure we only set a default if absent. + if (pluginSettings[id] == null) { + pluginSettings[id] = { enabled: preenabled ?? true }; + } + } catch (e) { + // Best-effort: if storage isn't available yet, ignore and continue. + console.error( + "Failed to set default pluginSettings for core plugin", + id, + e, + ); + } + } catch (e) { + console.error("Failed to register core plugin", id, e); + } + } + } catch (e) { + console.error("registerCorePlugins: unexpected error", e); + } +} + +// Eager core plugin registration removed to avoid circular require / timing issues. +// Call `registerCorePlugins()` explicitly from initialization code when appropriate, +// for example during a safe deferred startup step. + export const pluginRepositories = createStorage( "plugins/repositories.json", ); @@ -628,6 +675,16 @@ export async function initPlugins( ) { const { staggerInterval = 100, batchSize = 3 } = options; + // Ensure core plugins are registered prior to any plugin startup logic so + // settings pages and UI that read registered/core plugins show them. + // registerCorePlugins is synchronous and idempotent, but call it here as a + // safety-net in case module-load registration didn't run earlier. + try { + registerCorePlugins(); + } catch { + // ignore + } + await awaitStorage(pluginRepositories, pluginSettings); // Collect enabled plugin ids diff --git a/src/lib/utils/constants.ts b/src/lib/utils/constants.ts index d74c404..d1587b5 100644 --- a/src/lib/utils/constants.ts +++ b/src/lib/utils/constants.ts @@ -5,10 +5,7 @@ export const HTTP_REGEX = export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&\/=]*)/g; -export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy"; - export const NEXPID_PLUGINS_REPO_URL = "https://revenge.nexpid.xyz"; -//export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json"; export const OFFICIAL_PLUGINS_REPO_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json"; diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts index 964b116..a02fddb 100644 --- a/src/metro/internals/modules.ts +++ b/src/metro/internals/modules.ts @@ -1,4 +1,8 @@ -import { getMetroCache, indexBlacklistFlag, indexExportsFlags } from "@metro/internals/caches"; +import { + getMetroCache, + indexBlacklistFlag, + indexExportsFlags, +} from "@metro/internals/caches"; import { Metro } from "@metro/types"; import { ModuleFlags, ModulesMapInternal } from "./enums"; @@ -20,88 +24,103 @@ let patchedNativeComponentRegistry = false; let _importingModuleId: number = -1; for (const key in metroModules) { - const id = Number(key); - const metroModule = metroModules[id]; + const id = Number(key); + const metroModule = metroModules[id]; - const cache = getMetroCache().flagsIndex[id]; - if (cache & ModuleFlags.BLACKLISTED) { + const cache = getMetroCache().flagsIndex[id]; + if (cache & ModuleFlags.BLACKLISTED) { + blacklistModule(id); + continue; + } + + if (metroModule!.factory) { + instead("factory", metroModule, (( + args: Parameters, + origFunc: Metro.FactoryFn, + ) => { + const originalImportingId = _importingModuleId; + _importingModuleId = id; + + const { 1: metroRequire, 4: moduleObject } = args; + + args[2 /* metroImportDefault */] = (id) => { + const exps = metroRequire(id); + return exps && exps.__esModule ? exps.default : exps; + }; + + args[3 /* metroImportAll */] = (id) => { + const exps = metroRequire(id); + if (exps && exps.__esModule) return exps; + + const importAll: Record = {}; + if (exps) Object.assign(importAll, exps); + importAll.default = exps; + return importAll; + }; + + origFunc(...args); + if (!isBadExports(moduleObject.exports)) { + onModuleRequire(moduleObject.exports, id); + } else { blacklistModule(id); - continue; - } + } - if (metroModule!.factory) { - instead("factory", metroModule, ((args: Parameters, origFunc: Metro.FactoryFn) => { - const originalImportingId = _importingModuleId; - _importingModuleId = id; - - const { 1: metroRequire, 4: moduleObject } = args; - - args[2 /* metroImportDefault */] = id => { - const exps = metroRequire(id); - return exps && exps.__esModule ? exps.default : exps; - }; - - args[3 /* metroImportAll */] = id => { - const exps = metroRequire(id); - if (exps && exps.__esModule) return exps; - - const importAll: Record = {}; - if (exps) Object.assign(importAll, exps); - importAll.default = exps; - return importAll; - }; - - origFunc(...args); - if (!isBadExports(moduleObject.exports)) { - onModuleRequire(moduleObject.exports, id); - } else { - blacklistModule(id); - } - - _importingModuleId = originalImportingId; - }) as any); // If only spitroast had better types - } + _importingModuleId = originalImportingId; + }) as any); // If only spitroast had better types + } } /** Makes the module associated with the specified ID non-enumberable. */ function blacklistModule(id: number) { - Object.defineProperty(metroModules, id, { enumerable: false }); - blacklistedIds.add(id); - indexBlacklistFlag(Number(id)); + Object.defineProperty(metroModules, id, { enumerable: false }); + blacklistedIds.add(id); + indexBlacklistFlag(Number(id)); } function isBadExports(exports: any) { - return !exports - || exports === window - || exports[""] === null - || (exports.__proto__ === Object.prototype && Reflect.ownKeys(exports).length === 0) - || (exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy"); + return ( + !exports || + exports === window || + exports[""] === + null || + (exports.__proto__ === Object.prototype && + Reflect.ownKeys(exports).length === 0) || + exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy" + ); } function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { - indexExportsFlags(id, moduleExports); + indexExportsFlags(id, moduleExports); - // Temporary - moduleExports.initSentry &&= () => undefined; - if (moduleExports.default?.track && moduleExports.default.trackMaker) - moduleExports.default.track = () => Promise.resolve(); + // Temporary + moduleExports.initSentry &&= () => undefined; + if (moduleExports.default?.track && moduleExports.default.trackMaker) + moduleExports.default.track = () => Promise.resolve(); - if (moduleExports.registerAsset) { - require("@lib/api/assets/patches").patchAssets(moduleExports); - } + if (moduleExports.registerAsset) { + require("@lib/api/assets/patches").patchAssets(moduleExports); + } - // There are modules registering the same native component - if (!patchedNativeComponentRegistry && ["customBubblingEventTypes", "customDirectEventTypes", "register", "get"].every(x => moduleExports[x])) { - instead("register", moduleExports, ([name, cb]: any, origFunc: any) => { - try { - return origFunc(name, cb); - } catch { - return name; - } - }); + // There are modules registering the same native component + if ( + !patchedNativeComponentRegistry && + [ + "customBubblingEventTypes", + "customDirectEventTypes", + "register", + "get", + ].every((x) => moduleExports[x]) + ) { + instead("register", moduleExports, ([name, cb]: any, origFunc: any) => { + try { + return origFunc(name, cb); + } catch { + return name; + } + }); - patchedNativeComponentRegistry = true; - } + patchedNativeComponentRegistry = true; + } // Hook DeveloperExperimentStore @@ -141,100 +160,133 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { if (!metroModules[id + inc]?.isInitialized) { blacklistModule(id + inc); } - } - // Hindi timestamps moment - if (moduleExports.isMoment) { - instead("defineLocale", moduleExports, (args: [string], orig: (lcl: string) => string) => { - const origLocale = moduleExports.locale(); - orig(...args); - moduleExports.locale(origLocale); - }); - } + return Reflect.get(target, property, receiver); + }, + }); + } - const subs = moduleSubscriptions.get(Number(id)); - if (subs) { - subs.forEach(s => s()); - moduleSubscriptions.delete(Number(id)); + if (!patchedImportTracker && moduleExports.fileFinishedImporting) { + before("fileFinishedImporting", moduleExports, ([filePath]: [string]) => { + if (_importingModuleId === -1 || !filePath) return; + metroModules[_importingModuleId]!.__filePath = filePath; + }); + patchedImportTracker = true; + } + + // Funny infinity recursion caused by a race condition + if (!patchedInspectSource && window["__core-js_shared__"]) { + const inspect = (f: unknown) => + typeof f === "function" && functionToString.apply(f, []); + window["__core-js_shared__"].inspectSource = inspect; + patchedInspectSource = true; + } + + // + if (moduleExports.findHostInstance_DEPRECATED) { + const prevExports = metroModules[id - 1]?.publicModule.exports; + const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1; + if (!metroModules[id + inc]?.isInitialized) { + blacklistModule(id + inc); } + } + + // Hindi timestamps moment + if (moduleExports.isMoment) { + instead( + "defineLocale", + moduleExports, + (args: [string], orig: (lcl: string) => string) => { + const origLocale = moduleExports.locale(); + orig(...args); + moduleExports.locale(origLocale); + }, + ); + } + + const subs = moduleSubscriptions.get(Number(id)); + if (subs) { + subs.forEach((s) => s()); + moduleSubscriptions.delete(Number(id)); + } } export function getImportingModuleId() { - return _importingModuleId; + return _importingModuleId; } export function subscribeModule(id: number, cb: () => void): () => void { - const subs = moduleSubscriptions.get(id) ?? new Set(); + const subs = moduleSubscriptions.get(id) ?? new Set(); - subs.add(cb); - moduleSubscriptions.set(id, subs); + subs.add(cb); + moduleSubscriptions.set(id, subs); - return () => subs.delete(cb); + return () => subs.delete(cb); } export function requireModule(id: Metro.ModuleID) { - if (!metroModules[0]?.isInitialized) metroRequire(0); - if (blacklistedIds.has(id)) return undefined; + if (!metroModules[0]?.isInitialized) metroRequire(0); + if (blacklistedIds.has(id)) return undefined; - if (Number(id) === -1) return require("@metro/polyfills/redesign"); + if (Number(id) === -1) return require("@metro/polyfills/redesign"); - if (metroModules[id]?.isInitialized && !metroModules[id]?.hasError) { - return metroRequire(id); - } + if (metroModules[id]?.isInitialized && !metroModules[id]?.hasError) { + return metroRequire(id); + } - // Disable Internal RN error reporting logic - const originalHandler = ErrorUtils.getGlobalHandler(); - ErrorUtils.setGlobalHandler(noopHandler); + // Disable Internal RN error reporting logic + const originalHandler = ErrorUtils.getGlobalHandler(); + ErrorUtils.setGlobalHandler(noopHandler); - let moduleExports; - try { - moduleExports = metroRequire(id); - } catch { - blacklistModule(id); - moduleExports = undefined; - } + let moduleExports; + try { + moduleExports = metroRequire(id); + } catch { + blacklistModule(id); + moduleExports = undefined; + } - // Done initializing! Now, revert our hacks - ErrorUtils.setGlobalHandler(originalHandler); + // Done initializing! Now, revert our hacks + ErrorUtils.setGlobalHandler(originalHandler); - return moduleExports; + return moduleExports; } export function* getModules(uniq: string, all = false) { - yield [-1, require("@metro/polyfills/redesign")]; + yield [-1, require("@metro/polyfills/redesign")]; - let cache = getMetroCache().findIndex[uniq]; - if (all && !cache?.[`_${ModulesMapInternal.FULL_LOOKUP}`]) cache = undefined; - if (cache?.[`_${ModulesMapInternal.NOT_FOUND}`]) return; + let cache = getMetroCache().findIndex[uniq]; + if (all && !cache?.[`_${ModulesMapInternal.FULL_LOOKUP}`]) cache = undefined; + if (cache?.[`_${ModulesMapInternal.NOT_FOUND}`]) return; - for (const id in cache) { - if (id[0] === "_") continue; - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in cache) { + if (id[0] === "_") continue; + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } - for (const id in metroModules) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in metroModules) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } } export function* getCachedPolyfillModules(name: string) { - const cache = getMetroCache().polyfillIndex[name]!; + const cache = getMetroCache().polyfillIndex[name]!; - for (const id in cache) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in cache) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } - if (!cache[`_${ModulesMapInternal.FULL_LOOKUP}`]) { - for (const id in metroModules) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + if (!cache[`_${ModulesMapInternal.FULL_LOOKUP}`]) { + for (const id in metroModules) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; } + } }