142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
import type { PiniaPluginContext } from 'pinia'
|
|
import localforage from 'localforage'
|
|
|
|
export type PersistOption = boolean | {
|
|
key?: string
|
|
debounce?: number
|
|
}
|
|
|
|
type PersistStoreExt = {
|
|
$persistNow?: () => Promise<void>
|
|
$clearPersisted?: () => Promise<void>
|
|
}
|
|
|
|
type PiniaStorageConfig = LocalForageOptions & {
|
|
mode?: 'single' | 'multiple'
|
|
debounce?: number
|
|
}
|
|
|
|
const baseConfig: PiniaStorageConfig = {
|
|
driver: localforage.INDEXEDDB,
|
|
name: 'DB',
|
|
version: 1,
|
|
description: 'pinia persisted storage',
|
|
storeName: 'pinia-storage',
|
|
debounce: 300
|
|
}
|
|
|
|
let singleStore: LocalForage | null = null
|
|
const multipleStores = new Map<string, LocalForage>()
|
|
|
|
const getStore = (mode: 'single' | 'multiple', storeName: string, config: LocalForageOptions) => {
|
|
if (mode === 'single') {
|
|
if (!singleStore) {
|
|
singleStore = localforage.createInstance({ ...baseConfig, ...config })
|
|
}
|
|
return singleStore
|
|
}
|
|
if (!multipleStores.has(storeName)) {
|
|
multipleStores.set(
|
|
storeName,
|
|
localforage.createInstance({ ...baseConfig, ...config, storeName })
|
|
)
|
|
}
|
|
return multipleStores.get(storeName) as LocalForage
|
|
}
|
|
|
|
const resolvePersistOptions = (persist: PersistOption | undefined) => {
|
|
if (!persist) return null
|
|
if (persist === true) return {}
|
|
return persist
|
|
}
|
|
|
|
const trimStringValuesDeep = (value: unknown): unknown => {
|
|
if (typeof value === 'string') return value.trim()
|
|
if (Array.isArray(value)) return value.map(item => trimStringValuesDeep(item))
|
|
if (value && typeof value === 'object') {
|
|
const normalized: Record<string, unknown> = {}
|
|
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
normalized[key] = trimStringValuesDeep(child)
|
|
}
|
|
return normalized
|
|
}
|
|
return value
|
|
}
|
|
|
|
export default (config?: PiniaStorageConfig) => {
|
|
const finalConfig = { ...baseConfig, ...(config || {}) }
|
|
const { mode = 'single', debounce = 300, ...forageConfig } = finalConfig
|
|
|
|
return (context: PiniaPluginContext) => {
|
|
const persistOptions = resolvePersistOptions(context.options.persist as PersistOption | undefined)
|
|
if (!persistOptions) return
|
|
|
|
const storeId = context.store.$id
|
|
const baseStoreName = forageConfig.storeName || baseConfig.storeName || 'pinia-storage'
|
|
const resolvedStoreName = mode === 'multiple' ? `${baseStoreName}-${storeId}` : baseStoreName
|
|
const key = persistOptions.key || (mode === 'single' ? `${baseStoreName}-${storeId}` : resolvedStoreName)
|
|
const lf = getStore(mode, resolvedStoreName, forageConfig)
|
|
const persistDebounce = persistOptions.debounce ?? debounce
|
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
let hydrating = false
|
|
let userMutatedBeforeHydrate = false
|
|
const writeState = (state: unknown) => {
|
|
const clonedState = JSON.parse(JSON.stringify(state))
|
|
return lf.setItem(key, trimStringValuesDeep(clonedState))
|
|
}
|
|
const storeExt = context.store as typeof context.store & PersistStoreExt
|
|
|
|
storeExt.$persistNow = async () => {
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
timer = null
|
|
}
|
|
try {
|
|
await writeState(context.store.$state)
|
|
} catch (error) {
|
|
console.error('pinia persist failed:', error)
|
|
}
|
|
}
|
|
|
|
storeExt.$clearPersisted = async () => {
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
timer = null
|
|
}
|
|
try {
|
|
await lf.removeItem(key)
|
|
} catch (error) {
|
|
console.error('pinia clear persisted failed:', error)
|
|
}
|
|
}
|
|
|
|
context.store.$subscribe(
|
|
(_mutation, state) => {
|
|
if (!hydrating) userMutatedBeforeHydrate = true
|
|
if (timer) clearTimeout(timer)
|
|
timer = setTimeout(() => {
|
|
void writeState(state).catch(error => {
|
|
console.error('pinia persist failed:', error)
|
|
})
|
|
}, Math.max(0, persistDebounce))
|
|
},
|
|
{ detached: true }
|
|
)
|
|
|
|
hydrating = true
|
|
void lf.getItem<Record<string, unknown>>(key)
|
|
.then(state => {
|
|
if (!state || typeof state !== 'object') return
|
|
// 若在异步hydrate返回前,store已被用户修改(如removeTab),不再回填旧缓存覆盖当前状态。
|
|
if (userMutatedBeforeHydrate) return
|
|
context.store.$patch(state as any)
|
|
})
|
|
.catch(error => {
|
|
console.error('pinia hydrate failed:', error)
|
|
})
|
|
.finally(() => {
|
|
hydrating = false
|
|
})
|
|
}
|
|
}
|