topfans/frontend/utils/preloadApi/storage.js
2026-07-13 14:40:34 +08:00

161 lines
4.9 KiB
JavaScript

// frontend/utils/preloadApi/storage.js
// 缓存适配器 — 用 uni.setStorage 系(SQLite 后端,不受 Android 10+ 分区存储影响)
//
// Android 10+ 适配说明(2026-07-13):
// 旧实现用 plus.io.requestFileSystem(PRIVATE_DOC=2, ...) + getDirectory + getFile +
// createWriter 写文件缓存,在某些 HBuilderX 版本 + targetSdkVersion >= 29 下,
// DCloud 内部仍把 _doc/ PRIVATE_DOC 映射到 PUBLIC_DOCUMENTS 路径(外部存储),
// 分区存储机制下报 "targetSdkVersion设置>=29后在Android10+系统设备不支持当前路径"。
// 修正:完全绕开 plus.io,改用 uni.setStorage(底层 plus.storage 是 SQLite,
// 走应用数据目录,与 Android 分区存储无关,无大小限制)。
// ── djb2 hash(与 core.js 共用逻辑,此处独立一份避免循环依赖)──
function hashStr(str) {
let hash = 5381
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0
}
return (hash >>> 0).toString(16)
}
// ── key 工具 ──
// 统一前缀,方便 clearForUser 时按前缀删
const KEY_PREFIX = 'preload'
function makeKey(userId, cacheKey) {
return `${KEY_PREFIX}:${userId || 'guest'}:${hashStr(cacheKey)}`
}
function makeUserPrefix(userId) {
return `${KEY_PREFIX}:${userId || 'guest'}:`
}
function makeGlobalPrefix() {
return `${KEY_PREFIX}:`
}
// ── 公共 API ──
/**
* 读缓存条目
* @returns {Promise<{data, ts, ttl}|null>} null = 未命中
*/
export async function readEntry(userId, cacheKey) {
try {
const value = uni.getStorageSync(makeKey(userId, cacheKey))
if (!value) return null
// value 可能是字符串也可能是对象,统一处理
const obj = typeof value === 'string' ? JSON.parse(value) : value
if (!obj || typeof obj.ts !== 'number') return null
return obj
} catch (e) {
return null
}
}
/**
* 写缓存条目(fire-and-forget,内部捕获异常)
* 与旧实现兼容:签名不变,但底层从 plus.io 改为 uni.setStorage
*/
export function writeEntry(userId, cacheKey, data, ts, ttl) {
const key = makeKey(userId, cacheKey)
const payload = { data, ts, ttl }
try {
uni.setStorageSync(key, payload)
} catch (e) {
console.warn('[preload] storage write failed:', key, e.message || e)
}
}
/**
* 删除指定用户的全部缓存条目
* 注意:uni 没有"按前缀删"接口,需要 getStorageInfo 拿所有 key 后过滤
*/
export async function clearForUser(userId) {
try {
const info = uni.getStorageInfoSync()
const prefix = makeUserPrefix(userId)
const toRemove = (info.keys || []).filter((k) => k.startsWith(prefix))
toRemove.forEach((k) => {
try { uni.removeStorageSync(k) } catch (e) { /* skip */ }
})
} catch (e) {
console.warn('[preload] clearForUser failed:', userId, e.message || e)
}
}
/**
* 获取用户缓存总大小(字节)
* uni.getStorageInfoSync().currentSize 单位是 KB,需乘 1024
*/
export async function getTotalCacheSize(userId) {
try {
const info = uni.getStorageInfoSync()
const prefix = makeUserPrefix(userId)
const keys = (info.keys || []).filter((k) => k.startsWith(prefix))
if (keys.length === 0) return 0
// info.currentSize 是全局所有 storage 的 KB 数,无法按 user 拆。
// 退而求其次:遍历用户的每个 key,getStorageSync 估算字节大小
let totalBytes = 0
for (const k of keys) {
try {
const v = uni.getStorageSync(k)
if (v != null) {
// 粗估:JSON 序列化字节数
totalBytes += JSON.stringify(v).length
}
} catch (e) { /* skip */ }
}
return totalBytes
} catch (e) {
return 0
}
}
/**
* FIFO 淘汰最旧条目,直到总大小 < maxSize 字节
* uni 没有"按 mtime 排序"接口,改用 ts 字段(我们在写入时设置的)
*/
export async function evictOldest(userId, maxSize) {
try {
const prefix = makeUserPrefix(userId)
const info = uni.getStorageInfoSync()
const keys = (info.keys || []).filter((k) => k.startsWith(prefix))
if (keys.length === 0) return
// 收集每个 entry 的 ts
const entries = []
for (const k of keys) {
try {
const v = uni.getStorageSync(k)
const obj = typeof v === 'string' ? JSON.parse(v) : v
if (obj && typeof obj.ts === 'number') {
entries.push({ key: k, ts: obj.ts })
}
} catch (e) { /* skip corrupt entry */ }
}
// 按 ts 升序(最旧的在前)
entries.sort((a, b) => a.ts - b.ts)
// 累加当前估算大小,超出 maxSize * 0.8 就开始删
let totalSize = 0
for (const e of entries) {
try {
const v = uni.getStorageSync(e.key)
if (v != null) totalSize += JSON.stringify(v).length
} catch (err) { /* skip */ }
}
for (const e of entries) {
if (totalSize <= maxSize * 0.8) break
try {
const v = uni.getStorageSync(e.key)
if (v != null) totalSize -= JSON.stringify(v).length
uni.removeStorageSync(e.key)
} catch (err) { /* skip */ }
}
} catch (e) {
console.warn('[preload] evictOldest failed:', userId, e.message || e)
}
}