Skip to content

Commit 80e4bac

Browse files
saagar210claude
andcommitted
fix(contexts): batch AppStatus polls into one setState, memoize value
Each poll cycle fired up to 5 separate setState calls (one per backend probe: llm, embeddings, vector, kb, memoryKernel) on different microtasks after parallel awaits. Auto-batching is not guaranteed across those microtask boundaries, so every consumer of useAppStatus could re-render up to 5× per cycle — and the context value object was recreated on every render because it wasn't memoized. Restructures: - Extracts 5 module-scoped compute* helpers that return Promise<Partial<AppStatusState>>. They perform the invoke calls but never touch setState, making them pure orchestration inputs. - refresh() awaits all 5 computes via Promise.all, then does ONE setState merging every slice plus initialized/lastUpdated. Collapses up to 6 renders per poll cycle to 1. - refreshLlm() and refreshKb() (the two public targeted refreshers exposed through context) are thin wrappers around their compute helpers — one setState each. - Wraps the context value in useMemo so consumers get a stable reference when state is unchanged. No behavior change in the status data surfaced to consumers; only the number of renders per update is reduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 04473dc commit 80e4bac

1 file changed

Lines changed: 144 additions & 124 deletions

File tree

src/contexts/AppStatusContext.tsx

Lines changed: 144 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {
44
useState,
55
useEffect,
66
useCallback,
7+
useMemo,
78
useRef,
89
} from "react";
910
import { invoke } from "@tauri-apps/api/core";
@@ -77,6 +78,120 @@ const defaultState: AppStatusState = {
7778
lastUpdated: null,
7879
};
7980

81+
// ---------------------------------------------------------------------------
82+
// Compute helpers — pure async functions returning the partial state slice
83+
// for each backend probe. Intentionally free of setState so the caller can
84+
// batch updates from multiple probes into a single render.
85+
// ---------------------------------------------------------------------------
86+
87+
async function computeLlmStatus(): Promise<Partial<AppStatusState>> {
88+
try {
89+
const isLoaded = await invoke<boolean>("is_model_loaded");
90+
if (isLoaded) {
91+
const info = await invoke<ModelInfo | null>("get_model_info");
92+
return {
93+
llmLoaded: true,
94+
llmModelName: info?.name ?? info?.id ?? "Unknown",
95+
llmModelInfo: info,
96+
llmLoading: false,
97+
};
98+
}
99+
return {
100+
llmLoaded: false,
101+
llmModelName: null,
102+
llmModelInfo: null,
103+
llmLoading: false,
104+
};
105+
} catch (e) {
106+
console.error("Failed to check LLM status:", e);
107+
return {};
108+
}
109+
}
110+
111+
async function computeEmbeddingsStatus(): Promise<Partial<AppStatusState>> {
112+
try {
113+
const isLoaded = await invoke<boolean>("is_embedding_model_loaded");
114+
return {
115+
embeddingsLoaded: isLoaded,
116+
embeddingsModelName: isLoaded ? "default" : null,
117+
};
118+
} catch {
119+
// Embedding check may not exist
120+
return {};
121+
}
122+
}
123+
124+
async function computeVectorStatus(): Promise<Partial<AppStatusState>> {
125+
try {
126+
const consent = await invoke<{
127+
enabled: boolean;
128+
consented_at: string | null;
129+
}>("get_vector_consent");
130+
return {
131+
vectorEnabled: consent.enabled,
132+
vectorConsent: consent.enabled,
133+
};
134+
} catch {
135+
// Vector consent may not be available
136+
return {};
137+
}
138+
}
139+
140+
async function computeKbStatus(): Promise<Partial<AppStatusState>> {
141+
try {
142+
const stats = await invoke<{
143+
document_count: number;
144+
chunk_count: number;
145+
namespace_count: number;
146+
}>("get_kb_stats");
147+
return {
148+
kbIndexed: stats.chunk_count > 0,
149+
kbDocumentCount: stats.document_count,
150+
kbChunkCount: stats.chunk_count,
151+
};
152+
} catch {
153+
// KB stats may fail if not initialized
154+
return {};
155+
}
156+
}
157+
158+
async function computeMemoryKernelStatus(): Promise<Partial<AppStatusState>> {
159+
try {
160+
const preflight = await invoke<MemoryKernelPreflightStatus>(
161+
"get_memory_kernel_preflight_status",
162+
);
163+
return {
164+
memoryKernelFeatureEnabled: preflight.enabled,
165+
memoryKernelReady: preflight.ready && preflight.enrichment_enabled,
166+
memoryKernelStatus: preflight.status,
167+
memoryKernelDetail: preflight.message,
168+
memoryKernelReleaseTag: preflight.release_tag ?? null,
169+
memoryKernelCommitSha: preflight.commit_sha ?? null,
170+
memoryKernelServiceContract:
171+
preflight.service_contract_version ??
172+
preflight.expected_service_contract_version ??
173+
null,
174+
memoryKernelApiContract:
175+
preflight.api_contract_version ??
176+
preflight.expected_api_contract_version ??
177+
null,
178+
memoryKernelIntegrationBaseline: preflight.integration_baseline ?? null,
179+
};
180+
} catch (err) {
181+
return {
182+
memoryKernelFeatureEnabled: false,
183+
memoryKernelReady: false,
184+
memoryKernelStatus: "error",
185+
memoryKernelDetail: `Preflight unavailable: ${String(err)}`,
186+
memoryKernelReleaseTag: null,
187+
memoryKernelCommitSha: null,
188+
memoryKernelServiceContract: null,
189+
memoryKernelApiContract: null,
190+
memoryKernelIntegrationBaseline: null,
191+
};
192+
}
193+
}
194+
80195
const AppStatusContext = createContext<AppStatusContextValue | null>(null);
81196

