feat: add cache, fix ui

This commit is contained in:
2026-09-18 10:45:11 +08:00
parent 7b0de7056d
commit edf28006b2
10 changed files with 372 additions and 63 deletions
+152
View File
@@ -0,0 +1,152 @@
export interface SWRCacheOptions {
/**
* Data becomes stale after this duration.
* Stale data is returned immediately while a refresh happens.
*
* Default: 60 seconds
*/
minTimeToStale?: number;
/**
* Data is completely discarded after this duration.
* Requests wait for a fresh fetch.
*
* Default: Infinity
*/
maxTimeToLive?: number;
}
export interface SWRCacheResult<T> {
value: T;
stale: boolean;
updatedAt: number;
}
interface CacheEntry<T> {
value?: T;
updatedAt: number;
promise?: Promise<T>;
revalidating: boolean;
}
export class SWRCache {
private cache = new Map<string, CacheEntry<any>>();
async get<T>(
key: string,
fetcher: () => Promise<T>,
options: SWRCacheOptions = {},
): Promise<SWRCacheResult<T>> {
const {
minTimeToStale = 60_000,
maxTimeToLive = Number.POSITIVE_INFINITY,
} = options;
const now = Date.now();
let entry = this.cache.get(key) as CacheEntry<T> | undefined;
// No cache
if (!entry) {
entry = {
updatedAt: now,
revalidating: false,
};
entry.promise = fetcher().then((value) => {
entry!.value = value;
entry!.updatedAt = Date.now();
entry!.promise = undefined;
return value;
});
this.cache.set(key, entry);
return {
value: await entry.promise,
stale: false,
updatedAt: entry.updatedAt,
};
}
// Initial fetch still running
if (entry.promise) {
return {
value: await entry.promise,
stale: false,
updatedAt: entry.updatedAt,
};
}
const age = now - entry.updatedAt;
// Fresh
if (age < minTimeToStale) {
return {
value: entry.value!,
stale: false,
updatedAt: entry.updatedAt,
};
}
// Expired
if (age >= maxTimeToLive) {
entry.promise = fetcher().then((value) => {
entry!.value = value;
entry!.updatedAt = Date.now();
entry!.promise = undefined;
return value;
});
return {
value: await entry.promise,
stale: false,
updatedAt: entry.updatedAt,
};
}
// Stale -> return immediately and refresh in background
if (!entry.revalidating) {
entry.revalidating = true;
void fetcher()
.then((value) => {
entry!.value = value;
entry!.updatedAt = Date.now();
})
.catch(() => {
// ignore
})
.finally(() => {
entry!.revalidating = false;
});
}
return {
value: entry.value!,
stale: true,
updatedAt: entry.updatedAt,
};
}
invalidate(key: string) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
has(key: string) {
return this.cache.has(key);
}
peek<T>(key: string): T | undefined {
return this.cache.get(key)?.value;
}
}
const cacheMgr = new SWRCache();
export default cacheMgr;
+18 -5
View File
@@ -3,6 +3,7 @@ import { WebSocketServer, WebSocket } from "ws";
import type { BridgeRequest, RpcResponse } from "../shared/protocol";
import { initDb, insertMessage } from "./db";
import type { MessageDTO } from "../shared/dto";
import cacheMgr from "./lib/cache";
const PORT = 8787;
@@ -198,8 +199,10 @@ wss.on("connection", (ws) => {
let result: unknown;
const wa = getWhatsApp(request.deviceId);
const { method, params } = request;
const deviceId = request.deviceId || "";
switch (request.method) {
switch (method) {
case "listDevices":
result = [...bridgeSockets].map((ws) => ({
id: ws.deviceId,
@@ -208,16 +211,26 @@ wss.on("connection", (ws) => {
}));
break;
case "status":
case "listChats":
// cached
case "listContacts":
case "listGroups":
case "listChats":
const res = await cacheMgr.get(
method + ":" + deviceId,
async () => {
return call(wa, method, params);
},
{ minTimeToStale: method === "listContacts" ? 60 * 60_000 : 5_000 },
);
result = res.value;
break;
case "status":
case "getMessages":
case "sendText":
case "sendFile":
case "getMedia":
case "exec":
result = await call(wa, request.method, request.params);
result = await call(wa, method, params);
break;
default: