Files
OmniRoute/src/lib/db/readCache.ts
oyi77 0420144104 feat: memory 500 fix, skills marketplace (SkillsMP), DB cleanup, LKGP toggle, and upstream 400 fixes
- fix(memory): wrap JSON.parse in try/catch in retrieval.ts to prevent 500 on corrupt metadata; add stats to GET /api/memory response
- fix(#949): add sanitizeToolId() to replace invalid chars in cross-provider tool IDs; clamp max_tokens to Math.max(1, value)
- feat(skills): add SkillsMP marketplace integration (search + install via /api/skills/marketplace); add skill upload/install modal and DELETE endpoint; wire skillsEnabled guard in executor
- feat(settings): add Maintenance section to General tab (Clear Cache + Purge Expired Logs buttons)
- feat(lkgp): add lkgpEnabled toggle and Clear LKGP Cache button to Routing settings; add clearAllLKGP(); respect toggle in LKGPStrategyImpl
- test: add vitest coverage for all new functionality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:53:22 +07:00

144 lines
4.5 KiB
TypeScript

/**
* DB Read Cache — In-memory TTL cache for hot read paths.
*
* SQLite reads are already fast since better-sqlite3 is synchronous and
* memory-mapped. However, some functions (getSettings, getPricing,
* getProviderConnections) are called on every request by multiple callers.
* A short TTL cache (5s) eliminates redundant I/O without staling data for
* long enough to matter (settings changes are applied within one cache cycle).
*
* Usage:
* import { dbCache } from '@/lib/db/readCache';
* const settings = await dbCache.getSettings();
*/
type CacheEntry<T> = {
value: T;
expiresAt: number;
};
class TTLCache<T> {
private cache = new Map<string, CacheEntry<T>>();
private readonly ttlMs: number;
constructor(ttlMs: number) {
this.ttlMs = ttlMs;
}
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
set(key: string, value: T): void {
this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
}
invalidate(key?: string): void {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
// Cache with 5s TTL — short enough to pick up dashboard changes quickly,
// long enough to serve burst request bursts without hammering SQLite.
const SETTINGS_TTL_MS = 5_000;
const PRICING_TTL_MS = 30_000;
const CONNECTIONS_TTL_MS = 5_000;
const settingsCache = new TTLCache<Record<string, unknown>>(SETTINGS_TTL_MS);
const pricingCache = new TTLCache<Record<string, unknown>>(PRICING_TTL_MS);
const connectionsCache = new TTLCache<unknown[]>(CONNECTIONS_TTL_MS);
/**
* Cached wrapper for getSettings.
* Invalidated on every updateSettings() call.
*/
export async function getCachedSettings(): Promise<Record<string, unknown>> {
const cached = settingsCache.get("settings");
if (cached) return cached;
const { getSettings } = await import("@/lib/db/settings");
const value = await getSettings();
settingsCache.set("settings", value);
return value;
}
/**
* Cached wrapper for getPricing.
* Longer TTL since pricing rarely changes mid-session.
*/
export async function getCachedPricing(): Promise<Record<string, unknown>> {
const cached = pricingCache.get("pricing");
if (cached) return cached as Record<string, unknown>;
const { getPricing } = await import("@/lib/db/settings");
const value = await getPricing();
pricingCache.set("pricing", value);
return value;
}
/**
* Cached wrapper for getProviderConnections.
* Used in request hot-paths (usageStats, callLogs, usageHistory).
*/
export async function getCachedProviderConnections(
filter?: Record<string, unknown>
): Promise<unknown[]> {
// Only cache the unfiltered "all connections" query (most common)
if (filter && Object.keys(filter).length > 0) {
const { getProviderConnections } = await import("@/lib/db/providers");
return getProviderConnections(filter);
}
const cached = connectionsCache.get("all");
if (cached) return cached;
const { getProviderConnections } = await import("@/lib/db/providers");
const value = await getProviderConnections();
connectionsCache.set("all", value);
return value;
}
// ──────────────── LKGP Cache Wrappers ────────────────
const lkgpCache = new TTLCache<string | null>(SETTINGS_TTL_MS);
export async function getCachedLKGP(comboName: string, modelId: string): Promise<string | null> {
const cacheKey = `lkgp:${comboName}:${modelId}`;
const cached = lkgpCache.get(cacheKey);
if (cached !== undefined) return cached;
const { getLKGP } = await import("@/lib/db/settings");
const value = await getLKGP(comboName, modelId);
lkgpCache.set(cacheKey, value);
return value;
}
export async function setCachedLKGP(
comboName: string,
modelId: string,
providerId: string
): Promise<void> {
const { setLKGP } = await import("@/lib/db/settings");
await setLKGP(comboName, modelId, providerId);
lkgpCache.invalidate(`lkgp:${comboName}:${modelId}`);
}
/**
* Invalidate all caches (call after writes to any of: settings, pricing, connections).
*/
export function invalidateDbCache(scope?: "settings" | "pricing" | "connections"): void {
if (!scope || scope === "settings") settingsCache.invalidate();
if (!scope || scope === "pricing") pricingCache.invalidate();
if (!scope || scope === "connections") connectionsCache.invalidate();
}