import { onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions' import { isModuleInitialized } from '@revenge-mod/modules/metro/utils' import { asap, noop } from '@revenge-mod/utils/callback' import { FilterScopes } from './filters' import { lookupModule, lookupModules } from './lookup' import { waitForModules } from './wait' import type { Metro } from '../types' import type { Filter, FilterResult } from './filters' import type { LookupModulesOptions } from './lookup' import type { WaitForModulesOptions, WaitForModulesResult } from './wait' export type GetModulesOptions = WaitForModulesOptions & LookupModulesOptions & { /** * The maximum number of modules to get. * * @default 1 */ max?: number } export type GetModulesResult< F extends Filter, O extends GetModulesOptions, > = WaitForModulesResult export type GetModulesCallback = (exports: T, id: Metro.ModuleID) => any export type GetModulesUnsubscribeFunction = () => void /** * Get modules matching the filter. * If the matching modules are already initialized, the callback will be called immediately. * Otherwise, it will be called when the matching modules are initialized. * * @param filter The filter to use to find the module. * @param options The options to use for the find. * @returns A function to unsubscribe. * * @example * ```ts * getModules(withProps('createElement'), React => { * // Immediately called because React is always initialized when plugins are loaded * }) * * getModules(withProps('FlashList'), FlashList => { * // Called when the module is initialized * }) * * // Get multiple modules matching the filter * getModules(withProps('registerAsset'), AssetsRegistry => { * // Called 2 times, once for each module that matches the filter * }, { max: 2 }) * ``` */ export function getModules( filter: F, callback: GetModulesCallback>, ): GetModulesUnsubscribeFunction export function getModules( filter: F, callback: GetModulesCallback>, options: O, ): GetModulesUnsubscribeFunction export function getModules( filter: Filter, callback: GetModulesCallback, options?: GetModulesOptions, ) { let max = options?.max ?? 1 // Only lookup modules that are already initialized const lookupFilter = filter.scope(FilterScopes.Initialized) function handleModule( exports: Metro.ModuleExports | undefined, id: Metro.ModuleID, ) { if (isModuleInitialized(id)) { // Run callback at the end of the event loop. This ensures that the noop is returned first. // Module is already initialized, there is likely no harm calling the callback late. asap(() => { callback(exports, id) }) } else { // Module is not initialized, wait for it to be initialized before calling the callback. onModuleInitialized(id, (_, exports) => { callback(exports, id) }) } } if (max === 1) { const [exports, id] = lookupModule(lookupFilter, options!) if (id !== undefined) { handleModule(exports, id) return noop } } else for (const [exports, id] of lookupModules(lookupFilter, options!)) { handleModule(exports, id) if (!--max) return noop } const unsub = waitForModules( filter, (exports, id) => { if (!--max) unsub() callback(exports, id) }, options!, ) return unsub }