perf(dashboard): speed up startup and eliminate API request storms

Multiple coordinated fixes for the perceived "dashboard takes forever to
load" complaint. Per-page-load HTTP requests drop from ~177 to ~101 and
duplicate per-project InProcessRuntime creation is eliminated.

- engine: shouldUseHybridExecutor no longer auto-enables for local-only
  multi-project setups (set FUSION_HYBRID_EXECUTOR=1 to force). The
  duplicate-runtime path was running self-healing twice per project and
  contending on the same SQLite file. ProjectEngineManager already
  handles N local projects with one InProcessRuntime each.
- dashboard cli: parallelized independent store inits, started
  CentralCore.init early in background, ran plugin loading concurrently
  with extension resolution. Sequenced SQLite store inits to avoid a
  TOCTOU race in addColumnIfMissing migrations across TaskStore /
  AutomationStore / PluginStore / AgentStore (all open the same
  .fusion/fusion.db). Restored try/catch around HybridExecutor.initialize
  and engineManager.ensureEngine so a paused or broken cwd project no
  longer aborts dashboard startup.
- dashboard client: added in-flight request dedupe wrapped around the
  top API offenders. /api/plugins/ui-slots drops from 17x to 1x per load.
  dedupe.forceFresh redirects ALL in-flight waiters to receive the fresh
  post-mutation response, not just the forcing caller. Generation
  counters in useAgents and AgentListModal protect against slow polls
  overwriting fresh state.
- dashboard SSE: agent event handler now debounces 250ms with a
  trailing-edge guard so multi-agent activity bursts coalesce to at
  most 2 refetches per burst.
- dashboard route: PATCH /api/projects/:id with isolationMode change
  returns 503 with actionable guidance when HybridExecutor is
  unavailable, instead of silently persisting a config the live runtime
  won't honor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-26 21:45:13 -07:00
parent 9bea9510c0
commit 6a6c6fdbfd
15 changed files with 515 additions and 164 deletions

View File

@@ -0,0 +1,109 @@
// In-flight request deduplication with redirect-on-forceFresh.
//
// Basic case: when N components mount and each calls the same fetcher, the
// "check cache → fetch → store on completion" pattern fires N concurrent
// requests because every caller sees an empty cache. Routing those callers
// through this helper collapses the burst into a single network request.
//
// forceFresh case: a caller that has just committed a mutation passes
// `forceFresh: true` so it doesn't join a pre-mutation in-flight request and
// return stale data. The OLD callers that already joined the in-flight
// request are ALSO redirected — they receive the fresh response from the new
// fetch, not the stale one. This is implemented by decoupling the external
// promise that callers await from the inner fetch promise: a forceFresh
// invocation discards the old inner fetch's eventual resolution and assigns
// the new inner fetch's resolution to the SAME external promise that old
// callers are waiting on. No caller ever observes pre-mutation data once a
// post-mutation forceFresh has been requested.
//
// Layered caching (e.g. usePluginUiSlots' 60s TTL) is unaffected — that runs
// at the hook layer, above this helper.
interface InFlightEntry<T> {
/** The promise callers await. Resolves to whichever inner fetch wins. */
external: Promise<T>;
/** Resolves the external promise. Guarded by `done`. */
resolve: (value: T) => void;
/** Rejects the external promise. Guarded by `done`. */
reject: (err: unknown) => void;
/** Once resolved or rejected, further fetches must not write to external. */
done: boolean;
}
const inFlight = new Map<string, InFlightEntry<unknown>>();
export interface DedupeOptions {
/**
* Skip joining an existing in-flight request and start a new fetch. Any
* callers already awaiting the prior in-flight request will be redirected
* to receive the new fetch's response instead — they will NOT see the
* pre-forceFresh response. Use after a mutation when callers must observe
* the post-mutation server snapshot.
*/
forceFresh?: boolean;
}
function makeEntry<T>(): InFlightEntry<T> {
// Manually-constructed Deferred so we can assign whichever inner fetch
// wins to the same external promise.
let resolve!: (v: T) => void;
let reject!: (e: unknown) => void;
const external = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { external, resolve, reject, done: false };
}
function attachInnerToEntry<T>(entry: InFlightEntry<T>, inner: Promise<T>): void {
inner.then(
(value) => {
if (entry.done) return;
entry.done = true;
entry.resolve(value);
},
(err) => {
if (entry.done) return;
entry.done = true;
entry.reject(err);
},
);
}
export function dedupe<T>(
key: string,
fn: () => Promise<T>,
options?: DedupeOptions,
): Promise<T> {
const existing = inFlight.get(key) as InFlightEntry<T> | undefined;
if (existing && !existing.done) {
if (!options?.forceFresh) {
// Standard dedupe — join the in-flight request.
return existing.external;
}
// forceFresh with an existing in-flight: start a new inner fetch and
// redirect existing.external to its result. The old inner fetch's
// eventual resolution is discarded by the `done` guard in
// attachInnerToEntry.
attachInnerToEntry(existing, fn());
return existing.external;
}
// No live entry (or forceFresh with nothing in flight) — start fresh.
const entry = makeEntry<T>();
attachInnerToEntry(entry, fn());
// Schedule cleanup once the external promise settles. We compare by
// identity so a later forceFresh that swaps in a new entry under the same
// key doesn't get deleted by this old cleanup.
void entry.external.then(
() => {
if (inFlight.get(key) === entry) inFlight.delete(key);
},
() => {
if (inFlight.get(key) === entry) inFlight.delete(key);
},
);
inFlight.set(key, entry);
return entry.external;
}

