Skip to content

Commit b4752fd

Browse files
authored
fix(cloud): 🐛 optimize context and debounce SSE refresh (#70)
1 parent 2a815ec commit b4752fd

2 files changed

Lines changed: 90 additions & 63 deletions

File tree

src/renderer/contexts/CloudContext.tsx

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, ReactNode, useCallback } from 'react';
1+
import React, { useState, useEffect, ReactNode, useCallback, useMemo } from 'react';
22
import {
33
CloudContext,
44
UserProfile,
@@ -501,38 +501,45 @@ export const CloudProvider: React.FC<CloudProviderProps> = ({ children }) => {
501501
return cleanup;
502502
}, [isAuthenticated, refreshCredits]);
503503

504+
const contextValue = useMemo(() => ({
505+
user,
506+
credits,
507+
isAuthenticated,
508+
isLoading,
509+
deviceFlowStatus,
510+
userCode,
511+
verificationUrl,
512+
authError,
513+
login,
514+
logout,
515+
cancelLogin,
516+
refreshCredits,
517+
convertFile,
518+
getTasks,
519+
getTaskById,
520+
getTaskPages,
521+
cancelTask,
522+
retryTask,
523+
deleteTask,
524+
retryPage,
525+
getTaskResult,
526+
downloadResult,
527+
createCheckout,
528+
getCheckoutStatus,
529+
reconcileCheckout,
530+
getCreditHistory,
531+
getPaymentHistory
532+
}), [
533+
user, credits, isAuthenticated, isLoading, deviceFlowStatus,
534+
userCode, verificationUrl, authError, login, logout, cancelLogin,
535+
refreshCredits, convertFile, getTasks, getTaskById, getTaskPages,
536+
cancelTask, retryTask, deleteTask, retryPage, getTaskResult,
537+
downloadResult, createCheckout, getCheckoutStatus, reconcileCheckout,
538+
getCreditHistory, getPaymentHistory
539+
]);
540+
504541
return (
505-
<CloudContext.Provider
506-
value={{
507-
user,
508-
credits,
509-
isAuthenticated,
510-
isLoading,
511-
deviceFlowStatus,
512-
userCode,
513-
verificationUrl,
514-
authError,
515-
login,
516-
logout,
517-
cancelLogin,
518-
refreshCredits,
519-
convertFile,
520-
getTasks,
521-
getTaskById,
522-
getTaskPages,
523-
cancelTask,
524-
retryTask,
525-
deleteTask,
526-
retryPage,
527-
getTaskResult,
528-
downloadResult,
529-
createCheckout,
530-
getCheckoutStatus,
531-
reconcileCheckout,
532-
getCreditHistory,
533-
getPaymentHistory
534-
}}
535-
>
542+
<CloudContext.Provider value={contextValue}>
536543
{children}
537544
</CloudContext.Provider>
538545
);

src/renderer/pages/List.tsx

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const List: React.FC = () => {
5050
const { t } = useTranslation('list');
5151
const { t: tCommon } = useTranslation('common');
5252
const cloudContext = useContext(CloudContext);
53+
const cloudIsAuthenticated = cloudContext?.isAuthenticated ?? false;
54+
const cloudGetTasks = cloudContext?.getTasks;
5355

5456
const [loading, setLoading] = useState(false);
5557
const [data, setData] = useState<(Task | CloudTask)[]>([]);
@@ -66,6 +68,12 @@ const List: React.FC = () => {
6668
const paginationRef = useRef(pagination);
6769
paginationRef.current = pagination;
6870

71+
// Refs for debounced cloud SSE refresh
72+
const pendingRefreshRef = useRef(false);
73+
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
74+
const dataRef = useRef(data);
75+
dataRef.current = data;
76+
6977
// Max items to fetch for unified sorting and local pagination
7078
const MAX_FETCH_ITEMS = 100;
7179
const buildLocalModelValue = (modelId: string, providerId: number) => `${modelId}@${providerId}`;
@@ -127,8 +135,8 @@ const List: React.FC = () => {
127135
];
128136

129137
// Only fetch cloud tasks if authenticated
130-
if (cloudContext?.isAuthenticated) {
131-
promises.push(cloudContext.getTasks(1, MAX_FETCH_ITEMS));
138+
if (cloudIsAuthenticated && cloudGetTasks) {
139+
promises.push(cloudGetTasks(1, MAX_FETCH_ITEMS));
132140
}
133141

134142
const results = await Promise.all(promises);
@@ -240,7 +248,7 @@ const List: React.FC = () => {
240248
} finally {
241249
setLoading(false);
242250
}
243-
}, [message, t, cloudContext]);
251+
}, [message, t, cloudIsAuthenticated, cloudGetTasks]);
244252

