topfans/frontend/utils/cacheManager.js
2026-07-29 16:03:33 +08:00

233 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// frontend/utils/cacheManager.js
// 缓存清理统一封装层
// 详见 docs/superpowers/specs/2026-07-28-cache-cleanup-design.md
import { invalidateAll } from './preloadApi/core'
import sandboxTmpHandler from './handlers/sandboxTmpHandler'
import progressHandler from './handlers/progressHandler'
import othersHandler from './handlers/othersHandler'
import preloadHandler from './handlers/preloadHandler'
import guideHandler from './handlers/guideHandler'
import draftHandler from './handlers/draftHandler'
// ── 黑名单§3.4.1)──
// 全等匹配
const PROTECTED_EXACT = new Set([
'access_token', 'user', 'star_id', 'login_mobile', 'cid',
'deviceFp', 'pending_scan_url', 'gallery_owner_id',
'needs_welcome', 'has_seen_welcome', 'is_new_user',
'liked_assets_exhibition',
])
// 前缀匹配
const PROTECTED_PREFIX = [
'daily_login_completed_',
'avatar_file_',
'temp_register_',
]
/**
* 判断 key 是否在黑名单中
*/
export function isProtectedKey(key) {
if (typeof key !== 'string') return false
if (PROTECTED_EXACT.has(key)) return true
return PROTECTED_PREFIX.some((p) => key.startsWith(p))
}
// ── 大小格式化§6.2)──
export function formatSize(bytes) {
if (typeof bytes !== 'number' || bytes < 0) return '—'
if (bytes < 1024) return '< 1 KB'
const kb = bytes / 1024
if (kb < 1024) return `${kb.toFixed(1)} KB`
const mb = kb / 1024
if (mb < 1024) return `${mb.toFixed(1)} MB`
return `${(mb / 1024).toFixed(2)} GB`
}
// ── 内部状态 ──
const handlers = new Map() // handlerId → handler
const cleanInFlight = new Map() // `${id}` 或 `${id}#${uid}` → Promise
// ── 注册 API ──
export function registerCategory(handler) {
if (!handler?.id) throw new Error('[cacheManager] handler.id is required')
handlers.set(handler.id, handler)
}
function getHandler(id) {
const h = handlers.get(id)
if (!h) throw new Error(`[cacheManager] unknown handler: ${id}`)
return h
}
// ── 公共 API ──
/**
* 列表页读取(汇总 + 存储配额 + 其他 section 数据)
*/
export async function getCacheInfo() {
// [并行 1] 所有 handler 的 computeSize
const categories = await Promise.all(
Array.from(handlers.values()).map(async (h) => {
try {
const info = await h.computeSize()
return { id: h.id, label: h.label, description: h.description || '', sizeBytes: info?.sizeBytes ?? 0, keyCount: info?.keyCount ?? 0, warning: !!h.warning }
} catch (e) {
console.warn(`[cacheManager] computeSize failed: ${h.id}`, e.message)
return { id: h.id, label: h.label, description: h.description || '', sizeBytes: -1, keyCount: 0, warning: !!h.warning, error: e.message }
}
})
)
// [串行] 配额 + 黑名单 + 沙盒
let currentSizeKB = 0, limitSizeKB = 0, sandboxBytes = 0, blacklistBytes = 0
try {
const info = uni.getStorageInfoSync()
currentSizeKB = info.currentSize || 0
limitSizeKB = info.limitSize || 0
const allKeys = info.keys || []
// 黑名单 key 大小
for (const k of allKeys) {
if (isProtectedKey(k)) {
try {
const v = uni.getStorageSync(k)
if (v != null) blacklistBytes += JSON.stringify(v).length
} catch (e) { /* skip */ }
}
}
} catch (e) {
console.warn('[cacheManager] getStorageInfoSync failed:', e.message)
}
try {
const { getSandboxTotalSize } = await import('./ioPath')
sandboxBytes = await getSandboxTotalSize()
} catch (e) {
console.warn('[cacheManager] getSandboxTotalSize failed:', e.message)
}
const totalBytes = categories.reduce((sum, c) => sum + (c.sizeBytes > 0 ? c.sizeBytes : 0), 0)
const appUsedBytes = currentSizeKB * 1024 + sandboxBytes
const quotaTotalBytes = limitSizeKB * 1024
const raw = quotaTotalBytes - appUsedBytes
const quotaAvailableBytes = Math.max(0, raw)
const quotaExceeded = raw < 0
const usagePercent = quotaTotalBytes > 0 ? (appUsedBytes / quotaTotalBytes) * 100 : 0
return {
totalBytes, appUsedBytes, quotaTotalBytes, quotaAvailableBytes, quotaExceeded, usagePercent,
othersBytes: blacklistBytes + sandboxBytes,
categories,
}
}
/**
* 详情页读取(分组详情;简单 handler 返回 null
*/
export async function getCategoryBreakdown(id) {
const h = getHandler(id)
if (typeof h.computeBreakdown !== 'function') return null
try {
return await h.computeBreakdown()
} catch (e) {
console.warn(`[cacheManager] computeBreakdown failed: ${id}`, e.message)
return []
}
}
/**
* 简单页清理(无分组维度)
*/
export function cleanCategory(id) {
return _runWithInFlight(`${id}`, async () => {
const h = getHandler(id)
if (typeof h.clean !== 'function') {
throw new Error(`[cacheManager] handler ${id} has no clean() (use cleanCategoryGroup)`)
}
try {
const result = await h.clean()
// preload 清理同步清内存§3.4.4
if (id === 'preload') {
try { invalidateAll() } catch (e) { console.warn('[cacheManager] invalidateAll failed:', e.message) }
}
return result
} catch (e) {
console.warn(`[cacheManager] clean failed: ${id}`, e.message)
throw e
}
})
}
/**
* 分组页清理(按 uid 维度)
* @param {string} id
* @param {object} opts
* @param {string|null} opts.uid 'self' = 当前用户null = 其他用户聚合,其他 = 具体 uid 字符串
*/
export function cleanCategoryGroup(id, opts = {}) {
const uidKey = opts.uid === undefined ? 'self' : String(opts.uid)
return _runWithInFlight(`${id}#${uidKey}`, async () => {
const h = getHandler(id)
if (typeof h.cleanGroup !== 'function') {
throw new Error(`[cacheManager] handler ${id} has no cleanGroup() (use cleanCategory)`)
}
try {
const result = await h.cleanGroup({ uid: opts.uid === undefined ? 'self' : opts.uid })
if (id === 'preload') {
try { invalidateAll() } catch (e) { console.warn('[cacheManager] invalidateAll failed:', e.message) }
}
return result
} catch (e) {
console.warn(`[cacheManager] cleanGroup failed: ${id}`, e.message)
throw e
}
})
}
/**
* 编程式清理所有UI 不调用;登出流程/测试用)
* 对每个 handler 调用其支持的清理方法:
* - 优先 clean()(简单型)
* - 否则调 cleanGroup({ uid: 'self' })(仅清当前用户的分组数据,符合"登出前清自己"的语义)
*/
export async function cleanAll() {
const results = []
for (const h of handlers.values()) {
try {
let r
if (typeof h.clean === 'function') {
r = await h.clean()
} else if (typeof h.cleanGroup === 'function') {
r = await h.cleanGroup({ uid: 'self' })
} else {
continue
}
results.push({ id: h.id, ...r, error: null })
} catch (e) {
console.warn(`[cacheManager] cleanAll failed for ${h.id}:`, e.message)
results.push({ id: h.id, freedBytes: 0, keyCount: 0, error: e.message })
}
}
try { invalidateAll() } catch (e) { console.warn('[cacheManager] cleanAll invalidateAll failed:', e.message) }
return { freedBytes: results.reduce((s, r) => s + (r.freedBytes || 0), 0), perCategory: results }
}
// NOTE: 必须非 async —— async 会把返回的 Promise 再包一层,破坏
// `p1 === p2` 同一性断言(参见 plan §Task 12 Step 2 自测)。
// 这里需要直接返回存储在 cleanInFlight 中的 Promise 引用本身。
function _runWithInFlight(key, fn) {
if (cleanInFlight.has(key)) return cleanInFlight.get(key)
const p = (async () => {
try { return await fn() } finally { cleanInFlight.delete(key) }
})()
cleanInFlight.set(key, p)
return p
}
// ── Handler 注册(模块加载时执行;新增 handler 在此追加 registerCategory 调用)──
registerCategory(sandboxTmpHandler)
registerCategory(progressHandler)
registerCategory(othersHandler)
registerCategory(preloadHandler)
registerCategory(guideHandler)
registerCategory(draftHandler)