feat(lib/discord)!: flatten api structure

This commit is contained in:
PalmDevs 2025-08-11 17:19:26 +07:00
parent 3963db69b8
commit 7712b818f8
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
10 changed files with 223 additions and 206 deletions

View file

@ -19,7 +19,7 @@
"default": "./src/design.ts"
},
"./flux": {
"default": "./src/flux.ts"
"default": "./src/flux/index.ts"
},
"./native": {
"default": "./src/native.ts"
@ -46,4 +46,4 @@
"default": "./src/modules/settings/_internal.ts"
}
}
}
}

View file

@ -1,23 +1,9 @@
import {
byDependencies,
byName,
byProps,
createFilterGenerator,
preferExports,
} from '@revenge-mod/modules/finders/filters'
import {
lookupModule,
lookupModules,
} from '@revenge-mod/modules/finders/lookup'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { asap, noop } from '@revenge-mod/utils/callback'
import { cached, cacheFilterResultForId } from '../../../modules/src/caches'
import { FilterResultFlags } from '../../../modules/src/finders/_internal'
import type {
Filter,
FilterGenerator,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from '../types'
@ -41,132 +27,3 @@ export const [Dispatcher, DispatcherModuleId] = lookupModule(
uninitialized: true,
},
) as [DiscordModules.Flux.Dispatcher, Metro.ModuleID]
const _stores: Record<string, DiscordModules.Flux.Store> = {}
/**
* A proxy object that allows you to access Flux stores by their name, including uninitialized stores.
*
* Use `Reflect.ownKeys()` on this proxy to get a list of all initialized stores.
*
* @see {@link getStore} for a way to get stores lazily.
*/
export const Stores = new Proxy(_stores, {
ownKeys: target => Reflect.ownKeys(target),
get: (target, prop: string) =>
target[prop] ??
// @ts-expect-error: This always uses cache
lookupModule(byStoreName(prop), { uninitialized: true })[0],
})
/**
* Gets a Flux store by its name, and calls the provided callback with the store.
*
* @param name The name of the store to get.
* @param callback A callback that will be called with the store once it is found.
* @returns A function that can be used to cancel the wait for the store.
*/
export function getStore<T>(
name: string,
callback: (store: DiscordModules.Flux.Store<T>) => void,
) {
const store = _stores[name]
if (store) {
callback(store as DiscordModules.Flux.Store<T>)
return noop
}
return waitForModules(byStoreName<T>(name), callback, { cached: true })
}
/// FILTERING
/*
Flux Store dependencies: [
6, // _classCallCheck
5, // _createClass
14, // _possibleConstructorReturn
16, // bound getPrototypeOf
17, // _inherits
(...), // (any amount of dependencies)
2 // ImportTracker
]
*/
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
const [, _possibleConstructorReturnModuleId] = lookupModule(
byName('_possibleConstructorReturn'),
)
const [, _bound_getPrototypeOfModuleId] = lookupModule(
byName('bound getPrototypeOf'),
)
const [, _inheritsModuleId] = lookupModule(byName('_inherits'))
const FluxStoreLeadingDeps = [
_classCallCheckModuleId,
_createClassModuleId,
_possibleConstructorReturnModuleId,
_bound_getPrototypeOfModuleId,
_inheritsModuleId,
]
export type ByStore = FilterGenerator<
<T>() => Filter<DiscordModules.Flux.Store<T>, boolean>
>
/**
* A dynamic filter that matches all Flux stores.
*/
export const byStore = createFilterGenerator(
(_, id, exports) => {
if (exports) return Boolean(exports._dispatchToken)
else {
const deps = getModuleDependencies(id)!
if (deps.length < FluxStoreLeadingDeps.length) return false
for (let i = 0; i < FluxStoreLeadingDeps.length; i++)
if (deps[i] !== FluxStoreLeadingDeps[i]) return false
return deps[deps.length - 1] === 2
}
},
() => 'revenge.discord.byStore',
) as ByStore
export type ByStoreName = FilterGenerator<
<T>(name: string) => Filter<DiscordModules.Flux.Store<T>, true>
>
/**
* A with-exports filter that matches a Flux store by its name.
*/
export const byStoreName = createFilterGenerator(
([name], _, exports) =>
exports.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.byStoreName(${name})`,
) as ByStoreName
/// CACHING
waitForModules(byStore(), (store, id) => {
const name = store.getName()
// Cache stores
cacheFilterResultForId(
byStoreName.keyFor([name]),
id,
FilterResultFlags.Default,
)
Stores[name] = store
})
cached.then(cached => {
if (!cached)
asap(() => {
const lookup = lookupModules(byStore(), {
uninitialized: true,
})
while (lookup.next().done);
})
})

View file

@ -1,38 +1,10 @@
import { Dispatcher } from './common/flux'
import type { DiscordModules } from './types'
const fPatchesAll: Set<FluxEventDispatchPatch<any>> = new Set()
const fPatches: Map<string, Set<FluxEventDispatchPatch<any>>> = new Map()
import { fPatches, fPatchesAll } from '../patches/flux'
import type { DiscordModules } from '../types'
export type FluxEventDispatchPatch<T extends object = object> = (
payload: DiscordModules.Flux.DispatcherPayload & T,
) => (DiscordModules.Flux.DispatcherPayload & T) | undefined | void
const originalDispatch = Dispatcher.dispatch
Dispatcher.dispatch = payload => {
let res: typeof payload | undefined | void = payload
for (const patch of fPatchesAll)
try {
res = patch(res!)
if (!res) break
} catch {}
if (res) {
const specifics = fPatches.get(payload.type)
if (specifics?.size)
for (const patch of specifics)
try {
res = patch(res!)
if (!res) break
} catch {}
}
if (res) return Reflect.apply(originalDispatch, Dispatcher, [res])
// If res is undefined, the event is blocked
return Promise.resolve()
}
/**
* Registers a patch for all Flux events.
*

View file

@ -0,0 +1,2 @@
export * from './dispatcher'
export * from './stores'

View file

@ -0,0 +1,149 @@
import {
byName,
createFilterGenerator,
} from '@revenge-mod/modules/finders/filters'
import {
lookupModule,
lookupModules,
} from '@revenge-mod/modules/finders/lookup'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { asap, noop } from '@revenge-mod/utils/callback'
import { cached, cacheFilterResultForId } from '../../../modules/src/caches'
import { FilterResultFlags } from '../../../modules/src/finders/_internal'
import type {
Filter,
FilterGenerator,
} from '@revenge-mod/modules/finders/filters'
import type { DiscordModules } from '../types'
/// STORES
const _stores: Record<string, DiscordModules.Flux.Store> = {}
/**
* A proxy that allows you to access Flux stores by their name, including uninitialized stores.
*
* Use `Reflect.ownKeys()` on this proxy to get a list of all initialized stores.
*
* @see {@link getStore} for a way to get stores lazily.
*/
export const Stores = new Proxy(_stores, {
ownKeys: target => Reflect.ownKeys(target),
get: (target, prop: string) =>
target[prop] ??
// @ts-expect-error: This always uses cache
lookupModule(byStoreName(prop), { uninitialized: true })[0],
})
/**
* Gets a Flux store by its name, and calls the provided callback with the store.
*
* @param name The name of the store to get.
* @param callback A callback that will be called with the store once it is found.
* @returns A function that can be used to cancel the wait for the store.
*/
export function getStore<T>(
name: string,
callback: (store: DiscordModules.Flux.Store<T>) => void,
) {
const store = _stores[name]
if (store) {
callback(store as DiscordModules.Flux.Store<T>)
return noop
}
return waitForModules(byStoreName<T>(name), callback, { cached: true })
}
/// STORE FILTERING
/*
Flux Store dependencies: [
6, // _classCallCheck
5, // _createClass
14, // _possibleConstructorReturn
16, // bound getPrototypeOf
17, // _inherits
(...), // (any amount of dependencies)
2 // ImportTracker
]
*/
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
const [, _possibleConstructorReturnModuleId] = lookupModule(
byName('_possibleConstructorReturn'),
)
const [, _bound_getPrototypeOfModuleId] = lookupModule(
byName('bound getPrototypeOf'),
)
const [, _inheritsModuleId] = lookupModule(byName('_inherits'))
const FluxStoreLeadingDeps = [
_classCallCheckModuleId,
_createClassModuleId,
_possibleConstructorReturnModuleId,
_bound_getPrototypeOfModuleId,
_inheritsModuleId,
]
export type ByStore = FilterGenerator<
<T>() => Filter<DiscordModules.Flux.Store<T>, boolean>
>
/**
* A dynamic filter that matches all Flux stores.
*/
export const byStore = createFilterGenerator(
(_, id, exports) => {
if (exports) return Boolean(exports._dispatchToken)
else {
const deps = getModuleDependencies(id)!
if (deps.length < FluxStoreLeadingDeps.length) return false
for (let i = 0; i < FluxStoreLeadingDeps.length; i++)
if (deps[i] !== FluxStoreLeadingDeps[i]) return false
return deps[deps.length - 1] === 2
}
},
() => 'revenge.discord.byStore',
) as ByStore
export type ByStoreName = FilterGenerator<
<T>(name: string) => Filter<DiscordModules.Flux.Store<T>, true>
>
/**
* A with-exports filter that matches a Flux store by its name.
*/
export const byStoreName = createFilterGenerator(
([name], _, exports) =>
exports.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.byStoreName(${name})`,
) as ByStoreName
/// STORE CACHING
waitForModules(byStore(), (store, id) => {
const name = store.getName()
// Cache stores
cacheFilterResultForId(
byStoreName.keyFor([name]),
id,
FilterResultFlags.Default,
)
Stores[name] = store
})
cached.then(cached => {
if (!cached)
asap(() => {
const lookup = lookupModules(byStore(), {
uninitialized: true,
})
while (lookup.next().done);
})
})

