mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 00:48:06 +00:00
fix(stage-ui): useVersionedLocalStorage did not return the patched value back
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useVersionedLocalStorage } from '.'
|
||||
|
||||
class MemoryStorage {
|
||||
#items = new Map<string, string>()
|
||||
|
||||
getItem(key: string) {
|
||||
return this.#items.get(key) ?? null
|
||||
}
|
||||
|
||||
removeItem(key: string) {
|
||||
this.#items.delete(key)
|
||||
}
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
this.#items.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const storage = new MemoryStorage()
|
||||
|
||||
afterEach(() => {
|
||||
storage.removeItem('settings/live2d/auto-blink-enabled')
|
||||
})
|
||||
|
||||
describe('useVersionedLocalStorage', () => {
|
||||
/**
|
||||
* @example
|
||||
* expect(JSON.parse(localStorage.getItem('settings/live2d/auto-blink-enabled')!)).toEqual({ version: '2.0.0', data: false })
|
||||
*/
|
||||
it('persists returned ref changes into the versioned localStorage wrapper', async () => {
|
||||
const value = useVersionedLocalStorage('settings/live2d/auto-blink-enabled', true, {
|
||||
defaultVersion: '2.0.0',
|
||||
storage,
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Versioned storage exposed an unwrapped `data` ref but only synchronized
|
||||
// storage -> data. UI changes updated the returned ref, while localStorage
|
||||
// stayed at the old `{ version, data }` wrapper value.
|
||||
//
|
||||
// We fixed this by syncing data -> storage without echoing storage reads
|
||||
// back into writes.
|
||||
value.value = false
|
||||
await nextTick()
|
||||
|
||||
expect(JSON.parse(storage.getItem('settings/live2d/auto-blink-enabled')!)).toEqual({
|
||||
version: '2.0.0',
|
||||
data: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { UseStorageOptions } from '@vueuse/core'
|
||||
import type { StorageLike, UseStorageOptions } from '@vueuse/core'
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defaultWindow, useStorage } from '@vueuse/core'
|
||||
import { ref, toValue, watch } from 'vue'
|
||||
|
||||
export interface Versioned<T> { version?: string, data?: T }
|
||||
export interface UseVersionedStorageOptions<T> {
|
||||
defaultVersion?: string
|
||||
storage?: StorageLike
|
||||
satisfiesVersionBy?: (beforeVersion: string, afterVersion: string) => boolean
|
||||
onVersionMismatch?: (value: Versioned<T>) => OnVersionMismatchActions<T>
|
||||
}
|
||||
@@ -22,7 +23,18 @@ export function useVersionedLocalStorage<T>(
|
||||
): Ref<T, T> {
|
||||
const defaultVersion = options?.defaultVersion || '1.0.0'
|
||||
const data = ref(toValue(initialValue)) as Ref<T, T>
|
||||
const rawValue = useLocalStorage<Versioned<T>>(key, { version: defaultVersion, data: toValue(initialValue) }, options as unknown as UseStorageOptions<Versioned<T>>)
|
||||
const rawValue = useStorage<Versioned<T>>(
|
||||
key,
|
||||
{ version: defaultVersion, data: toValue(initialValue) },
|
||||
options?.storage ?? defaultWindow?.localStorage,
|
||||
options as unknown as UseStorageOptions<Versioned<T>>,
|
||||
)
|
||||
|
||||
const syncDataToStorage = watch(data, (value) => {
|
||||
rawValue.value = { version: defaultVersion, data: value }
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
watch(rawValue, (value) => {
|
||||
try {
|
||||
@@ -32,28 +44,38 @@ export function useVersionedLocalStorage<T>(
|
||||
const action = options.onVersionMismatch(rawValue.value)
|
||||
if (action.action === 'reset') {
|
||||
rawValue.value = { version: defaultVersion, data: toValue(initialValue) }
|
||||
syncDataToStorage.pause()
|
||||
data.value = toValue(initialValue)
|
||||
syncDataToStorage.resume()
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn(`version ${rawValue.value.version} doesn't satisfy the version ${defaultVersion} for key ${key}, will reset the value to default value ${toValue(initialValue)}`)
|
||||
rawValue.value = { version: defaultVersion, data: toValue(initialValue) }
|
||||
syncDataToStorage.pause()
|
||||
data.value = toValue(initialValue)
|
||||
syncDataToStorage.resume()
|
||||
}
|
||||
}
|
||||
|
||||
syncDataToStorage.pause()
|
||||
data.value = rawValue.value.data!
|
||||
syncDataToStorage.resume()
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(`property key 'version' wasn't found in the value of key ${key} as ${value}, will keep the current ${toValue(initialValue)}`)
|
||||
rawValue.value = { version: defaultVersion, data: toValue(initialValue) }
|
||||
syncDataToStorage.pause()
|
||||
data.value = toValue(initialValue)
|
||||
syncDataToStorage.resume()
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`failed to un-marshal Local Storage value, possibly due to incompatible or corrupted for key ${key} value ${value}, falling back to default value ${toValue(initialValue)}`, err)
|
||||
rawValue.value = { version: defaultVersion, data: toValue(initialValue) }
|
||||
syncDataToStorage.pause()
|
||||
data.value = toValue(initialValue)
|
||||
syncDataToStorage.resume()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
|
||||
Reference in New Issue
Block a user