238 lines
6.1 KiB
TypeScript
238 lines
6.1 KiB
TypeScript
import type {
|
|
ContentItem,
|
|
ContentModpackCardCategory,
|
|
ContentModpackCardProject,
|
|
ContentModpackCardVersion,
|
|
ContentOwner,
|
|
} from '@modrinth/ui'
|
|
|
|
export interface InstanceContentModpackCache {
|
|
project: ContentModpackCardProject
|
|
version: ContentModpackCardVersion | null
|
|
owner: ContentOwner | null
|
|
categories: ContentModpackCardCategory[]
|
|
hasUpdate: boolean
|
|
updateVersionId: string | null
|
|
}
|
|
|
|
export interface InstanceContentCache {
|
|
instanceId: string
|
|
updatedAt: number
|
|
|
|
// 内容数据(大,变动频率低)
|
|
contentItems: ContentItem[] | null
|
|
modpack: InstanceContentModpackCache | null
|
|
linkedContentItems: ContentItem[]
|
|
|
|
// UI 偏好(关闭软件后保留)
|
|
modpackHintDismissed: boolean
|
|
}
|
|
|
|
// ---- 内部实现:数据拆分存储,避免单 key 过大导致 localStorage 静默写入失败 ----
|
|
|
|
interface CacheDataSlice {
|
|
contentItems: ContentItem[] | null
|
|
modpack: InstanceContentModpackCache | null
|
|
linkedContentItems: ContentItem[]
|
|
}
|
|
|
|
interface CacheUiSlice {
|
|
modpackHintDismissed: boolean
|
|
}
|
|
|
|
function dataKey(instanceId: string): string {
|
|
return `instance:${instanceId}:data`
|
|
}
|
|
|
|
function uiKey(instanceId: string): string {
|
|
return `instance:${instanceId}:ui`
|
|
}
|
|
|
|
function safeGetItem(key: string): string | null {
|
|
try {
|
|
return localStorage.getItem(key)
|
|
} catch (err) {
|
|
console.error(`[InstanceCache] Failed to read localStorage key "${key}"`, err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function safeSetItem(key: string, value: string): boolean {
|
|
try {
|
|
localStorage.setItem(key, value)
|
|
return true
|
|
} catch (err) {
|
|
console.error(
|
|
`[InstanceCache] Failed to write localStorage key "${key}" (size: ${value.length} bytes)`,
|
|
err,
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
function safeRemoveItem(key: string): void {
|
|
try {
|
|
localStorage.removeItem(key)
|
|
} catch (err) {
|
|
console.error(`[InstanceCache] Failed to remove localStorage key "${key}"`, err)
|
|
}
|
|
}
|
|
|
|
function defaultData(): CacheDataSlice {
|
|
return {
|
|
contentItems: null,
|
|
modpack: null,
|
|
linkedContentItems: [],
|
|
}
|
|
}
|
|
|
|
function defaultUi(): CacheUiSlice {
|
|
return {
|
|
modpackHintDismissed: false,
|
|
}
|
|
}
|
|
|
|
function readDataSlice(instanceId: string): CacheDataSlice | null {
|
|
try {
|
|
const raw = safeGetItem(dataKey(instanceId))
|
|
if (!raw) return null
|
|
return JSON.parse(raw) as CacheDataSlice
|
|
} catch (err) {
|
|
console.error(`[InstanceCache] Failed to parse data slice for "${instanceId}"`, err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeDataSlice(instanceId: string, patch: Partial<CacheDataSlice>): boolean {
|
|
const existing = readDataSlice(instanceId)
|
|
const base = existing ?? defaultData()
|
|
const merged = { ...base, ...patch }
|
|
return safeSetItem(dataKey(instanceId), JSON.stringify(merged))
|
|
}
|
|
|
|
function readUiSlice(instanceId: string): CacheUiSlice | null {
|
|
try {
|
|
const raw = safeGetItem(uiKey(instanceId))
|
|
if (!raw) return null
|
|
return JSON.parse(raw) as CacheUiSlice
|
|
} catch (err) {
|
|
console.error(`[InstanceCache] Failed to parse UI slice for "${instanceId}"`, err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeUiSlice(instanceId: string, patch: Partial<CacheUiSlice>): boolean {
|
|
const existing = readUiSlice(instanceId)
|
|
const base = existing ?? defaultUi()
|
|
const merged = { ...base, ...patch }
|
|
return safeSetItem(uiKey(instanceId), JSON.stringify(merged))
|
|
}
|
|
|
|
// ---- 旧缓存迁移 ----
|
|
|
|
/**
|
|
* 旧的分散缓存 key 列表。在首次写入新缓存后清理,释放 localStorage 空间。
|
|
*/
|
|
const LEGACY_KEYS = [
|
|
'instance-content-cache',
|
|
'instance-linked-content-cache',
|
|
'content-ui-state',
|
|
'content-tab-modpack-hint-dismissed',
|
|
]
|
|
|
|
/** 匹配旧的 content-filters-* key */
|
|
const LEGACY_FILTER_KEY_PREFIX = 'content-filters-'
|
|
|
|
let migrationDone = false
|
|
|
|
/**
|
|
* 清理旧版缓存系统遗留的 localStorage key。
|
|
* 只执行一次(per session),释放被旧数据占用的配额。
|
|
*/
|
|
function migrateFromLegacyCache(): void {
|
|
if (migrationDone) return
|
|
migrationDone = true
|
|
|
|
// 删除固定名称的旧 key
|
|
for (const key of LEGACY_KEYS) {
|
|
safeRemoveItem(key)
|
|
}
|
|
|
|
// 删除所有 content-filters-type:* 和 content-filters-status:* 旧 key
|
|
try {
|
|
const keysToRemove: string[] = []
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i)
|
|
if (key?.startsWith(LEGACY_FILTER_KEY_PREFIX)) {
|
|
keysToRemove.push(key)
|
|
}
|
|
}
|
|
for (const key of keysToRemove) {
|
|
safeRemoveItem(key)
|
|
}
|
|
} catch (err) {
|
|
console.error('[InstanceCache] Failed to enumerate localStorage keys during migration', err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 读取实例的统一内容缓存。
|
|
* 返回 null 表示该实例没有任何数据缓存(data slice 为空)。
|
|
*/
|
|
export function readInstanceCache(instanceId: string): InstanceContentCache | null {
|
|
const data = readDataSlice(instanceId)
|
|
if (!data) return null
|
|
const hasContent =
|
|
(data.contentItems?.length ?? 0) > 0 || (data.linkedContentItems?.length ?? 0) > 0
|
|
if (!hasContent) {
|
|
removeInstanceCache(instanceId)
|
|
return null
|
|
}
|
|
|
|
const ui = readUiSlice(instanceId)
|
|
|
|
return {
|
|
instanceId,
|
|
updatedAt: 0,
|
|
contentItems: data.contentItems ?? [],
|
|
modpack: data.modpack,
|
|
linkedContentItems: data.linkedContentItems ?? [],
|
|
modpackHintDismissed: ui?.modpackHintDismissed ?? false,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 写入实例的统一内容缓存(merge 模式)。
|
|
* 大小数据自动分流:内容数据写入 data key,UI 状态写入 ui key。
|
|
*/
|
|
export function writeInstanceCache(
|
|
instanceId: string,
|
|
patch: Partial<Omit<InstanceContentCache, 'instanceId' | 'updatedAt'>>,
|
|
): void {
|
|
migrateFromLegacyCache()
|
|
|
|
const dataPatch: Partial<CacheDataSlice> = {}
|
|
const uiPatch: Partial<CacheUiSlice> = {}
|
|
|
|
if ('contentItems' in patch) dataPatch.contentItems = patch.contentItems
|
|
if ('modpack' in patch) dataPatch.modpack = patch.modpack
|
|
if ('linkedContentItems' in patch) dataPatch.linkedContentItems = patch.linkedContentItems
|
|
if ('modpackHintDismissed' in patch) uiPatch.modpackHintDismissed = patch.modpackHintDismissed
|
|
|
|
if (Object.keys(dataPatch).length > 0) {
|
|
writeDataSlice(instanceId, dataPatch)
|
|
}
|
|
if (Object.keys(uiPatch).length > 0) {
|
|
writeUiSlice(instanceId, uiPatch)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除实例的统一内容缓存。
|
|
* 在实例被删除时调用。
|
|
*/
|
|
export function removeInstanceCache(instanceId: string): void {
|
|
safeRemoveItem(dataKey(instanceId))
|
|
safeRemoveItem(uiKey(instanceId))
|
|
}
|