topfans/frontend/utils/preloadApi/storage.js

369 lines
9.4 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/preloadApi/storage.js
// 文件缓存适配器 — _doc/preload/{userId}/ 目录下的 JSON 文件读写
// APP-PLUS: 优先 plus.iopromisify降级 uni.getFileSystemManager
// H5/小程序: uni.getFileSystemManager
const BASE_DIR = '_doc/preload'
const NAMESPACE = 'preload'
// ── 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)
}
// ── 路径工具 ──
function getUserDir(userId) {
return `${BASE_DIR}/${userId || 'guest'}`
}
function getFilePath(userId, cacheKey) {
return `${getUserDir(userId)}/${hashStr(cacheKey)}.json`
}
// ── plus.io promisify 工具 ──
function promisifyPlusIO(fn) {
return new Promise((resolve, reject) => {
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
})
}
// ── 确保目录存在 ──
async function ensureDir(dirPath) {
// #ifdef APP-PLUS
return promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
`_doc/`,
(docEntry) => {
// 逐级创建 preload/{userId}
const parts = dirPath.replace('_doc/', '').split('/')
let currentEntry = docEntry
const createNext = (idx) => {
if (idx >= parts.length) return resolve()
currentEntry.getDirectory(
parts[idx],
{ create: true },
(dirEntry) => {
currentEntry = dirEntry
createNext(idx + 1)
},
(err) => reject(err)
)
}
createNext(0)
},
(err) => reject(err)
)
})
// #endif
// #ifndef APP-PLUS
try {
const fs = uni.getFileSystemManager()
// uni.getFileSystemManager 的 mkdir 需要父目录已存在,逐级创建
const parts = dirPath.replace('_doc/', '').split('/')
let current = '_doc'
for (const part of parts) {
current += '/' + part
try { fs.accessSync(current) } catch (e) { fs.mkdirSync(current) }
}
} catch (e) {
// 目录已存在或创建失败,静默
}
// #endif
}
// ── 公共 API ──
/**
* 读文件缓存条目
* @returns {Promise<{data, ts, ttl}|null>} null = 未命中
*/
export async function readEntry(userId, cacheKey) {
const filePath = getFilePath(userId, cacheKey)
try {
// #ifdef APP-PLUS
const content = await promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
filePath,
(fileEntry) => {
fileEntry.file(
(file) => {
const reader = new plus.io.FileReader()
reader.onloadend = (e) => resolve(e.target.result)
reader.onerror = (e) => reject(e)
reader.readAsText(file, 'utf-8')
},
(err) => reject(err)
)
},
(err) => reject(err) // 文件不存在 = 未命中
)
})
return JSON.parse(content)
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager()
const raw = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(raw)
// #endif
} catch (e) {
return null // 文件不存在 / 损坏 → 未命中
}
}
/**
* 写文件缓存条目fire-and-forget调用方不 await
* 内部自建 .catch 防止 unhandled rejection
*/
export function writeEntry(userId, cacheKey, data, ts, ttl) {
const dirPath = getUserDir(userId)
const filePath = getFilePath(userId, cacheKey)
const content = JSON.stringify({ data, ts, ttl })
ensureDir(dirPath).then(() => {
// #ifdef APP-PLUS
return promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
dirPath,
(dirEntry) => {
dirEntry.getFile(
hashStr(cacheKey) + '.json',
{ create: true },
(fileEntry) => {
fileEntry.createWriter(
(writer) => {
writer.onwriteend = () => resolve()
writer.onerror = (e) => reject(e)
writer.write(content)
},
(err) => reject(err)
)
},
(err) => reject(err)
)
},
(err) => reject(err)
)
})
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager()
fs.writeFileSync(filePath, content, 'utf-8')
// #endif
}).catch((err) => {
console.warn('[preload] storage write failed:', filePath, err.message)
})
}
/**
* 删除指定用户的文件缓存目录
*/
export async function clearForUser(userId) {
const dirPath = getUserDir(userId)
try {
// #ifdef APP-PLUS
await promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
dirPath,
(dirEntry) => {
dirEntry.removeRecursively(
() => resolve(),
(err) => reject(err)
)
},
// 目录不存在不算错误
() => resolve()
)
})
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager()
try { fs.rmdirSync(dirPath, true) } catch (e) { /* absent = ok */ }
// #endif
} catch (e) {
console.warn('[preload] clearForUser failed:', userId, e.message)
}
}
/**
* 获取用户缓存目录总大小(字节)
* 用于 FIFO 容量检查
* 注意:累加所有文件的 file.size异步回调全部完成后才 resolve
*/
export async function getTotalCacheSize(userId) {
const dirPath = getUserDir(userId)
let totalSize = 0
try {
// #ifdef APP-PLUS
await promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
dirPath,
(dirEntry) => {
const reader = dirEntry.createReader()
let pending = 0
let done = false
const readAll = () => {
reader.readEntries(
(entries) => {
if (entries.length === 0) {
done = true
if (pending === 0) resolve()
return
}
for (const entry of entries) {
if (entry.isFile) {
pending++
entry.file(
(f) => {
totalSize += (f.size || 0)
pending--
if (done && pending === 0) resolve()
},
() => {
pending--
if (done && pending === 0) resolve()
}
)
}
}
readAll() // 递归读下一批
},
(err) => reject(err)
)
}
readAll()
},
() => resolve() // 目录不存在 → size = 0
)
})
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager()
try {
const files = fs.readdirSync(dirPath)
for (const f of files) {
try {
const stat = fs.statSync(dirPath + '/' + f)
totalSize += stat.size || 0
} catch (e) { /* skip */ }
}
} catch (e) { /* absent = ok */ }
// #endif
} catch (e) {
// ignore
}
return totalSize
}
/**
* FIFO 淘汰最旧文件,直到总大小 < maxSize 字节
* APP-PLUS递归 readEntries 收集所有文件 → 按 mtime 排序 → 从最旧的开始删除
* 非 APP-PLUSreaddir + stat → 按 mtime 排序 → 删除最旧的
* 注getTotalCacheSize 的异步回调方式不适用于"删除后重算"循环,
* 此处改为一次 scan 出文件列表 → 排序 → 按需删除
*/
export async function evictOldest(userId, maxSize) {
const dirPath = getUserDir(userId)
try {
// #ifdef APP-PLUS
// 1. 收集所有文件及其 mtime 和 size
const files = await promisifyPlusIO((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
dirPath,
(dirEntry) => {
const reader = dirEntry.createReader()
const collected = []
let pending = 0
let done = false
const readAll = () => {
reader.readEntries(
(entries) => {
if (entries.length === 0) {
done = true
if (pending === 0) resolve(collected)
return
}
for (const entry of entries) {
if (entry.isFile) {
pending++
entry.file(
(f) => {
// plus.io File 的 modificationTime 或直接用 lastModified
const mtime = f.lastModified || f.lastModifiedDate?.getTime?.() || 0
collected.push({ name: entry.name, entry, size: f.size || 0, mtime })
pending--
if (done && pending === 0) resolve(collected)
},
() => {
pending--
if (done && pending === 0) resolve(collected)
}
)
}
}
readAll()
},
(err) => reject(err)
)
}
readAll()
},
() => resolve([]) // 目录不存在 → 空列表
)
})
// 2. 按 mtime 升序排列(最旧的在前)
files.sort((a, b) => a.mtime - b.mtime)
// 3. 计算当前总大小,按 FIFO 删除直到 < maxSize * 0.8
let totalSize = files.reduce((sum, f) => sum + f.size, 0)
for (const f of files) {
if (totalSize <= maxSize * 0.8) break
f.entry.remove(() => {}, () => {})
totalSize -= f.size
}
// #endif
// #ifndef APP-PLUS
const fs = uni.getFileSystemManager()
try {
const fileNames = fs.readdirSync(dirPath)
const files = fileNames.map(name => {
try {
const stat = fs.statSync(dirPath + '/' + name)
return { name, size: stat.size || 0, mtime: stat.lastModified || stat.lastModifiedTime || 0 }
} catch (e) {
return { name, size: 0, mtime: 0 }
}
})
// 按 mtime 升序(最旧的在前)
files.sort((a, b) => a.mtime - b.mtime)
let totalSize = files.reduce((sum, f) => sum + f.size, 0)
for (const f of files) {
if (totalSize <= maxSize * 0.8) break
try { fs.unlinkSync(dirPath + '/' + f.name) } catch (e) { /* skip */ }
totalSize -= f.size
}
} catch (e) { /* absent = ok */ }
// #endif
} catch (e) {
console.warn('[preload] evictOldest failed:', userId, e.message)
}
}