import { randomUUID } from "node:crypto" ;
import type { AcpSession } from "./types.js" ;
export type AcpSessionStore = {
createSession: (params: { sessionKey: string; cwd: string; sessionId?: string }) => AcpSession;
hasSession: (sessionId: string) => boolean ;
getSession: (sessionId: string) => AcpSession | undefined;
getSessionByRunId: (runId: string) => AcpSession | undefined;
setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void ;
clearActiveRun: (sessionId: string) => void ;
cancelActiveRun: (sessionId: string) => boolean ;
clearAllSessionsForTest: () => void ;
};
type AcpSessionStoreOptions = {
maxSessions?: number;
idleTtlMs?: number;
now?: () => number;
};
const DEFAULT_MAX_SESSIONS = 5 _000 ;
const DEFAULT_IDLE_TTL_MS = 24 * 60 * 60 * 1 _000 ;
export function createInMemorySessionStore(options: AcpSessionStoreOptions = {}): AcpSessionStore {
const maxSessions = Math.max(1 , options.maxSessions ?? DEFAULT_MAX_SESSIONS);
const idleTtlMs = Math.max(1 _000 , options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS);
const now = options.now ?? Date.now;
const sessions = new Map<string, AcpSession>();
const runIdToSessionId = new Map<string, string>();
const touchSession = (session: AcpSession, nowMs: number) => {
session.lastTouchedAt = nowMs;
};
const removeSession = (sessionId: string) => {
const session = sessions.get(sessionId);
if (!session) {
return false ;
}
if (session.activeRunId) {
runIdToSessionId.delete (session.activeRunId);
}
session.abortController?.abort();
sessions.delete (sessionId);
return true ;
};
const reapIdleSessions = (nowMs: number) => {
const idleBefore = nowMs - idleTtlMs;
for (const [sessionId, session] of sessions.entries()) {
if (session.activeRunId || session.abortController) {
continue ;
}
if (session.lastTouchedAt > idleBefore) {
continue ;
}
removeSession(sessionId);
}
};
const evictOldestIdleSession = () => {
let oldestSessionId: string | null = null ;
let oldestLastTouchedAt = Number.POSITIVE_INFINITY;
for (const [sessionId, session] of sessions.entries()) {
if (session.activeRunId || session.abortController) {
continue ;
}
if (session.lastTouchedAt >= oldestLastTouchedAt) {
continue ;
}
oldestLastTouchedAt = session.lastTouchedAt;
oldestSessionId = sessionId;
}
if (!oldestSessionId) {
return false ;
}
return removeSession(oldestSessionId);
};
const createSession: AcpSessionStore["createSession" ] = (params) => {
const nowMs = now();
const sessionId = params.sessionId ?? randomUUID();
const existingSession = sessions.get(sessionId);
if (existingSession) {
existingSession.sessionKey = params.sessionKey;
existingSession.cwd = params.cwd;
touchSession(existingSession, nowMs);
return existingSession;
}
reapIdleSessions(nowMs);
if (sessions.size >= maxSessions && !evictOldestIdleSession()) {
throw new Error(
`ACP session limit reached (max ${maxSessions}). Close idle ACP clients and retry.`,
);
}
const session: AcpSession = {
sessionId,
sessionKey: params.sessionKey,
cwd: params.cwd,
createdAt: nowMs,
lastTouchedAt: nowMs,
abortController: null ,
activeRunId: null ,
};
sessions.set(sessionId, session);
return session;
};
const hasSession: AcpSessionStore["hasSession" ] = (sessionId) => sessions.has(sessionId);
const getSession: AcpSessionStore["getSession" ] = (sessionId) => {
const session = sessions.get(sessionId);
if (session) {
touchSession(session, now());
}
return session;
};
const getSessionByRunId: AcpSessionStore["getSessionByRunId" ] = (runId) => {
const sessionId = runIdToSessionId.get(runId);
if (!sessionId) {
return undefined;
}
const session = sessions.get(sessionId);
if (session) {
touchSession(session, now());
}
return session;
};
const setActiveRun: AcpSessionStore["setActiveRun" ] = (sessionId, runId, abortController) => {
const session = sessions.get(sessionId);
if (!session) {
return ;
}
session.activeRunId = runId;
session.abortController = abortController;
runIdToSessionId.set(runId, sessionId);
touchSession(session, now());
};
const clearActiveRun: AcpSessionStore["clearActiveRun" ] = (sessionId) => {
const session = sessions.get(sessionId);
if (!session) {
return ;
}
if (session.activeRunId) {
runIdToSessionId.delete (session.activeRunId);
}
session.activeRunId = null ;
session.abortController = null ;
touchSession(session, now());
};
const cancelActiveRun: AcpSessionStore["cancelActiveRun" ] = (sessionId) => {
const session = sessions.get(sessionId);
if (!session?.abortController) {
return false ;
}
session.abortController.abort();
if (session.activeRunId) {
runIdToSessionId.delete (session.activeRunId);
}
session.abortController = null ;
session.activeRunId = null ;
touchSession(session, now());
return true ;
};
const clearAllSessionsForTest: AcpSessionStore["clearAllSessionsForTest" ] = () => {
for (const session of sessions.values()) {
session.abortController?.abort();
}
sessions.clear();
runIdToSessionId.clear();
};
return {
createSession,
hasSession,
getSession,
getSessionByRunId,
setActiveRun,
clearActiveRun,
cancelActiveRun,
clearAllSessionsForTest,
};
}
export const defaultAcpSessionStore = createInMemorySessionStore();
Messung V0.5 in Prozent C=99 H=95 G=96
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland