From d75137336869f6cdb665086d45253f4f6e690fe7 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:36:44 +0800 Subject: [PATCH] fix(realtime): handle issue/inbox events granularly to prevent full refetch When viewing an issue in the inbox, WS events like issue:updated and inbox:new triggered full store refetches, causing unnecessary loading flashes and redundant API calls. Now these events update the store in-place using the event payload data instead of refetching everything. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/realtime/use-realtime-sync.ts | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/web/features/realtime/use-realtime-sync.ts b/apps/web/features/realtime/use-realtime-sync.ts index 74ba9a71..8e9dcbb3 100644 --- a/apps/web/features/realtime/use-realtime-sync.ts +++ b/apps/web/features/realtime/use-realtime-sync.ts @@ -14,6 +14,9 @@ import type { WorkspaceDeletedPayload, MemberRemovedPayload, IssueUpdatedPayload, + IssueCreatedPayload, + IssueDeletedPayload, + InboxNewPayload, } from "@/shared/types"; const logger = createLogger("realtime-sync"); @@ -34,8 +37,12 @@ export function useRealtimeSync(ws: WSClient | null) { useEffect(() => { if (!ws) return; + // Event types handled by specific handlers below — skip generic refresh + const specificEvents = new Set([ + "issue:updated", "issue:created", "issue:deleted", "inbox:new", + ]); + const refreshMap: Record void> = { - issue: () => void useIssueStore.getState().fetch(), inbox: () => void useInboxStore.getState().fetch(), agent: () => void useWorkspaceStore.getState().refreshAgents(), member: () => void useWorkspaceStore.getState().refreshMembers(), @@ -74,21 +81,40 @@ export function useRealtimeSync(ws: WSClient | null) { logger.debug("skipping self-event", msg.type); return; } + if (specificEvents.has(msg.type)) return; const prefix = msg.type.split(":")[0] ?? ""; const refresh = refreshMap[prefix]; if (refresh) debouncedRefresh(prefix, refresh); }); - // --- Side-effect handlers (toast, navigation, cross-store sync) --- + // --- Specific event handlers (granular updates, no full refetch) --- - // Keep inbox issue_status in sync when issues change const unsubIssueUpdated = ws.on("issue:updated", (p) => { const { issue } = p as IssueUpdatedPayload; - if (issue?.id && issue?.status) { + if (!issue?.id) return; + useIssueStore.getState().updateIssue(issue.id, issue); + if (issue.status) { useInboxStore.getState().updateIssueStatus(issue.id, issue.status); } }); + const unsubIssueCreated = ws.on("issue:created", (p) => { + const { issue } = p as IssueCreatedPayload; + if (issue) useIssueStore.getState().addIssue(issue); + }); + + const unsubIssueDeleted = ws.on("issue:deleted", (p) => { + const { issue_id } = p as IssueDeletedPayload; + if (issue_id) useIssueStore.getState().removeIssue(issue_id); + }); + + const unsubInboxNew = ws.on("inbox:new", (p) => { + const { item } = p as InboxNewPayload; + if (item) useInboxStore.getState().addItem(item); + }); + + // --- Side-effect handlers (toast, navigation) --- + const unsubWsDeleted = ws.on("workspace:deleted", (p) => { const { workspace_id } = p as WorkspaceDeletedPayload; const currentWs = useWorkspaceStore.getState().workspace; @@ -123,6 +149,9 @@ export function useRealtimeSync(ws: WSClient | null) { return () => { unsubAny(); unsubIssueUpdated(); + unsubIssueCreated(); + unsubIssueDeleted(); + unsubInboxNew(); unsubWsDeleted(); unsubMemberRemoved(); unsubMemberAdded();