repo_description: The flexible backend for all your projects 🐰 Turn your DB into a headless CMS, admin panels, or apps with a custom UI, instant APIs, auth & more.
repo_topics: api app cms composable data-visualization database directus graphql headless-cms javascript mariadb mssql mysql no-code node postgresql sql sqlite typescript vue
code: import { createRequire } from 'node:module';
  import { useEnv } from '@directus/env';
  import type { SchemaOverview } from '@directus/types';
  import Keyv, { type KeyvOptions } from 'keyv';
  import { useBus } from './bus/index.js';
  import { useLogger } from './logger/index.js';
  import { clearCache as clearPermissionCache } from './permissions/cache.js';
  import { redisConfigAvailable } from './redis/index.js';
  import { compress, decompress } from './utils/compress.js';
  import { freezeSchema, unfreezeSchema } from './utils/freeze-schema.js';
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
  import { getMilliseconds } from './utils/get-milliseconds.js';
  import { validateEnv } from './utils/validate-env.js';
  
  const logger = useLogger();
  const env = useEnv();
  
  const require = createRequire(import.meta.url);
  
  let cache: Keyv | null = null;
  let systemCache: Keyv | null = null;
  let deploymentCache: Keyv | null = null;
  let lockCache: Keyv | null = null;
  let messengerSubscribed = false;
  
  let localSchemaCache: Keyv | null = null;
  let memorySchemaCache: Readonly<SchemaOverview> | null = null;
  
  type Store = 'memory' | 'redis';
  
  const messenger = useBus();
  
  interface CacheMessage {
  	autoPurgeCache: boolean | undefined;
  }
  
  interface CacheMessage {
  	autoPurgeCache: boolean | undefined;
  }
  
  if (redisConfigAvailable() && !messengerSubscribed) {
  	messengerSubscribed = true;
  
  	messenger.subscribe<CacheMessage>('schemaChanged', async (opts) => {
  		if (env['CACHE_STORE'] === 'memory' && env['CACHE_AUTO_PURGE'] && cache && opts?.['autoPurgeCache'] !== false) {
  			await cache.clear();
  		}
  
  		await localSchemaCache?.clear();
  		memorySchemaCache = null;
  	});
  }
  
  export function getCache(): {
  	cache: Keyv | null;
  	systemCache: Keyv;
  	deploymentCache: Keyv;
  	localSchemaCache: Keyv;
  	lockCache: Keyv;
  } {
  	if (env['CACHE_ENABLED'] === true && cache === null) {
  		validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
  		cache = getKeyvInstance(env['CACHE_STORE'] as Store, getMilliseconds(env['CACHE_TTL']));
  		cache.on('error', (err) => logger.warn(err, `[cache] ${err}`));
  	}
  
  	if (systemCache === null) {
  		systemCache = getKeyvInstance(env['CACHE_STORE'] as Store, getMilliseconds(env['CACHE_SYSTEM_TTL']), '_system');
  		systemCache.on('error', (err) => logger.warn(err, `[system-cache] ${err}`));
  	}
  
  	if (deploymentCache === null) {
  		const ttl = getMilliseconds(env['CACHE_DEPLOYMENT_TTL']) || 5000; // Default 5s
  		deploymentCache = getKeyvInstance(env['CACHE_STORE'] as Store, ttl, '_deployment');
  		deploymentCache.on('error', (err) => logger.warn(err, `[deployment-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'] as Store, undefined, '_lock');
  		lockCache.on('error', (err) => logger.warn(err, `[lock-cache] ${err}`));
  	}
  
  	return { cache, systemCache, deploymentCache, localSchemaCache, lockCache };
  }
  
  export async function flushCaches(forced?: boolean): Promise<void> {
  	const { cache } = getCache();
  	await clearSystemCache({ forced });
  	await cache?.clear();
  }
  
  export async function clearSystemCache(opts?: {
  	forced?: boolean | undefined;
  	autoPurgeCache?: false | undefined;
  }): Promise<void> {
  	const { systemCache, localSchemaCache, lockCache } = 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 localSchemaCache.clear();
  	memorySchemaCache = null;
  
  	// Since a lot of cached permission function rely on the schema it needs to be cleared as well
  	await clearPermissionCache();
  
  	messenger.publish<CacheMessage>('schemaChanged', { autoPurgeCache: opts?.autoPurgeCache });
  }
  
  export async function setSystemCache(key: string, value: any, ttl?: number): Promise<void> {
  	const { systemCache, lockCache } = getCache();
  
  	if (!(await lockCache.get('system-cache-lock'))) {
  		await setCacheValue(systemCache, key, value, ttl);
  	}
  }
  
  export async function getSystemCache(key: string): Promise<Record<string, any>> {
  	const { systemCache } = getCache();
  
  	return await getCacheValue(systemCache, key);
  }
  
  export function setMemorySchemaCache(schema: SchemaOverview) {
  	if (Object.isFrozen(schema)) {
  		memorySchemaCache = schema;
  	} else {
  		memorySchemaCache = freezeSchema(schema);
  	}
  }
  
  export function getMemorySchemaCache(): Readonly<SchemaOverview> | undefined {
  	if (env['CACHE_SCHEMA_FREEZE_ENABLED']) {
  		return memorySchemaCache ?? undefined;
  	} else if (memorySchemaCache) {
  		return unfreezeSchema(memorySchemaCache);
  	}
  
  	return undefined;
  }
  
  export async function setCacheValue(
  	cache: Keyv,
  	key: string,
  	value: Record<string, any> | Record<string, any>[],
  	ttl?: number,
  ) {
  	const compressed = await compress(value);
  	await cache.set(key, compressed, ttl);
  }
  
  export async function getCacheValue(cache: Keyv, key: string): Promise<any> {
  	const value = await cache.get(key);
  	if (!value) return undefined;
  	const decompressed = await decompress(value);
  	return decompressed;
  }
  
  /**
   * Store a value in cache with its expiration timestamp for TTL tracking
   */
  export async function setCacheValueWithExpiry(
  	cache: Keyv,
  	key: string,
  	value: Record<string, any> | Record<string, any>[],
  	ttl: number,
  ) {
  	await setCacheValue(cache, key, value, ttl);
  	await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + ttl }, ttl);
  }
  
  /**
   * Get a cached value along with its remaining TTL
   */
  export async function getCacheValueWithTTL(
  	cache: Keyv,
  	key: string,
  ): Promise<{ data: any; remainingTTL: number } | undefined> {
  	const value = await getCacheValue(cache, key);
  
  	if (!value) return undefined;
  
  	const expiryData = await getCacheValue(cache, `${key}__expires_at`);
  	const remainingTTL = expiryData?.exp ? Math.max(0, expiryData.exp - Date.now()) : 0;
  
  	return { data: value, remainingTTL };
  }
  
  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 = ''): KeyvOptions {
  	const config: KeyvOptions = {
  		namespace: `${env['CACHE_NAMESPACE']}${namespaceSuffix}`,
  		...(ttl && { ttl }),
  	};
  
  	if (store === 'redis') {
  		const { default: KeyvRedis } = require('@keyv/redis');
  		config.store = new KeyvRedis(env['REDIS'] || getConfigFromEnv('REDIS'), { useRedisSets: false });
  	}
  
  	return config;
  }
  