Files
cmr-mini/miniprogram/engine/renderer/tilePersistentCache.ts

214 lines
4.8 KiB
TypeScript

const STORAGE_KEY = 'cmr-tile-disk-cache-index-v1'
const ROOT_DIR = `${wx.env.USER_DATA_PATH}/cmr-tile-cache`
const MAX_PERSISTED_TILES = 600
const PERSIST_DELAY_MS = 300
export interface TilePersistentCacheRecord {
filePath: string
lastAccessedAt: number
}
type TilePersistentCacheIndex = Record<string, TilePersistentCacheRecord>
let sharedTilePersistentCache: TilePersistentCache | null = null
function getFileExtension(url: string): string {
const matched = url.match(/\.(png|jpg|jpeg|webp)(?:$|\?)/i)
if (!matched) {
return '.tile'
}
return `.${matched[1].toLowerCase()}`
}
function hashUrl(url: string): string {
let hash = 2166136261
for (let index = 0; index < url.length; index += 1) {
hash ^= url.charCodeAt(index)
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
}
return (hash >>> 0).toString(36)
}
function cloneIndex(rawIndex: any): TilePersistentCacheIndex {
const nextIndex: TilePersistentCacheIndex = {}
if (!rawIndex || typeof rawIndex !== 'object') {
return nextIndex
}
Object.keys(rawIndex).forEach((url) => {
const record = rawIndex[url]
if (!record || typeof record.filePath !== 'string') {
return
}
nextIndex[url] = {
filePath: record.filePath,
lastAccessedAt: typeof record.lastAccessedAt === 'number' ? record.lastAccessedAt : 0,
}
})
return nextIndex
}
export class TilePersistentCache {
fs: WechatMiniprogram.FileSystemManager
rootDir: string
index: TilePersistentCacheIndex
persistTimer: number
constructor() {
this.fs = wx.getFileSystemManager()
this.rootDir = ROOT_DIR
this.index = {}
this.persistTimer = 0
this.ensureRootDir()
this.loadIndex()
this.pruneMissingFiles()
this.pruneIfNeeded()
}
ensureRootDir(): void {
try {
this.fs.accessSync(this.rootDir)
} catch {
this.fs.mkdirSync(this.rootDir, true)
}
}
loadIndex(): void {
try {
this.index = cloneIndex(wx.getStorageSync(STORAGE_KEY))
} catch {
this.index = {}
}
}
getCount(): number {
return Object.keys(this.index).length
}
schedulePersist(): void {
if (this.persistTimer) {
return
}
this.persistTimer = setTimeout(() => {
this.persistTimer = 0
wx.setStorageSync(STORAGE_KEY, this.index)
}, PERSIST_DELAY_MS) as unknown as number
}
pruneMissingFiles(): void {
let changed = false
Object.keys(this.index).forEach((url) => {
const record = this.index[url]
try {
this.fs.accessSync(record.filePath)
} catch {
delete this.index[url]
changed = true
}
})
if (changed) {
this.schedulePersist()
}
}
getCachedPath(url: string): string | null {
const record = this.index[url]
if (!record) {
return null
}
try {
this.fs.accessSync(record.filePath)
record.lastAccessedAt = Date.now()
this.schedulePersist()
return record.filePath
} catch {
delete this.index[url]
this.schedulePersist()
return null
}
}
getTargetPath(url: string): string {
const existingRecord = this.index[url]
if (existingRecord) {
existingRecord.lastAccessedAt = Date.now()
this.schedulePersist()
return existingRecord.filePath
}
const filePath = `${this.rootDir}/${hashUrl(url)}${getFileExtension(url)}`
this.index[url] = {
filePath,
lastAccessedAt: Date.now(),
}
this.schedulePersist()
return filePath
}
markReady(url: string, filePath: string): void {
this.index[url] = {
filePath,
lastAccessedAt: Date.now(),
}
this.pruneIfNeeded()
this.schedulePersist()
}
remove(url: string): void {
const record = this.index[url]
if (!record) {
return
}
try {
this.fs.unlinkSync(record.filePath)
} catch {
// Ignore unlink errors for already-missing files.
}
delete this.index[url]
this.schedulePersist()
}
pruneIfNeeded(): void {
const urls = Object.keys(this.index)
if (urls.length <= MAX_PERSISTED_TILES) {
return
}
const removableUrls = urls.sort((leftUrl, rightUrl) => {
return this.index[leftUrl].lastAccessedAt - this.index[rightUrl].lastAccessedAt
})
while (removableUrls.length > MAX_PERSISTED_TILES) {
const nextUrl = removableUrls.shift() as string
const record = this.index[nextUrl]
if (record) {
try {
this.fs.unlinkSync(record.filePath)
} catch {
// Ignore unlink errors for already-missing files.
}
}
delete this.index[nextUrl]
}
}
}
export function getTilePersistentCache(): TilePersistentCache {
if (!sharedTilePersistentCache) {
sharedTilePersistentCache = new TilePersistentCache()
}
return sharedTilePersistentCache
}