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 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 }