From dba76a9f81d921dbdb760021e2f85c6fce78ba76 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Tue, 19 Aug 2025 22:05:24 +0700 Subject: [PATCH] feat(lib): proper caching for `assets` and `modules` lib --- lib/assets/src/caches.ts | 63 ++++++++++++----------- lib/assets/src/index.ts | 8 +-- lib/assets/src/preinit.ts | 28 +++++------ lib/discord/src/flux/stores.ts | 18 +++---- lib/modules/package.json | 1 - lib/modules/src/caches.ts | 84 ++++++++++++++++++------------- lib/modules/src/finders/lookup.ts | 6 +-- lib/modules/src/finders/wait.ts | 6 +-- lib/modules/src/metro/patches.ts | 13 ++--- 9 files changed, 112 insertions(+), 115 deletions(-) diff --git a/lib/assets/src/caches.ts b/lib/assets/src/caches.ts index eedcb75..926e693 100644 --- a/lib/assets/src/caches.ts +++ b/lib/assets/src/caches.ts @@ -1,51 +1,50 @@ -// TODO(lib/assets): do not depend on Discord's ClientInfoModule for versioning, use native interop -import { ClientInfoModule } from '@revenge-mod/discord/native' -import { getStorage } from '@revenge-mod/storage' +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' import { debounce } from '@revenge-mod/utils/callback' -import { mergeDeep } from '@revenge-mod/utils/object' import type { Metro } from '@revenge-mod/modules/types' import type { Asset } from './types' -const Version = 1 -const Key = `${Version}.${ClientInfoModule.getConstants().Build}` +const ExpectedCacheVersion = 2 + +export const Uncached: Cache = { + data: {}, + version: ExpectedCacheVersion, +} // In-memory cache -export const cache: Cache = {} +export let cache: Cache = + callBridgeMethodSync('revenge.caches.assets.read', []) ?? Uncached -const CacheStorage = getStorage(`revenge/assets.${Key}`, { - default: cache, - directory: 'cache', -}) - -// TODO(lib/assets/caches): This loads way too late and requires native interop to load earlier -export const cached = CacheStorage.get().then(cache_ => { - const cached = cache_ !== cache - // If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache. - if (cached) { - Object.assign(cache, cache_) - CacheStorage.cache = cache - } - - return cached -}) +if (cache.version !== ExpectedCacheVersion) { + // TODO: Alert to user to update build + cache = Uncached +} export interface Cache { - [key: Asset['name']]: { - [key: Asset['type']]: Metro.ModuleID + data: { + [key: Asset['name']]: { + [key: Asset['type']]: Metro.ModuleID + } } + version: number } const save = debounce(() => { - CacheStorage.set({}) + callBridgeMethod('revenge.caches.assets.write', [cache.data]) }, 1000) export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) { - // Merge directly into the cache, only debouncing actual writes - mergeDeep(cache, { - [asset.name]: { - [asset.type]: moduleId, - }, - }) + const reg = (cache.data[asset.name] ??= {}) + reg[asset.type] = moduleId save() } + +declare module '@revenge-mod/modules/native' { + interface Methods { + 'revenge.caches.assets.read': [[], Cache | null] + 'revenge.caches.assets.write': [[data: Cache['data']], void] + } +} diff --git a/lib/assets/src/index.ts b/lib/assets/src/index.ts index a4b9adb..4f0bef7 100644 --- a/lib/assets/src/index.ts +++ b/lib/assets/src/index.ts @@ -48,7 +48,7 @@ export function* getCustomAssets(): Generator { * Yields all registered packager assets, including ones with same name but different types. */ export function* getPackagerAssets(): Generator { - for (const reg of Object.values(cache)) + for (const reg of Object.values(cache.data)) for (const moduleId of Object.values(reg)) yield AssetsRegistry.getAssetByID(metroRequire(moduleId)) } @@ -77,7 +77,7 @@ export function getAssetByName( export function getAssetsByName( name: string, ): Record | undefined { - const reg = cache[name] + const reg = cache.data[name] if (!reg) return return Object.entries(reg).reduce( @@ -106,7 +106,7 @@ export function getAssetIdByName( name: string, type?: Asset['type'], ): AssetId | undefined { - const reg = cache[name] + const reg = cache.data[name] if (!reg) return if (type !== undefined) { @@ -127,7 +127,7 @@ export function getAssetIdByName( * @returns The asset ID. */ export function registerAsset(asset: RegisterableAsset): AssetId { - if (cache[asset.name]?.[asset.type] !== undefined) + if (cache.data[asset.name]?.[asset.type] !== undefined) throw new Error( `Asset with name ${asset.name} and type ${asset.type} already exists!`, ) diff --git a/lib/assets/src/preinit.ts b/lib/assets/src/preinit.ts index 21b4ec0..8d2e585 100644 --- a/lib/assets/src/preinit.ts +++ b/lib/assets/src/preinit.ts @@ -15,7 +15,7 @@ import { import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { proxify } from '@revenge-mod/utils/proxy' import { aOverrides } from './_internal' -import { cacheAsset, cached } from './caches' +import { cache, cacheAsset, Uncached } from './caches' import type { Metro } from '@revenge-mod/modules/types' import type { ReactNative } from '@revenge-mod/react/types' import type { Asset, PackagerAsset } from './types' @@ -52,24 +52,20 @@ const unsubAR = waitForModules( if (getModuleDependencies(id)!.length) { unsubAR() - // TODO(lib/assets/caches): Ideally we should not wait for the cache promise here, see the promise impl for more info. - cached.then(cached => { - if (!cached) { - // Resolve reference once and keep in closure - const metroRequire = __r + if (cache === Uncached) { + // Resolve reference once and keep in closure + const metroRequire = __r - // More fragile way, but also more performant: - // There is exactly one asset before the reexported asset registry :/ - const firstAssetModuleId = id - 1 - for (const mId of mUninitialized) { - if (mId < firstAssetModuleId) continue + // More fragile way, but also more performant: + // There is exactly one asset before the reexported asset registry :/ + const firstAssetModuleId = id - 1 + for (const mId of mUninitialized) { + if (mId < firstAssetModuleId) continue - const deps = getModuleDependencies(mId)! - if (deps.length === 1 && deps[0] === id) - metroRequire(mId) - } + const deps = getModuleDependencies(mId)! + if (deps.length === 1 && deps[0] === id) metroRequire(mId) } - }) + } // We already patched the original asset registry, so we don't need to patch again. return diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts index 9b89d07..3bbd727 100644 --- a/lib/discord/src/flux/stores.ts +++ b/lib/discord/src/flux/stores.ts @@ -9,7 +9,7 @@ import { } from '@revenge-mod/modules/finders/filters' import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { asap, noop } from '@revenge-mod/utils/callback' -import { cached, cacheFilterResultForId } from '#modules/src/caches' +import { cache, cacheFilterResultForId, Uncached } from '#modules/src/caches' import { FilterResultFlags } from '#modules/src/finders/_internal' import { FilterFlag } from '#modules/src/finders/filters/constants' import type { @@ -140,13 +140,11 @@ waitForModules(withStore(), (store, id) => { Stores[name] = store }) -cached.then(cached => { - if (!cached) - asap(() => { - const lookup = lookupModules(withStore(), { - uninitialized: true, - }) - - while (lookup.next().done); +if (cache === Uncached) + asap(() => { + const lookup = lookupModules(withStore(), { + uninitialized: true, }) -}) + + while (lookup.next().done); + }) diff --git a/lib/modules/package.json b/lib/modules/package.json index b36edcf..204575c 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -21,7 +21,6 @@ "./native": { "default": "./src/native/index.ts" }, - }, "./types": { "types": "./src/types.ts" } diff --git a/lib/modules/src/caches.ts b/lib/modules/src/caches.ts index 84d2625..314db95 100644 --- a/lib/modules/src/caches.ts +++ b/lib/modules/src/caches.ts @@ -1,59 +1,61 @@ -// TODO(lib/modules/caches): do not depend on Discord's ClientInfoModule for versioning, use native interop -import { ClientInfoModule } from '@revenge-mod/discord/native' -import { getStorage } from '@revenge-mod/storage' +import { + callBridgeMethod, + callBridgeMethodSync, +} from '@revenge-mod/modules/native' import { debounce } from '@revenge-mod/utils/callback' import type { Metro } from '@revenge-mod/modules/types' import type { FilterResultFlag } from './finders/_internal' import type { Filter } from './finders/filters' -const Version = 2 -const Key = `${Version}.${ClientInfoModule.getConstants().Build}` +const ExpectedCacheVersion = 3 + +export const Uncached: Cache = { + blacklist: [], + finds: {}, + version: ExpectedCacheVersion, +} // In-memory cache -const cache: Cache = { b: [], f: {} } +export let cache: Cache = + callBridgeMethodSync('revenge.caches.modules.read', []) ?? Uncached -const CacheStorage = getStorage(`revenge/modules.${Key}`, { - default: cache, - directory: 'cache', -}) +if (cache.version !== ExpectedCacheVersion) { + // TODO: Alert to user to update build + cache = Uncached +} -// TODO(lib/modules/caches): This loads way too late and requires native interop to load earlier -export const cached = CacheStorage.get().then(cache_ => { - const cached = cache_ !== cache - // If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache. - if (cached) { - Object.assign(cache, cache_) - CacheStorage.cache = cache - } - - return cached -}) +export type Blacklist = Metro.ModuleID[] +export type Finds = Record< + Filter['key'], + Record | null +> export interface Cache { - b: Metro.ModuleID[] - f: Record< - Filter['key'], - Record | null | undefined - > + blacklist: Blacklist + finds: Finds + version: number } const save = debounce(() => { - CacheStorage.set({}) + callBridgeMethod('revenge.caches.modules.write', [ + cache.blacklist, + cache.finds, + ]) }, 1000) export function cacheBlacklistedModule(id: Metro.ModuleID): boolean { - cache.b.push(id) + cache.blacklist.push(id) save() return true } export function cacheFilterResultForId( - key: Filter['key'], + key: keyof Finds, id: Metro.ModuleID, flag: FilterResultFlag, ): FilterResultFlag { - const reg = (cache.f[key] ??= {}) + const reg = (cache.finds[key] ??= {}) reg[id] = flag save() @@ -61,12 +63,22 @@ export function cacheFilterResultForId( return flag } -export function cacheFilterNotFound(key: Filter['key']) { - cache.f[key] = null +export function cacheFilterNotFound(key: keyof Finds) { + cache.finds[key] = null } -export const getCachedFilterRegistry = ( - key: Filter['key'], -): Cache['f'][Filter['key']] | undefined => cache.f[key] +export const getFilterMatches = ( + key: keyof Finds, +): Finds[keyof Finds] | undefined => cache.finds[key] -export const getCachedBlacklistedModules = () => cache.b +export const getBlacklistedModules = () => cache.blacklist + +declare module '@revenge-mod/modules/native' { + interface Methods { + 'revenge.caches.modules.read': [[], Cache | null] + 'revenge.caches.modules.write': [ + [blacklist: Blacklist, finds: Finds], + void, + ] + } +} diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 34938b3..11ba5a7 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -1,6 +1,6 @@ import { getCurrentStack } from '@revenge-mod/utils/error' import { proxify } from '@revenge-mod/utils/proxy' -import { cacheFilterNotFound, getCachedFilterRegistry } from '../caches' +import { cacheFilterNotFound, getFilterMatches } from '../caches' import { metroRequire } from '../metro/custom' import { mImportedPaths, @@ -163,7 +163,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { if (options?.cached ?? true) { const notInit = !(options?.initialize ?? true) - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) // Return early if previous lookup was a full lookup and no modules were found if (reg === null) return @@ -307,7 +307,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { if (options?.cached ?? true) { const notInit = !(options?.initialize ?? true) - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) // Return early if previous lookup was a full lookup and no modules were found if (reg === null) return NotFoundResult diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index ca072a1..9b675e2 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -1,5 +1,5 @@ import { noop } from '@revenge-mod/utils/callback' -import { getCachedFilterRegistry } from '../caches' +import { getFilterMatches } from '../caches' import { mInitialized } from '../metro/patches' import { onAnyModuleInitialized, @@ -97,13 +97,13 @@ export function waitForModules( options?: WaitForModulesOptions, ): WaitForModulesUnsubscribeFunction { if (options?.cached) { - const reg = getCachedFilterRegistry(filter.key) + const reg = getFilterMatches(filter.key) if (reg === null) return noop if (reg) { if (__BUILD_FLAG_DEBUG_MODULE_WAITS__) nativeLoggingHook( - `\u001b[32mUsing cached results for wait \u001b[33m${filter.key}\u001b[0m`, + `\u001b[32mUsing cached results for wait: \u001b[33m${filter.key}\u001b[0m`, 1, ) diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index 29bd422..bcc3cac 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -1,8 +1,4 @@ -import { - cacheBlacklistedModule, - cached, - getCachedBlacklistedModules, -} from '../caches' +import { cache, cacheBlacklistedModule, Uncached } from '../caches' import { global, metroImportAll, @@ -144,11 +140,8 @@ function handleFactoryCall( /// MODULE PATCHES AND BLACKLISTS // Restore blacklists -cached.then(cached => { - if (cached) - for (const id of getCachedBlacklistedModules()) - mUninitialized.delete(id) -}) +if (cache !== Uncached) + for (const id of cache.blacklist) mUninitialized.delete(id) const ImportTrackerModuleId = 2