mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
perf(chat): resume before shared resource hydration
This commit is contained in:
parent
d0965d19b1
commit
6b2a20cf0d
5 changed files with 101 additions and 29 deletions
|
|
@ -53,6 +53,7 @@ function handleChatConnectionExit(host: ChatConnectionExitHost): void {
|
|||
}
|
||||
|
||||
export interface ChatConnectionActions {
|
||||
ensureInitialized(): Promise<void>;
|
||||
ensureConnected(): Promise<void>;
|
||||
invalidate(): void;
|
||||
handleExit(): void;
|
||||
|
|
@ -63,29 +64,40 @@ export interface ChatConnectionActions {
|
|||
|
||||
export function createChatConnectionActions(host: ChatConnectionActionsHost): ChatConnectionActions {
|
||||
let generation = 0;
|
||||
let activeConnection: { generation: number; promise: Promise<void> } | null = null;
|
||||
let activeConnection: { generation: number; initialization: Promise<void>; hydration: Promise<void> } | null = null;
|
||||
const invalidate = (): void => {
|
||||
generation += 1;
|
||||
activeConnection = null;
|
||||
};
|
||||
const isStale = (candidateGeneration: number): boolean => candidateGeneration !== generation;
|
||||
const startConnection = (): NonNullable<typeof activeConnection> => {
|
||||
const connectionGeneration = generation;
|
||||
const connectionIsStale = (): boolean => isStale(connectionGeneration);
|
||||
const initialization = initializeConnection(host, connectionIsStale);
|
||||
const hydration = initialization.then(async () => {
|
||||
if (connectionIsStale() || !host.connection.isConnected()) return;
|
||||
await hydrateConnectedResources(host, connectionIsStale);
|
||||
});
|
||||
const active = { generation: connectionGeneration, initialization, hydration };
|
||||
activeConnection = active;
|
||||
const clear = (): void => {
|
||||
if (activeConnection === active) activeConnection = null;
|
||||
};
|
||||
void hydration.then(clear, clear);
|
||||
return active;
|
||||
};
|
||||
const actions: ChatConnectionActions = {
|
||||
ensureInitialized: async () => {
|
||||
if (!host.canConnect()) return;
|
||||
if (activeConnection) return activeConnection.initialization;
|
||||
if (host.connection.isConnected()) return;
|
||||
await startConnection().initialization;
|
||||
},
|
||||
ensureConnected: async () => {
|
||||
if (!host.canConnect()) return;
|
||||
if (activeConnection) return activeConnection.promise;
|
||||
if (activeConnection) return activeConnection.hydration;
|
||||
if (host.connection.isConnected()) return;
|
||||
|
||||
const connectionGeneration = generation;
|
||||
const promise = initializeConnection(host, () => isStale(connectionGeneration));
|
||||
const active = { generation: connectionGeneration, promise };
|
||||
activeConnection = active;
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
if (activeConnection === active) {
|
||||
activeConnection = null;
|
||||
}
|
||||
}
|
||||
await startConnection().hydration;
|
||||
},
|
||||
invalidate,
|
||||
handleExit: () => {
|
||||
|
|
@ -150,8 +162,6 @@ async function initializeConnection(host: ChatConnectionActionsHost, isStale: ()
|
|||
host.notifyConnectionFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
await hydrateConnectedResources(host, isStale);
|
||||
}
|
||||
|
||||
async function hydrateConnectedResources(host: ChatConnectionActionsHost, isStale: () => boolean): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ interface ChatPanelThreadLifecycleInput {
|
|||
appServer: ChatAppServerGateway;
|
||||
localItemIds: LocalIdSource;
|
||||
ensureConnected: () => Promise<void>;
|
||||
ensureInitialized: () => Promise<void>;
|
||||
status: ChatPanelThreadStatus;
|
||||
threadStart: ThreadStartActions;
|
||||
foundation: ChatPanelThreadFoundation;
|
||||
|
|
@ -191,9 +192,19 @@ export function createThreadLifecycleBundle(
|
|||
host: ChatPanelThreadHost,
|
||||
input: ChatPanelThreadLifecycleInput,
|
||||
): ChatPanelThreadLifecycleBundle {
|
||||
const { appServer, localItemIds, ensureConnected, status, threadStart, foundation, notifyActiveThreadIdentityChanged } = input;
|
||||
const {
|
||||
appServer,
|
||||
localItemIds,
|
||||
ensureConnected,
|
||||
ensureInitialized,
|
||||
status,
|
||||
threadStart,
|
||||
foundation,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
} = input;
|
||||
const lifecycle = createSessionThreadLifecycle(host, {
|
||||
appServer,
|
||||
ensureInitialized,
|
||||
status,
|
||||
goalSync: foundation.goalSync,
|
||||
autoTitleCoordinator: foundation.autoTitleCoordinator,
|
||||
|
|
@ -298,6 +309,7 @@ function createSessionThreadLifecycle(
|
|||
host: ChatPanelThreadHost,
|
||||
input: {
|
||||
appServer: ChatAppServerGateway;
|
||||
ensureInitialized: () => Promise<void>;
|
||||
status: ChatPanelThreadStatus;
|
||||
goalSync: ChatPanelGoalSyncActions;
|
||||
autoTitleCoordinator: AutoTitleCoordinator;
|
||||
|
|
@ -306,7 +318,16 @@ function createSessionThreadLifecycle(
|
|||
notifyActiveThreadIdentityChanged: () => void;
|
||||
},
|
||||
): ChatPanelThreadLifecycle {
|
||||
const { appServer, status, goalSync, autoTitleCoordinator, history, invalidateThreadWork, notifyActiveThreadIdentityChanged } = input;
|
||||
const {
|
||||
appServer,
|
||||
ensureInitialized,
|
||||
status,
|
||||
goalSync,
|
||||
autoTitleCoordinator,
|
||||
history,
|
||||
invalidateThreadWork,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
} = input;
|
||||
const restoration = new RestorationController({
|
||||
stateStore: host.stateStore,
|
||||
});
|
||||
|
|
@ -315,7 +336,13 @@ function createSessionThreadLifecycle(
|
|||
};
|
||||
const resume = createResumeActions({
|
||||
stateStore: host.stateStore,
|
||||
resumeTransport: appServer.threadResume,
|
||||
resumeTransport: {
|
||||
ensureConnected: async () => {
|
||||
await ensureInitialized();
|
||||
return appServer.connectionAvailable();
|
||||
},
|
||||
resumeThread: (threadId) => appServer.threadResume.resumeThread(threadId),
|
||||
},
|
||||
resumeWork: host.resumeWork,
|
||||
history,
|
||||
closing: host.getClosing,
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
appServer,
|
||||
localItemIds,
|
||||
ensureConnected,
|
||||
ensureInitialized: () => connectionActions.ensureInitialized(),
|
||||
status,
|
||||
threadStart,
|
||||
foundation: threadFoundation,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,32 @@ describe("ChatConnectionActions", () => {
|
|||
expect(host.setStatus).toHaveBeenCalledWith("Connected.", { kind: "connected" });
|
||||
});
|
||||
|
||||
it("publishes initialization readiness before shared resources finish hydrating", async () => {
|
||||
const { actions, host, refreshAppServerMetadata, stateStore } = createActionsHarness();
|
||||
const metadata = deferred<void>();
|
||||
refreshAppServerMetadata.mockReturnValueOnce(metadata.promise);
|
||||
|
||||
await actions.ensureInitialized();
|
||||
|
||||
expect(stateStore.getState().connection.initializeResponse).toMatchObject({ codexHome: "/codex" });
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Connected.", { kind: "connected" });
|
||||
expect(host.refreshSharedThreads).not.toHaveBeenCalled();
|
||||
expect(host.scheduleDeferredDiagnostics).not.toHaveBeenCalled();
|
||||
|
||||
let connected = false;
|
||||
const fullyConnected = actions.ensureConnected().then(() => {
|
||||
connected = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(connected).toBe(false);
|
||||
|
||||
metadata.resolve(undefined);
|
||||
await fullyConnected;
|
||||
|
||||
expect(host.refreshSharedThreads).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredDiagnostics).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps the initialized connection usable when metadata hydration fails", async () => {
|
||||
const { actions, host, refreshAppServerMetadata, stateStore } = createActionsHarness();
|
||||
refreshAppServerMetadata.mockRejectedValueOnce(new Error("config unavailable"));
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
expectRequestTimes(client, "thread/list", 1);
|
||||
});
|
||||
|
||||
it("keeps connect calls joined while metadata is still loading", async () => {
|
||||
it("resumes while metadata is still loading without reconnecting", async () => {
|
||||
const config = deferred<unknown>();
|
||||
const client = connectedClient({
|
||||
"config/read": vi.fn(() => config.promise),
|
||||
|
|
@ -47,25 +47,31 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
connectionMockState().client = client;
|
||||
const view = await chatView();
|
||||
|
||||
const firstConnect = view.surface.connect();
|
||||
const opening = view.surface.openThread("thread-1");
|
||||
await waitForAsyncWork(() => {
|
||||
expectRequestTimes(client, "config/read", 1);
|
||||
expect(client.request).toHaveBeenCalledWith("thread/resume", expect.objectContaining({ threadId: "thread-1", cwd: "/vault" }));
|
||||
});
|
||||
|
||||
let secondResolved = false;
|
||||
const secondConnect = view.surface.connect().then(() => {
|
||||
secondResolved = true;
|
||||
await opening;
|
||||
let connected = false;
|
||||
const fullyConnected = view.surface.connect().then(() => {
|
||||
connected = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(connectionMockState().connected).toBe(true);
|
||||
expect(secondResolved).toBe(false);
|
||||
expect(connectionMockState().connectCalls).toBe(1);
|
||||
expect(connected).toBe(false);
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ connected: true, threadId: "thread-1" });
|
||||
expectRequestTimes(client, "thread/list", 0);
|
||||
|
||||
config.resolve({});
|
||||
await Promise.all([firstConnect, secondConnect]);
|
||||
await fullyConnected;
|
||||
await waitForAsyncWork(() => {
|
||||
expectRequestTimes(client, "thread/list", 1);
|
||||
});
|
||||
|
||||
expectRequestTimes(client, "config/read", 1);
|
||||
expectRequestTimes(client, "thread/list", 1);
|
||||
});
|
||||
|
||||
it("loads app-server metadata after connecting", async () => {
|
||||
|
|
@ -209,7 +215,9 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
const view = await chatView();
|
||||
|
||||
const connecting = view.surface.connect();
|
||||
await Promise.resolve();
|
||||
await waitForAsyncWork(() => {
|
||||
expectRequestTimes(client, "config/read", 1);
|
||||
});
|
||||
await view.onClose();
|
||||
resolveConfig({});
|
||||
await connecting;
|
||||
|
|
|
|||
Loading…
Reference in a new issue