Files
calculator2026/src/pinia/Plugin/indexdb.ts
T
2026-03-13 18:27:42 +08:00

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
})
}
}