tilePersistentCache.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. const STORAGE_KEY = 'cmr-tile-disk-cache-index-v1'
  2. const ROOT_DIR = `${wx.env.USER_DATA_PATH}/cmr-tile-cache`
  3. const MAX_PERSISTED_TILES = 600
  4. const PERSIST_DELAY_MS = 300
  5. export interface TilePersistentCacheRecord {
  6. filePath: string
  7. lastAccessedAt: number
  8. }
  9. type TilePersistentCacheIndex = Record<string, TilePersistentCacheRecord>
  10. let sharedTilePersistentCache: TilePersistentCache | null = null
  11. function getFileExtension(url: string): string {
  12. const matched = url.match(/\.(png|jpg|jpeg|webp)(?:$|\?)/i)
  13. if (!matched) {
  14. return '.tile'
  15. }
  16. return `.${matched[1].toLowerCase()}`
  17. }
  18. function hashUrl(url: string): string {
  19. let hash = 2166136261
  20. for (let index = 0; index < url.length; index += 1) {
  21. hash ^= url.charCodeAt(index)
  22. hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
  23. }
  24. return (hash >>> 0).toString(36)
  25. }
  26. function cloneIndex(rawIndex: any): TilePersistentCacheIndex {
  27. const nextIndex: TilePersistentCacheIndex = {}
  28. if (!rawIndex || typeof rawIndex !== 'object') {
  29. return nextIndex
  30. }
  31. Object.keys(rawIndex).forEach((url) => {
  32. const record = rawIndex[url]
  33. if (!record || typeof record.filePath !== 'string') {
  34. return
  35. }
  36. nextIndex[url] = {
  37. filePath: record.filePath,
  38. lastAccessedAt: typeof record.lastAccessedAt === 'number' ? record.lastAccessedAt : 0,
  39. }
  40. })
  41. return nextIndex
  42. }
  43. export class TilePersistentCache {
  44. fs: WechatMiniprogram.FileSystemManager
  45. rootDir: string
  46. index: TilePersistentCacheIndex
  47. persistTimer: number
  48. constructor() {
  49. this.fs = wx.getFileSystemManager()
  50. this.rootDir = ROOT_DIR
  51. this.index = {}
  52. this.persistTimer = 0
  53. this.ensureRootDir()
  54. this.loadIndex()
  55. this.pruneMissingFiles()
  56. this.pruneIfNeeded()
  57. }
  58. ensureRootDir(): void {
  59. try {
  60. this.fs.accessSync(this.rootDir)
  61. } catch {
  62. this.fs.mkdirSync(this.rootDir, true)
  63. }
  64. }
  65. loadIndex(): void {
  66. try {
  67. this.index = cloneIndex(wx.getStorageSync(STORAGE_KEY))
  68. } catch {
  69. this.index = {}
  70. }
  71. }
  72. getCount(): number {
  73. return Object.keys(this.index).length
  74. }
  75. schedulePersist(): void {
  76. if (this.persistTimer) {
  77. return
  78. }
  79. this.persistTimer = setTimeout(() => {
  80. this.persistTimer = 0
  81. wx.setStorageSync(STORAGE_KEY, this.index)
  82. }, PERSIST_DELAY_MS) as unknown as number
  83. }
  84. pruneMissingFiles(): void {
  85. let changed = false
  86. Object.keys(this.index).forEach((url) => {
  87. const record = this.index[url]
  88. try {
  89. this.fs.accessSync(record.filePath)
  90. } catch {
  91. delete this.index[url]
  92. changed = true
  93. }
  94. })
  95. if (changed) {
  96. this.schedulePersist()
  97. }
  98. }
  99. getCachedPath(url: string): string | null {
  100. const record = this.index[url]
  101. if (!record) {
  102. return null
  103. }
  104. try {
  105. this.fs.accessSync(record.filePath)
  106. record.lastAccessedAt = Date.now()
  107. this.schedulePersist()
  108. return record.filePath
  109. } catch {
  110. delete this.index[url]
  111. this.schedulePersist()
  112. return null
  113. }
  114. }
  115. getTargetPath(url: string): string {
  116. const existingRecord = this.index[url]
  117. if (existingRecord) {
  118. existingRecord.lastAccessedAt = Date.now()
  119. this.schedulePersist()
  120. return existingRecord.filePath
  121. }
  122. const filePath = `${this.rootDir}/${hashUrl(url)}${getFileExtension(url)}`
  123. this.index[url] = {
  124. filePath,
  125. lastAccessedAt: Date.now(),
  126. }
  127. this.schedulePersist()
  128. return filePath
  129. }
  130. markReady(url: string, filePath: string): void {
  131. this.index[url] = {
  132. filePath,
  133. lastAccessedAt: Date.now(),
  134. }
  135. this.pruneIfNeeded()
  136. this.schedulePersist()
  137. }
  138. remove(url: string): void {
  139. const record = this.index[url]
  140. if (!record) {
  141. return
  142. }
  143. try {
  144. this.fs.unlinkSync(record.filePath)
  145. } catch {
  146. // Ignore unlink errors for already-missing files.
  147. }
  148. delete this.index[url]
  149. this.schedulePersist()
  150. }
  151. pruneIfNeeded(): void {
  152. const urls = Object.keys(this.index)
  153. if (urls.length <= MAX_PERSISTED_TILES) {
  154. return
  155. }
  156. const removableUrls = urls.sort((leftUrl, rightUrl) => {
  157. return this.index[leftUrl].lastAccessedAt - this.index[rightUrl].lastAccessedAt
  158. })
  159. while (removableUrls.length > MAX_PERSISTED_TILES) {
  160. const nextUrl = removableUrls.shift() as string
  161. const record = this.index[nextUrl]
  162. if (record) {
  163. try {
  164. this.fs.unlinkSync(record.filePath)
  165. } catch {
  166. // Ignore unlink errors for already-missing files.
  167. }
  168. }
  169. delete this.index[nextUrl]
  170. }
  171. }
  172. }
  173. export function getTilePersistentCache(): TilePersistentCache {
  174. if (!sharedTilePersistentCache) {
  175. sharedTilePersistentCache = new TilePersistentCache()
  176. }
  177. return sharedTilePersistentCache
  178. }