Cache session replay on client
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user