mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 02:12:59 +00:00
perf(backend): cache global_settings reads in DatabaseService (#814)
getGlobalSettings() runs a SELECT * on every call and is hit from 22 files, including the auth middleware (every authenticated request), the WebSocket upgrade handler (every connection), and the debug-mode gate (every diagnostic log line). Cache the result inside the service on first read and invalidate on updateGlobalSetting(). The cached snapshot is Object.freeze'd and the public return type is now Readonly<Record<string, string>> so accidental mutations are caught at compile time. The settings GET handler that delete'd private keys now takes a defensive shallow copy first. The 5-second TTL cache in utils/debug.ts is now redundant and removed; the service-level cache is strictly fresher (invalidates on write rather than going stale for up to 5s).
This commit is contained in:
@@ -41,7 +41,7 @@ export const settingsRouter = Router();
|
||||
|
||||
settingsRouter.get('/', authMiddleware, async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const settings = DatabaseService.getInstance().getGlobalSettings();
|
||||
const settings = { ...DatabaseService.getInstance().getGlobalSettings() };
|
||||
for (const key of PRIVATE_SETTINGS_KEYS) {
|
||||
delete settings[key];
|
||||
}
|
||||
|
||||
@@ -470,6 +470,13 @@ export interface ScanSummary {
|
||||
export class DatabaseService {
|
||||
private static instance: DatabaseService;
|
||||
private db: Database.Database;
|
||||
// Cache of the global_settings table, populated on first read and
|
||||
// invalidated by updateGlobalSetting(). Hot paths (auth middleware,
|
||||
// WS upgrade, the audit-log debug gate) read this on every request,
|
||||
// so the round-trip to SQLite is worth eliminating. Assumes this
|
||||
// process is the sole writer to global_settings; sidecar tools that
|
||||
// edit the row directly will not invalidate the cache.
|
||||
private cachedGlobalSettings: Readonly<Record<string, string>> | null = null;
|
||||
|
||||
private constructor() {
|
||||
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
||||
@@ -497,6 +504,11 @@ export class DatabaseService {
|
||||
this.migrateAgentsAndNotificationsNodeId();
|
||||
this.migratePolicyEvaluationColumn();
|
||||
this.migrateNotificationCategory();
|
||||
|
||||
// Reset the cache once at end of constructor in case any migration
|
||||
// populated it via getGlobalSettings() and a subsequent migration
|
||||
// changed the underlying rows.
|
||||
this.cachedGlobalSettings = null;
|
||||
}
|
||||
|
||||
public static getInstance(): DatabaseService {
|
||||
@@ -1330,18 +1342,20 @@ export class DatabaseService {
|
||||
|
||||
// --- Global Settings ---
|
||||
|
||||
public getGlobalSettings(): Record<string, string> {
|
||||
public getGlobalSettings(): Readonly<Record<string, string>> {
|
||||
if (this.cachedGlobalSettings) return this.cachedGlobalSettings;
|
||||
const stmt = this.db.prepare('SELECT * FROM global_settings');
|
||||
const rows = stmt.all() as Array<{ key: string; value: string }>;
|
||||
const settings: Record<string, string> = {};
|
||||
stmt.all().forEach((row: any) => {
|
||||
settings[row.key] = row.value;
|
||||
});
|
||||
return settings;
|
||||
for (const row of rows) settings[row.key] = row.value;
|
||||
this.cachedGlobalSettings = Object.freeze(settings);
|
||||
return this.cachedGlobalSettings;
|
||||
}
|
||||
|
||||
public updateGlobalSetting(key: string, value: string): void {
|
||||
const stmt = this.db.prepare('INSERT OR REPLACE INTO global_settings (key, value) VALUES (?, ?)');
|
||||
stmt.run(key, value);
|
||||
this.cachedGlobalSettings = null;
|
||||
}
|
||||
|
||||
// --- System State (operational/runtime values - not user-defined config) ---
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
/**
|
||||
* Shared diagnostic logging gate.
|
||||
*
|
||||
* Reads `developer_mode` from the global settings, cached for a short
|
||||
* window so hot paths (per-request, per-log-line) do not hit SQLite
|
||||
* on every call.
|
||||
* Shared diagnostic logging gate. Reads `developer_mode` from
|
||||
* DatabaseService, which caches the global_settings snapshot internally
|
||||
* and invalidates on write, so hot-path callers can query freely.
|
||||
*/
|
||||
|
||||
let cachedValue = false;
|
||||
let cacheExpiry = 0;
|
||||
const CACHE_TTL_MS = 5_000;
|
||||
|
||||
export function isDebugEnabled(): boolean {
|
||||
const now = Date.now();
|
||||
if (now < cacheExpiry) return cachedValue;
|
||||
|
||||
try {
|
||||
// Dynamic require avoids circular-dependency issues when this
|
||||
// utility is imported from services that DatabaseService itself
|
||||
// depends on, and prevents SQLite side effects during tests.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { DatabaseService } = require('../services/DatabaseService');
|
||||
cachedValue = DatabaseService.getInstance().getGlobalSettings().developer_mode === '1';
|
||||
return DatabaseService.getInstance().getGlobalSettings().developer_mode === '1';
|
||||
} catch {
|
||||
cachedValue = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
cacheExpiry = now + CACHE_TTL_MS;
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user