diff --git a/lib/config/index.ts b/lib/config/index.ts index 0ef1f72..49a280f 100644 --- a/lib/config/index.ts +++ b/lib/config/index.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { env } from '../env'; -import { loadYaml, validateUuid } from './parser'; +import { loadYaml, clearYamlCache, validateUuid } from './parser'; import { resolveTheme, VALID_LAYOUTS } from './theme'; import { slugify, @@ -22,6 +22,7 @@ let _fallbackSecret: string | null = null; /** Invalidate the cached config so the next getConfig() call re-reads YAML files. */ export function invalidateConfigCache(): void { _config = null; + clearYamlCache(); } /** Converts raw YAML grid overrides into a typed partial GridConfig. */ @@ -49,8 +50,11 @@ export function buildSubpageGrid(raw?: { } export function getConfig(): AppConfig { - const isDev = process.env.NODE_ENV !== 'production'; - if (_config && !isDev) return _config; + // _config is a per-worker in-memory cache. + // invalidateConfigCache() clears it + the underlying YAML mtime cache, + // so after an admin save every worker re-parses on next request. + // In dev we always re-parse so hot-reload works. + if (_config && process.env.NODE_ENV === 'production') return _config; const apiUrl = env.IMMICH_API_URL; const apiKey = env.IMMICH_API_KEY; diff --git a/lib/config/parser.ts b/lib/config/parser.ts index 9b22de7..e89a837 100644 --- a/lib/config/parser.ts +++ b/lib/config/parser.ts @@ -2,39 +2,41 @@ import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; -// ⚡ Bolt Optimization: Cache parsed YAML to prevent repetitive synchronous -// file I/O and expensive parsing during production runtime. -const yamlCache = new Map(); +interface YamlCacheEntry { + data: unknown; + mtimeMs: number; +} + +// mtime-based cache: re-reads the file if it has been modified since last parse. +// Works correctly across admin saves without requiring a Docker restart. +const yamlCache = new Map(); + +export function clearYamlCache(): void { + yamlCache.clear(); +} export function loadYaml(filename: string): T | null { - const isProd = process.env.NODE_ENV === 'production'; - - if (isProd && yamlCache.has(filename)) { - const cached = yamlCache.get(filename); - // ⚡ Bolt Optimization: Use `structuredClone` to prevent caller mutations - // from corrupting the cached object state, while preserving JS native types. - return cached === null ? null : structuredClone(cached as T); - } - const yamlPath = path.join(process.cwd(), 'content', filename); - if (!fs.existsSync(yamlPath)) { - // ⚡ Bolt Optimization: Implement negative caching for missing files. - if (isProd) { - yamlCache.set(filename, null); - } + let mtimeMs = 0; + try { + mtimeMs = fs.statSync(yamlPath).mtimeMs; + } catch { + // File does not exist + yamlCache.delete(filename); return null; } + const cached = yamlCache.get(filename); + if (cached && cached.mtimeMs === mtimeMs) { + return cached.data === null ? null : structuredClone(cached.data as T); + } + const raw = fs.readFileSync(yamlPath, 'utf8'); const parsed = (yaml.load(raw) || {}) as T; - if (isProd) { - yamlCache.set(filename, parsed); - return structuredClone(parsed); - } - - return parsed; + yamlCache.set(filename, { data: parsed, mtimeMs }); + return structuredClone(parsed); } const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;