Fix cache removal queue processing
| @@ -33,6 +33,12 @@ const useShallowCharacters = !!getConfigValue('performance.lazyLoadCharacters', | |||
| 33 | const useDiskCache = !!getConfigValue('performance.useDiskCache', true, 'boolean'); | 33 | const useDiskCache = !!getConfigValue('performance.useDiskCache', true, 'boolean'); |
| 34 | 34 | ||
| 35 | class DiskCache { | 35 | class DiskCache { |
| 36 | /** | ||
| 37 | * @typedef {object} CacheRemovalQueueItem | ||
| 38 | * @property {string} item Path to the character file | ||
| 39 | * @property {number} timestamp Timestamp of the last access | ||
| 40 | */ | ||
| 41 | |||
| 36 | /** @type {string} */ | 42 | /** @type {string} */ |
| 37 | static DIRECTORY = 'characters'; | 43 | static DIRECTORY = 'characters'; |
| 38 | 44 | ||
| @@ -47,9 +53,9 @@ class DiskCache { | |||
| 47 | 53 | ||
| 48 | /** | 54 | /** |
| 49 | * Queue for removal of cache entries. | 55 | * Queue for removal of cache entries. |
| 50 | * @type {Set<string>} | 56 | * @type {CacheRemovalQueueItem[]} |
| 51 | */ | 57 | */ |
| 52 | removalQueue = new Set(); | 58 | removalQueue = []; |
| 53 | 59 | ||
| 54 | /** | 60 | /** |
| 55 | * Processes the removal queue. | 61 | * Processes the removal queue. |
| @@ -57,22 +63,43 @@ class DiskCache { | |||
| 57 | */ | 63 | */ |
| 58 | async #removeCacheEntries() { | 64 | async #removeCacheEntries() { |
| 59 | try { | 65 | try { |
| 60 | if (!useDiskCache || this.removalQueue.size === 0) { | 66 | if (!useDiskCache || this.removalQueue.length === 0) { |
| 61 | return; | 67 | return; |
| 62 | } | 68 | } |
| 63 | 69 | ||
| 64 | const keys = await this.instance().then(i => i.keys()); | 70 | /** @type {Map<string, number>} */ |
| 65 | for (const item of this.removalQueue) { | 71 | const latestTimestamps = new Map(); |
| 66 | const key = keys.find(k => k.startsWith(item)); | 72 | for (const { item, timestamp } of this.removalQueue) { |
| 67 | if (key) { | 73 | if (!latestTimestamps.has(item) || timestamp > (latestTimestamps.get(item) ?? 0)) { |
| 68 | await this.instance().then(i => i.removeItem(key)); | 74 | latestTimestamps.set(item, timestamp); |
| 75 | } | ||
| 76 | } | ||
| 77 | this.removalQueue.length = 0; | ||
| 78 | |||
| 79 | const cache = await this.instance(); | ||
| 80 | const keys = await cache.keys(); | ||
| 81 | |||
| 82 | for (const [item, timestamp] of latestTimestamps.entries()) { | ||
| 83 | const itemKeys = keys.filter(k => k.startsWith(item)); | ||
| 84 | if (!itemKeys.length) { | ||
| 85 | continue; | ||
| 86 | } | ||
| 87 | for (const key of itemKeys) { | ||
| 88 | const datumPath = cache.getDatumPath(key); | ||
| 89 | if (!fs.existsSync(datumPath)) { | ||
| 90 | continue; | ||
| 91 | } | ||
| 92 | const stat = fs.statSync(datumPath); | ||
| 93 | if (stat.mtimeMs > timestamp) { | ||
| 94 | continue; | ||
| 95 | } | ||
| 96 | await cache.removeItem(key); | ||
| 69 | } | 97 | } |
| 70 | } | 98 | } |
| 71 | this.removalQueue.clear(); | ||
| 72 | } catch (error) { | 99 | } catch (error) { |
| 73 | console.error('Error while removing cache entries:', error); | 100 | console.error('Error while removing cache entries:', error); |
| 74 | } | 101 | } |
| 75 | }; | 102 | } |
| 76 | 103 | ||
| 77 | /** | 104 | /** |
| 78 | * Gets the disk cache instance. | 105 | * Gets the disk cache instance. |
| @@ -176,7 +203,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u | |||
| 176 | } | 203 | } |
| 177 | } | 204 | } |
| 178 | if (useDiskCache && !Buffer.isBuffer(inputFile)) { | 205 | if (useDiskCache && !Buffer.isBuffer(inputFile)) { |
| 179 | diskCache.removalQueue.add(inputFile); | 206 | diskCache.removalQueue.push({ item: inputFile, timestamp: Date.now() }); |
| 180 | } | 207 | } |
| 181 | /** | 208 | /** |
| 182 | * Read the image, resize, and save it as a PNG into the buffer. | 209 | * Read the image, resize, and save it as a PNG into the buffer. |