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)
This commit is contained in:
kmmiio99o 2025-10-25 12:55:57 +02:00
parent 9f6f62093d
commit 6745ca2514
8 changed files with 311 additions and 209 deletions

View file

@ -77,7 +77,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.1.6-1"`,
version: `"1.1.6-2"`,
},
},
},

View file

@ -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,

View file

@ -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",
<AlertModalComponent
title="Hold On!"
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
extraContent={
<Card>
<Text variant="text-md/bold">{addon.installUrl}</Text>
</Card>
}
actions={
<AlertActionsComponent>
<AlertActionButton
text="Continue"
variant="primary"
onPress={performInstall}
/>
<AlertActionButton text="Cancel" variant="secondary" />
</AlertActionsComponent>
}
/>,
);
} 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<typeof m> => 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={{

View file

@ -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.
// 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<void>((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(
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)
),
),
batch.map(async (id) => {
try {
if (!plugins[id]) return;
await this.startPlugin(id).catch((e) =>
logger.error(`Vendetta plugin ${id} failed to start:`, e),
);
// 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));
} 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();

View file

@ -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 () => {

View file

@ -33,6 +33,53 @@ export const apiObjects = new Map<
ReturnType<typeof createBunnyPluginApi>
>();
/**
* 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<t.PluginRepoStorage>(
"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

View file

@ -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";

View file

@ -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";
@ -30,18 +34,21 @@ for (const key in metroModules) {
}
if (metroModule!.factory) {
instead("factory", metroModule, ((args: Parameters<Metro.FactoryFn>, origFunc: Metro.FactoryFn) => {
instead("factory", metroModule, ((
args: Parameters<Metro.FactoryFn>,
origFunc: Metro.FactoryFn,
) => {
const originalImportingId = _importingModuleId;
_importingModuleId = id;
const { 1: metroRequire, 4: moduleObject } = args;
args[2 /* metroImportDefault */] = id => {
args[2 /* metroImportDefault */] = (id) => {
const exps = metroRequire(id);
return exps && exps.__esModule ? exps.default : exps;
};
args[3 /* metroImportAll */] = id => {
args[3 /* metroImportAll */] = (id) => {
const exps = metroRequire(id);
if (exps && exps.__esModule) return exps;
@ -71,11 +78,15 @@ function blacklistModule(id: number) {
}
function isBadExports(exports: any) {
return !exports
|| exports === window
|| exports["<!@ pylix was here :fuyusquish: \n Hi pylix! -cocobo1!@>"] === null
|| (exports.__proto__ === Object.prototype && Reflect.ownKeys(exports).length === 0)
|| (exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy");
return (
!exports ||
exports === window ||
exports["<!@ pylix was here :fuyusquish: \n Hi pylix! -cocobo1!@>"] ===
null ||
(exports.__proto__ === Object.prototype &&
Reflect.ownKeys(exports).length === 0) ||
exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy"
);
}
function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
@ -91,7 +102,15 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
}
// There are modules registering the same native component
if (!patchedNativeComponentRegistry && ["customBubblingEventTypes", "customDirectEventTypes", "register", "get"].every(x => moduleExports[x])) {
if (
!patchedNativeComponentRegistry &&
[
"customBubblingEventTypes",
"customDirectEventTypes",
"register",
"get",
].every((x) => moduleExports[x])
) {
instead("register", moduleExports, ([name, cb]: any, origFunc: any) => {
try {
return origFunc(name, cb);
@ -134,6 +153,35 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
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);
}
return Reflect.get(target, property, receiver);
},
});
}
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;
@ -145,16 +193,20 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
// Hindi timestamps moment
if (moduleExports.isMoment) {
instead("defineLocale", moduleExports, (args: [string], orig: (lcl: string) => string) => {
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());
subs.forEach((s) => s());
moduleSubscriptions.delete(Number(id));
}
}