214 lines
4.8 KiB
TypeScript
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
|
|
}
|