feat(lib/assets)!: modify cache struct, change how overrides work, cleanup APIs
This commit is contained in:
parent
3cc039ae73
commit
a1db2dbb2c
7 changed files with 171 additions and 91 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { Asset, AssetId } from './types'
|
||||
|
||||
export const _assets = new Map<string, [first: Asset, all: Record<Asset['type'], Asset>]>()
|
||||
export const _overrides = new Map<string, Asset>()
|
||||
export const _customs = new Set<Asset>()
|
||||
export const _overrides = new WeakMap<Asset, Asset>()
|
||||
export const _metas = new WeakMap<Asset, [assetId: AssetId, moduleId: Metro.ModuleID]>()
|
||||
|
|
|
|||
60
lib/assets/src/caches.ts
Normal file
60
lib/assets/src/caches.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// 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 { debounce } from '@revenge-mod/utils/callbacks'
|
||||
import { createLogger } from '@revenge-mod/utils/logger'
|
||||
import { mergeDeep } from '@revenge-mod/utils/objects'
|
||||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { Asset } from './types'
|
||||
|
||||
const logger = createLogger('revenge.assets.caches')
|
||||
|
||||
const Version = 1
|
||||
const CurrentKey = `${Version}.${ClientInfoModule.Build}`
|
||||
|
||||
const CacheStorage = getStorage<Cache>('revenge/assets', {
|
||||
default: { k: CurrentKey, c: {} },
|
||||
directory: 'cache',
|
||||
})
|
||||
|
||||
export const cache: Cache['c'] = proxify(
|
||||
() => {
|
||||
if (CacheStorage.cache) return CacheStorage.cache.c
|
||||
},
|
||||
{ hint: 'object' },
|
||||
)!
|
||||
|
||||
// biome-ignore lint/complexity/useArrowFunction: Arrow functions are not supported
|
||||
CacheStorage.get().then(async function (cache) {
|
||||
// TODO(lib/assets/caches): This loads way too late and requires native interop to load earlier
|
||||
logger.log(`Loaded cache version: ${cache.k}`)
|
||||
if (cache.k !== CurrentKey) {
|
||||
await CacheStorage.delete()
|
||||
await CacheStorage.set({ k: CurrentKey, c: {} })
|
||||
}
|
||||
})
|
||||
|
||||
export interface Cache {
|
||||
k: string
|
||||
c: {
|
||||
[key: Asset['name']]: {
|
||||
[key: Asset['type']]: Metro.ModuleID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const save = debounce(() => CacheStorage.set({}), 1000)
|
||||
|
||||
export async function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) {
|
||||
// Merge directly into the cache, only debouncing actual writes
|
||||
mergeDeep(CacheStorage.cache!.c, {
|
||||
[asset.name]: {
|
||||
[asset.type]: moduleId,
|
||||
},
|
||||
})
|
||||
|
||||
// Commented out because caching currently does not work properly because it is loaded too late
|
||||
// await save()
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { ReactNative } from '@revenge-mod/react'
|
||||
|
||||
import { _assets, _metas, _overrides } from './_internal'
|
||||
import { _customs, _metas, _overrides } from './_internal'
|
||||
import { cache } from './caches'
|
||||
import { AssetRegistry } from './preinit'
|
||||
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
import type { Asset, AssetId, RegisterableAsset } from './types'
|
||||
|
||||
export { AssetRegistry, AssetRegistryModuleId } from './preinit'
|
||||
|
|
@ -10,63 +12,6 @@ export { AssetRegistry, AssetRegistryModuleId } from './preinit'
|
|||
// iOS cannot display SVGs
|
||||
let _preferredType: Asset['type'] = ReactNative.Platform.OS === 'ios' ? 'png' : 'svg'
|
||||
|
||||
/**
|
||||
* Yields all registered assets, including ones with same name but different types, custom assets, and overriden assets.
|
||||
*/
|
||||
export function* getAllAssets() {
|
||||
for (const name of _assets.keys()) for (const asset of Object.values(_assets.get(name)![1])) yield asset
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset by its ID. **This may throw an error if the asset-registry has not been initialized.**
|
||||
*
|
||||
* @param id The asset ID.
|
||||
*/
|
||||
export function getAssetById(id: AssetId): Asset | undefined {
|
||||
return AssetRegistry.getAssetByID(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of an asset.
|
||||
*
|
||||
* @param asset The asset to get the ID for.
|
||||
*/
|
||||
export function getAssetId(asset: Asset) {
|
||||
return _metas.get(asset)?.[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module ID that registered the asset. Returns `-1` if the asset is a custom asset.
|
||||
*
|
||||
* @param asset The asset to get the module ID for.
|
||||
*/
|
||||
export function getAssetModuleId(asset: Asset) {
|
||||
return _metas.get(asset)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset by its name.
|
||||
* If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one.
|
||||
*
|
||||
* @param name The asset name.
|
||||
*/
|
||||
export function getAssetByName(name: string): Asset | undefined {
|
||||
const reg = _assets.get(name)
|
||||
if (!reg) return
|
||||
|
||||
return _overrides.get(name) ?? reg[1][_preferredType] ?? reg[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all assets matching the name.
|
||||
*
|
||||
* @param name The asset name.
|
||||
* @returns A record keyed by the type of the asset, with the value being the asset itself.
|
||||
*/
|
||||
export function getAssetsByName(name: string): Record<Asset['type'], Asset> | undefined {
|
||||
return _assets.get(name)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the preferred asset type. This is used to determine which asset to use when multiple types are available.
|
||||
*
|
||||
|
|
@ -76,31 +21,110 @@ export function setPreferredAssetType(type: Asset['type']) {
|
|||
_preferredType = type
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields all registered assets, including ones with same name but different types, custom assets, and overriden assets.
|
||||
*/
|
||||
export function* getAssets(): Generator<Asset> {
|
||||
for (const name of Object.keys(cache!))
|
||||
for (const mid of Object.values(cache[name])) yield AssetRegistry.getAssetByID(__r(mid)) as Asset
|
||||
|
||||
for (const asset of _customs) yield asset
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset by its name.
|
||||
* If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one.
|
||||
*
|
||||
* @param name The asset name.
|
||||
*/
|
||||
export function getAssetByName(name: string): Asset | undefined {
|
||||
const id = getAssetIdByName(name)
|
||||
if (id !== undefined) return AssetRegistry.getAssetByID(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all assets matching the name.
|
||||
*
|
||||
* @param name The asset name.
|
||||
* @returns A record keyed by the type of the asset, with the value being the asset itself.
|
||||
*/
|
||||
export function getAssetsByName(name: string): Record<Asset['type'], Asset> | undefined {
|
||||
const reg = cache[name]
|
||||
if (!reg) return
|
||||
|
||||
return Object.entries(reg).reduce(
|
||||
(acc, [type, mid]) => {
|
||||
acc[type as Asset['type']] = AssetRegistry.getAssetByID(__r(mid))!
|
||||
return acc
|
||||
},
|
||||
{} as Record<Asset['type'], Asset>,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of an asset.
|
||||
*
|
||||
* @param asset The asset to get the ID for.
|
||||
*/
|
||||
export function getAssetId(asset: Asset): AssetId | undefined {
|
||||
return _metas.get(asset)?.[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an asset ID by its name.
|
||||
* If more than one asset is registered with the same name, this will return the one with the preferred type, or the first registered one.
|
||||
*
|
||||
* @param name The asset name.
|
||||
*/
|
||||
export function getAssetIdByName(name: string): AssetId | undefined {
|
||||
const reg = cache[name]
|
||||
if (!reg) return
|
||||
|
||||
const mid = reg[_preferredType] ?? reg[Object.keys(reg)[0]!]
|
||||
if (mid !== undefined) return __r(mid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module ID that registered the asset. Returns `-1` if the asset is a custom asset.
|
||||
*
|
||||
* @param asset The asset to get the module ID for.
|
||||
*/
|
||||
export function getAssetModuleId(asset: Asset): Metro.ModuleID | -1 | undefined {
|
||||
return _metas.get(asset)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an asset with the given name.
|
||||
* If an asset with the same name already exists, it will be overridden if `override` is set to true.
|
||||
*
|
||||
* @param asset The asset to register.
|
||||
* @param override Whether to override the existing asset if one exists. `false` by default.
|
||||
* @throws An error if an asset with the same name and type already exists and `override` is not set to true.
|
||||
* @returns The asset ID.
|
||||
*/
|
||||
export function registerAsset(asset: RegisterableAsset, override?: boolean) {
|
||||
const { name, type } = asset
|
||||
export function registerAsset(asset: RegisterableAsset): AssetId {
|
||||
if (cache[asset.name]?.[asset.type] !== undefined)
|
||||
throw new Error(`Asset with name ${asset.name} and type ${asset.type} already exists!`)
|
||||
|
||||
if (override) _overrides.set(name, asset)
|
||||
// Make sure to not override the asset if it already exists, unless explicitly set to override
|
||||
else if (_assets.get(name)?.[1]?.[type]) throw new Error(`Asset "${name}" with type "${type}" already exists`)
|
||||
_customs.add(asset)
|
||||
|
||||
// @ts-expect-error
|
||||
return AssetRegistry.registerAsset(asset)
|
||||
}
|
||||
|
||||
/**
|
||||
* Override an asset with a custom asset.
|
||||
*
|
||||
* @param asset The asset to override.
|
||||
* @param override The custom asset to override with.
|
||||
*/
|
||||
export function addAssetOverride(asset: Asset, override: Asset) {
|
||||
_overrides.set(asset, override)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an asset override.
|
||||
*
|
||||
* @param name The name of the asset to remove the override for.
|
||||
* @returns Whether the override was removed.
|
||||
* @param asset The asset to remove the override for.
|
||||
* @returns The asset that was removed.
|
||||
*/
|
||||
export function removeAssetOverride(name: string) {
|
||||
return _overrides.delete(name)
|
||||
export function removeAssetOverride(asset: Asset) {
|
||||
return _overrides.delete(asset)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { byName, byProps } from '@revenge-mod/modules/finders/filters'
|
|||
import { waitForModules } from '@revenge-mod/modules/finders/wait'
|
||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
||||
|
||||
import { _assets, _metas, _overrides } from './_internal'
|
||||
import { _metas, _overrides } from './_internal'
|
||||
import { cacheAsset } from './caches'
|
||||
|
||||
import type { ReactNative } from '@revenge-mod/react/types'
|
||||
import type { Asset, ReactNativeAsset } from './types'
|
||||
|
|
@ -43,18 +44,13 @@ const unsubAR = waitForModules(byProps<ReactNative.AssetsRegistry>('registerAsse
|
|||
exports.registerAsset = (asset: Asset) => {
|
||||
const result = orig(asset as ReactNativeAsset)
|
||||
|
||||
const { name, type } = asset
|
||||
|
||||
let reg = _assets.get(name)
|
||||
if (!reg) {
|
||||
reg = [asset, {}]
|
||||
_assets.set(name, reg)
|
||||
// Cache packager assets only
|
||||
if ((asset as ReactNativeAsset).__packager_asset) {
|
||||
cacheAsset(asset, _initing)
|
||||
// In-memory cache
|
||||
_metas.set(asset, [result, _initing])
|
||||
}
|
||||
|
||||
// In-memory cache
|
||||
reg[1][type] = asset
|
||||
_metas.set(asset, [result, _initing])
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
|
@ -68,7 +64,7 @@ const unsubRAS = waitForModules(
|
|||
unsubRAS()
|
||||
|
||||
// Custom assets
|
||||
// Why do we need to do this? Because RN/Dicord (unsure which) will attempt to resolve the asset via its path, which we don't provide via custom assets.
|
||||
// Why do we need to do this? Because RN/Discord (unsure which) will attempt to resolve the asset via its path, which we don't provide via custom assets.
|
||||
// This will result in a crash, because it tries to read the path and run checks on it, but the path is undefined.
|
||||
// @ts-expect-error
|
||||
rAS.addCustomSourceTransformer(({ asset }) => {
|
||||
|
|
@ -78,6 +74,6 @@ const unsubRAS = waitForModules(
|
|||
|
||||
// Asset overrides
|
||||
// @ts-expect-error
|
||||
rAS.addCustomSourceTransformer(({ asset }) => _overrides.get(asset.name))
|
||||
rAS.addCustomSourceTransformer(({ asset }) => _overrides.get(asset))
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Page from '~/components/Page'
|
|||
import SearchInput from '~/components/SearchInput'
|
||||
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
|
||||
|
||||
import { getAllAssets, getAssetId, getAssetModuleId } from '@revenge-mod/assets'
|
||||
import { getAssetId, getAssetModuleId, getAssets } from '@revenge-mod/assets'
|
||||
|
||||
import type { Asset, AssetId } from '@revenge-mod/assets/types'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
|
@ -28,8 +28,8 @@ const UndisplayableFallback = {
|
|||
export default function AssetBrowserSettingScreen() {
|
||||
const [search, setSearch] = React.useState('')
|
||||
const assets = React.useMemo(
|
||||
() => [...getAllAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const),
|
||||
[getAllAssets],
|
||||
() => [...getAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const),
|
||||
[getAssets],
|
||||
)
|
||||
|
||||
const filteredAssets = React.useMemo(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ToastActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
|
||||
import { React, ReactNative } from '@revenge-mod/react'
|
||||
import { ToastActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
|
||||
import { api } from '..'
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import FormSwitch from '~/components/FormSwitch'
|
|||
import Page from '~/components/Page'
|
||||
import SearchInput from '~/components/SearchInput'
|
||||
|
||||
import { getAssetByName, getAssetId } from '@revenge-mod/assets'
|
||||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
|
|
@ -118,7 +118,7 @@ function InstalledPluginCard({
|
|||
<Card style={[styles.card, rightGap && styles.rightGap]}>
|
||||
<Stack direction="horizontal" style={[styles.grow, styles.topContainer]}>
|
||||
<Stack spacing={8} direction="horizontal" style={[styles.topContainer, styles.autoSize]}>
|
||||
<Image source={getAssetId(getAssetByName(icon ?? 'PuzzlePieceIcon')!)!} style={styles.icon} />
|
||||
<Image source={getAssetIdByName(icon ?? 'PuzzlePieceIcon')!} style={styles.icon} />
|
||||
<Text variant="heading-lg/semibold">{name}</Text>
|
||||
</Stack>
|
||||
<FormSwitch
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue