feat(caching): cache metro finds, persists across reboots
This commit is contained in:
parent
2e3dc3c487
commit
e4df998bb3
9 changed files with 183 additions and 57 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
14
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
19
src/entry.ts
19
src/entry.ts
|
|
@ -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
67
src/lib/metro/caches.ts
Normal 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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
20
src/lib/utils/throttle.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue