diff --git a/packages/core/src/composables/useDataSyncPersistence.ts b/packages/core/src/composables/useDataSyncPersistence.ts index 7011d151..de16fe4a 100644 --- a/packages/core/src/composables/useDataSyncPersistence.ts +++ b/packages/core/src/composables/useDataSyncPersistence.ts @@ -87,10 +87,10 @@ function getSyncClient(client?: SupabaseClient | null): SupabaseClient | null { async function getLocalPersistMeta(type: SyncDataType): Promise { if (type === SyncDataType.practice_word) { - return getPracticeWordCacheLocalWithMeta() + return await getPracticeWordCacheLocalWithMeta() } if (type === SyncDataType.practice_article) { - return getPracticeArticleCacheLocalWithMeta() + return await getPracticeArticleCacheLocalWithMeta() } const raw = await get(getPersistKey(type)) try { @@ -102,11 +102,11 @@ async function getLocalPersistMeta(type: SyncDataType): Promise { if (type === SyncDataType.practice_word) { - setPracticeWordCacheLocal(val as PracticeWordCacheStored, updated_at) + await setPracticeWordCacheLocal(val as PracticeWordCacheStored, updated_at) return } if (type === SyncDataType.practice_article) { - setPracticeArticleCacheLocal(val as PracticeArticleCache, updated_at) + await setPracticeArticleCacheLocal(val as PracticeArticleCache, updated_at) return } await set( @@ -327,10 +327,8 @@ export async function saveHashSnapshot(currentHash: string, previousHash: string data: { dict: await get(SAVE_DICT_KEY.key), setting: await get(SAVE_SETTING_KEY.key), - //@ts-ignore - [PRACTICE_WORD_CACHE.key]: import.meta.client ? localStorage.getItem(PRACTICE_WORD_CACHE.key) : null, - //@ts-ignore - [PRACTICE_ARTICLE_CACHE.key]: import.meta.client ? localStorage.getItem(PRACTICE_ARTICLE_CACHE.key) : null, + [PRACTICE_WORD_CACHE.key]: await get(PRACTICE_WORD_CACHE.key) ?? null, + [PRACTICE_ARTICLE_CACHE.key]: await get(PRACTICE_ARTICLE_CACHE.key) ?? null, }, } if (!snapshot.data.dict) { @@ -569,9 +567,9 @@ export function useDataSyncPersistence() { await saveLocalAndSync(SyncDataType.dict, data, { ...options, canSyncRemote }) } - function getLocalCompactDataByType(type: SyncDataType) { - if (type === SyncDataType.practice_word) return getPracticeWordCacheLocal() - if (type === SyncDataType.practice_article) return getPracticeArticleCacheLocal() + async function getLocalCompactDataByType(type: SyncDataType) { + if (type === SyncDataType.practice_word) return await getPracticeWordCacheLocal() + if (type === SyncDataType.practice_article) return await getPracticeArticleCacheLocal() if (type === SyncDataType.dict) return shakeCommonDict(store.$state) if (type === SyncDataType.setting) return settingStore.$state } diff --git a/packages/core/src/composables/usePracticePersistence.ts b/packages/core/src/composables/usePracticePersistence.ts index 73508611..606afee5 100644 --- a/packages/core/src/composables/usePracticePersistence.ts +++ b/packages/core/src/composables/usePracticePersistence.ts @@ -137,7 +137,7 @@ export function usePracticeWordPersistence() { async function load(): Promise { const res = await fetch() - return res ?? restorePracticeWordCache(getPracticeWordCacheLocal()) + return res ?? restorePracticeWordCache(await getPracticeWordCacheLocal()) } async function fetch(): Promise { @@ -150,7 +150,7 @@ export function usePracticeWordPersistence() { } async function getLocalDataCompact(): Promise { - return getPracticeWordCacheLocal() + return await getPracticeWordCacheLocal() } async function save(data: PracticeWordCache | null) { @@ -170,11 +170,11 @@ export function usePracticeArticlePersistence() { async function load(): Promise { const res = await fetch() - return res ?? getPracticeArticleCacheLocal() + return res ?? await getPracticeArticleCacheLocal() } async function getLocalDataCompact(): Promise { - return getPracticeArticleCacheLocal() + return await getPracticeArticleCacheLocal() } async function fetch(): Promise { @@ -195,4 +195,4 @@ export function usePracticeArticlePersistence() { } return { load, save, clear, fetch, getLocalDataCompact } -} \ No newline at end of file +} diff --git a/packages/core/src/utils/cache.ts b/packages/core/src/utils/cache.ts index 4b2b88a8..42b9fd56 100644 --- a/packages/core/src/utils/cache.ts +++ b/packages/core/src/utils/cache.ts @@ -1,5 +1,6 @@ import type { PracticeData, TaskWords } from '../types' import type { PracticeState } from '../stores' +import { get, set } from 'idb-keyval' type CacheConfig = { key: string; version: number } @@ -47,52 +48,80 @@ export type PracticeArticleCache = { export type LocalCacheResult = { val: T; updated_at?: string; version: number } -function getLocal(config: CacheConfig): T | null { - const result = getLocalWithMeta(config) - return result?.val ?? null -} - -/** 返回带 updated_at 的本地缓存,供同步时比较时间戳用;无数据或解析失败返回 null */ -function getLocalWithMeta(config: CacheConfig): LocalCacheResult | null { - const d = localStorage.getItem(config.key) - if (!d) return null +/** + * 尝试从 localStorage 迁移老数据到 IndexedDB。 + * 如果 idb 中无数据,但 localStorage 中有,则迁移并删除 localStorage 中的 key。 + * 老数据是 JSON 字符串格式,迁移时解析为对象再存入 idb。 + */ +async function migrateFromLocalStorage(config: CacheConfig): Promise | null> { try { - return JSON.parse(d) - } catch { + const raw = localStorage.getItem(config.key) + if (!raw) return null + const parsed = JSON.parse(raw) as LocalCacheResult + // 迁移到 idb(直接存对象,不再序列化字符串) + await set(config.key, parsed) + // 删除 localStorage 中的老数据 localStorage.removeItem(config.key) + console.log(`[cache] migrated ${config.key} from localStorage to idb`) + return parsed + } catch { return null } } -function setLocal(config: CacheConfig, val: T | null, updated_at: string): void { - const payload: { version: number; val: T; updated_at?: string } = { +/** 从 idb 读取带 meta 的缓存;无数据或解析失败返回 null */ +async function getLocalWithMeta(config: CacheConfig): Promise | null> { + const raw = await get(config.key) + if (raw) { + // 兼容旧版本写入的 JSON 字符串格式 + if (typeof raw === 'string') { + try { + return JSON.parse(raw) as LocalCacheResult + } catch { + return null + } + } + return raw as LocalCacheResult + } + // idb 中没有数据,尝试从 localStorage 迁移(兼容老数据) + return migrateFromLocalStorage(config) +} + +async function getLocal(config: CacheConfig): Promise { + const result = await getLocalWithMeta(config) + return result?.val ?? null +} + +async function setLocal(config: CacheConfig, val: T | null, updated_at: string): Promise { + // idb 原生支持对象存储,直接存对象,无需 JSON.stringify + const payload: LocalCacheResult = { version: config.version, val, updated_at, } - localStorage.setItem(config.key, JSON.stringify(payload)) + await set(config.key, payload) } -export function getPracticeWordCacheLocal(): PracticeWordCacheStored | null { +export async function getPracticeWordCacheLocal(): Promise { return getLocal(PRACTICE_WORD_CACHE) } -export function getPracticeWordCacheLocalWithMeta(): LocalCacheResult | null { +export async function getPracticeWordCacheLocalWithMeta(): Promise | null> { return getLocalWithMeta(PRACTICE_WORD_CACHE) } -export function setPracticeWordCacheLocal(cache: PracticeWordCacheStored | null, updated_at?: string): void { - setLocal(PRACTICE_WORD_CACHE, cache, updated_at) +export async function setPracticeWordCacheLocal(cache: PracticeWordCacheStored | null, updated_at?: string): Promise { + await setLocal(PRACTICE_WORD_CACHE, cache, updated_at) } -export function getPracticeArticleCacheLocal(): PracticeArticleCache | null { +export async function getPracticeArticleCacheLocal(): Promise { return getLocal(PRACTICE_ARTICLE_CACHE) } -export function getPracticeArticleCacheLocalWithMeta(): LocalCacheResult | null { +export async function getPracticeArticleCacheLocalWithMeta(): Promise | null> { return getLocalWithMeta(PRACTICE_ARTICLE_CACHE) } -export function setPracticeArticleCacheLocal(cache: PracticeArticleCache | null, updated_at?: string): void { - setLocal(PRACTICE_ARTICLE_CACHE, cache, updated_at) +export async function setPracticeArticleCacheLocal(cache: PracticeArticleCache | null, updated_at?: string): Promise { + await setLocal(PRACTICE_ARTICLE_CACHE, cache, updated_at) }