diff --git a/lib/modules/src/caches.ts b/lib/modules/src/caches.ts new file mode 100644 index 0000000..e627997 --- /dev/null +++ b/lib/modules/src/caches.ts @@ -0,0 +1,52 @@ +// 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 { debounce } from '@revenge-mod/utils/callbacks' +import type { Metro } from '@revenge-mod/modules/types' +import type { FilterResultFlag } from './finders/_internal' +import type { Filter } from './finders/filters' + +const Version = 1 +const Key = `${Version}.${ClientInfoModule.Build}` + +// In-memory cache +export const cache: Cache = {} + +const CacheStorage = getStorage(`revenge/modules.${Key}`, { + default: cache, + directory: 'cache', +}) + +// 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 interface Cache { + [key: Filter['key']]: + | Record + | null + | undefined +} + +const save = debounce(() => CacheStorage.set({}), 1000) + +export function cacheFilterResult( + key: Filter['key'], + id: Metro.ModuleID, + flag: FilterResultFlag, +): FilterResultFlag { + const reg = (cache[key] ??= {}) + reg[id] = flag + + save() + + return flag +} diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 369dc67..5678aa5 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -1,3 +1,5 @@ +import { getCurrentStack } from '@revenge-mod/utils/errors' +import { cacheFilterResult } from '../caches' import { isModuleExportBad } from '../metro/utils' import type { If } from '@revenge-mod/utils/types' import type { Metro } from '../types' @@ -73,27 +75,28 @@ export function runFilter( ): FilterResultFlag | undefined { if (exports === undefined) { if ((filter as Filter)(id)) { - // TODO(modules/finders/caches) - return FilterResultFlags.Found + if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) { + const flag = runFilter(filter, id, __r(id), options) + if (!flag) warnDeveloperAboutPartialFilterMatch(id, filter.key) + return flag + } else { + return runFilter(filter, id, __r(id), options) + } } return } - if (filter(id, exports)) { - // TODO(modules/finders/caches::namespace) - return FilterResultFlags.Namespace - } + if (filter(id, exports)) + return cacheFilterResult(filter.key, id, FilterResultFlags.Namespace) const defaultExport = exports.default if ( !options?.skipDefault && !isModuleExportBad(defaultExport) && filter(id, defaultExport) - ) { - // TODO(modules/finders/caches::default) - return FilterResultFlags.Default - } + ) + return cacheFilterResult(filter.key, id, FilterResultFlags.Default) } export function exportsFromFilterResultFlag( @@ -105,3 +108,15 @@ export function exportsFromFilterResultFlag( return exports.default return exports } + +/** + * Warns the developer that the filter matched during exportsless comparison, but not during the full comparison. + * This will cause modules to be initialized unnecessarily, and may cause issues. + * @internal + */ +function warnDeveloperAboutPartialFilterMatch(id: Metro.ModuleID, key: string) { + nativeLoggingHook( + `\u001b[33m${key} matched module ${id} partially.\n${getCurrentStack()}\u001b[0m`, + 2, + ) +} diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 54e34ec..ce28719 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -1,6 +1,11 @@ import { getCurrentStack } from '@revenge-mod/utils/errors' +import { proxify } from '@revenge-mod/utils/proxy' +import { cache } from '../caches' import { _inits, _paths, _uninits } from '../metro/_internal' -import { getInitializedModuleExports } from '../metro/utils' +import { + getInitializedModuleExports, + isModuleInitialized, +} from '../metro/utils' import { exportsFromFilterResultFlag, runFilter } from './_internal' import type { If } from '@revenge-mod/utils/types' import type { MaybeDefaultExportMatched, Metro } from '../types' @@ -29,6 +34,10 @@ export interface BaseLookupModulesOptions< * @default false */ includeUninitialized?: IncludeUninitialized + /** + * If the lookup is cached, whether to initialize the cached modules. + */ + initializeCached?: boolean } export type LookupModulesOptions< @@ -103,8 +112,37 @@ export function lookupModules< export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { let warn = true + const reg = cache[filter.key] + // Return early if previous lookup was a full lookup and no modules were found + if (reg === null) return + + const cached = new Set() + + if (reg) + for (const sId in reg) { + const flag = reg[sId] + const id = Number(sId) + let exports: Metro.ModuleExports | undefined + + if (!isModuleInitialized(id)) { + if (!(options?.initializeCached ?? true)) continue + exports = __r(id) + } + + yield [ + exportsFromFilterResultFlag( + flag, + (exports ??= getInitializedModuleExports(id)), + options, + ), + id, + ] + } + if (options?.includeInitialized ?? true) for (const id of _inits) { + if (cached.has(id)) continue + const exports = getInitializedModuleExports(id) const flag = runFilter(filter, id, exports, options) if (flag) { @@ -114,23 +152,27 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { } if (options?.includeUninitialized) - for (const id of _uninits) - if (runFilter(filter, id)) { - // Run the filter again to ensure we have the correct exports - const exports = __r(id) - const flag = runFilter(filter, id, exports, options) - if (flag) { - warn = false - yield [ - exportsFromFilterResultFlag(flag, exports, options), - id, - ] - } else if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) - warnDeveloperAboutPartialFilterMatch(id, filter.key) + for (const id of _uninits) { + if (cached.has(id)) continue + + const flag = runFilter(filter, id) + if (flag) { + warn = false + yield [ + exportsFromFilterResultFlag( + flag, + getInitializedModuleExports(id), + options, + ), + id, + ] } + } if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) if (warn) warnDeveloperAboutNoFilterMatch(filter) + + cache[filter.key] = null // Full lookup, and still not found! } /** @@ -157,6 +199,31 @@ export function lookupModule( ): LookupModulesResult | [] export function lookupModule(filter: Filter, options?: LookupModulesOptions) { + const reg = cache[filter.key] + // Return early if previous lookup was a full lookup and no modules were found + if (reg === null) return [] + + if (reg) + for (const sId in reg) { + const flag = reg[sId] + const id = Number(sId) + let exports: Metro.ModuleExports + + if (!isModuleInitialized(id)) { + if (!(options?.initializeCached ?? true)) continue + exports = __r(id) + } + + return [ + exportsFromFilterResultFlag( + flag, + (exports ??= getInitializedModuleExports(id)), + options, + ), + id, + ] + } + if (options?.includeInitialized ?? true) for (const id of _inits) { const exports = getInitializedModuleExports(id) @@ -166,23 +233,24 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { } if (options?.includeUninitialized) - for (const id of _uninits) - if (runFilter(filter, id)) { - const exports = __r(id) - // Run the filter again to ensure we have the correct exports - const flag = runFilter(filter, id, exports, options) - if (flag) - return [ - exportsFromFilterResultFlag(flag, exports, options), - id, - ] - else if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) - warnDeveloperAboutPartialFilterMatch(id, filter.key) - } + for (const id of _uninits) { + const flag = runFilter(filter, id) + if (flag) + return [ + exportsFromFilterResultFlag( + flag, + getInitializedModuleExports(id), + options, + ), + id, + ] + } if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) warnDeveloperAboutNoFilterMatch(filter) + cache[filter.key] = null // Full lookup, and still not found! + return [] } @@ -209,35 +277,25 @@ export function lookupModuleByImportedPath( return [getInitializedModuleExports(id), id] } -/** - * Warns the developer that the filter matched during exportsless comparison, but not during the full comparison. - * This will cause modules to be initialized unnecessarily, and may cause issues. - * @internal - */ -function warnDeveloperAboutPartialFilterMatch(id: Metro.ModuleID, key: string) { - nativeLoggingHook( - `\u001b[33m${key} matched module ${id} partially.\n${getCurrentStack()}\u001b[0m`, - 2, - ) -} - const __blacklistedFunctions = __BUILD_FLAG_DEBUG_MODULE_LOOKUPS__ - ? [ - import('./get').then(m => m.getModule), - import('@revenge-mod/utils/discord').then( - m => m.lookupGeneratedIconComponent, - ), - ] + ? proxify( + () => [ + require('./get').getModule, + require('@revenge-mod/utils/discord') + .lookupGeneratedIconComponent, + ], + { hint: [] }, + ) : [] /** * Warns the developer that no module was found for the given filter. * This is useful for debugging purposes, especially when using filters that are expected to match a module. */ -async function warnDeveloperAboutNoFilterMatch(filter: Filter) { +function warnDeveloperAboutNoFilterMatch(filter: Filter) { const stack = getCurrentStack() for (const func of __blacklistedFunctions) - if (stack.includes((await func).name)) return + if (stack.includes(func.name)) return nativeLoggingHook( `\u001b[31mNo module found for filter: ${filter.key}\n${stack}\u001b[0m`,