Ensure asynchronous dashboard data and real-time streams remain bound to their active project context. - Resolve SSE and badge WebSocket stores through the canonical project resolver - Guard agents, artifacts, and documents hooks against stale project responses and events - Add scoped cache handling, regression coverage, and architecture documentation Files changed: docs/architecture.md | 7 ++++ .../app/hooks/__tests__/useAgents.test.ts | 24 +++++++++++ .../app/hooks/__tests__/useArtifacts.test.ts | 47 ++++++++++++++++++++++ .../app/hooks/__tests__/useDocuments.test.ts | 34 ++++++++++++++++ packages/dashboard/app/hooks/useAgents.ts | 47 +++++++++++++++++----- packages/dashboard/app/hooks/useArtifacts.ts | 26 +++++++++--- packages/dashboard/app/hooks/useDocuments.ts | 18 ++++++++- .../dashboard/app/hooks/useProjectContextGuard.ts | 38 +++++++++++++++++ .../routes-context-project-identity.test.ts | 12 ++++++ packages/dashboard/src/server.ts | 21 +++++++--- 10 files changed, 251 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-8366 Fusion-Task-Lineage: 0fcd38c1-727f-4511-8209-5d70cbf9eb0d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
39 lines
1.3 KiB
TypeScript
39 lines
1.3 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import { pushTrace } from "../utils/dashboardTraceBuffer";
|
|
|
|
/**
|
|
* FNXC:ProjectScoping 2026-07-15-20:10:
|
|
* Project-scoped hooks capture this monotonically increasing context version
|
|
* before an async request or SSE callback is registered. A changed active
|
|
* project invalidates that work, preventing a late prior-project result from
|
|
* being applied to the current view.
|
|
*/
|
|
export function useProjectContextGuard(projectId: string | undefined, source: string) {
|
|
const versionRef = useRef(0);
|
|
const previousProjectIdRef = useRef<string | undefined>(projectId);
|
|
const projectIdRef = useRef<string | undefined>(projectId);
|
|
projectIdRef.current = projectId;
|
|
|
|
if (previousProjectIdRef.current !== projectId) {
|
|
previousProjectIdRef.current = projectId;
|
|
versionRef.current += 1;
|
|
}
|
|
|
|
const capture = useCallback(() => {
|
|
const projectIdAtStart = projectIdRef.current;
|
|
const contextVersionAtStart = versionRef.current;
|
|
return {
|
|
projectIdAtStart,
|
|
isStale: () => {
|
|
const stale = versionRef.current !== contextVersionAtStart || projectIdRef.current !== projectIdAtStart;
|
|
if (stale) {
|
|
pushTrace(source, "dropped-stale-event", { projectId: projectIdAtStart });
|
|
}
|
|
return stale;
|
|
},
|
|
};
|
|
}, [source]);
|
|
|
|
return { capture };
|
|
}
|