View file

@ -0,0 +1,30 @@
import { Dispatcher } from '../common/flux'
import type { FluxEventDispatchPatch } from '../flux/dispatcher'
export const fPatchesAll: Set<FluxEventDispatchPatch<any>> = new Set()
export const fPatches: Map<string, Set<FluxEventDispatchPatch<any>>> = new Map()
const originalDispatch = Dispatcher.dispatch
Dispatcher.dispatch = payload => {
let res: typeof payload | undefined | void = payload
for (const patch of fPatchesAll)
try {
res = patch(res!)
if (!res) break
} catch {}
if (res) {
const specifics = fPatches.get(payload.type)
if (specifics?.size)
for (const patch of specifics)
try {
res = patch(res!)
if (!res) break
} catch {}
}
if (res) return Reflect.apply(originalDispatch, Dispatcher, [res])
// If res is undefined, the event is blocked
return Promise.resolve()
}

View file

@ -21,3 +21,5 @@ export const [AppStartPerformance] = lookupModule(
) as [DiscordModules.AppStartPerformance, Metro.ModuleID]
AppStartPerformance.mark('👊', 'Pre-init')
import './patches/flux'

View file

@ -18,9 +18,8 @@ export namespace PluginApiDiscord {
export interface Modules {
mainTabsV2: typeof import('@revenge-mod/discord/modules/main_tabs_v2')
settings: typeof import('@revenge-mod/discord/modules/settings') & {
renderer: typeof import('@revenge-mod/discord/modules/settings/renderer')
}
settings: typeof import('@revenge-mod/discord/modules/settings') &
typeof import('@revenge-mod/discord/modules/settings/renderer')
}
}

