feat(caching): cache metro finds, persists across reboots

This commit is contained in:
amsyarasyiq 2024-05-10 00:20:37 +08:00
parent 2e3dc3c487
commit e4df998bb3
9 changed files with 183 additions and 57 deletions

View file

@ -1,5 +1,6 @@
/* eslint-disable no-restricted-syntax */
// eslint-disable-next-line simple-import-sort/imports
// todo: rewrite aaaaaaaaa
import swc from "@swc/core";
import { execSync } from "child_process";
import esbuild from "esbuild";
@ -122,7 +123,7 @@ try {
}
}
],
external: ["commit-hash"]
external: []
});
if (shouldWatch) {

View file

@ -28,6 +28,7 @@
"@types/node": "^20.11.25",
"@types/react": "~18.2.0",
"@types/react-native": "~0.72.0",
"@types/spark-md5": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@typescript-eslint/typescript-estree": "^7.1.1",
@ -45,6 +46,7 @@
},
"dependencies": {
"fuzzysort": "^2.0.4",
"spark-md5": "^3.0.2",
"spitroast": "^1.4.4"
},
"pnpm": {

14
pnpm-lock.yaml generated
View file

@ -8,6 +8,9 @@ dependencies:
fuzzysort:
specifier: ^2.0.4
version: 2.0.4
spark-md5:
specifier: ^3.0.2
version: 3.0.2
spitroast:
specifier: ^1.4.4
version: 1.4.4
@ -37,6 +40,9 @@ devDependencies:
'@types/react-native':
specifier: ~0.72.0
version: 0.72.3
'@types/spark-md5':
specifier: ^3.0.4
version: 3.0.4
'@typescript-eslint/eslint-plugin':
specifier: ^7.1.1
version: 7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.5)
@ -607,6 +613,10 @@ packages:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
dev: true
/@types/spark-md5@3.0.4:
resolution: {integrity: sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==}
dev: true
/@typescript-eslint/eslint-plugin@7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==}
engines: {node: ^16.0.0 || >=18.0.0}
@ -2159,6 +2169,10 @@ packages:
engines: {node: '>=8'}
dev: true
/spark-md5@3.0.2:
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
dev: false
/spitroast@1.4.4:
resolution: {integrity: sha512-S69rzIFjbGoEW5YCLpm1kEdqMEYqC09lmE1ZMY4gyUXfke0wi9qZjKYa0DDSv5eIy/D+/UGJT8yxgJddU3oqlA==}
dev: false

View file

