refactor: streamline query requests and session storage
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { config } from "../config.js";
|
||||
import {
|
||||
atomicWriteJson,
|
||||
ensureDirectory,
|
||||
listJsonFiles,
|
||||
readJsonFile,
|
||||
toStableId,
|
||||
} from "../utils/fileStore.js";
|
||||
@@ -50,11 +45,7 @@ const DEFAULT_SEARCH_MAX_QUERY_CHARS = 240;
|
||||
export class SessionTranscriptStore {
|
||||
private readonly writeQueues = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(private readonly baseDir = config.SESSION_TRANSCRIPT_STORAGE_DIR, private readonly workspaces?: SessionWorkspaceStore) {}
|
||||
|
||||
async initialize() {
|
||||
if (!this.workspaces) await ensureDirectory(this.baseDir);
|
||||
}
|
||||
constructor(private readonly workspaces: SessionWorkspaceStore) {}
|
||||
|
||||
async appendTurn(
|
||||
context: SessionTranscriptContext,
|
||||
@@ -66,7 +57,7 @@ export class SessionTranscriptStore {
|
||||
) {
|
||||
const key = this.filePath(context);
|
||||
return this.serializeWrite(key, async () => {
|
||||
if (this.workspaces) await this.workspaces.ensure(this.workspaceContext(context));
|
||||
await this.workspaces.ensure(this.workspaceContext(context));
|
||||
// 同一会话的多次写入串行化,防止流式结束和 UI 同步同时写同一个 transcript。
|
||||
const transcript = (await this.readTranscript(context)) ?? {
|
||||
actorKey: context.actorKey,
|
||||
@@ -148,10 +139,8 @@ export class SessionTranscriptStore {
|
||||
turns: projectTurnsForFork(sourceTranscript?.turns ?? [], keepMessageCount),
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
if (this.workspaces) {
|
||||
await this.workspaces.ensure(this.workspaceContext(targetContext));
|
||||
await this.workspaces.validate(this.workspaceContext(targetContext), "transcript.json");
|
||||
}
|
||||
await this.workspaces.ensure(this.workspaceContext(targetContext));
|
||||
await this.workspaces.validate(this.workspaceContext(targetContext), "transcript.json");
|
||||
await this.persist(targetContext, nextTranscript);
|
||||
return nextTranscript;
|
||||
}
|
||||
@@ -168,7 +157,7 @@ export class SessionTranscriptStore {
|
||||
}
|
||||
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
||||
const hits: SessionSearchHit[] = [];
|
||||
const files = this.workspaces ? await this.workspaces.list(context, "transcript.json") : await listJsonFiles(this.baseDir);
|
||||
const files = await this.workspaces.list(context, "transcript.json");
|
||||
for (const file of files) {
|
||||
const transcript = await readJsonFile<SessionTranscriptRecord>(file);
|
||||
if (!transcript) {
|
||||
@@ -205,7 +194,7 @@ export class SessionTranscriptStore {
|
||||
}
|
||||
|
||||
private async readTranscript(context: SessionTranscriptContext) {
|
||||
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "transcript.json")) return null;
|
||||
if (!await this.workspaces.validate(this.workspaceContext(context), "transcript.json")) return null;
|
||||
const record = await readJsonFile<SessionTranscriptRecord>(this.filePath(context));
|
||||
if (record && (record.actorKey !== context.actorKey || record.projectKey !== context.projectKey ||
|
||||
(record.clientSessionId || record.sessionId) !== (context.clientSessionId || context.sessionId))) throw new Error("chat transcript ownership mismatch");
|
||||
@@ -213,16 +202,11 @@ export class SessionTranscriptStore {
|
||||
}
|
||||
|
||||
private filePath(context: SessionTranscriptContext) {
|
||||
if (this.workspaces) return this.workspaces.path(this.workspaceContext(context), "transcript.json");
|
||||
return join(
|
||||
this.baseDir,
|
||||
`${context.actorKey}__${context.projectKey}__${context.sessionId}.json`,
|
||||
);
|
||||
return this.workspaces.path(this.workspaceContext(context), "transcript.json");
|
||||
}
|
||||
|
||||
private async persist(context: SessionTranscriptContext, transcript: SessionTranscriptRecord) {
|
||||
if (this.workspaces) await this.workspaces.writeRecord(this.workspaceContext(context), "transcript.json", transcript);
|
||||
else await atomicWriteJson(this.filePath(context), transcript);
|
||||
await this.workspaces.writeRecord(this.workspaceContext(context), "transcript.json", transcript);
|
||||
}
|
||||
|
||||
private workspaceContext(context: SessionTranscriptContext) {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import type { SessionWorkspaceContext } from "../runtime/conversationWorkspace.js";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { config } from "../config.js";
|
||||
import { type SessionWorkspaceStore } from "./workspace-store.js";
|
||||
import {
|
||||
atomicWriteJson,
|
||||
ensureDirectory,
|
||||
readJsonFile,
|
||||
removeFileIfExists,
|
||||
slugify,
|
||||
} from "../utils/fileStore.js";
|
||||
|
||||
export type SessionUiStateRecord = {
|
||||
@@ -21,49 +16,36 @@ export type SessionUiStateRecord = {
|
||||
type SessionUiStateContext = SessionWorkspaceContext;
|
||||
|
||||
export class SessionUiStateStore {
|
||||
constructor(private readonly baseDir = config.SESSION_UI_STATE_STORAGE_DIR, private readonly workspaces?: SessionWorkspaceStore) {}
|
||||
|
||||
async initialize() {
|
||||
if (!this.workspaces) await ensureDirectory(this.baseDir);
|
||||
}
|
||||
constructor(private readonly workspaces: SessionWorkspaceStore) {}
|
||||
|
||||
async read(context: SessionUiStateContext) {
|
||||
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return null;
|
||||
if (!await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return null;
|
||||
return await readJsonFile<SessionUiStateRecord>(this.filePath(context));
|
||||
}
|
||||
|
||||
async write(context: SessionUiStateContext, state: SessionUiStateRecord) {
|
||||
if (state.sessionId !== context.sessionId) throw new Error("chat state session mismatch");
|
||||
if (this.workspaces) {
|
||||
await this.workspaces.ensure(this.workspaceContext(context));
|
||||
await this.workspaces.validate(this.workspaceContext(context), "messages.json");
|
||||
}
|
||||
if (this.workspaces) await this.workspaces.writeRecord(this.workspaceContext(context), "messages.json", state);
|
||||
else await atomicWriteJson(this.filePath(context), state);
|
||||
await this.workspaces.writeRecord(this.workspaceContext(context), "messages.json", state);
|
||||
return state;
|
||||
}
|
||||
|
||||
async update(context: SessionUiStateContext, change: (state: SessionUiStateRecord) => SessionUiStateRecord) {
|
||||
if (this.workspaces) return this.workspaces.updateRecord(this.workspaceContext(context), "messages.json", async ({ current }) => {
|
||||
return this.workspaces.updateRecord(this.workspaceContext(context), "messages.json", async ({ current }) => {
|
||||
const state = current as SessionUiStateRecord | null;
|
||||
if (!state || state.sessionId !== context.sessionId || !Array.isArray(state.messages)) throw new Error("chat state unavailable");
|
||||
const next = change(state);
|
||||
if (next.sessionId !== context.sessionId) throw new Error("chat state session mismatch");
|
||||
return { result: next, value: next };
|
||||
});
|
||||
const state = await this.read(context);
|
||||
if (!state) throw new Error("chat state unavailable");
|
||||
return this.write(context, change(state));
|
||||
}
|
||||
|
||||
async remove(context: SessionUiStateContext) {
|
||||
if (this.workspaces && !await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return;
|
||||
if (!await this.workspaces.validate(this.workspaceContext(context), "messages.json")) return;
|
||||
await removeFileIfExists(this.filePath(context));
|
||||
}
|
||||
|
||||
private filePath(context: SessionUiStateContext) {
|
||||
if (this.workspaces) return this.workspaces.path(this.workspaceContext(context), "messages.json");
|
||||
return join(this.baseDir, `${slugify(context.sessionId)}.json`);
|
||||
return this.workspaces.path(this.workspaceContext(context), "messages.json");
|
||||
}
|
||||
|
||||
private workspaceContext(context: SessionUiStateContext) {
|
||||
|
||||
Reference in New Issue
Block a user