fix(chat): refresh message caches on reconnect (MUL-3831) (#4677)

Invalidate per-session chat message caches (messages, messages-page,
pending-task, task-messages) on websocket reconnect / WS instance change so
a chat that missed chat/task events while disconnected recovers without a
full reload, matching the existing per-issue recovery pattern.

Co-authored-by: Ryan <1141524679@qq.com>
This commit is contained in:
Ryan Yu
2026-06-29 15:34:36 +08:00
committed by GitHub
parent 5206d7c613
commit c2e8892194
3 changed files with 41 additions and 7 deletions

View File

@@ -14,13 +14,17 @@ export const chatKeys = {
/** Full sessions list (active + archived); the dropdown splits locally. */
sessions: (wsId: string) => [...chatKeys.all(wsId), "sessions"] as const,
session: (wsId: string, id: string) => [...chatKeys.all(wsId), "session", id] as const,
messages: (sessionId: string) => ["chat", "messages", sessionId] as const,
messagesPage: (sessionId: string) => ["chat", "messages-page", sessionId] as const,
pendingTask: (sessionId: string) => ["chat", "pending-task", sessionId] as const,
messagesAll: () => ["chat", "messages"] as const,
messages: (sessionId: string) => [...chatKeys.messagesAll(), sessionId] as const,
messagesPageAll: () => ["chat", "messages-page"] as const,
messagesPage: (sessionId: string) => [...chatKeys.messagesPageAll(), sessionId] as const,
pendingTaskAll: () => ["chat", "pending-task"] as const,
pendingTask: (sessionId: string) => [...chatKeys.pendingTaskAll(), sessionId] as const,
/** Aggregate of in-flight chat tasks for the current user — FAB reads this. */
pendingTasks: (wsId: string) => [...chatKeys.all(wsId), "pending-tasks"] as const,
/** Per-task execution messages — shared with issue agent cards. */
taskMessages: (taskId: string) => ["task-messages", taskId] as const,
taskMessagesAll: () => ["task-messages"] as const,
taskMessages: (taskId: string) => [...chatKeys.taskMessagesAll(), taskId] as const,
};
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

View File

@@ -102,9 +102,9 @@ describe("useRealtimeSync — ws instance change", () => {
rerender({ ws: ws2 });
// Should have called invalidateQueries for all workspace-scoped keys
// (15 workspace-scoped + 6 per-issue prefixes + 1 workspaceKeys.list()
// + 1 cross-workspace inbox unread summary = 23 calls)
expect(invalidateSpy).toHaveBeenCalledTimes(23);
// (15 workspace-scoped + 6 per-issue prefixes + 4 per-chat prefixes
// + 1 workspaceKeys.list() + 1 cross-workspace inbox unread summary = 27 calls)
expect(invalidateSpy).toHaveBeenCalledTimes(27);
});
it("does not re-invalidate when rerendered with the same ws instance", () => {
@@ -164,4 +164,26 @@ describe("useRealtimeSync — ws instance change", () => {
expect(calls).toContainEqual(["issues", "attachments"]);
expect(calls).toContainEqual(["issues", "tasks"]);
});
it("invalidates per-chat-session caches (no wsId in key) on ws instance change", () => {
// These keys are not under the ["chat", wsId] prefix, so they need their
// own recovery invalidation when reconnecting after missed chat/task events.
const ws1 = createMockWs();
const { rerender } = renderHook(
({ ws }) => useRealtimeSync(ws, stores),
{ initialProps: { ws: ws1 as WSClient | null }, wrapper: createWrapper(qc) },
);
invalidateSpy.mockClear();
rerender({ ws: null });
const ws2 = createMockWs();
rerender({ ws: ws2 });
const calls = invalidateSpy.mock.calls.map((call: [{ queryKey?: unknown }, ...unknown[]]) => call[0].queryKey);
expect(calls).toContainEqual(["chat", "messages"]);
expect(calls).toContainEqual(["chat", "messages-page"]);
expect(calls).toContainEqual(["chat", "pending-task"]);
expect(calls).toContainEqual(["task-messages"]);
});
});

View File

@@ -340,6 +340,14 @@ function invalidateWorkspaceScopedQueries(qc: QueryClient): void {
qc.invalidateQueries({ queryKey: issueKeys.usageAll() });
qc.invalidateQueries({ queryKey: issueKeys.attachmentsAll() });
qc.invalidateQueries({ queryKey: issueKeys.tasksAll() });
// Per-chat-session caches are also keyed without wsId, so the
// chatKeys.all(wsId) prefix above only reaches session lists / aggregates.
// Message streams rely on WS invalidation with staleTime: Infinity; recover
// sessions that missed chat/task events while the socket was disconnected.
qc.invalidateQueries({ queryKey: chatKeys.messagesAll() });
qc.invalidateQueries({ queryKey: chatKeys.messagesPageAll() });
qc.invalidateQueries({ queryKey: chatKeys.pendingTaskAll() });
qc.invalidateQueries({ queryKey: chatKeys.taskMessagesAll() });
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
}