import type { SchemaOverview } from '@directus/types'; import { getSimpleHash } from '@directus/utils'; import type { Options } from 'keyv'; import Keyv from 'keyv'; import env from './env.js'; import logger from './logger.js'; import { getMessenger } from './messenger.js'; import { compress, decompress } from './utils/compress.js'; import { getConfigFromEnv } from './utils/get-config-from-env.js'; import { getMilliseconds } from './utils/get-milliseconds.js'; import { validateEnv } from './utils/validate-env.js'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); let cache: Keyv | null = null; let systemCache: Keyv | null = null; let localSchemaCache: Keyv | null = null; let sharedSchemaCache: Keyv | null = null; let lockCache: Keyv | null = null; let messengerSubscribed = false; type Store = 'memory' | 'redis'; const messenger = getMessenger(); if ( env['MESSENGER_STORE'] === 'redis' && env['CACHE_STORE'] === 'memory' && env['CACHE_AUTO_PURGE'] && !messengerSubscribed ) { messengerSubscribed = true; messenger.subscribe('schemaChanged', async (opts) => { if (cache && opts?.['autoPurgeCache'] !== false) { await cache.clear(); } }); } export function getCache(): { cache: Keyv | null; systemCache: Keyv; sharedSchemaCache: Keyv; localSchemaCache: Keyv; lockCache: Keyv; } { if (env['CACHE_ENABLED'] === true && cache === null) { validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']); cache = getKeyvInstance(env['CACHE_STORE'], getMilliseconds(env['CACHE_TTL'])); cache.on('error', (err) => logger.warn(err, `[cache] ${err}`)); } if (systemCache === null) { systemCache = getKeyvInstance(env['CACHE_STORE'], getMilliseconds(env['CACHE_SYSTEM_TTL']), '_system'); systemCache.on('error', (err) => logger.warn(err, `[system-cache] ${err}`)); } if (sharedSchemaCache === null) { sharedSchemaCache = getKeyvInstance(env['CACHE_STORE'], getMilliseconds(env['CACHE_SYSTEM_TTL']), '_schema_shared'); sharedSchemaCache.on('error', (err) => logger.warn(err, `[shared-schema-cache] ${err}`)); } if (localSchemaCache === null) { localSchemaCache = getKeyvInstance('memory', getMilliseconds(env['CACHE_SYSTEM_TTL']), '_schema'); localSchemaCache.on('error', (err) => logger.warn(err, `[schema-cache] ${err}`)); } if (lockCache === null) { lockCache = getKeyvInstance(env['CACHE_STORE'], undefined, '_lock'); lockCache.on('error', (err) => logger.warn(err, `[lock-cache] ${err}`)); } return { cache, systemCache, sharedSchemaCache, localSchemaCache, lockCache }; } export async function flushCaches(forced?: boolean): Promise { const { cache } = getCache(); await clearSystemCache({ forced }); await cache?.clear(); } export async function clearSystemCache(opts?: { forced?: boolean | undefined; autoPurgeCache?: false | undefined; }): Promise { const { systemCache, localSchemaCache, lockCache, sharedSchemaCache } = getCache(); // Flush system cache when forced or when system cache lock not set if (opts?.forced || !(await lockCache.get('system-cache-lock'))) { await lockCache.set('system-cache-lock', true, 10000); await systemCache.clear(); await lockCache.delete('system-cache-lock'); } await sharedSchemaCache.clear(); await localSchemaCache.clear(); messenger.publish('schemaChanged', { autoPurgeCache: opts?.autoPurgeCache }); } export async function setSystemCache(key: string, value: any, ttl?: number): Promise { const { systemCache, lockCache } = getCache(); if (!(await lockCache.get('system-cache-lock'))) { await setCacheValue(systemCache, key, value, ttl); } } export async function getSystemCache(key: string): Promise> { const { systemCache } = getCache(); return await getCacheValue(systemCache, key); } export async function setSchemaCache(schema: SchemaOverview): Promise { const { localSchemaCache, sharedSchemaCache } = getCache(); const schemaHash = await getSimpleHash(JSON.stringify(schema)); await sharedSchemaCache.set('hash', schemaHash); await localSchemaCache.set('schema', schema); await localSchemaCache.set('hash', schemaHash); } export async function getSchemaCache(): Promise { const { localSchemaCache, sharedSchemaCache } = getCache(); const sharedSchemaHash = await sharedSchemaCache.get('hash'); if (!sharedSchemaHash) return; const localSchemaHash = await localSchemaCache.get('hash'); if (!localSchemaHash || localSchemaHash !== sharedSchemaHash) return; return await localSchemaCache.get('schema'); } export async function setCacheValue( cache: Keyv, key: string, value: Record | Record[], ttl?: number ) { const compressed = await compress(value); await cache.set(key, compressed, ttl); } export async function getCacheValue(cache: Keyv, key: string): Promise { const value = await cache.get(key); if (!value) return undefined; const decompressed = await decompress(value); return decompressed; } function getKeyvInstance(store: Store, ttl: number | undefined, namespaceSuffix?: string): Keyv { switch (store) { case 'redis': return new Keyv(getConfig('redis', ttl, namespaceSuffix)); case 'memory': default: return new Keyv(getConfig('memory', ttl, namespaceSuffix)); } } function getConfig(store: Store = 'memory', ttl: number | undefined, namespaceSuffix = ''): Options { const config: Options = { namespace: `${env['CACHE_NAMESPACE']}${namespaceSuffix}`, ttl, }; if (store === 'redis') { const KeyvRedis = require('@keyv/redis'); config.store = new KeyvRedis(env['REDIS'] || getConfigFromEnv('REDIS')); } return config; }