mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Refactor DiskCache to use a synchronization queue and update cache key generation
This commit is contained in:
@@ -21,6 +21,7 @@ import { parse, write } from '../character-card-parser.js';
|
|||||||
import { readWorldInfoFile } from './worldinfo.js';
|
import { readWorldInfoFile } from './worldinfo.js';
|
||||||
import { invalidateThumbnail } from './thumbnails.js';
|
import { invalidateThumbnail } from './thumbnails.js';
|
||||||
import { importRisuSprites } from './sprites.js';
|
import { importRisuSprites } from './sprites.js';
|
||||||
|
import { getUserDirectories } from '../users.js';
|
||||||
const defaultAvatarPath = './public/img/ai4.png';
|
const defaultAvatarPath = './public/img/ai4.png';
|
||||||
|
|
||||||
// With 100 MB limit it would take roughly 3000 characters to reach this limit
|
// 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
|
|||||||
|
|
||||||
class DiskCache {
|
class DiskCache {
|
||||||
/**
|
/**
|
||||||
* @typedef {object} CacheRemovalQueueItem
|
* @type {string}
|
||||||
* @property {string} item Path to the character file
|
* @readonly
|
||||||
* @property {number} timestamp Timestamp of the last access
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
static DIRECTORY = 'characters';
|
static DIRECTORY = 'characters';
|
||||||
|
|
||||||
/** @type {number} */
|
/**
|
||||||
static REMOVAL_INTERVAL = 5 * 60 * 1000;
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
static SYNC_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
/** @type {import('node-persist').LocalStorage} */
|
/** @type {import('node-persist').LocalStorage} */
|
||||||
#instance;
|
#instance;
|
||||||
|
|
||||||
/** @type {NodeJS.Timeout} */
|
/** @type {NodeJS.Timeout} */
|
||||||
#removalInterval;
|
#syncInterval;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue for removal of cache entries.
|
* Queue of user handles to sync.
|
||||||
* @type {CacheRemovalQueueItem[]}
|
* @type {Set<string>}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
removalQueue = [];
|
syncQueue = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the cache directory.
|
* Path to the cache directory.
|
||||||
@@ -74,49 +76,21 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the removal queue.
|
* Processes the synchronization queue.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async #removeCacheEntries() {
|
async #syncCacheEntries() {
|
||||||
// TODO: consider running this.verify() for a user instead as getting all cache keys
|
|
||||||
// is a heavy operation, since it requires to read all files in the disk cache.
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!useDiskCache || this.removalQueue.length === 0) {
|
if (!useDiskCache || this.syncQueue.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Map<string, number>} */
|
const directories = [...this.syncQueue].map(entry => getUserDirectories(entry));
|
||||||
const latestTimestamps = new Map();
|
this.syncQueue.clear();
|
||||||
for (const { item, timestamp } of this.removalQueue) {
|
|
||||||
if (!latestTimestamps.has(item) || timestamp > (latestTimestamps.get(item) ?? 0)) {
|
|
||||||
latestTimestamps.set(item, timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.removalQueue.length = 0;
|
|
||||||
|
|
||||||
const cache = await this.instance();
|
await this.verify(directories);
|
||||||
const keys = await cache.keys();
|
|
||||||
|
|
||||||
for (const [item, timestamp] of latestTimestamps.entries()) {
|
|
||||||
const itemKeys = keys.filter(k => k.startsWith(item));
|
|
||||||
if (!itemKeys.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const key of itemKeys) {
|
|
||||||
const datumPath = cache.getDatumPath(key);
|
|
||||||
if (!fs.existsSync(datumPath)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const stat = fs.statSync(datumPath);
|
|
||||||
if (stat.mtimeMs > timestamp) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await cache.removeItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while removing cache entries:', error);
|
console.error('Error while synchronizing cache entries:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +105,7 @@ class DiskCache {
|
|||||||
|
|
||||||
this.#instance = storage.create({ dir: this.cachePath, ttl: false });
|
this.#instance = storage.create({ dir: this.cachePath, ttl: false });
|
||||||
await this.#instance.init();
|
await this.#instance.init();
|
||||||
this.#removalInterval = setInterval(this.#removeCacheEntries.bind(this), DiskCache.REMOVAL_INTERVAL);
|
this.#syncInterval = setInterval(this.#syncCacheEntries.bind(this), DiskCache.SYNC_INTERVAL);
|
||||||
return this.#instance;
|
return this.#instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +125,8 @@ class DiskCache {
|
|||||||
const files = fs.readdirSync(dir.characters, { withFileTypes: true });
|
const files = fs.readdirSync(dir.characters, { withFileTypes: true });
|
||||||
for (const file of files.filter(f => f.isFile() && path.extname(f.name) === '.png')) {
|
for (const file of files.filter(f => f.isFile() && path.extname(f.name) === '.png')) {
|
||||||
const filePath = path.join(dir.characters, file.name);
|
const filePath = path.join(dir.characters, file.name);
|
||||||
const stat = fs.statSync(filePath);
|
const cacheKey = getCacheKey(filePath);
|
||||||
validKeys.add(path.parse(cache.getDatumPath(`${filePath}-${stat.mtimeMs}`)).base);
|
validKeys.add(path.parse(cache.getDatumPath(cacheKey)).base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key of this.hashedKeys) {
|
for (const key of this.hashedKeys) {
|
||||||
@@ -163,14 +137,28 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
if (this.#removalInterval) {
|
if (this.#syncInterval) {
|
||||||
clearInterval(this.#removalInterval);
|
clearInterval(this.#syncInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const diskCache = new DiskCache();
|
export const diskCache = new DiskCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cache key for the specified image file.
|
||||||
|
* @param {string} inputFile - Path to the image file
|
||||||
|
* @returns {string} - Cache key
|
||||||
|
*/
|
||||||
|
function getCacheKey(inputFile) {
|
||||||
|
if (fs.existsSync(inputFile)) {
|
||||||
|
const stat = fs.statSync(inputFile);
|
||||||
|
return `${inputFile}-${stat.mtimeMs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputFile;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the character card from the specified image file.
|
* Reads the character card from the specified image file.
|
||||||
* @param {string} inputFile - Path to the image file
|
* @param {string} inputFile - Path to the image file
|
||||||
@@ -178,8 +166,7 @@ export const diskCache = new DiskCache();
|
|||||||
* @returns {Promise<string | undefined>} - Character card data
|
* @returns {Promise<string | undefined>} - Character card data
|
||||||
*/
|
*/
|
||||||
async function readCharacterData(inputFile, inputFormat = 'png') {
|
async function readCharacterData(inputFile, inputFormat = 'png') {
|
||||||
const stat = fs.statSync(inputFile);
|
const cacheKey = getCacheKey(inputFile);
|
||||||
const cacheKey = `${inputFile}-${stat.mtimeMs}`;
|
|
||||||
if (memoryCache.has(cacheKey)) {
|
if (memoryCache.has(cacheKey)) {
|
||||||
return memoryCache.get(cacheKey);
|
return memoryCache.get(cacheKey);
|
||||||
}
|
}
|
||||||
@@ -220,7 +207,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (useDiskCache && !Buffer.isBuffer(inputFile)) {
|
if (useDiskCache && !Buffer.isBuffer(inputFile)) {
|
||||||
diskCache.removalQueue.push({ item: inputFile, timestamp: Date.now() });
|
diskCache.syncQueue.add(request.user.profile.handle);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Read the image, resize, and save it as a PNG into the buffer.
|
* Read the image, resize, and save it as a PNG into the buffer.
|
||||||
|
Reference in New Issue
Block a user