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>
45 lines
1.1 KiB
TypeScript
45 lines
1.1 KiB
TypeScript
/**
|
||
* 简单 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();
|
||
}
|