Refactor DiskCache to use a synchronization queue and update cache key generation

a20c8978f9d7e8e0117bc383a73845cbbc82fe90

Cohee <18619528+Cohee1207@users.noreply.github.com>

1 files changed, +41 -54Showing whitespace changes
src/endpoints/characters.js+41 -54
@@ -21,6 +21,7 @@ import { parse, write } from '../character-card-parser.js';
2121import { readWorldInfoFile } from './worldinfo.js';
2222import { invalidateThumbnail } from './thumbnails.js';
2323import { importRisuSprites } from './sprites.js';
24+import { getUserDirectories } from '../users.js';
2425const defaultAvatarPath = './public/img/ai4.png';
2526
2627// With 100 MB limit it would take roughly 3000 characters to reach this limit
@@ -34,28 +35,29 @@ const useDiskCache = !!getConfigValue('performance.useDiskCache', true, 'boolean
3435
3536class DiskCache {
3637 /**
3738 * @typedeftype {objectstring} CacheRemovalQueueItem
38- * @property {string} item Path to the character file
39+ * @readonly
39- * @property {number} timestamp Timestamp of the last access
4040 */
41-
42- /** @type {string} */
4341 static DIRECTORY = 'characters';
4442
45- /** @type {number} */
43+ /**
46- static REMOVAL_INTERVAL = 5 * 60 * 1000;
44+ * @type {number}
45+ * @readonly
46+ */
47+ static SYNC_INTERVAL = 5 * 60 * 1000;
4748
4849 /** @type {import('node-persist').LocalStorage} */
4950 #instance;
5051
5152 /** @type {NodeJS.Timeout} */
5253 #removalIntervalsyncInterval;
5354
5455 /**
5556 * Queue forof removaluser ofhandles cacheto entriessync.
5657 * @type {CacheRemovalQueueItem[]Set<string>}
58+ * @readonly
5759 */
5860 removalQueuesyncQueue = []new Set();
5961
6062 /**
6163 * Path to the cache directory.
@@ -74,49 +76,21 @@ class DiskCache {
7476 }
7577
7678 /**
7779 * Processes the removalsynchronization queue.
7880 * @returns {Promise<void>}
7981 */
8082 async #removeCacheEntriessyncCacheEntries() {
81- // TODO: consider running this.verify() for a user instead as getting all cache keys
82- // is a heavy operation, since it requires to read all files in the disk cache.
83-
8483 try {
8584 if (!useDiskCache || this.removalQueuesyncQueue.lengthsize === 0) {
8685 return;
8786 }
8887
89- /** @type {Map<string, number>} */
88+ const directories = [...this.syncQueue].map(entry => getUserDirectories(entry));
90- const latestTimestamps = new Map();
89+ this.syncQueue.clear();
91- for (const { item, timestamp } of this.removalQueue) {
92- if (!latestTimestamps.has(item) || timestamp > (latestTimestamps.get(item) ?? 0)) {
93- latestTimestamps.set(item, timestamp);
94- }
95- }
96- this.removalQueue.length = 0;
97-
98- const cache = await this.instance();
99- const keys = await cache.keys();
10090
101- for (const [item, timestamp] of latestTimestamps.entries()) {
91+ await this.verify(directories);
102- const itemKeys = keys.filter(k => k.startsWith(item));
103- if (!itemKeys.length) {
104- continue;
105- }
106- for (const key of itemKeys) {
107- const datumPath = cache.getDatumPath(key);
108- if (!fs.existsSync(datumPath)) {
109- continue;
110- }
111- const stat = fs.statSync(datumPath);
112- if (stat.mtimeMs > timestamp) {
113- continue;
114- }
115- await cache.removeItem(key);
116- }
117- }
11892 } catch (error) {
11993 console.error('Error while removingsynchronizing cache entries:', error);
12094 }
12195 }
12296
@@ -131,7 +105,7 @@ class DiskCache {
131105
132106 this.#instance = storage.create({ dir: this.cachePath, ttl: false });
133107 await this.#instance.init();
134108 this.#removalIntervalsyncInterval = setInterval(this.#removeCacheEntriessyncCacheEntries.bind(this), DiskCache.REMOVAL_INTERVALSYNC_INTERVAL);
135109 return this.#instance;
136110 }
137111
@@ -151,8 +125,8 @@ class DiskCache {
151125 const files = fs.readdirSync(dir.characters, { withFileTypes: true });
152126 for (const file of files.filter(f => f.isFile() && path.extname(f.name) === '.png')) {
153127 const filePath = path.join(dir.characters, file.name);
154128 const statcacheKey = fs.statSyncgetCacheKey(filePath);
155129 validKeys.add(path.parse(cache.getDatumPath(`${filePath}-${stat.mtimeMs}`cacheKey)).base);
156130 }
157131 }
158132 for (const key of this.hashedKeys) {
@@ -163,8 +137,8 @@ class DiskCache {
163137 }
164138
165139 dispose() {
166140 if (this.#removalIntervalsyncInterval) {
167141 clearInterval(this.#removalIntervalsyncInterval);
168142 }
169143 }
170144}
@@ -172,14 +146,27 @@ class DiskCache {
172146export const diskCache = new DiskCache();
173147
174148/**
149+ * Gets the cache key for the specified image file.
150+ * @param {string} inputFile - Path to the image file
151+ * @returns {string} - Cache key
152+ */
153+function getCacheKey(inputFile) {
154+ if (fs.existsSync(inputFile)) {
155+ const stat = fs.statSync(inputFile);
156+ return `${inputFile}-${stat.mtimeMs}`;
157+ }
158+
159+ return inputFile;
160+}
161+
162+/**
175163 * Reads the character card from the specified image file.
176164 * @param {string} inputFile - Path to the image file
177165 * @param {string} inputFormat - 'png'
178166 * @returns {Promise<string | undefined>} - Character card data
179167 */
180168async function readCharacterData(inputFile, inputFormat = 'png') {
181169 const statcacheKey = fs.statSyncgetCacheKey(inputFile);
182- const cacheKey = `${inputFile}-${stat.mtimeMs}`;
183170 if (memoryCache.has(cacheKey)) {
184171 return memoryCache.get(cacheKey);
185172 }
@@ -220,7 +207,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
220207 }
221208 }
222209 if (useDiskCache && !Buffer.isBuffer(inputFile)) {
223- diskCache.removalQueue.push({ item: inputFile, timestamp: Date.now() });
210+ diskCache.syncQueue.add(request.user.profile.handle);
224211 }
225212 /**
226213 * Read the image, resize, and save it as a PNG into the buffer.