@ -4,14 +4,13 @@ import { instead } from "spitroast";
// @ts-ignore - shut up fr
globalThis.window = globalThis;
const init = () => {
async function initializeBunny() {
try {
// This logs in the native logging implementation, e.g. logcat
console.log("Hello from Bunny! Pyon!");
// Make 'freeze' and 'seal' do nothing
Object.freeze = Object.seal = Object;
await require("@metro/caches").initMetroCache();
require(".").default();
} catch (e) {
const { ClientInfoManager } = require("@lib/api/native/modules");
@ -25,18 +24,18 @@ const init = () => {
stack || e?.toString?.(),
].join("\n"));
}
};
}
// @ts-ignore
if (typeof globalThis.__r !== "undefined") {
init();
initializeBunny();
} else {
// We hold calls from the native side
function onceIndexRequired(originalRequire: any) {
const batchedBridge = window.__fbBatchedBridge;
const callQueue = new Array<any>;
const unpatch = instead("callFunctionReturnFlushedQueue", batchedBridge, (args, orig) => {
const unpatchHook = instead("callFunctionReturnFlushedQueue", batchedBridge, (args, orig) => {
if (args[0] === "AppRegistry" || !batchedBridge.getCallableModule(args[0])) {
callQueue.push(args);
return batchedBridge.flushedQueue();
@ -45,9 +44,9 @@ if (typeof globalThis.__r !== "undefined") {
return orig.apply(batchedBridge, args);
});
const startDiscord = () => {
init();
unpatch();
const startDiscord = async () => {
await initializeBunny();
unpatchHook();
originalRequire(0);
callQueue.forEach(arg =>

67
src/lib/metro/caches.ts Normal file
View file

@ -0,0 +1,67 @@
import { ClientInfoManager, MMKVManager } from "@lib/api/native/modules";
import { throttle } from "@lib/utils/throttle";
import SparkMD5 from "spark-md5";
import { before } from "spitroast";
// fucked types right here
interface MetroCacheStore {
v: 1;
buildNumber: number;
findCache: Record<string, Record<number | "_", 1 | undefined> | undefined>;
polyfillCache: Record<string, Record<number, 1 | undefined> | undefined>;
}
const PYON_METRO_CACHE_KEY = "__bunny_metro_cache_key_v1__";
let metroCache = null as unknown as MetroCacheStore;
export function getMetroCache() {
return metroCache;
}
window.getMetroCache = getMetroCache;
export function getFuncUniqCall() {
const { stack } = new Error();
return SparkMD5.hash(stack!!);
}
function buildInitCache() {
return metroCache = {
v: 1,
buildNumber: ClientInfoManager.Build,
findCache: {},
polyfillCache: {}
} as const;
}
export async function initMetroCache() {
before("refresh", MMKVManager, ([a]) => void (a.push(PYON_METRO_CACHE_KEY)), true);
const rawCache = await MMKVManager.getItem(PYON_METRO_CACHE_KEY);
if (rawCache == null) return buildInitCache();
try {
metroCache = JSON.parse(rawCache);
if (metroCache.buildNumber !== ClientInfoManager.Build) {
throw "cache invalidated; version mismatch";
}
} catch {
buildInitCache();
}
}
const saveCache = throttle(() => {
MMKVManager.setItem(PYON_METRO_CACHE_KEY, JSON.stringify(metroCache));
});
export function registerModuleFindCacheId(uniqueId: string, moduleId: number, all: boolean) {
(metroCache.findCache[uniqueId] ??= { _: undefined })[moduleId] = 1;
if (all) metroCache.findCache[uniqueId]!._ ||= 1;
saveCache();
}
export function registerPolyfillCacheId(name: string, moduleId: number, remove = false) {
if (remove) delete metroCache.polyfillCache[name]?.[moduleId];
else (metroCache.polyfillCache[name] ??= {})[moduleId] = 1;
saveCache();
}

View file

@ -2,16 +2,24 @@ import { instead } from "@lib/api/patcher";
import type { Metro } from "@metro/types";
import { metroRequire, modules } from "@metro/utils";
import { getFuncUniqCall, getMetroCache, registerModuleFindCacheId } from "./caches";
export type PropIntellisense<P extends PropertyKey> = Record<P, any> & Record<PropertyKey, any>;
export type PropsFinder = <T extends PropertyKey>(...props: T[]) => PropIntellisense<T>;
export type PropsFinderAll = <T extends PropertyKey>(...props: T[]) => PropIntellisense<T>[];
metroRequire(0);
const blacklistedIds = new Set<String>();
/** Makes the module associated with the specified ID non-enumberable. */
function blacklistModule(id: Metro.ModuleID) {
function blacklistModule(id: string) {
Object.defineProperty(modules, id, { enumerable: false });
blacklistedIds.add(String(id));
blacklistedIds.add(id);
}
function isBadExports(exports: any) {
return !exports || exports === window || exports["<!@ pylix was here :fuyusquish: !@>"] === null;
}
const functionToString = Function.prototype.toString;
@ -39,20 +47,15 @@ for (const id in modules) {
};
origFunc(...args);
if (moduleObject.exports) onModuleRequire(moduleObject.exports, id);
if (!isBadExports(moduleObject.exports)) {
onModuleRequire(moduleObject.exports, id);
} else {
blacklistModule(id);
}
}) as any); // If only spitroast had better types
}
}
// Blacklist any "bad-actor" modules, e.g. the dreaded null proxy, the window itself, or undefined modules
for (const id in modules) {
if (blacklistedIds.has(id)) continue;
const moduleExports = requireModule(id);
if (!moduleExports || moduleExports === window || moduleExports["i think pyoncord eol tomorrow idk tho"] === null)
blacklistModule(id);
}
const noopHandler = () => undefined;
let patchedInspectSource = false;
@ -101,13 +104,26 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
const prevExports = modules[numberedId - 1]?.publicModule.exports;
const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1;
if (!modules[numberedId + inc]?.isInitialized) {
blacklistModule(numberedId + inc);
blacklistModule(String(numberedId + inc));
}
}
// Hindi timestamps moment
if (moduleExports.isMoment) {
instead("defineLocale", moduleExports, (args, orig) => {
const origLocale = moduleExports.locale();
orig(...args);
moduleExports.locale(origLocale);
});
}
}
function requireModule(id: Metro.ModuleID) {
if (modules[id]!.isInitialized) return metroRequire(id);
export function requireModule(id: Metro.ModuleID) {
if (blacklistedIds.has(String(id))) return undefined;
if (modules[id]?.isInitialized && !modules[id]?.hasError) {
return metroRequire(id);
}
// Disable Internal Metro error reporting logic
const originalHandler = ErrorUtils.getGlobalHandler();
@ -117,6 +133,7 @@ function requireModule(id: Metro.ModuleID) {
try {
moduleExports = metroRequire(id);
} catch {
blacklistModule(String(id));
moduleExports = undefined;
}
@ -126,11 +143,16 @@ function requireModule(id: Metro.ModuleID) {
return moduleExports;
}
function* getModuleExports() {
yield require("@metro/polyfills/redesign");
function* getModules(uniqueId: string, all: boolean) {
yield [-1, require("@metro/polyfills/redesign")];
for (const id in modules) {
yield requireModule(id);
let cache = getMetroCache().findCache[uniqueId];
if (all && !cache?._) cache = undefined;
for (const id in cache ?? modules) {
const exports = requireModule(id);
if (isBadExports(exports)) continue;
yield [id, exports];
}
}
@ -148,10 +170,14 @@ function testExports(moduleExports: any, filter: ExportsFilter) {
* @param filter find calls filter once for each enumerable module's exports until it finds one where filter returns a thruthy value.
*/
export function find(filter: ExportsFilter) {
for (const moduleExports of getModuleExports()) {
const uniqueId = getFuncUniqCall();
for (const [id, moduleExports] of getModules(uniqueId, false)) {
const testedExports = testExports(moduleExports, filter);
if (testedExports !== undefined)
if (testedExports !== undefined) {
if (id !== -1) registerModuleFindCacheId(uniqueId, Number(id), false);
return testedExports;
}
}
}
@ -161,10 +187,14 @@ export function find(filter: ExportsFilter) {
*/
export function findAll(filter: ExportsFilter) {
const foundExports: any[] = [];
for (const moduleExports of getModuleExports()) {
const uniqueId = getFuncUniqCall();
for (const [id, moduleExports] of getModules(uniqueId, true)) {
const testedExports = testExports(moduleExports, filter);
if (testedExports !== undefined)
if (testedExports !== undefined) {
if (id !== -1) registerModuleFindCacheId(uniqueId, Number(id), true);
foundExports.push(testedExports);
}
}
return foundExports;
}

View file

@ -1,16 +1,5 @@
export * from "./utils";
export {
find,
findAll,
findByDisplayName,
findByDisplayNameAll,
findByName,
findByNameAll,
findByProps,
findByPropsAll,
findByStoreName,
findByTypeName,
findByTypeNameAll
} from "./filters";
export * as filters from "./filters";
export * as cache from "./caches";
export * as common from "./common";
export * from "./filters";
export * as filters from "./filters";
export * from "./utils";

View file

@ -1,5 +1,5 @@
import { metroRequire, modules } from "@metro";
import { getMetroCache, registerPolyfillCacheId } from "@lib/metro/caches";
import { modules, requireModule } from "@metro";
import { LiteralUnion } from "type-fest";
const redesignProps = new Set([
@ -103,19 +103,23 @@ type Keys = LiteralUnion<typeof redesignProps extends Set<infer U> ? U : string,
const redesignPropSource = {} as Record<Keys, any>;
const redesignModule = {} as Record<Keys, any>;
for (const id in modules) {
const moduleExports = metroRequire(id);
polyfillRedesignModule(moduleExports);
const cache = getMetroCache().polyfillCache.redesign_module;
for (const id in cache ?? modules) {
const moduleExports = requireModule(id);
polyfillRedesignModule(moduleExports, Number(id));
}
function polyfillRedesignModule(moduleExports: any) {
function polyfillRedesignModule(moduleExports: any, id: number) {
const propMap = new Map<string, string | null>();
for (const prop of redesignProps) {
if (moduleExports?.[prop])
if (moduleExports?.[prop]) {
registerPolyfillCacheId("redesign_module", id);
propMap.set(prop, null);
else if (moduleExports?.default?.[prop])
} else if (moduleExports?.default?.[prop]) {
registerPolyfillCacheId("redesign_module", id);
propMap.set(prop, "default");
}
}
for (const [prop, defaultKey] of propMap) {

20
src/lib/utils/throttle.ts Normal file
View file

@ -0,0 +1,20 @@
/**
* Returns a function that ensures that only one call
* executes every given milliseconds.
* @param funct Function to throttle
* @param time Time to wait before allowing the next call
* @returns Throttled function with the same signature as `funct`
*/
export function throttle<T extends (...args: any[]) => any>(
funct: T,
time: number = 200
): T {
let active = false;
return function (this: ReturnType<T>, ...args: Parameters<T>) {
if (!active) {
active = true;
funct.apply(this, args);
setTimeout(() => { active = false; }, time);
}
} as T;
}