diff --git a/lib/discord/package.json b/lib/discord/package.json index 059a77b..0b1c1a3 100644 --- a/lib/discord/package.json +++ b/lib/discord/package.json @@ -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" } } -} +} \ No newline at end of file diff --git a/lib/discord/src/common/flux.ts b/lib/discord/src/common/flux.ts index 5b98420..56a83c5 100644 --- a/lib/discord/src/common/flux.ts +++ b/lib/discord/src/common/flux.ts @@ -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 = {} - -/** - * 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( - name: string, - callback: (store: DiscordModules.Flux.Store) => void, -) { - const store = _stores[name] - if (store) { - callback(store as DiscordModules.Flux.Store) - return noop - } - - return waitForModules(byStoreName(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< - () => Filter, 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< - (name: string) => Filter, 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); - }) -}) diff --git a/lib/discord/src/flux.ts b/lib/discord/src/flux/dispatcher.ts similarity index 67% rename from lib/discord/src/flux.ts rename to lib/discord/src/flux/dispatcher.ts index ca7f33e..b840048 100644 --- a/lib/discord/src/flux.ts +++ b/lib/discord/src/flux/dispatcher.ts @@ -1,38 +1,10 @@ -import { Dispatcher } from './common/flux' -import type { DiscordModules } from './types' - -const fPatchesAll: Set> = new Set() -const fPatches: Map>> = new Map() +import { fPatches, fPatchesAll } from '../patches/flux' +import type { DiscordModules } from '../types' export type FluxEventDispatchPatch = ( 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. * diff --git a/lib/discord/src/flux/index.ts b/lib/discord/src/flux/index.ts new file mode 100644 index 0000000..0d7e25e --- /dev/null +++ b/lib/discord/src/flux/index.ts @@ -0,0 +1,2 @@ +export * from './dispatcher' +export * from './stores' diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts new file mode 100644 index 0000000..99ad0bb --- /dev/null +++ b/lib/discord/src/flux/stores.ts @@ -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 = {} + +/** + * 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( + name: string, + callback: (store: DiscordModules.Flux.Store) => void, +) { + const store = _stores[name] + if (store) { + callback(store as DiscordModules.Flux.Store) + return noop + } + + return waitForModules(byStoreName(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< + () => Filter, 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< + (name: string) => Filter, 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); + }) +}) diff --git a/lib/discord/src/patches/flux.ts b/lib/discord/src/patches/flux.ts new file mode 100644 index 0000000..b32693b --- /dev/null +++ b/lib/discord/src/patches/flux.ts @@ -0,0 +1,30 @@ +import { Dispatcher } from '../common/flux' +import type { FluxEventDispatchPatch } from '../flux/dispatcher' + +export const fPatchesAll: Set> = new Set() +export const fPatches: Map>> = 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() +} diff --git a/lib/discord/src/preinit.ts b/lib/discord/src/preinit.ts index 70d2d82..4257104 100644 --- a/lib/discord/src/preinit.ts +++ b/lib/discord/src/preinit.ts @@ -21,3 +21,5 @@ export const [AppStartPerformance] = lookupModule( ) as [DiscordModules.AppStartPerformance, Metro.ModuleID] AppStartPerformance.mark('👊', 'Pre-init') + +import './patches/flux' diff --git a/lib/discord/src/types/revenge.ts b/lib/discord/src/types/revenge.ts index 9ec20ae..53cc80f 100644 --- a/lib/discord/src/types/revenge.ts +++ b/lib/discord/src/types/revenge.ts @@ -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') } } diff --git a/src/plugins/init/api.discord/index.ts b/src/plugins/init/api.discord/index.ts index d8ac69e..37b8fd8 100644 --- a/src/plugins/init/api.discord/index.ts +++ b/src/plugins/init/api.discord/index.ts @@ -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, diff --git a/src/plugins/start/staff-settings/index.ts b/src/plugins/start/staff-settings/index.ts index 032677d..09516d1 100644 --- a/src/plugins/start/staff-settings/index.ts +++ b/src/plugins/start/staff-settings/index.ts @@ -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'