fix(renderer): recover failed dynamic imports

This commit is contained in:
777genius 2026-05-25 15:42:56 +03:00
parent 9b169f335f
commit 086a8b3e29
3 changed files with 125 additions and 0 deletions

View file

@ -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;

View file

@ -0,0 +1,62 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { registerDynamicImportRecovery } from './dynamicImportRecovery';
class MemoryStorage implements Pick<Storage, 'getItem' | 'setItem'> {
private readonly values = new Map<string, string>();
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();
});
});

View file

@ -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<Storage, 'getItem' | 'setItem'>;
}
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);
};
}