Skip to content

Commit b2b3999

Browse files
authored
🤖 Guarantee consumer calculations always complete (#285)
## Problem Consumer breakdown can get stuck in "Calculating..." state forever when the Web Worker hangs and never resolves/rejects its promise. **Root cause:** No timeout on `tokenWorker.calculate()` means if the worker hangs: - Promise never settles - Code never reaches catch/finally blocks - `pendingCalcs` never cleaned up - UI stuck showing "Calculating..." indefinitely This was observed during development when errors occurred. ## Solution Add a 10-second timeout using `Promise.race`: ```typescript const timeoutPromise = new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Calculation timeout")), 10_000) ); const fullStats = await Promise.race([ this.tokenWorker.calculate(messages, model), timeoutPromise, ]); ``` **Why 10 seconds?** - Generous enough for large message histories - Responsive - anything longer feels like a bug to users - Treated as error → caches empty result → prevents retry spam ## Unchanged: Original Cancellation Logic The existing cancellation handling is **preserved and correct**: ```typescript catch (error) { if (error.message === "Cancelled by newer request") { return; // Don't cache, let lazy trigger retry } // Real errors: cache empty to prevent infinite retries this.cache.set(workspaceId, { ..., isCalculating: false }); } ``` **Why this matters:** - **Cancellations** (transient): Don't cache → Lazy trigger retries naturally - **Real errors** (permanent): Cache empty → Prevent retry spam - Removing this distinction breaks the system ## Net Changes - **Added**: 6 lines (timeout constant + Promise.race wrapper) - **Removed**: 0 lines - **Net**: +6 lines, 0 complexity increase ## Testing - ✅ Fresh load with large history (timeout doesn't trigger prematurely) - ✅ Rapid workspace switching (cancellations still work correctly) - ✅ Simulated worker hang (timeout fires, UI exits calculating state) _Generated with `cmux`_
1 parent e028021 commit b2b3999

File tree

1 file changed

+13
-3
lines changed

1 file changed

+13
-3
lines changed

src/stores/WorkspaceConsumerManager.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { WorkspaceConsumersState } from "./WorkspaceStore";
22
import { TokenStatsWorker } from "@/utils/tokens/TokenStatsWorker";
33
import type { StreamingMessageAggregator } from "@/utils/messages/StreamingMessageAggregator";
44

5+
// Timeout for Web Worker calculations (10 seconds - generous but responsive)
6+
const CALCULATION_TIMEOUT_MS = 10_000;
7+
58
/**
69
* Manages consumer token calculations for workspaces.
710
*
@@ -148,8 +151,15 @@ export class WorkspaceConsumerManager {
148151
const messages = aggregator.getAllMessages();
149152
const model = aggregator.getCurrentModel() ?? "unknown";
150153

151-
// Calculate in Web Worker (off main thread)
152-
const fullStats = await this.tokenWorker.calculate(messages, model);
154+
// Calculate in Web Worker with timeout protection
155+
const timeoutPromise = new Promise<never>((_, reject) =>
156+
setTimeout(() => reject(new Error("Calculation timeout")), CALCULATION_TIMEOUT_MS)
157+
);
158+
159+
const fullStats = await Promise.race([
160+
this.tokenWorker.calculate(messages, model),
161+
timeoutPromise,
162+
]);
153163

154164
// Store result in cache
155165
this.cache.set(workspaceId, {
@@ -168,7 +178,7 @@ export class WorkspaceConsumerManager {
168178
return;
169179
}
170180

171-
// Real errors: log and cache empty result
181+
// Real errors (including timeout): log and cache empty result
172182
console.error(`[WorkspaceConsumerManager] Calculation failed for ${workspaceId}:`, error);
173183
this.cache.set(workspaceId, {
174184
consumers: [],

0 commit comments

Comments
 (0)