245253
const handleTaskEvent = useCallback((event: any) => {
246254
const { type, taskId, task } = event;
@@ -306,36 +314,44 @@ const List: React.FC = () => {
306314

307315
console.log('[List] Registering cloud SSE event listener');
308316

309-
// Track tasks not found in list to trigger a single refresh
310-
let pendingRefresh = false;
311-
312317
const handleCloudEvent = (event: CloudSSEEvent) => {
313-
const { type, data } = event;
318+
const { type, data: eventData } = event;
314319

315320
// Skip non-business events
316321
if (type === 'heartbeat' || type === 'connected') return;
317322

318-
const taskId = (data as any).task_id;
323+
const taskId = (eventData as any).task_id;
319324
if (!taskId) return;
320325

321326
console.log(`[List] Cloud SSE event: type=${type}, task_id=${taskId}`);
322327

323-
setData(prevData => {
324-
const index = prevData.findIndex(t => t.id === taskId);
325-
if (index === -1) {
326-
// Task not in list, schedule a refresh outside of setState
327-
if (!pendingRefresh) {
328-
pendingRefresh = true;
329-
queueMicrotask(() => {
330-
pendingRefresh = false;
331-
fetchTasks(paginationRef.current.current, paginationRef.current.pageSize);
332-
});
333-
}
334-
return prevData;
328+
// Check if task is in current list using ref (avoids scheduling inside setState)
329+
const currentData = dataRef.current;
330+
const index = currentData.findIndex(t => t.id === taskId);
331+
332+
if (index === -1) {
333+
// Task not in list — debounced refresh (500ms merge window)
334+
if (!pendingRefreshRef.current) {
335+
pendingRefreshRef.current = true;
336+
refreshTimerRef.current = setTimeout(async () => {
337+
try {
338+
await fetchTasks(paginationRef.current.current, paginationRef.current.pageSize);
339+
} finally {
340+
pendingRefreshRef.current = false;
341+
refreshTimerRef.current = null;
342+
}
343+
}, 500);
335344
}
345+
return;
346+
}
347+
348+
// Task found in list — apply incremental update
349+
setData(prevData => {
350+
const idx = prevData.findIndex(t => t.id === taskId);
351+
if (idx === -1) return prevData;
336352

337353
const newData = [...prevData];
338-
const task = { ...newData[index] };
354+
const task = { ...newData[idx] };
339355

340356
switch (type) {
341357
case 'page_started':
@@ -344,16 +360,15 @@ const List: React.FC = () => {
344360
break;
345361
}
346362
case 'page_completed': {
347-
const pageNumber = (data as any).page;
348-
const totalPages = (data as any).total_pages || task.pages || 1;
363+
const pageNumber = (eventData as any).page;
364+
const totalPages = (eventData as any).total_pages || task.pages || 1;
349365

350366
if (task.status === 8) {
351367
// Retry scenario (PARTIAL_FAILED): increment completed, decrement failed
352368
task.completed_count = (task.completed_count || 0) + 1;
353369
task.failed_count = Math.max(0, (task.failed_count || 0) - 1);
354370
} else {
355371
// Normal processing: use page number as progress indicator
356-
// page is 1-based, so completed_count = page number when pages complete in order
357372
task.completed_count = Math.max(task.completed_count || 0, pageNumber || 0);
358373
}
359374

@@ -362,20 +377,19 @@ const List: React.FC = () => {
362377
break;
363378
}
364379
case 'page_failed': {
365-
// Increment as approximation; the 'completed' event provides authoritative pages_failed
366380
task.failed_count = (task.failed_count || 0) + 1;
367381
break;
368382
}
369383
case 'completed': {
370-
task.status = (data as any).status || 6;
384+
task.status = (eventData as any).status || 6;
371385
task.progress = 100;
372-
task.completed_count = (data as any).pages_completed;
373-
task.failed_count = (data as any).pages_failed;
386+
task.completed_count = (eventData as any).pages_completed;
387+
task.failed_count = (eventData as any).pages_failed;
374388
break;
375389
}
376390
case 'error': {
377391
task.status = 0; // FAILED
378-
task.error = (data as any).error;
392+
task.error = (eventData as any).error;
379393
break;
380394
}
381395
case 'cancelled': {
@@ -384,14 +398,14 @@ const List: React.FC = () => {
384398
}
385399
case 'pdf_ready': {
386400
task.status = 3; // PROCESSING (splitting done, pages ready for conversion)
387-
task.pages = (data as any).page_count;
401+
task.pages = (eventData as any).page_count;
388402
break;
389403
}
390404
default:
391405
return prevData;
392406
}
393407

394-
newData[index] = task;
408+
newData[idx] = task;
395409
// Sync module-level progress cache so it survives component remount
396410
if (task.id) {
397411
const terminalStatuses = [0, 6, 7, 8]; // FAILED, COMPLETED, CANCELLED, PARTIAL_FAILED
@@ -415,6 +429,12 @@ const List: React.FC = () => {
415429
return () => {
416430
console.log('[List] Cleaning up cloud SSE event listener');
417431
cleanup();
432+
// Clear any pending debounced refresh
433+
if (refreshTimerRef.current) {
434+
clearTimeout(refreshTimerRef.current);
435+
refreshTimerRef.current = null;
436+
pendingRefreshRef.current = false;
437+
}
418438
};
419439
}, [fetchTasks]);
420440

0 commit comments

Comments
 (0)