fix(lib/storage): allow imports during preinit
This commit is contained in:
parent
f05885e559
commit
8bd9f4f8b2
4 changed files with 159 additions and 110 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
import type { DiscordNativeModules } from './types/native'
|
||||
|
||||
/**
|
||||
|
|
@ -9,22 +10,52 @@ import type { DiscordNativeModules } from './types/native'
|
|||
* - If the module name ends with "Manager", include it
|
||||
*/
|
||||
|
||||
export let CacheModule: DiscordNativeModules.CacheModule = proxify(() => {
|
||||
const module = __turboModuleProxy('NativeCacheModule') as typeof CacheModule
|
||||
|
||||
if (module) return (CacheModule = module)
|
||||
})!
|
||||
|
||||
export let FileModule: DiscordNativeModules.FileModule = proxify(() => {
|
||||
const module = __turboModuleProxy('NativeFileModule') as typeof FileModule
|
||||
|
||||
if (module) return (FileModule = module)
|
||||
})!
|
||||
|
||||
export let ClientInfoModule: DiscordNativeModules.ClientInfoModule = proxify(
|
||||
() => {
|
||||
const module = __turboModuleProxy(
|
||||
'NativeClientInfoModule',
|
||||
) as typeof ClientInfoModule
|
||||
|
||||
if (module) return (ClientInfoModule = module)
|
||||
},
|
||||
)!
|
||||
|
||||
export let DeviceModule: DiscordNativeModules.DeviceModule = proxify(() => {
|
||||
const module = __turboModuleProxy(
|
||||
'NativeDeviceModule',
|
||||
) as typeof DeviceModule
|
||||
|
||||
if (module) return (DeviceModule = module)
|
||||
})!
|
||||
|
||||
export let ThemeModule: DiscordNativeModules.ThemeModule = proxify(() => {
|
||||
const module = __turboModuleProxy('NativeThemeModule') as typeof ThemeModule
|
||||
|
||||
if (ThemeModule) return (ThemeModule = module)
|
||||
})!
|
||||
|
||||
const nmp = nativeModuleProxy
|
||||
|
||||
export const CacheModule = __turboModuleProxy(
|
||||
'NativeCacheModule',
|
||||
) as DiscordNativeModules.CacheModule
|
||||
export const FileModule = __turboModuleProxy(
|
||||
'NativeFileModule',
|
||||
) as DiscordNativeModules.FileModule
|
||||
export const ClientInfoModule = __turboModuleProxy(
|
||||
'NativeClientInfoModule',
|
||||
) as DiscordNativeModules.ClientInfoModule
|
||||
export const DeviceModule = __turboModuleProxy(
|
||||
'NativeDeviceModule',
|
||||
) as DiscordNativeModules.DeviceModule
|
||||
export const BundleUpdaterManager =
|
||||
nmp.BundleUpdaterManager as DiscordNativeModules.BundleUpdaterManager
|
||||
export const ThemeModule = __turboModuleProxy(
|
||||
'NativeThemeModule',
|
||||
) as DiscordNativeModules.ThemeModule
|
||||
export let BundleUpdaterManager: DiscordNativeModules.BundleUpdaterManager =
|
||||
proxify(() => {
|
||||
// Need try-catch here because if you access this before RN sets it up, it will throw an error
|
||||
try {
|
||||
const module =
|
||||
nmp.BundleUpdaterManager as typeof BundleUpdaterManager
|
||||
return (BundleUpdaterManager = module)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})!
|
||||
|
|
|
|||
|
|
@ -1,12 +1,81 @@
|
|||
// TODO(storage): Implement using NativeModules interop instead
|
||||
|
||||
import { FileModule } from '@revenge-mod/discord/native'
|
||||
// This file is usually imported way too early, so we can't import our shim
|
||||
import { React } from '@revenge-mod/react'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
import { mergeDeep } from '@revenge-mod/utils/objects'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import type { AnyObject, DeepPartial } from '@revenge-mod/utils/types'
|
||||
|
||||
export type StorageSubscription<T extends AnyObject = AnyObject> = (
|
||||
v: DeepPartial<T>,
|
||||
) => void
|
||||
|
||||
export function Storage<T extends AnyObject>(
|
||||
this: Storage<T>,
|
||||
path: string,
|
||||
options?: StorageOptions<T>,
|
||||
) {
|
||||
const { CacheDirPath, DocumentsDirPath } = FileModule.getConstants()
|
||||
|
||||
const directory = options?.directory ?? 'documents'
|
||||
const dirPath = directory === 'cache' ? CacheDirPath : DocumentsDirPath
|
||||
const fullPath = `${dirPath}/${path}`
|
||||
|
||||
this._s = new Set<StorageSubscription<T>>()
|
||||
|
||||
this.loaded = false
|
||||
this.cache = options?.default
|
||||
|
||||
this.exists = () => FileModule.fileExists(fullPath)
|
||||
this.delete = () => FileModule.removeFile(directory, path)
|
||||
|
||||
this.get = async function () {
|
||||
if (!(await this.exists())) {
|
||||
this.cache = options?.default ?? {}
|
||||
await this.set({})
|
||||
this.loaded = true
|
||||
return this.cache
|
||||
}
|
||||
|
||||
const contents = await FileModule.readFile(fullPath, 'utf8')
|
||||
if (contents) {
|
||||
this.loaded = true
|
||||
try {
|
||||
const cache = (this.cache = JSON.parse(contents))
|
||||
for (const sub of this._s) sub(cache)
|
||||
return cache
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`Failed to parse storage (<${directory}>/${path}): ${getErrorStack(e)}`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.set = async function (value) {
|
||||
if (!this.cache) await this.get()
|
||||
mergeDeep(this.cache!, value)
|
||||
|
||||
try {
|
||||
const contents = JSON.stringify(this.cache)
|
||||
await FileModule.writeFile(directory, path, contents, 'utf8')
|
||||
|
||||
for (const sub of this._s) sub(value)
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`Failed to write storage (<${directory}>/${path}): ${getErrorStack(e)}`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// React is only initialized right before the init stage, so this is a dummy method
|
||||
// See init.ts for the actual implementation
|
||||
Storage.prototype.use = () => {
|
||||
throw new Error('Storage#use can only be called after the init stage!')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a storage object for a given path and directory.
|
||||
*
|
||||
|
|
@ -17,91 +86,8 @@ export function getStorage<T extends AnyObject = AnyObject>(
|
|||
path: string,
|
||||
options?: StorageOptions<T>,
|
||||
): Storage<T> {
|
||||
const { CacheDirPath, DocumentsDirPath } = FileModule.getConstants()
|
||||
|
||||
const directory = options?.directory ?? 'documents'
|
||||
const dirPath = directory === 'cache' ? CacheDirPath : DocumentsDirPath
|
||||
const fullPath = `${dirPath}/${path}`
|
||||
|
||||
type Subscription = (v: DeepPartial<T>) => void
|
||||
const subs = new Set<Subscription>()
|
||||
|
||||
const storage: Storage<T> = {
|
||||
loaded: false,
|
||||
cache: options?.default,
|
||||
use(filter) {
|
||||
if (!this.cache) this.get()
|
||||
|
||||
const reRender = useReRender()
|
||||
|
||||
React.useEffect(() => {
|
||||
const sub = (
|
||||
filter
|
||||
? v => {
|
||||
if (filter(v)) reRender()
|
||||
}
|
||||
: reRender
|
||||
) as Subscription
|
||||
|
||||
subs.add(sub)
|
||||
|
||||
return () => {
|
||||
subs.delete(sub)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return this.cache
|
||||
},
|
||||
exists() {
|
||||
return FileModule.fileExists(fullPath)
|
||||
},
|
||||
delete() {
|
||||
return FileModule.removeFile(directory, path)
|
||||
},
|
||||
async get() {
|
||||
if (!(await this.exists())) {
|
||||
this.cache = options?.default ?? {}
|
||||
await this.set({})
|
||||
this.loaded = true
|
||||
return this.cache
|
||||
}
|
||||
|
||||
const contents = await FileModule.readFile(fullPath, 'utf8')
|
||||
if (contents) {
|
||||
this.loaded = true
|
||||
try {
|
||||
const cache = (this.cache = JSON.parse(contents))
|
||||
for (const sub of subs) sub(cache)
|
||||
return cache
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Failed to parse storage file (most likely corrupted)',
|
||||
directory,
|
||||
path,
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
async set(value) {
|
||||
if (!this.cache) await this.get()
|
||||
mergeDeep(this.cache!, value)
|
||||
|
||||
try {
|
||||
const contents = JSON.stringify(this.cache)
|
||||
await FileModule.writeFile(directory, path, contents, 'utf8')
|
||||
|
||||
for (const sub of subs) sub(value)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Failed to write storage file',
|
||||
directory,
|
||||
path,
|
||||
e,
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
const storage: Storage<T> = Object.create(Storage.prototype)
|
||||
Storage.call(storage, path, options)
|
||||
|
||||
if (options?.load) storage.get()
|
||||
|
||||
|
|
@ -128,6 +114,10 @@ export type UseStorageFilter<T extends AnyObject> = (
|
|||
) => any
|
||||
|
||||
export interface Storage<T extends AnyObject> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_s: Set<StorageSubscription<T>>
|
||||
/**
|
||||
* Whether the storage has been loaded. If the storage is not loaded, `storage.cache` may be `undefined`.
|
||||
* If you have `options.default` set, you can use this property to check if `storage.cache` is the default value or not.
|
||||
|
|
@ -141,6 +131,8 @@ export interface Storage<T extends AnyObject> {
|
|||
/**
|
||||
* Use the storage in a React component. The component will re-render when the storage is updated.
|
||||
*
|
||||
* This can only be used in the `init` stage or later, as it requires React to be initialized.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* type Settings = { key: boolean, nested: { key: boolean } }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
import { pEmitter } from '@revenge-mod/plugins/_'
|
||||
import { PluginsStorageDirectory } from '@revenge-mod/plugins/constants'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/objects'
|
||||
import { getStorage } from '.'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useEffect } from 'react'
|
||||
import { getStorage, Storage } from '.'
|
||||
import type { StorageSubscription } from '.'
|
||||
|
||||
// Actual implementation of Storage#use
|
||||
const SP: Storage<any> = Storage.prototype
|
||||
SP.use = function (filter) {
|
||||
if (!this.cache) this.get()
|
||||
|
||||
const reRender = useReRender()
|
||||
|
||||
useEffect(() => {
|
||||
const sub: StorageSubscription = filter
|
||||
? v => {
|
||||
if (filter(v)) reRender()
|
||||
}
|
||||
: reRender
|
||||
|
||||
this._s.add(sub)
|
||||
|
||||
return () => {
|
||||
this._s.delete(sub)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return this.cache
|
||||
}
|
||||
|
||||
pEmitter.on('init', ({ _s: storageOptions, manifest: { id } }, api) =>
|
||||
defineLazyProperty(api, 'storage', () =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// @revenge-mod/storage depends on this file, and it is imported way too early, so we can't import our shim
|
||||
import { React } from '@revenge-mod/react'
|
||||
import { useEffect, useReducer } from 'react'
|
||||
import { findInTree } from './trees'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { SearchFilter } from './trees'
|
||||
|
|
@ -7,7 +6,7 @@ import type { SearchFilter } from './trees'
|
|||
export function useIsFirstRender() {
|
||||
let firstRender = false
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
firstRender = true
|
||||
}, [])
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ export function useIsFirstRender() {
|
|||
}
|
||||
|
||||
export function useReRender() {
|
||||
const [, forceUpdate] = React.useReducer(x => ~x, 0)
|
||||
const [, forceUpdate] = useReducer(x => ~x, 0)
|
||||
return forceUpdate
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue