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>
This commit is contained in:
kkfluous
2026-04-28 17:50:48 +08:00
parent 9a4f1945d9
commit c02c1aa62c
2 changed files with 63 additions and 4 deletions

View File

@@ -0,0 +1,44 @@
/**
* 简单 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();
}