fix(renderer): recover failed dynamic imports
This commit is contained in:
parent
9b169f335f
commit
086a8b3e29
3 changed files with 125 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
62
src/renderer/utils/dynamicImportRecovery.test.ts
Normal file
62
src/renderer/utils/dynamicImportRecovery.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
61
src/renderer/utils/dynamicImportRecovery.ts
Normal file
61
src/renderer/utils/dynamicImportRecovery.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue