fix: reload config without docker restart – mtime-based yaml cache instead of immortal in-memory cache
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<string, unknown>();
|
||||
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<string, YamlCacheEntry>();
|
||||
|
||||
export function clearYamlCache(): void {
|
||||
yamlCache.clear();
|
||||
}
|
||||
|
||||
export function loadYaml<T>(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;
|
||||
|
||||
Reference in New Issue
Block a user