feat: native bridge (#4)
* feat(lib/modules/native): types for calls * feat(lib): proper caching for `assets` and `modules` lib * fix(lib/modules/metro/runtime): add flag once `importedAll` or `importedDefault` exports cached * feat(lib/modules/native): add `fs` * feat(plugins/settings): add Loader info in Revenge page * feat: use native bridge for alerting errors * chore(lib/modules/native): remove useless `String()` call on error * feat(lib/modules/native/fs): rename `deleteFile` to `rm` * feat(lib/plugins): save plugin states * chore(preinit): correct arg name for native method
This commit is contained in:
commit
f89d523c0d
23 changed files with 317 additions and 171 deletions
|
|
@ -1,51 +1,50 @@
|
|||
// TODO(lib/assets): 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 {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import { mergeDeep } from '@revenge-mod/utils/object'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { Asset } from './types'
|
||||
|
||||
const Version = 1
|
||||
const Key = `${Version}.${ClientInfoModule.getConstants().Build}`
|
||||
const ExpectedCacheVersion = 2
|
||||
|
||||
export const Uncached: Cache = {
|
||||
data: {},
|
||||
version: ExpectedCacheVersion,
|
||||
}
|
||||
|
||||
// In-memory cache
|
||||
export const cache: Cache = {}
|
||||
export let cache: Cache =
|
||||
callBridgeMethodSync('revenge.caches.assets.read', []) ?? Uncached
|
||||
|
||||
const CacheStorage = getStorage<Cache>(`revenge/assets.${Key}`, {
|
||||
default: cache,
|
||||
directory: 'cache',
|
||||
})
|
||||
|
||||
// TODO(lib/assets/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
|
||||
})
|
||||
if (cache.version !== ExpectedCacheVersion) {
|
||||
// TODO: Alert to user to update build
|
||||
cache = Uncached
|
||||
}
|
||||
|
||||
export interface Cache {
|
||||
[key: Asset['name']]: {
|
||||
[key: Asset['type']]: Metro.ModuleID
|
||||
data: {
|
||||
[key: Asset['name']]: {
|
||||
[key: Asset['type']]: Metro.ModuleID
|
||||
}
|
||||
}
|
||||
version: number
|
||||
}
|
||||
|
||||
const save = debounce(() => {
|
||||
CacheStorage.set({})
|
||||
callBridgeMethod('revenge.caches.assets.write', [cache.data])
|
||||
}, 1000)
|
||||
|
||||
export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) {
|
||||
// Merge directly into the cache, only debouncing actual writes
|
||||
mergeDeep(cache, {
|
||||
[asset.name]: {
|
||||
[asset.type]: moduleId,
|
||||
},
|
||||
})
|
||||
const reg = (cache.data[asset.name] ??= {})
|
||||
reg[asset.type] = moduleId
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
'revenge.caches.assets.read': [[], Cache | null]
|
||||
'revenge.caches.assets.write': [[data: Cache['data']], void]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function* getCustomAssets(): Generator<CustomAsset> {
|
|||
* Yields all registered packager assets, including ones with same name but different types.
|
||||
*/
|
||||
export function* getPackagerAssets(): Generator<PackagerAsset> {
|
||||
for (const reg of Object.values(cache))
|
||||
for (const reg of Object.values(cache.data))
|
||||
for (const moduleId of Object.values(reg))
|
||||
yield AssetsRegistry.getAssetByID(metroRequire(moduleId))
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ export function getAssetByName(
|
|||
export function getAssetsByName(
|
||||
name: string,
|
||||
): Record<Asset['type'], Asset> | undefined {
|
||||
const reg = cache[name]
|
||||
const reg = cache.data[name]
|
||||
if (!reg) return
|
||||
|
||||
return Object.entries(reg).reduce(
|
||||
|
|
@ -106,7 +106,7 @@ export function getAssetIdByName(
|
|||
name: string,
|
||||
type?: Asset['type'],
|
||||
): AssetId | undefined {
|
||||
const reg = cache[name]
|
||||
const reg = cache.data[name]
|
||||
if (!reg) return
|
||||
|
||||
if (type !== undefined) {
|
||||
|
|
@ -127,7 +127,7 @@ export function getAssetIdByName(
|
|||
* @returns The asset ID.
|
||||
*/
|
||||
export function registerAsset(asset: RegisterableAsset): AssetId {
|
||||
if (cache[asset.name]?.[asset.type] !== undefined)
|
||||
if (cache.data[asset.name]?.[asset.type] !== undefined)
|
||||
throw new Error(
|
||||
`Asset with name ${asset.name} and type ${asset.type} already exists!`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
||||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
import { aOverrides } from './_internal'
|
||||
import { cacheAsset, cached } from './caches'
|
||||
import { cache, cacheAsset, Uncached } from './caches'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { ReactNative } from '@revenge-mod/react/types'
|
||||
import type { Asset, PackagerAsset } from './types'
|
||||
|
|
@ -52,24 +52,20 @@ const unsubAR = waitForModules(
|
|||
if (getModuleDependencies(id)!.length) {
|
||||
unsubAR()
|
||||
|
||||
// TODO(lib/assets/caches): Ideally we should not wait for the cache promise here, see the promise impl for more info.
|
||||
cached.then(cached => {
|
||||
if (!cached) {
|
||||
// Resolve reference once and keep in closure
|
||||
const metroRequire = __r
|
||||
if (cache === Uncached) {
|
||||
// Resolve reference once and keep in closure
|
||||
const metroRequire = __r
|
||||
|
||||
// More fragile way, but also more performant:
|
||||
// There is exactly one asset before the reexported asset registry :/
|
||||
const firstAssetModuleId = id - 1
|
||||
for (const mId of mUninitialized) {
|
||||
if (mId < firstAssetModuleId) continue
|
||||
// More fragile way, but also more performant:
|
||||
// There is exactly one asset before the reexported asset registry :/
|
||||
const firstAssetModuleId = id - 1
|
||||
for (const mId of mUninitialized) {
|
||||
if (mId < firstAssetModuleId) continue
|
||||
|
||||
const deps = getModuleDependencies(mId)!
|
||||
if (deps.length === 1 && deps[0] === id)
|
||||
metroRequire(mId)
|
||||
}
|
||||
const deps = getModuleDependencies(mId)!
|
||||
if (deps.length === 1 && deps[0] === id) metroRequire(mId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We already patched the original asset registry, so we don't need to patch again.
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@revenge-mod/modules/finders/filters'
|
||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
||||
import { asap, noop } from '@revenge-mod/utils/callback'
|
||||
import { cached, cacheFilterResultForId } from '#modules/src/caches'
|
||||
import { cache, cacheFilterResultForId, Uncached } from '#modules/src/caches'
|
||||
import { FilterResultFlags } from '#modules/src/finders/_internal'
|
||||
import { FilterFlag } from '#modules/src/finders/filters/constants'
|
||||
import type {
|
||||
|
|
@ -140,13 +140,11 @@ waitForModules(withStore(), (store, id) => {
|
|||
Stores[name] = store
|
||||
})
|
||||
|
||||
cached.then(cached => {
|
||||
if (!cached)
|
||||
asap(() => {
|
||||
const lookup = lookupModules(withStore(), {
|
||||
uninitialized: true,
|
||||
})
|
||||
|
||||
while (lookup.next().done);
|
||||
if (cache === Uncached)
|
||||
asap(() => {
|
||||
const lookup = lookupModules(withStore(), {
|
||||
uninitialized: true,
|
||||
})
|
||||
})
|
||||
|
||||
while (lookup.next().done);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@
|
|||
"default": "./src/metro/utils.ts"
|
||||
},
|
||||
"./native": {
|
||||
"default": "./src/native.ts"
|
||||
"default": "./src/native/index.ts"
|
||||
},
|
||||
"./native/fs": {
|
||||
"default": "./src/native/fs.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/types.ts"
|
||||
|
|
|
|||
|
|
@ -1,59 +1,61 @@
|
|||
// 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 {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { FilterResultFlag } from './finders/_internal'
|
||||
import type { Filter } from './finders/filters'
|
||||
|
||||
const Version = 2
|
||||
const Key = `${Version}.${ClientInfoModule.getConstants().Build}`
|
||||
const ExpectedCacheVersion = 3
|
||||
|
||||
export const Uncached: Cache = {
|
||||
blacklist: [],
|
||||
finds: {},
|
||||
version: ExpectedCacheVersion,
|
||||
}
|
||||
|
||||
// In-memory cache
|
||||
const cache: Cache = { b: [], f: {} }
|
||||
export let cache: Cache =
|
||||
callBridgeMethodSync('revenge.caches.modules.read', []) ?? Uncached
|
||||
|
||||
const CacheStorage = getStorage<Cache>(`revenge/modules.${Key}`, {
|
||||
default: cache,
|
||||
directory: 'cache',
|
||||
})
|
||||
if (cache.version !== ExpectedCacheVersion) {
|
||||
// TODO: Alert to user to update build
|
||||
cache = Uncached
|
||||
}
|
||||
|
||||
// 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 type Blacklist = Metro.ModuleID[]
|
||||
export type Finds = Record<
|
||||
Filter['key'],
|
||||
Record<Metro.ModuleID, FilterResultFlag> | null
|
||||
>
|
||||
|
||||
export interface Cache {
|
||||
b: Metro.ModuleID[]
|
||||
f: Record<
|
||||
Filter['key'],
|
||||
Record<Metro.ModuleID, FilterResultFlag> | null | undefined
|
||||
>
|
||||
blacklist: Blacklist
|
||||
finds: Finds
|
||||
version: number
|
||||
}
|
||||
|
||||
const save = debounce(() => {
|
||||
CacheStorage.set({})
|
||||
callBridgeMethod('revenge.caches.modules.write', [
|
||||
cache.blacklist,
|
||||
cache.finds,
|
||||
])
|
||||
}, 1000)
|
||||
|
||||
export function cacheBlacklistedModule(id: Metro.ModuleID): boolean {
|
||||
cache.b.push(id)
|
||||
cache.blacklist.push(id)
|
||||
save()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function cacheFilterResultForId(
|
||||
key: Filter['key'],
|
||||
key: keyof Finds,
|
||||
id: Metro.ModuleID,
|
||||
flag: FilterResultFlag,
|
||||
): FilterResultFlag {
|
||||
const reg = (cache.f[key] ??= {})
|
||||
const reg = (cache.finds[key] ??= {})
|
||||
reg[id] = flag
|
||||
|
||||
save()
|
||||
|
|
@ -61,12 +63,22 @@ export function cacheFilterResultForId(
|
|||
return flag
|
||||
}
|
||||
|
||||
export function cacheFilterNotFound(key: Filter['key']) {
|
||||
cache.f[key] = null
|
||||
export function cacheFilterNotFound(key: keyof Finds) {
|
||||
cache.finds[key] = null
|
||||
}
|
||||
|
||||
export const getCachedFilterRegistry = (
|
||||
key: Filter['key'],
|
||||
): Cache['f'][Filter['key']] | undefined => cache.f[key]
|
||||
export const getFilterMatches = (
|
||||
key: keyof Finds,
|
||||
): Finds[keyof Finds] | undefined => cache.finds[key]
|
||||
|
||||
export const getCachedBlacklistedModules = () => cache.b
|
||||
export const getBlacklistedModules = () => cache.blacklist
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
'revenge.caches.modules.read': [[], Cache | null]
|
||||
'revenge.caches.modules.write': [
|
||||
[blacklist: Blacklist, finds: Finds],
|
||||
void,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
import { cacheFilterResultForId } from '../caches'
|
||||
import { metroRequire } from '../metro/custom'
|
||||
import { mInitialized } from '../metro/patches'
|
||||
import { metroRequire } from '../metro/runtime'
|
||||
import { isModuleExportBad } from '../metro/utils'
|
||||
import { FilterFlag } from './filters'
|
||||
import type { If } from '@revenge-mod/utils/types'
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
import { cacheFilterNotFound, getCachedFilterRegistry } from '../caches'
|
||||
import { metroRequire } from '../metro/custom'
|
||||
import { cacheFilterNotFound, getFilterMatches } from '../caches'
|
||||
import {
|
||||
mImportedPaths,
|
||||
mInitialized,
|
||||
mList,
|
||||
mUninitialized,
|
||||
} from '../metro/patches'
|
||||
import { metroRequire } from '../metro/runtime'
|
||||
import {
|
||||
getInitializedModuleExports,
|
||||
isModuleInitialized,
|
||||
|
|
@ -163,7 +163,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
|
|||
if (options?.cached ?? true) {
|
||||
const notInit = !(options?.initialize ?? true)
|
||||
|
||||
const reg = getCachedFilterRegistry(filter.key)
|
||||
const reg = getFilterMatches(filter.key)
|
||||
// Return early if previous lookup was a full lookup and no modules were found
|
||||
if (reg === null) return
|
||||
|
||||
|
|
@ -307,7 +307,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
|
|||
if (options?.cached ?? true) {
|
||||
const notInit = !(options?.initialize ?? true)
|
||||
|
||||
const reg = getCachedFilterRegistry(filter.key)
|
||||
const reg = getFilterMatches(filter.key)
|
||||
// Return early if previous lookup was a full lookup and no modules were found
|
||||
if (reg === null) return NotFoundResult
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { noop } from '@revenge-mod/utils/callback'
|
||||
import { getCachedFilterRegistry } from '../caches'
|
||||
import { getFilterMatches } from '../caches'
|
||||
import { mInitialized } from '../metro/patches'
|
||||
import {
|
||||
onAnyModuleInitialized,
|
||||
|
|
@ -97,13 +97,13 @@ export function waitForModules(
|
|||
options?: WaitForModulesOptions,
|
||||
): WaitForModulesUnsubscribeFunction {
|
||||
if (options?.cached) {
|
||||
const reg = getCachedFilterRegistry(filter.key)
|
||||
const reg = getFilterMatches(filter.key)
|
||||
if (reg === null) return noop
|
||||
|
||||
if (reg) {
|
||||
if (__BUILD_FLAG_DEBUG_MODULE_WAITS__)
|
||||
nativeLoggingHook(
|
||||
`\u001b[32mUsing cached results for wait \u001b[33m${filter.key}\u001b[0m`,
|
||||
`\u001b[32mUsing cached results for wait: \u001b[33m${filter.key}\u001b[0m`,
|
||||
1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import {
|
||||
cacheBlacklistedModule,
|
||||
cached,
|
||||
getCachedBlacklistedModules,
|
||||
} from '../caches'
|
||||
import { cache, cacheBlacklistedModule, Uncached } from '../caches'
|
||||
import {
|
||||
global,
|
||||
metroImportAll,
|
||||
metroImportDefault,
|
||||
metroRequire,
|
||||
} from './custom'
|
||||
} from './runtime'
|
||||
import { onModuleInitialized } from './subscriptions'
|
||||
import {
|
||||
executeImportedPathSubscriptions,
|
||||
|
|
@ -144,11 +140,8 @@ function handleFactoryCall(
|
|||
/// MODULE PATCHES AND BLACKLISTS
|
||||
|
||||
// Restore blacklists
|
||||
cached.then(cached => {
|
||||
if (cached)
|
||||
for (const id of getCachedBlacklistedModules())
|
||||
mUninitialized.delete(id)
|
||||
})
|
||||
if (cache !== Uncached)
|
||||
for (const id of cache.blacklist) mUninitialized.delete(id)
|
||||
|
||||
const ImportTrackerModuleId = 2
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* A more performant implementation of Metro's core functions.
|
||||
* A minimal implementation of Metro's runtime with little overhead.
|
||||
* Making initialization faster and use less resources.
|
||||
*
|
||||
* Also avoids cloning exports, allowing for patches to be applied directly without checking for clones.
|
||||
*/
|
||||
|
||||
import { mList } from './patches'
|
||||
|
|
@ -58,6 +60,9 @@ export const metroImportDefault: Metro.RequireFn = moduleId => {
|
|||
if (mod.flags & HasImportedDefault) return mod.importedDefault
|
||||
|
||||
const exports = metroRequire(moduleId)
|
||||
|
||||
mod.flags |= HasImportedDefault
|
||||
|
||||
return (mod.importedDefault = exports?.__esModule
|
||||
? exports.default
|
||||
: exports)
|
||||
|
|
@ -68,7 +73,11 @@ export const metroImportAll: Metro.RequireFn = moduleId => {
|
|||
if (mod.flags & HasImportedAll) return mod.importedAll
|
||||
|
||||
const exports = metroRequire(moduleId)
|
||||
// Our implementation doesn't match Metro's because we modify the exports directly instead of cloning
|
||||
// But this is why ours is superior, it allows patching the exports without needing to do it more than a single time
|
||||
if (!exports?.__esModule) exports.default = exports
|
||||
|
||||
mod.flags |= HasImportedAll
|
||||
|
||||
return (mod.importedAll = exports)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { isProxy } from '@revenge-mod/utils/proxy'
|
||||
import { Initialized } from './custom'
|
||||
import { mDeps, mList } from './patches'
|
||||
import { Initialized } from './runtime'
|
||||
import type { Metro } from '../types'
|
||||
|
||||
/**
|
||||
|
|
|
|||
45
lib/modules/src/native/fs.ts
Normal file
45
lib/modules/src/native/fs.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
|
||||
export function readFile(path: string) {
|
||||
return callBridgeMethod('revenge.fs.read', [path])
|
||||
}
|
||||
|
||||
export function writeFile(path: string, data: string) {
|
||||
return callBridgeMethod('revenge.fs.write', [path, data])
|
||||
}
|
||||
|
||||
export function exists(path: string) {
|
||||
return callBridgeMethod('revenge.fs.exists', [path])
|
||||
}
|
||||
|
||||
export function rm(path: string) {
|
||||
return callBridgeMethod('revenge.fs.delete', [path])
|
||||
}
|
||||
|
||||
export function readFileSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.read', [path])
|
||||
}
|
||||
|
||||
export function writeFileSync(path: string, data: string) {
|
||||
return callBridgeMethodSync('revenge.fs.write', [path, data])
|
||||
}
|
||||
|
||||
export function rmSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.exists', [path])
|
||||
}
|
||||
|
||||
export function deleteFileSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.delete', [path])
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface Methods {
|
||||
'revenge.fs.read': [[path: string], string]
|
||||
'revenge.fs.write': [[path: string, data: string], void]
|
||||
'revenge.fs.exists': [[path: string], boolean]
|
||||
'revenge.fs.delete': [[path: string], boolean]
|
||||
}
|
||||
}
|
||||
|
|
@ -45,16 +45,22 @@ function makePayload(name: string, args: any[]): object {
|
|||
* @param args The arguments to pass to the native method.
|
||||
* @returns A promise that resolves with the result of the native method call.
|
||||
*/
|
||||
export async function callMethod<T>(name: string, args: any[]): Promise<T> {
|
||||
const result = await BridgePromise.readAsDataURL(makePayload(name, args))
|
||||
export async function callBridgeMethod<N extends MethodName>(
|
||||
name: N,
|
||||
args: MethodArgs<N>,
|
||||
): Promise<MethodResult<N>> {
|
||||
try {
|
||||
const result = await BridgePromise.readAsDataURL(
|
||||
makePayload(name, args),
|
||||
)
|
||||
|
||||
if ('error' in result)
|
||||
throw new Error(`Call failed: ${result.error as string}`)
|
||||
if ('result' in result) return result.result as T
|
||||
if ('error' in result) throw result.error
|
||||
if ('result' in result) return result.result as MethodResult<N>
|
||||
|
||||
throw new Error(
|
||||
'Call failed: The module did not return a valid result. The native hook must have failed.',
|
||||
)
|
||||
throw 'The module did not return a valid result. The native hook must have failed.'
|
||||
} catch (error) {
|
||||
throw new Error(`Call failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,17 +72,46 @@ export async function callMethod<T>(name: string, args: any[]): Promise<T> {
|
|||
* @param args The arguments to pass to the native method.
|
||||
* @returns The result of the native method call.
|
||||
*/
|
||||
export function callMethodSync<T>(name: string, args: any[]): T {
|
||||
export function callBridgeMethodSync<N extends MethodName>(
|
||||
name: N,
|
||||
args: MethodArgs<N>,
|
||||
): MethodResult<N> {
|
||||
try {
|
||||
const result = Bridge.getBBox(0, makePayload(name, args))
|
||||
|
||||
if ('error' in result) throw result.error
|
||||
if ('result' in result) return result.result as T
|
||||
if ('result' in result) return result.result as MethodResult<N>
|
||||
|
||||
throw 'The module did not return a valid result. The native hook must have failed.'
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Call failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw new Error(`Call failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bridge information.
|
||||
*/
|
||||
export function getBridgeInfo(): BridgeInfo | null {
|
||||
try {
|
||||
return callBridgeMethodSync('revenge.info', [])
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mFailed to get native bridge info: ${e}\u001b[0m`,
|
||||
2,
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface BridgeInfo {
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export type MethodName = Extract<keyof Methods, string>
|
||||
export type MethodArgs<T extends MethodName> = Methods[T][0]
|
||||
export type MethodResult<T extends MethodName> = Methods[T][1]
|
||||
|
||||
export interface Methods {
|
||||
'revenge.info': [[], BridgeInfo]
|
||||
}
|
||||
|
|
@ -1,8 +1,16 @@
|
|||
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
|
||||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { sleepReject } from '@revenge-mod/utils/promise'
|
||||
import { pUnscopedApi as uapi } from '../apis'
|
||||
import { PluginFlags as Flag, PluginStatus as Status } from '../constants'
|
||||
import {
|
||||
PluginFlags as Flag,
|
||||
PersistentPluginFlags,
|
||||
PluginStatus as Status,
|
||||
} from '../constants'
|
||||
import {
|
||||
addPluginApiDecorator,
|
||||
decoratePluginApi,
|
||||
|
|
@ -83,13 +91,30 @@ export const pEmitter = new TypedEventEmitter<{
|
|||
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
|
||||
export const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
||||
|
||||
const { flags: PersistedFlags }: PersistedPluginStates = callBridgeMethodSync(
|
||||
'revenge.plugins.states.read',
|
||||
[],
|
||||
) ?? {
|
||||
flags: {},
|
||||
}
|
||||
|
||||
pEmitter.on('flagUpdate', plugin => {
|
||||
PersistedFlags[plugin.manifest.id] = plugin.flags & PersistentPluginFlags
|
||||
callBridgeMethod('revenge.plugins.states.write', [PersistedFlags])
|
||||
})
|
||||
|
||||
/**
|
||||
* Registers a new plugin with the system.
|
||||
*
|
||||
* @param manifest The manifest of the plugin.
|
||||
* @param options The options for the plugin.
|
||||
* @param defflags The default flags for the plugin.
|
||||
* @param iflags The internal flags for the plugin.
|
||||
*/
|
||||
export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
options: PluginOptions<O>,
|
||||
flags: number,
|
||||
defflags: number,
|
||||
iflags: number,
|
||||
) {
|
||||
// TODO(plugins): verify plugin manifest
|
||||
|
|
@ -127,7 +152,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
dependents: [],
|
||||
handleError: e => handlePluginError(e, plugin),
|
||||
options,
|
||||
flags,
|
||||
flags: PersistedFlags[manifest.id] ?? defflags,
|
||||
}
|
||||
|
||||
pMetadata.set(plugin, meta)
|
||||
|
|
@ -299,7 +324,6 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
|
||||
// TODO(plugins): write to storage
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
}
|
||||
|
|
@ -317,9 +341,7 @@ export async function enablePlugin(plugin: AnyPlugin) {
|
|||
}),
|
||||
)
|
||||
|
||||
// TODO(plugins): write to storage
|
||||
plugin.flags |= Flag.Enabled
|
||||
|
||||
pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
|
||||
|
|
@ -557,3 +579,18 @@ async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
|||
|
||||
await Promise.all(proms)
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
|
||||
'revenge.plugins.states.write': [[statuses: PersistedPluginFlags], void]
|
||||
}
|
||||
}
|
||||
|
||||
interface PersistedPluginStates {
|
||||
flags: PersistedPluginFlags
|
||||
}
|
||||
|
||||
interface PersistedPluginFlags {
|
||||
[id: PluginManifest['id']]: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as PluginApiModulesFindersFilters from '@revenge-mod/modules/finders/fi
|
|||
import * as PluginApiModulesMetroSubscriptions from '@revenge-mod/modules/metro/subscriptions'
|
||||
import * as PluginApiModulesMetroUtils from '@revenge-mod/modules/metro/utils'
|
||||
import * as PluginApiModulesNative_ from '@revenge-mod/modules/native'
|
||||
import * as PluginApiModulesNativeFileSystem from '@revenge-mod/modules/native/fs'
|
||||
import { spreadDescriptors } from '.'
|
||||
|
||||
export interface PluginApiModules {
|
||||
|
|
@ -11,7 +12,9 @@ export interface PluginApiModules {
|
|||
native: PluginApiModulesNative
|
||||
}
|
||||
|
||||
export type PluginApiModulesNative = typeof PluginApiModulesNative_
|
||||
export type PluginApiModulesNative = typeof PluginApiModulesNative_ & {
|
||||
fs: typeof PluginApiModulesNativeFileSystem
|
||||
}
|
||||
|
||||
export type PluginApiModulesMetro =
|
||||
// biome-ignore format: Don't
|
||||
|
|
@ -30,5 +33,7 @@ export const modules: PluginApiModules = {
|
|||
PluginApiModulesMetroUtils,
|
||||
spreadDescriptors(PluginApiModulesMetroSubscriptions, {}),
|
||||
),
|
||||
native: PluginApiModulesNative_,
|
||||
native: spreadDescriptors(PluginApiModulesNative_, {
|
||||
fs: PluginApiModulesNativeFileSystem,
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ export const PluginFlags = {
|
|||
EnabledLate: 1 << 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitmask of {@link PluginFlags} that are persisted to storage.
|
||||
*/
|
||||
export const PersistentPluginFlags = PluginFlags.Enabled
|
||||
|
||||
/**
|
||||
* The plugin status.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import '@revenge-mod/react/init'
|
|||
import '@revenge-mod/storage/init'
|
||||
|
||||
import { onRunApplication } from '@revenge-mod/react/native'
|
||||
import { onError } from './preinit'
|
||||
import { onError } from '~preinit'
|
||||
|
||||
const unsub = onRunApplication(() => {
|
||||
unsub()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const Setting = {
|
|||
ReactVersion: 'ReactVersion',
|
||||
ReactNativeVersion: 'ReactNativeVersion',
|
||||
HermesVersion: 'HermesVersion',
|
||||
LoaderVersion: 'LoaderVersion',
|
||||
} as const
|
||||
|
||||
export const RouteNames = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { getBridgeInfo } from '@revenge-mod/modules/native'
|
||||
import { Setting } from '../constants'
|
||||
import { CopyableSetting } from './shared'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
const bridgeInfo = getBridgeInfo()
|
||||
|
||||
const LoaderVersionSetting: SettingsItem = CopyableSetting(
|
||||
{
|
||||
parent: Setting.Revenge,
|
||||
IconComponent: () => <TableRowAssetIcon name="SendMessageIcon" />,
|
||||
title: () => 'Loader',
|
||||
usePredicate: () => Boolean(bridgeInfo),
|
||||
},
|
||||
() => `${bridgeInfo!.name} (${bridgeInfo!.version})`,
|
||||
)
|
||||
|
||||
export default LoaderVersionSetting
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { Setting } from './constants'
|
||||
import HermesVersionSetting from './definitions/HermesVersionSetting'
|
||||
import LoaderVersionSetting from './definitions/LoaderVersionSetting'
|
||||
import ReactNativeVersionSetting from './definitions/ReactNativeVersionSetting'
|
||||
import ReactVersionSetting from './definitions/ReactVersionSetting'
|
||||
import ReloadSetting from './definitions/ReloadSetting'
|
||||
|
|
@ -25,6 +26,7 @@ registerSettingsItems({
|
|||
[Setting.ReactVersion]: ReactVersionSetting,
|
||||
[Setting.ReactNativeVersion]: ReactNativeVersionSetting,
|
||||
[Setting.HermesVersion]: HermesVersionSetting,
|
||||
[Setting.LoaderVersion]: LoaderVersionSetting,
|
||||
})
|
||||
|
||||
registerSettingsSection('REVENGE', {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
|
||||
import { Setting } from '../constants'
|
||||
|
||||
|
|
@ -10,24 +9,11 @@ export default function RevengeSettingScreen() {
|
|||
label: 'Revenge',
|
||||
settings: [
|
||||
Setting.RevengeVersion,
|
||||
Setting.LoaderVersion,
|
||||
Setting.RevengeDiscord,
|
||||
Setting.RevengeSourceRepository,
|
||||
Setting.RevengeLicense,
|
||||
],
|
||||
subLabel: (
|
||||
<>
|
||||
<Design.Text variant="text-xs/medium">
|
||||
You are using the next version of Revenge!
|
||||
</Design.Text>
|
||||
<Design.Text
|
||||
color="text-danger"
|
||||
variant="text-xs/semibold"
|
||||
>
|
||||
This version is experimental and may be
|
||||
unstable.
|
||||
</Design.Text>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import {
|
|||
onModuleFirstRequired,
|
||||
onModuleInitialized,
|
||||
} from '@revenge-mod/modules/metro/subscriptions'
|
||||
import { callBridgeMethod } from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { BuildEnvironment, FullVersion } from './constants'
|
||||
import { BuildEnvironment, FullVersion } from '~constants'
|
||||
|
||||
const IndexModuleId = 0
|
||||
|
||||
|
|
@ -49,19 +50,19 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
|
|||
}
|
||||
})
|
||||
|
||||
export function onError(e: unknown) {
|
||||
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
|
||||
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
|
||||
const { ClientInfoModule, DeviceModule } =
|
||||
require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native')
|
||||
export function onError(error: unknown) {
|
||||
const stack = getErrorStack(error) ?? String(error)
|
||||
|
||||
const Client = ClientInfoModule.getConstants()
|
||||
const Device = DeviceModule.getConstants()
|
||||
callBridgeMethod('revenge.alertError', [
|
||||
stack,
|
||||
`${FullVersion} (${BuildEnvironment})`,
|
||||
])
|
||||
|
||||
alert(
|
||||
`Failed to load Revenge (${FullVersion} (${BuildEnvironment}))\n` +
|
||||
`Discord: ${Client.Version} (${Client.Build})\n` +
|
||||
`Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` +
|
||||
getErrorStack(e),
|
||||
)
|
||||
nativeLoggingHook(`\u001b[31m${stack}\u001b[0m`, 2)
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface Methods {
|
||||
'revenge.alertError': [[error: string, version: string], void]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue