From 086a8b3e2962c61bc51db2233152351b2d10c367 Mon Sep 17 00:00:00 2001 From: 777genius Date: Mon, 25 May 2026 15:42:56 +0300 Subject: [PATCH] fix(renderer): recover failed dynamic imports --- src/renderer/main.tsx | 2 + .../utils/dynamicImportRecovery.test.ts | 62 +++++++++++++++++++ src/renderer/utils/dynamicImportRecovery.ts | 61 ++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/renderer/utils/dynamicImportRecovery.test.ts create mode 100644 src/renderer/utils/dynamicImportRecovery.ts diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 91643c58..8a605dcc 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -5,6 +5,7 @@ import 'react-resizable/css/styles.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; +import { registerDynamicImportRecovery } from './utils/dynamicImportRecovery'; import { App } from './App'; import { initSentryRenderer } from './sentry'; import { initializeNotificationListeners } from './store'; @@ -23,6 +24,7 @@ declare global { // Prepare Sentry before React renders. Actual init waits for telemetry config. initSentryRenderer(); +registerDynamicImportRecovery(); let root: ReactDOM.Root | null = null; let latestStartupStatus: AppStartupStatus | null = null; diff --git a/src/renderer/utils/dynamicImportRecovery.test.ts b/src/renderer/utils/dynamicImportRecovery.test.ts new file mode 100644 index 00000000..d58d3948 --- /dev/null +++ b/src/renderer/utils/dynamicImportRecovery.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { registerDynamicImportRecovery } from './dynamicImportRecovery'; + +class MemoryStorage implements Pick { + private readonly values = new Map(); + + getItem(key: string): string | null { + return this.values.get(key) ?? null; + } + + setItem(key: string, value: string): void { + this.values.set(key, value); + } +} + +describe('registerDynamicImportRecovery', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('prevents Vite preload errors from reaching React and reloads the renderer once', () => { + vi.useFakeTimers(); + const reload = vi.fn(); + const storage = new MemoryStorage(); + const cleanup = registerDynamicImportRecovery({ + now: () => 1_000, + reload, + storage, + }); + + const event = new Event('vite:preloadError', { cancelable: true }); + window.dispatchEvent(event); + + expect(event.defaultPrevented).toBe(true); + expect(reload).not.toHaveBeenCalled(); + + vi.runOnlyPendingTimers(); + + expect(reload).toHaveBeenCalledTimes(1); + cleanup(); + }); + + it('throttles repeated failed chunk reloads to avoid a reload loop', () => { + vi.useFakeTimers(); + const reload = vi.fn(); + const storage = new MemoryStorage(); + const cleanup = registerDynamicImportRecovery({ + now: () => 5_000, + reload, + storage, + }); + + window.dispatchEvent(new Event('vite:preloadError', { cancelable: true })); + vi.runOnlyPendingTimers(); + window.dispatchEvent(new Event('vite:preloadError', { cancelable: true })); + vi.runOnlyPendingTimers(); + + expect(reload).toHaveBeenCalledTimes(1); + cleanup(); + }); +}); diff --git a/src/renderer/utils/dynamicImportRecovery.ts b/src/renderer/utils/dynamicImportRecovery.ts new file mode 100644 index 00000000..d3f6df31 --- /dev/null +++ b/src/renderer/utils/dynamicImportRecovery.ts @@ -0,0 +1,61 @@ +const LAST_RELOAD_AT_KEY = 'agent-teams-ai:dynamic-import-recovery:last-reload-at'; +const RELOAD_THROTTLE_MS = 10_000; + +interface DynamicImportRecoveryOptions { + now?: () => number; + reload?: () => void; + storage?: Pick; +} + +function readLastReloadAt(storage: DynamicImportRecoveryOptions['storage']): number { + if (!storage) { + return 0; + } + + try { + const value = Number(storage.getItem(LAST_RELOAD_AT_KEY)); + return Number.isFinite(value) ? value : 0; + } catch { + return 0; + } +} + +function writeLastReloadAt( + storage: DynamicImportRecoveryOptions['storage'], + timestamp: number +): void { + if (!storage) { + return; + } + + try { + storage.setItem(LAST_RELOAD_AT_KEY, String(timestamp)); + } catch { + // Session storage can be unavailable in constrained renderer contexts. + } +} + +export function registerDynamicImportRecovery({ + now = () => Date.now(), + reload = () => window.location.reload(), + storage = window.sessionStorage, +}: DynamicImportRecoveryOptions = {}): () => void { + const handlePreloadError = (event: Event): void => { + event.preventDefault(); + + const timestamp = now(); + const lastReloadAt = readLastReloadAt(storage); + if (lastReloadAt > 0 && timestamp - lastReloadAt < RELOAD_THROTTLE_MS) { + return; + } + + writeLastReloadAt(storage, timestamp); + window.setTimeout(reload, 0); + }; + + window.addEventListener('vite:preloadError', handlePreloadError); + + return () => { + window.removeEventListener('vite:preloadError', handlePreloadError); + }; +}