feat(lib/storage): allow set-replacing, add update mode to subscriptions

This commit is contained in:
PalmDevs 2025-09-17 16:53:08 +07:00
parent f89d523c0d
commit 82153eb512
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
2 changed files with 56 additions and 23 deletions

View file

@ -1,12 +1,28 @@
import { FileModule } from '@revenge-mod/discord/native'
import { getErrorStack } from '@revenge-mod/utils/error'
import { mergeDeep } from '@revenge-mod/utils/object'
import type { AnyObject, DeepPartial } from '@revenge-mod/utils/types'
import type { AnyObject, DeepPartial, If } from '@revenge-mod/utils/types'
export type StorageSubscription<T extends AnyObject = AnyObject> = (
v: DeepPartial<T>,
update: DeepPartial<T>,
mode: (typeof StorageUpdateMode)[keyof typeof StorageUpdateMode],
) => void
export const StorageUpdateMode = {
/**
* The update will be merged into the existing storage.
*/
Merge: 0,
/**
* The update will replace the existing storage.
*/
Replace: 1,
/**
* Same behavior as {@link StorageUpdateMode.Replace}, but for the intial load of the storage.
*/
Load: 2,
} as const
export function Storage<T extends AnyObject>(
this: Storage<T>,
path: string,
@ -31,10 +47,22 @@ export function Storage<T extends AnyObject>(
return () => subscriptions.delete(callback)
}
async function write(storage: Storage<T>) {
try {
const contents = JSON.stringify(storage.cache)
await FileModule.writeFile(directory, path, contents, 'utf8')
} catch (e) {
nativeLoggingHook(
`Failed to write storage (<${directory}>/${path}): ${getErrorStack(e)}`,
2,
)
}
}
this.get = async function () {
if (!(await this.exists())) {
this.cache = options?.default ?? {}
await this.set({})
await write(this)
this.loaded = true
return this.cache
}
@ -44,7 +72,8 @@ export function Storage<T extends AnyObject>(
this.loaded = true
try {
const cache = (this.cache = JSON.parse(contents))
for (const sub of subscriptions) sub(cache)
for (const sub of subscriptions)
sub(cache, StorageUpdateMode.Load)
return cache
} catch (e) {
nativeLoggingHook(
@ -55,21 +84,18 @@ export function Storage<T extends AnyObject>(
}
}
this.set = async function (value) {
this.set = async function (value: any, replace?: boolean) {
if (!this.cache) await this.get()
mergeDeep(this.cache!, value)
if (replace) this.cache = value as T
else mergeDeep(this.cache as T, value as DeepPartial<T>)
try {
const contents = JSON.stringify(this.cache)
await FileModule.writeFile(directory, path, contents, 'utf8')
await write(this)
for (const sub of subscriptions) sub(value)
} catch (e) {
nativeLoggingHook(
`Failed to write storage (<${directory}>/${path}): ${getErrorStack(e)}`,
2,
for (const sub of subscriptions)
sub(
value,
replace ? StorageUpdateMode.Replace : StorageUpdateMode.Merge,
)
}
}
if (options?.load) this.get()
@ -112,8 +138,8 @@ export interface StorageOptions<T extends AnyObject = AnyObject> {
load?: boolean
}
export type UseStorageFilter<T extends AnyObject> = (
newValue: DeepPartial<T>,
export type UseStorageFilter<T extends AnyObject = AnyObject> = (
...params: Parameters<StorageSubscription<T>>
) => any
export interface Storage<T extends AnyObject> {
@ -156,7 +182,8 @@ export interface Storage<T extends AnyObject> {
*/
use(filter?: UseStorageFilter<T>): T | undefined
/**
* Subscribe to storage updates. The callback will be called with what was called in `Storage#set`.
* Subscribe to storage updates.
*
* @param callback The callback to call when the storage is updated.
* @returns A function to unsubscribe.
*/
@ -169,8 +196,13 @@ export interface Storage<T extends AnyObject> {
* Set the storage.
*
* @param value The value to merge into the storage.
* @param replace If true, replaces the entire storage instead of merging.
*/
set(value: DeepPartial<T>): Promise<void>
set<Replace extends boolean>(
value: If<Replace, T, DeepPartial<T>>,
replace: Replace,
): Promise<void>
/**
* Whether the storage is exists.
*/

View file

@ -1,18 +1,19 @@
import { Storage } from '@revenge-mod/storage'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { useLayoutEffect } from 'react'
import type { StorageSubscription } from '@revenge-mod/storage'
const proto = Storage.prototype as Storage<any>
// Actual implementation of Storage#use
;(Storage.prototype as Storage<any>).use = function (filter) {
proto.use = function (filter) {
if (!this.cache) this.get()
const reRender = useReRender()
useEffect(() => {
useLayoutEffect(() => {
const sub: StorageSubscription = filter
? v => {
if (filter(v)) reRender()
? (update, mode) => {
if (filter(update, mode)) reRender()
}
: reRender