82197
interface Props {
@@ -89,139 +204,41 @@ export function AppStatusProvider({ children, pollInterval = 10000 }: Props) {
89204
const pollRef = useRef<number | null>(null);
90205

91206
const refreshLlm = useCallback(async () => {
92-
try {
93-
const isLoaded = await invoke<boolean>("is_model_loaded");
94-
if (isLoaded) {
95-
const info = await invoke<ModelInfo | null>("get_model_info");
96-
setState((prev) => ({
97-
...prev,
98-
llmLoaded: true,
99-
llmModelName: info?.name ?? info?.id ?? "Unknown",
100-
llmModelInfo: info,
101-
llmLoading: false,
102-
}));
103-
} else {
104-
setState((prev) => ({
105-
...prev,
106-
llmLoaded: false,
107-
llmModelName: null,
108-
llmModelInfo: null,
109-
llmLoading: false,
110-
}));
111-
}
112-
} catch (e) {
113-
console.error("Failed to check LLM status:", e);
114-
}
115-
}, []);
116-
117-
const refreshEmbeddings = useCallback(async () => {
118-
try {
119-
const isLoaded = await invoke<boolean>("is_embedding_model_loaded");
120-
setState((prev) => ({
121-
...prev,
122-
embeddingsLoaded: isLoaded,
123-
embeddingsModelName: isLoaded ? "default" : null,
124-
}));
125-
} catch {
126-
// Embedding check may not exist
127-
}
128-
}, []);
129-
130-
const refreshVector = useCallback(async () => {
131-
try {
132-
const consent = await invoke<{
133-
enabled: boolean;
134-
consented_at: string | null;
135-
}>("get_vector_consent");
136-
setState((prev) => ({
137-
...prev,
138-
vectorEnabled: consent.enabled,
139-
vectorConsent: consent.enabled,
140-
}));
141-
} catch {
142-
// Vector consent may not be available
207+
const partial = await computeLlmStatus();
208+
if (Object.keys(partial).length > 0) {
209+
setState((prev) => ({ ...prev, ...partial }));
143210
}
144211
}, []);
145212

146213
const refreshKb = useCallback(async () => {
147-
try {
148-
const stats = await invoke<{
149-
document_count: number;
150-
chunk_count: number;
151-
namespace_count: number;
152-
}>("get_kb_stats");
153-
setState((prev) => ({
154-
...prev,
155-
kbIndexed: stats.chunk_count > 0,
156-
kbDocumentCount: stats.document_count,
157-
kbChunkCount: stats.chunk_count,
158-
}));
159-
} catch {
160-
// KB stats may fail if not initialized
161-
}
162-
}, []);
163-
164-
const refreshMemoryKernel = useCallback(async () => {
165-
try {
166-
const preflight = await invoke<MemoryKernelPreflightStatus>(
167-
"get_memory_kernel_preflight_status",
168-
);
169-
setState((prev) => ({
170-
...prev,
171-
memoryKernelFeatureEnabled: preflight.enabled,
172-
memoryKernelReady: preflight.ready && preflight.enrichment_enabled,
173-
memoryKernelStatus: preflight.status,
174-
memoryKernelDetail: preflight.message,
175-
memoryKernelReleaseTag: preflight.release_tag ?? null,
176-
memoryKernelCommitSha: preflight.commit_sha ?? null,
177-
memoryKernelServiceContract:
178-
preflight.service_contract_version ??
179-
preflight.expected_service_contract_version ??
180-
null,
181-
memoryKernelApiContract:
182-
preflight.api_contract_version ??
183-
preflight.expected_api_contract_version ??
184-
null,
185-
memoryKernelIntegrationBaseline: preflight.integration_baseline ?? null,
186-
}));
187-
} catch (err) {
188-
setState((prev) => ({
189-
...prev,
190-
memoryKernelFeatureEnabled: false,
191-
memoryKernelReady: false,
192-
memoryKernelStatus: "error",
193-
memoryKernelDetail: `Preflight unavailable: ${String(err)}`,
194-
memoryKernelReleaseTag: null,
195-
memoryKernelCommitSha: null,
196-
memoryKernelServiceContract: null,
197-
memoryKernelApiContract: null,
198-
memoryKernelIntegrationBaseline: null,
199-
}));
214+
const partial = await computeKbStatus();
215+
if (Object.keys(partial).length > 0) {
216+
setState((prev) => ({ ...prev, ...partial }));
200217
}
201218
}, []);
202219

203220
const refresh = useCallback(async () => {
204-
await Promise.all([
205-
refreshLlm(),
206-
refreshEmbeddings(),
207-
refreshVector(),
208-
refreshKb(),
209-
refreshMemoryKernel(),
221+
const [llm, embeddings, vector, kb, memoryKernel] = await Promise.all([
222+
computeLlmStatus(),
223+
computeEmbeddingsStatus(),
224+
computeVectorStatus(),
225+
computeKbStatus(),
226+
computeMemoryKernelStatus(),
210227
]);
211228
setState((prev) => ({
212229
...prev,
230+
...llm,
231+
...embeddings,
232+
...vector,
233+
...kb,
234+
...memoryKernel,
213235
initialized: true,
214236
lastUpdated: new Date(),
215237
}));
216-
}, [
217-
refreshLlm,
218-
refreshEmbeddings,
219-
refreshVector,
220-
refreshKb,
221-
refreshMemoryKernel,
222-
]);
238+
}, []);
223239

224-
// Initial load and polling
240+
// Initial load and polling. `refresh` has stable identity (empty deps),
241+
// so the effect only re-arms when `pollInterval` changes.
225242
useEffect(() => {
226243
refresh();
227244

@@ -236,12 +253,15 @@ export function AppStatusProvider({ children, pollInterval = 10000 }: Props) {
236253
};
237254
}, [refresh, pollInterval]);
238255

239-
const value: AppStatusContextValue = {
240-
...state,
241-
refresh,
242-
refreshLlm,
243-
refreshKb,
244-
};
256+
const value = useMemo<AppStatusContextValue>(
257+
() => ({
258+
...state,
259+
refresh,
260+
refreshLlm,
261+
refreshKb,
262+
}),
263+
[state, refresh, refreshLlm, refreshKb],
264+
);
245265

246266
return (
247267
<AppStatusContext.Provider value={value}>

0 commit comments

Comments
 (0)