feat(lib/modules/metro/subscriptions): add ability to subscribe when modules are first required

This commit is contained in:
PalmDevs 2025-04-15 15:02:44 +07:00
parent 466881c1a6
commit e6e46a175d
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
5 changed files with 107 additions and 57 deletions

View file

@ -3,12 +3,12 @@ import './preinit'
import './patches/prevent-immutablize'
import './patches/proxy'
import { onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions'
import { onModuleFirstRequired } from '@revenge-mod/modules/metro/subscriptions'
import { getErrorStack } from '@revenge-mod/utils/errors'
import { logger } from './shared'
onModuleInitialized(0, async function onceIndexModuleRequired() {
onModuleFirstRequired(0, async function onceIndexModuleRequired() {
if (!__BUILD_FLAG_INIT_DISABLE_PATCH_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections')
try {

View file

@ -1,15 +1,13 @@
import { _executeSubscription } from './subscriptions/_internal'
import { isBlacklisted, isModuleExportsBad } from './utils'
import { _executeInitializedSubscription, _executeRequiredSubscription } from './subscriptions/_internal'
import { isModuleExportsBad } from './utils'
import type { Metro } from '../../types/metro'
export const _bl = new Set<Metro.ModuleID>()
/** Initializing ID */
export let _mInitingId = -1
/** Uninitialized IDs */
/** Known uninitialized IDs (not blacklisted) */
export const _mUninited = new Set<Metro.ModuleID>()
/** Known initialized IDs (exports aren't bad) */
/** Known initialized IDs (not blacklisted) */
export const _mInited = new Set<Metro.ModuleID>()
export const _mPaths = new Map<string, Metro.ModuleID>()
@ -20,53 +18,51 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) {
// deps won't be undefined last time I checked
const metadata = [deps!] as typeof _mMd extends Map<any, infer V> ? V : never
_mMd.set(id, metadata)
_mUninited.add(id)
if (isBlacklisted(id)) metroDefine(origFactory, id, deps)
else {
_mUninited.add(id)
metroDefine(
((global, req, importDefault, importAll, module, exports, deps) => {
metadata[1] = module
metroDefine(
((global, req, importDefault, importAll, module, exports, deps) => {
metadata[1] = module
const prevIId = _mInitingId
_mInitingId = id
const prevIId = _mInitingId
_mInitingId = id
_executeRequiredSubscription()
try {
origFactory(global, req, importDefault, importAll, module, exports, deps)
// Don't use exports here, as modules can set module.exports to a different object
if (isModuleExportsBad(module.exports)) throw undefined
try {
origFactory(global, req, importDefault, importAll, module, exports, deps)
// Don't use exports here, as modules can set module.exports to a different object
if (isModuleExportsBad(module.exports)) throw undefined
// Add the module to the initialized set only if the factory doesn't error or the exports aren't bad
_mInited.add(id)
} catch {
_blacklist(id)
} finally {
_mInitingId = prevIId
_mUninited.delete(id)
// Don't use exports here, as modules can set module.exports to a different object
_executeSubscription(id, module.exports)
}
}) satisfies Metro.FactoryFn,
id,
deps,
)
}
// Add the module to the initialized set only if the factory doesn't error or the exports aren't bad
_mInited.add(id)
} catch {
_blacklist(id)
} finally {
_mInitingId = prevIId
_mUninited.delete(id)
// Don't use exports here, as modules can set module.exports to a different object
_executeInitializedSubscription(module.exports)
}
}) satisfies Metro.FactoryFn,
id,
deps,
)
}) satisfies Metro.DefineFn
}
/**
* Blacklists a module.
* Blacklists a module. A module can be blacklisted for several reasons:
*
* Blacklisting modules prevents Revenge from doing the following things:
*
* - Iterating over the module during `find()`s.
* - If the module is already initialized, it either means:
* - The module factory threw an error during initialization
* - The module exports are bad after initialization
* - Otherwise, this module is known to be bad from previous caches
*
* @param id The module ID to blacklist.
* @internal
*/
export function _blacklist(id: Metro.ModuleID) {
_bl.add(id)
// In case blacklisting happens before the module is initialized
_mUninited.delete(id)

View file

@ -14,7 +14,7 @@ const unsubForImportTracker = waitForModules(byProps('fileFinishedImporting'), (
exports.fileFinishedImporting = (path: string) => {
orig(path)
_mPaths.set(path, _mInitingId)
_executeImportedPathSubscription(_mInitingId, path)
_executeImportedPathSubscription(path)
}
}
})

View file

@ -1,15 +1,26 @@
import type { ModuleInitializedCallback, ModuleWithImportedPathInitializedCallback } from '.'
import { _mInitingId } from '../_internal'
import type { ModuleInitializedCallback, ModuleRequiredCallback, ModuleWithImportedPathInitializedCallback } from '.'
import type { Metro } from '../../../types/metro'
export const _all = new Set<ModuleInitializedCallback>()
export const _specific = new Map<Metro.ModuleID, Set<ModuleInitializedCallback>>()
export const _reqAll = new Set<ModuleRequiredCallback>()
export const _reqSpecific = new Map<Metro.ModuleID, Set<ModuleRequiredCallback>>()
export const _initAll = new Set<ModuleInitializedCallback>()
export const _initSpecific = new Map<Metro.ModuleID, Set<ModuleInitializedCallback>>()
export const _importedPath = new Set<ModuleWithImportedPathInitializedCallback>()
export function _executeSubscription(id: Metro.ModuleID, exports: Metro.ModuleExports) {
for (const cb of _all) cb(id, exports)
if (_specific.has(id)) for (const cb of _specific.get(id)!) cb(id, exports)
export function _executeRequiredSubscription() {
const id = _mInitingId
for (const cb of _reqAll) cb(id)
if (_reqSpecific.has(id)) for (const cb of _reqSpecific.get(id)!) cb(id)
}
export function _executeImportedPathSubscription(id: Metro.ModuleID, path: string) {
for (const cb of _importedPath) cb(id, path)
export function _executeInitializedSubscription(exports: Metro.ModuleExports) {
const id = _mInitingId
for (const cb of _initAll) cb(id, exports)
if (_initSpecific.has(id)) for (const cb of _initSpecific.get(id)!) cb(id, exports)
}
export function _executeImportedPathSubscription(path: string) {
for (const cb of _importedPath) cb(_mInitingId, path)
}

View file

@ -1,36 +1,79 @@
import type { Metro } from '../../../types/metro'
import { _all, _importedPath, _specific } from './_internal'
import { _initAll, _importedPath, _initSpecific, _reqSpecific, _reqAll } from './_internal'
import type { isInitializedModuleBlacklisted } from '../utils'
import type { Metro } from '../../../types/metro'
export type ModuleRequiredCallback = (id: Metro.ModuleID) => void
export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void
export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, path: string) => void
/**
* Registers a callback to be called when any module is initialized. **Some module exports may be bad.**
* Registers a callback to be called when any module is initialized.
*
* @see {@link isInitializedModuleBlacklisted} to avoid bad module exports.
*
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onAnyModuleInitialized(callback: ModuleInitializedCallback) {
_all.add(callback)
return () => _all.delete(callback)
_initAll.add(callback)
return () => _initAll.delete(callback)
}
/**
* Registers a callback to be called when a specific module is initialized. **Module exports may be bad.**
* Registers a callback to be called when a specific module is initialized.
*
* @see {@link isInitializedModuleBlacklisted} to avoid bad module exports.
*
* @param id The ID of the module.
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitializedCallback) {
if (!_specific.has(id)) _specific.set(id, new Set())
const set = _specific.get(id)!
if (!_initSpecific.has(id)) _initSpecific.set(id, new Set())
const set = _initSpecific.get(id)!
set.add(callback)
return () => set.delete(callback)
}
/**
* Registers a callback to be called when a module with a specific import path is initialized.
*
* @see {@link isInitializedModuleBlacklisted} to avoid bad module exports.
*
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleFinishedImporting(callback: ModuleWithImportedPathInitializedCallback) {
_importedPath.add(callback)
return () => _importedPath.delete(callback)
}
/**
* Registers a callback to be called when any module is required the first time.
*
* @see {@link isInitializedModuleBlacklisted} to avoid bad module exports.
*
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onAnyModuleFirstRequired(callback: ModuleRequiredCallback) {
_reqAll.add(id => callback(id))
return () => _reqAll.delete(id => callback(id))
}
/**
* Registers a callback to be called when a specific module is required the first time.
*
* @see {@link isInitializedModuleBlacklisted} to avoid bad module exports.
*
* @param id The ID of the module.
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleFirstRequired(id: Metro.ModuleID, callback: ModuleRequiredCallback) {
if (!_reqSpecific.has(id)) _reqSpecific.set(id, new Set())
const set = _reqSpecific.get(id)!
set.add(callback)
return () => set.delete(callback)
}