Просмотр исходного кода

Fix browser tab crash on print completion (debounced invalidations)

  When a print completes, multiple WebSocket messages are sent rapidly
  (print_complete, archive_updated, etc.). Each was immediately invalidating
  the archives query, causing a cascade of rapid re-renders that crashed
  the browser tab.

  Added debounced query invalidation to useWebSocket.ts:
  - Multiple invalidation requests within 100ms are coalesced into one
  - Uses requestAnimationFrame to avoid blocking the main thread
  - Properly cleans up timeouts on unmount
maziggy 5 месяцев назад
Родитель
Сommit
5cd366ceac
4 измененных файлов с 41 добавлено и 10 удалено
  1. 40 9
      frontend/src/hooks/useWebSocket.ts
  2. BIN
      static/._.DS_Store
  3. 0 0
      static/assets/index-AT42oZVR.js
  4. 1 1
      static/index.html

+ 40 - 9
frontend/src/hooks/useWebSocket.ts

@@ -13,6 +13,10 @@ export function useWebSocket() {
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const [isConnected, setIsConnected] = useState(false);
   const [isConnected, setIsConnected] = useState(false);
 
 
+  // Debounce invalidations to prevent rapid re-render cascades
+  const pendingInvalidations = useRef<Set<string>>(new Set());
+  const invalidationTimeoutRef = useRef<number | null>(null);
+
   const connect = useCallback(() => {
   const connect = useCallback(() => {
     if (wsRef.current?.readyState === WebSocket.OPEN) {
     if (wsRef.current?.readyState === WebSocket.OPEN) {
       return;
       return;
@@ -68,6 +72,30 @@ export function useWebSocket() {
     wsRef.current = ws;
     wsRef.current = ws;
   }, []);
   }, []);
 
 
+  // Debounced invalidation helper - coalesces multiple rapid invalidations
+  const debouncedInvalidate = useCallback((queryKey: string) => {
+    pendingInvalidations.current.add(queryKey);
+
+    // Clear existing timeout
+    if (invalidationTimeoutRef.current) {
+      clearTimeout(invalidationTimeoutRef.current);
+    }
+
+    // Schedule invalidation after a short delay (100ms)
+    invalidationTimeoutRef.current = window.setTimeout(() => {
+      const keys = Array.from(pendingInvalidations.current);
+      pendingInvalidations.current.clear();
+      invalidationTimeoutRef.current = null;
+
+      // Use requestAnimationFrame to avoid blocking the main thread
+      requestAnimationFrame(() => {
+        keys.forEach((key) => {
+          queryClient.invalidateQueries({ queryKey: [key] });
+        });
+      });
+    }, 100);
+  }, [queryClient]);
+
   const handleMessage = useCallback((message: WebSocketMessage) => {
   const handleMessage = useCallback((message: WebSocketMessage) => {
     switch (message.type) {
     switch (message.type) {
       case 'printer_status':
       case 'printer_status':
@@ -91,27 +119,27 @@ export function useWebSocket() {
         break;
         break;
 
 
       case 'print_complete':
       case 'print_complete':
-        // Invalidate archives to refresh the list
-        queryClient.invalidateQueries({ queryKey: ['archives'] });
-        queryClient.invalidateQueries({ queryKey: ['archiveStats'] });
+        // Invalidate archives to refresh the list (debounced)
+        debouncedInvalidate('archives');
+        debouncedInvalidate('archiveStats');
         break;
         break;
 
 
       case 'archive_created':
       case 'archive_created':
-        // Invalidate archives to show new archive
-        queryClient.invalidateQueries({ queryKey: ['archives'] });
-        queryClient.invalidateQueries({ queryKey: ['archiveStats'] });
+        // Invalidate archives to show new archive (debounced)
+        debouncedInvalidate('archives');
+        debouncedInvalidate('archiveStats');
         break;
         break;
 
 
       case 'archive_updated':
       case 'archive_updated':
-        // Invalidate archives to refresh (e.g., timelapse attached)
-        queryClient.invalidateQueries({ queryKey: ['archives'] });
+        // Invalidate archives to refresh (debounced)
+        debouncedInvalidate('archives');
         break;
         break;
 
 
       case 'pong':
       case 'pong':
         // Keepalive response, ignore
         // Keepalive response, ignore
         break;
         break;
     }
     }
-  }, [queryClient]);
+  }, [queryClient, debouncedInvalidate]);
 
 
   useEffect(() => {
   useEffect(() => {
     connect();
     connect();
@@ -120,6 +148,9 @@ export function useWebSocket() {
       if (reconnectTimeoutRef.current) {
       if (reconnectTimeoutRef.current) {
         clearTimeout(reconnectTimeoutRef.current);
         clearTimeout(reconnectTimeoutRef.current);
       }
       }
+      if (invalidationTimeoutRef.current) {
+        clearTimeout(invalidationTimeoutRef.current);
+      }
       if (wsRef.current) {
       if (wsRef.current) {
         wsRef.current.close();
         wsRef.current.close();
       }
       }

BIN
static/._.DS_Store


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-AT42oZVR.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CFaicAOR.js"></script>
+    <script type="module" crossorigin src="/assets/index-AT42oZVR.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CbCN6LSA.css">
     <link rel="stylesheet" crossorigin href="/assets/index-CbCN6LSA.css">
   </head>
   </head>
   <body>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов