mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
Changes from fix/pwa-cache-fix (#794)
This commit is contained in:
@@ -10,8 +10,16 @@ interface Props {
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
isCrashLoop: boolean;
|
||||
}
|
||||
|
||||
/** Key used to track recent crash timestamps for crash loop detection */
|
||||
const CRASH_TIMESTAMPS_KEY = 'automaker-crash-timestamps';
|
||||
/** Number of crashes within the time window that constitutes a crash loop */
|
||||
const CRASH_LOOP_THRESHOLD = 3;
|
||||
/** Time window in ms for crash loop detection (30 seconds) */
|
||||
const CRASH_LOOP_WINDOW_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Root-level error boundary for the entire application.
|
||||
*
|
||||
@@ -21,14 +29,18 @@ interface State {
|
||||
* Provides a user-friendly error screen with a reload button to recover.
|
||||
* This is especially important for transient errors during initial app load
|
||||
* (e.g., race conditions during auth/hydration on fresh browser sessions).
|
||||
*
|
||||
* Includes crash loop detection: if the app crashes 3+ times within 30 seconds,
|
||||
* the UI cache is automatically cleared to break loops caused by stale cached
|
||||
* worktree paths or other corrupt persisted state.
|
||||
*/
|
||||
export class AppErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
this.state = { hasError: false, error: null, isCrashLoop: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
@@ -38,12 +50,48 @@ export class AppErrorBoundary extends Component<Props, State> {
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
});
|
||||
|
||||
// Track crash timestamps to detect crash loops.
|
||||
// If the app crashes multiple times in quick succession, it's likely due to
|
||||
// stale cached data (e.g., worktree paths that no longer exist on disk).
|
||||
try {
|
||||
const now = Date.now();
|
||||
const raw = sessionStorage.getItem(CRASH_TIMESTAMPS_KEY);
|
||||
const timestamps: number[] = raw ? JSON.parse(raw) : [];
|
||||
timestamps.push(now);
|
||||
// Keep only timestamps within the detection window
|
||||
const recent = timestamps.filter((t) => now - t < CRASH_LOOP_WINDOW_MS);
|
||||
sessionStorage.setItem(CRASH_TIMESTAMPS_KEY, JSON.stringify(recent));
|
||||
|
||||
if (recent.length >= CRASH_LOOP_THRESHOLD) {
|
||||
logger.error(
|
||||
`Crash loop detected (${recent.length} crashes in ${CRASH_LOOP_WINDOW_MS}ms) — clearing UI cache`
|
||||
);
|
||||
// Auto-clear the UI cache to break the loop
|
||||
localStorage.removeItem('automaker-ui-cache');
|
||||
sessionStorage.removeItem(CRASH_TIMESTAMPS_KEY);
|
||||
this.setState({ isCrashLoop: true });
|
||||
}
|
||||
} catch {
|
||||
// Storage may be unavailable — ignore
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleClearCacheAndReload = () => {
|
||||
// Clear the UI cache store that persists worktree selections and other UI state.
|
||||
// This breaks crash loops caused by stale worktree paths that no longer exist on disk.
|
||||
try {
|
||||
localStorage.removeItem('automaker-ui-cache');
|
||||
} catch {
|
||||
// localStorage may be unavailable in some contexts
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
@@ -82,11 +130,13 @@ export class AppErrorBoundary extends Component<Props, State> {
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-xl font-semibold">Something went wrong</h1>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
The application encountered an unexpected error. This is usually temporary and can be
|
||||
resolved by reloading the page.
|
||||
{this.state.isCrashLoop
|
||||
? 'The application crashed repeatedly, likely due to stale cached data. The cache has been cleared automatically. Reload to continue.'
|
||||
: 'The application encountered an unexpected error. This is usually temporary and can be resolved by reloading the page.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleReload}
|
||||
@@ -111,6 +161,30 @@ export class AppErrorBoundary extends Component<Props, State> {
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleClearCacheAndReload}
|
||||
className="inline-flex items-center gap-2 rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
Clear Cache & Reload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Collapsible technical details for debugging */}
|
||||
{this.state.error && (
|
||||
<details className="text-xs text-muted-foreground max-w-lg w-full">
|
||||
|
||||
@@ -160,11 +160,31 @@ export function restoreFromUICache(
|
||||
|
||||
// Restore last selected worktree per project so the board doesn't
|
||||
// reset to main branch after PWA memory eviction or tab discard.
|
||||
//
|
||||
// IMPORTANT: Only restore entries where path is null (main branch selection).
|
||||
// Non-null paths point to worktree directories on disk that may have been
|
||||
// deleted while the PWA was evicted. Restoring a stale worktree path causes
|
||||
// the board to render with an invalid selection, and if the server can't
|
||||
// validate it fast enough, the app enters an unrecoverable crash loop
|
||||
// (the error boundary reloads, which restores the same bad cache).
|
||||
// Main branch (path=null) is always valid and safe to restore.
|
||||
if (
|
||||
cache.cachedCurrentWorktreeByProject &&
|
||||
Object.keys(cache.cachedCurrentWorktreeByProject).length > 0
|
||||
) {
|
||||
stateUpdate.currentWorktreeByProject = cache.cachedCurrentWorktreeByProject;
|
||||
const sanitized: Record<string, { path: string | null; branch: string }> = {};
|
||||
for (const [projectPath, worktree] of Object.entries(cache.cachedCurrentWorktreeByProject)) {
|
||||
if (worktree.path === null) {
|
||||
// Main branch selection — always safe to restore
|
||||
sanitized[projectPath] = worktree;
|
||||
}
|
||||
// Non-null paths are dropped; the app will re-discover actual worktrees
|
||||
// from the server and the validation effect in use-worktrees will handle
|
||||
// resetting to main if the cached worktree no longer exists.
|
||||
}
|
||||
if (Object.keys(sanitized).length > 0) {
|
||||
stateUpdate.currentWorktreeByProject = sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the project context when the project object is available.
|
||||
|
||||
Reference in New Issue
Block a user