Skip to content

Commit d4c62bc

Browse files
authored
fix(list): 🐛 Prevent cloud task progress regression on page navigation (#66)
* fix(list): 🐛 prevent API lag from overwriting SSE real-time progress for cloud tasks * fix(list): 🐛 add module-level progress cache to prevent regression on remount The previous Math.max merge was ineffective because prevData is empty when the List component remounts (after navigation). This fix introduces a module-level cloudProgressCache that persists across mount/unmount cycles, ensuring SSE-driven progress is never lost when fetchTasks returns stale API data. - Cache is updated on every SSE event (page_completed, completed, etc.) - fetchTasks merges API data with both React state AND the cache - Cache entries are cleaned up when tasks reach terminal states
1 parent 15b5436 commit d4c62bc

1 file changed

Lines changed: 59 additions & 7 deletions

File tree

src/renderer/pages/List.tsx

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ import type { CloudModelTier, CloudSSEEvent } from "../../shared/types/cloud-api
1919
const { Text } = Typography;
2020
const CLOUD_MODEL_TIERS: CloudModelTier[] = ['lite', 'pro', 'ultra'];
2121

22+
/**
23+
* Module-level progress cache for cloud tasks.
24+
* Persists across component mount/unmount cycles within the same session,
25+
* preventing progress regression when the API returns stale data.
26+
*/
27+
const cloudProgressCache = new Map<string, {
28+
progress: number;
29+
completed_count: number;
30+
failed_count: number;
31+
status: number;
32+
}>();
33+
2234
interface LocalModelOption {
2335
value: string;
2436
label: string;
@@ -166,24 +178,49 @@ const List: React.FC = () => {
166178
const paginatedList = combinedList.slice(startIndex, startIndex + pageSize);
167179

168180
setData(prevData => {
169-
// Build a map of existing cloud tasks' real-time progress (driven by SSE)
181+
// Build a map of existing cloud tasks' real-time progress from both:
182+
// 1. Current React state (prevData) - covers same-mount SSE updates
183+
// 2. Module-level cache (cloudProgressCache) - covers cross-mount SSE updates
170184
const existingMap = new Map<string, any>();
171185
for (const item of prevData) {
172186
if ((item as any).isCloud && item.id) {
173187
existingMap.set(item.id, item);
174188
}
175189
}
176-
// Merge: for cloud tasks already in state, take Math.max of progress fields
190+
// Merge: for cloud tasks, take Math.max of progress fields from API, state, and cache
177191
// to prevent API lag from overwriting SSE real-time updates
178192
return paginatedList.map(task => {
179193
if ((task as any).isCloud && task.id) {
180-
const existing = existingMap.get(task.id);
181-
if (existing) {
194+
const fromState = existingMap.get(task.id);
195+
const fromCache = cloudProgressCache.get(task.id);
196+
const maxProgress = Math.max(
197+
task.progress || 0,
198+
fromState?.progress || 0,
199+
fromCache?.progress || 0
200+
);
201+
const maxCompleted = Math.max(
202+
task.completed_count || 0,
203+
fromState?.completed_count || 0,
204+
fromCache?.completed_count || 0
205+
);
206+
const maxFailed = Math.max(
207+
task.failed_count || 0,
208+
fromState?.failed_count || 0,
209+
fromCache?.failed_count || 0
210+
);
211+
// Update cache with merged values
212+
cloudProgressCache.set(task.id, {
213+
progress: maxProgress,
214+
completed_count: maxCompleted,
215+
failed_count: maxFailed,
216+
status: task.status ?? fromCache?.status ?? 0,
217+
});
218+
if (maxProgress > (task.progress || 0) || maxCompleted > (task.completed_count || 0)) {
182219
return {
183220
...task,
184-
progress: Math.max(task.progress || 0, existing.progress || 0),
185-
completed_count: Math.max(task.completed_count || 0, existing.completed_count || 0),
186-
failed_count: Math.max(task.failed_count || 0, existing.failed_count || 0),
221+
progress: maxProgress,
222+
completed_count: maxCompleted,
223+
failed_count: maxFailed,
187224
};
188225
}
189226
}
@@ -348,6 +385,21 @@ const List: React.FC = () => {
348385
}
349386

350387
newData[index] = task;
388+
// Sync module-level progress cache so it survives component remount
389+
if (task.id) {
390+
const terminalStatuses = [0, 6, 7, 8]; // FAILED, COMPLETED, CANCELLED, PARTIAL_FAILED
391+
if (terminalStatuses.includes(task.status ?? -1)) {
392+
// Terminal state: remove from cache, API is authoritative
393+
cloudProgressCache.delete(task.id);
394+
} else {
395+
cloudProgressCache.set(task.id, {
396+
progress: task.progress || 0,
397+
completed_count: task.completed_count || 0,
398+
failed_count: task.failed_count || 0,
399+
status: task.status ?? 0,
400+
});
401+
}
402+
}
351403
return newData;
352404
});
353405
};

0 commit comments

Comments
 (0)