Files
ln-bi/src/server/routes/energy/cache.ts
kkfluous c02c1aa62c perf(energy): add 60s TTL cache for all 4 endpoints
Hydrogen overview was 1.8-2.0s (3 full-table aggregations on 66K rows).
With cache: cold 1 user/min eats the full query, all subsequent within
60s window return in ~10ms.

Implementation:
- New cache.ts with cached(key, loader) helper
- Per-key in-flight de-duplication: concurrent requests share one loader
- Each handler wrapped, cache key includes query params
  (e.g. "hydrogen/daily?range=last30&customer=external")
- TTL 60s as requested

Measured speedups:
- hydrogen/overview: 1.96s → 12ms (165x)
- hydrogen/daily:    270ms → 11ms (24x)
- electric/overview:  93ms →  9ms (10x)
- electric/monthly:   36ms →  9ms (4x)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:50:48 +08:00

45 lines
1.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 简单 TTL 内存缓存。
* 命中:直接返回缓存值;过期或未命中:运行 loader、存入缓存。
* 同一 key 并发请求只会触发一次 loader共享 in-flight Promise
*/
interface Entry<T> {
value: T;
expiresAt: number;
}
const TTL_MS = 60 * 1000;
const cache = new Map<string, Entry<unknown>>();
const inflight = new Map<string, Promise<unknown>>();
export async function cached<T>(key: string, loader: () => Promise<T>): Promise<T> {
const now = Date.now();
const hit = cache.get(key);
if (hit && hit.expiresAt > now) {
return hit.value as T;
}
// 同一 key 并发只跑一次 loader
const ongoing = inflight.get(key) as Promise<T> | undefined;
if (ongoing) return ongoing;
const p = loader()
.then(value => {
cache.set(key, { value, expiresAt: Date.now() + TTL_MS });
return value;
})
.finally(() => {
inflight.delete(key);
});
inflight.set(key, p as Promise<unknown>);
return p;
}
/** 仅用于测试或调试:清空所有缓存 */
export function _clearEnergyCache() {
cache.clear();
inflight.clear();
}