View File

@@ -90,6 +90,11 @@ import type {
ResearchProviderOption,
} from "../research-types";
import { appendTokenQuery, getAuthToken, withTokenHeader } from "../auth";
import { dedupe, type DedupeOptions } from "./dedupe";
/** Options accepted by deduped fetchers. Pass `{ forceFresh: true }` after a
* mutation to bypass any in-flight pre-mutation request and force a new one. */
export type FetchOptions = DedupeOptions;
// Re-export skills types for use by hooks and components
export type { DiscoveredSkill, CatalogEntry, CatalogFetchResult, ToggleSkillResult, SkillContent, SkillFileEntry };
@@ -607,11 +612,13 @@ export function rejectPlan(id: string, projectId?: string): Promise<Task> {
}
export function fetchConfig(projectId?: string): Promise<{ maxConcurrent: number; rootDir: string }> {
return api<{ maxConcurrent: number; rootDir: string }>(withProjectId("/config", projectId));
const path = withProjectId("/config", projectId);
return dedupe(path, () => api<{ maxConcurrent: number; rootDir: string }>(path));
}
export function fetchSettings(projectId?: string): Promise<Settings> {
return api<Settings>(withProjectId("/settings", projectId));
export function fetchSettings(projectId?: string, options?: FetchOptions): Promise<Settings> {
const path = withProjectId("/settings", projectId);
return dedupe(path, () => api<Settings>(path), options);
}
export function updateSettings(settings: Partial<Settings>, projectId?: string): Promise<Settings> {
@@ -1040,8 +1047,8 @@ export function testMemoryRetrieval(query: string, projectId?: string): Promise<
}
/** Fetch global (user-level) settings from ~/.fusion/settings.json */
export function fetchGlobalSettings(): Promise<GlobalSettings> {
return api<GlobalSettings>("/settings/global");
export function fetchGlobalSettings(options?: FetchOptions): Promise<GlobalSettings> {
return dedupe("/settings/global", () => api<GlobalSettings>("/settings/global"), options);
}
/** Update global (user-level) settings. These persist across all fn projects. */
@@ -2008,14 +2015,14 @@ export async function probeProviderModels(params: ProbeModelsParams): Promise<Pr
}
/** Fetch authentication status for all OAuth providers */
export function fetchAuthStatus(): Promise<{
export function fetchAuthStatus(options?: FetchOptions): Promise<{
providers: AuthProvider[];
ghCli?: { available: boolean; authenticated: boolean };
}> {
return api<{
return dedupe("/auth/status", () => api<{
providers: AuthProvider[];
ghCli?: { available: boolean; authenticated: boolean };
}>("/auth/status");
}>("/auth/status"), options);
}
/** Initiate OAuth login for a provider. Returns the auth URL to open in a new tab. */
@@ -4763,7 +4770,8 @@ export function clearActivityLog(projectId?: string): Promise<{ success: boolean
/** Fetch all workflow step definitions */
export function fetchWorkflowSteps(projectId?: string): Promise<WorkflowStep[]> {
return api<WorkflowStep[]>(withProjectId("/workflow-steps", projectId));
const path = withProjectId("/workflow-steps", projectId);
return dedupe(path, () => api<WorkflowStep[]>(path));
}
/** Create a new workflow step */
@@ -5153,6 +5161,7 @@ export function proxyApi<T>(path: string, opts?: RequestInit & { nodeId?: string
export function fetchAgents(
filter?: { state?: AgentState; role?: AgentCapability; includeEphemeral?: boolean },
projectId?: string,
options?: FetchOptions,
): Promise<Agent[]> {
const params = new URLSearchParams();
if (filter?.state) params.set("state", filter.state);
@@ -5160,7 +5169,8 @@ export function fetchAgents(
if (filter?.includeEphemeral === true) params.set("includeEphemeral", "true");
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<Agent[]>(`/agents${query}`);
const path = `/agents${query}`;
return dedupe(path, () => api<Agent[]>(path), options);
}
/** Fetch a single agent with heartbeat history */
@@ -5524,8 +5534,9 @@ export function fetchAgentRunTimeline(
}
/** Fetch aggregate agent stats */
export function fetchAgentStats(projectId?: string): Promise<AgentStats> {
return api<AgentStats>(withProjectId("/agents/stats", projectId));
export function fetchAgentStats(projectId?: string, options?: FetchOptions): Promise<AgentStats> {
const path = withProjectId("/agents/stats", projectId);
return dedupe(path, () => api<AgentStats>(path), options);
}
/** Fetch the chain of command for an agent (self → manager → grand-manager → ...) */
@@ -6242,12 +6253,12 @@ export function hasNodeMappingsSupport(project: ProjectInfoWithSource): boolean
/** Fetch all registered projects from all nodes (local + remote) */
export function fetchProjectsAcrossNodes(): Promise<ProjectInfoWithSource[]> {
return api<ProjectInfoWithSource[]>("/projects/across-nodes");
return dedupe("/projects/across-nodes", () => api<ProjectInfoWithSource[]>("/projects/across-nodes"));
}
/** Fetch all registered nodes */
export function fetchNodes(): Promise<NodeInfo[]> {
return api<NodeInfo[]>("/nodes");
return dedupe("/nodes", () => api<NodeInfo[]>("/nodes"));
}
/** Fetch discovery runtime status and active config. */
@@ -6520,12 +6531,13 @@ export function fetchExecutorStats(projectId?: string): Promise<{
maxConcurrent: number;
lastActivityAt?: string;
}> {
return api<{
const path = withProjectId("/executor/stats", projectId);
return dedupe(path, () => api<{
globalPause: boolean;
enginePaused: boolean;
maxConcurrent: number;
lastActivityAt?: string;
}>(withProjectId("/executor/stats", projectId));
}>(path));
}
export interface SystemStatsSnapshot {
@@ -8539,7 +8551,8 @@ export interface PluginRuntimeInfo {
/** Fetch all UI slot definitions from active plugins */
export async function fetchPluginUiSlots(projectId?: string): Promise<PluginUiSlotEntry[]> {
return api<PluginUiSlotEntry[]>(withProjectId("/plugins/ui-slots", projectId));
const path = withProjectId("/plugins/ui-slots", projectId);
return dedupe(path, () => api<PluginUiSlotEntry[]>(path));
}