View file

@ -26,33 +26,39 @@ registerPlugin(
)
})
defineLazyProperties((unscoped.discord = {} as PluginApiDiscord), {
actions: () => {
return require('@revenge-mod/discord/actions')
},
common: () => {
return require('@revenge-mod/discord/common')
},
design: () => {
return require('@revenge-mod/discord/design')
},
flux: () => {
return require('@revenge-mod/discord/flux')
},
native: () => {
return require('@revenge-mod/discord/native')
},
modules: () =>
defineLazyProperties({} as PluginApiDiscord.Modules, {
mainTabsV2: () => {
return require('@revenge-mod/discord/modules/main_tabs_v2')
defineLazyProperties(
(unscoped.discord = {
modules: defineLazyProperties(
{} as PluginApiDiscord.Modules,
{
mainTabsV2: () => {
return require('@revenge-mod/discord/modules/main_tabs_v2')
},
settings: () => ({
...require('@revenge-mod/discord/modules/settings'),
...require('@revenge-mod/discord/modules/settings/renderer'),
}),
},
settings: () => ({
...require('@revenge-mod/discord/modules/settings'),
renderer: require('@revenge-mod/discord/modules/settings/renderer'),
}),
}),
})
),
} as PluginApiDiscord),
{
actions: () => {
return require('@revenge-mod/discord/actions')
},
common: () => {
return require('@revenge-mod/discord/common')
},
flux: () => {
return require('@revenge-mod/discord/flux')
},
design: () => {
return require('@revenge-mod/discord/design')
},
native: () => {
return require('@revenge-mod/discord/native')
},
},
)
},
},
PluginFlags.Enabled,

View file

@ -1,5 +1,5 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { getStore, Stores } from '@revenge-mod/discord/common/flux'
import { getStore, Stores } from '@revenge-mod/discord/flux'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { getModule } from '@revenge-mod/modules/finders/get'
import { instead } from '@revenge-mod/patcher'