Files
fusion/packages/dashboard/app/hooks/useProjectContextGuard.ts
gsxdsm efa9580b93 FN-8366: enforce end-to-end project scoping
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>
2026-07-19 16:29:39 -07:00

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 };
}