feat(lib/modules): initial impl of modules caching
- Lookup caches have been fully implemented - Wait caches will not exist - Wait APIs should simply wait until a matching module is initialized. Caching is useless when the API is for this purpose. - Get APIs will inherit the benefits from using Lookup APIs
This commit is contained in:
parent
5171e2cfba
commit
981e9dc080
3 changed files with 182 additions and 57 deletions
52
lib/modules/src/caches.ts
Normal file
52
lib/modules/src/caches.ts
Normal file
|
|
@ -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<Cache>(`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<Metro.ModuleID, FilterResultFlag>
|
||||
| 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
|
||||
}
|
||||
|
|
@ -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<any, false>)(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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Metro.ModuleID>()
|
||||
|
||||
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<F extends Filter, O extends LookupModulesOptions>(
|
|||
): LookupModulesResult<F, O> | []
|
||||
|
||||
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<T = any>(
|
|||
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`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue