Cache session replay on client

This commit is contained in:
wuyang6
2026-06-12 17:28:15 +08:00
parent ddf06c727d
commit 00a83ba4c3
4 changed files with 286 additions and 56 deletions
+115
View File
@@ -0,0 +1,115 @@
"use client";
export type SessionReplaySnapshot<TSession = unknown, TRun = unknown> = {
sessionId: string;
session: TSession;
runStatus: TRun | null;
signature: string;
fetchedAt: number;
};
const MAX_REPLAY_CACHE_SIZE = 50;
const replayCache = new Map<string, SessionReplaySnapshot>();
export function readCachedSessionReplay<TSession = unknown, TRun = unknown>(
sessionId: string,
): SessionReplaySnapshot<TSession, TRun> | null {
const cached = replayCache.get(sessionId);
if (!cached) return null;
// Refresh recency for LRU behavior.
replayCache.delete(sessionId);
replayCache.set(sessionId, cached);
return cached as SessionReplaySnapshot<TSession, TRun>;
}
export function invalidateCachedSessionReplay(sessionId: string) {
replayCache.delete(sessionId);
}
export async function fetchSessionReplaySnapshot<
TSession = unknown,
TRun = unknown,
>(
sessionId: string,
options: { signal?: AbortSignal } = {},
): Promise<SessionReplaySnapshot<TSession, TRun> | null> {
const [sessionResponse, runStatus] = await Promise.all([
fetch(`/api/claw/sessions/${encodeURIComponent(sessionId)}`, {
cache: "no-store",
signal: options.signal,
}),
fetchLatestRunStatusForCache<TRun>(sessionId, options.signal),
]);
if (!sessionResponse.ok) return null;
const session = (await sessionResponse.json()) as TSession;
return writeCachedSessionReplay(sessionId, session, runStatus);
}
function writeCachedSessionReplay<TSession, TRun>(
sessionId: string,
session: TSession,
runStatus: TRun | null,
): SessionReplaySnapshot<TSession, TRun> {
const snapshot: SessionReplaySnapshot<TSession, TRun> = {
sessionId,
session,
runStatus,
signature: sessionReplaySignature(session, runStatus),
fetchedAt: Date.now(),
};
replayCache.set(sessionId, snapshot as SessionReplaySnapshot);
while (replayCache.size > MAX_REPLAY_CACHE_SIZE) {
const oldest = replayCache.keys().next().value;
if (!oldest) break;
replayCache.delete(oldest);
}
return snapshot;
}
async function fetchLatestRunStatusForCache<TRun>(
sessionId: string,
signal?: AbortSignal,
): Promise<TRun | null> {
try {
const url = new URL("/api/claw/runs/latest", window.location.origin);
url.searchParams.set("session_id", sessionId);
const response = await fetch(url, { cache: "no-store", signal });
if (!response.ok) return null;
return (await response.json()) as TRun;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw error;
}
return null;
}
}
function sessionReplaySignature(session: unknown, runStatus: unknown) {
const sessionObject = isObject(session) ? session : {};
const runObject = isObject(runStatus) ? runStatus : {};
const messages = Array.isArray(sessionObject.messages)
? sessionObject.messages
: [];
const lastMessage = messages.at(-1);
const lastObject = isObject(lastMessage) ? lastMessage : {};
const metadata = isObject(lastObject.metadata) ? lastObject.metadata : {};
return [
sessionObject.session_id,
sessionObject.turns,
sessionObject.tool_calls,
messages.length,
lastObject.role,
lastObject.message_id,
typeof lastObject.content === "string" ? lastObject.content.length : "",
metadata.elapsed_ms,
runObject.run_id,
runObject.status,
runObject.updated_at,
runObject.finished_at,
runObject.elapsed_ms,
].join("|");
}
function isObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}