feat(standalone): "open in a new tab" escape hatch on OOM dialog (0002)
The automatic recovery uses an in-place location.replace reload, which does NOT reliably release the OOM'd wasm heap in every browser (notably Firefox), so the respawn can land right back in an out-of-memory state. Add a user-gesture escape hatch to the terminal MemoryExhaustedDialog: an "Open in a new tab" button that opens the editor in a fresh top-level browsing context (clean retry chain) and closes the current tab. Because it runs from a real click, window.open is not popup-blocked; window.close() may still be refused on the original user-opened tab, which is fine — the new tab is the survivor. Copy tells the user this sometimes helps where the in-place reload didn't. - oom-watch.ts: export respawnInNewTab(win) — fresh-tab respawn with ?oomRetry cleared. - MemoryExhaustedDialog: "Open in a new tab" (primary) + "Reload this tab" (secondary), with honest copy about Firefox + unsaved edits. - WasmTool: wire onOpenNewTab to respawnInNewTab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d2f88b437
commit
0773acefaa
3 changed files with 61 additions and 10 deletions
|
|
@ -13,7 +13,7 @@ import { memfsProjectDir } from "@/wasm/constants";
|
||||||
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner";
|
||||||
import type { CollabWindow } from "@/wasm/collab";
|
import type { CollabWindow } from "@/wasm/collab";
|
||||||
import { clog, cwarn } from "@/wasm/collab/debug";
|
import { clog, cwarn } from "@/wasm/collab/debug";
|
||||||
import { createOomWatch } from "@/recovery/oom-watch";
|
import { createOomWatch, respawnInNewTab } from "@/recovery/oom-watch";
|
||||||
import { MemoryExhaustedDialog } from "@/recovery/MemoryExhaustedDialog";
|
import { MemoryExhaustedDialog } from "@/recovery/MemoryExhaustedDialog";
|
||||||
|
|
||||||
// Tools with a working collab bridge (kicadCollabSnapshot/Apply embind exports).
|
// Tools with a working collab bridge (kicadCollabSnapshot/Apply embind exports).
|
||||||
|
|
@ -349,7 +349,10 @@ export function WasmTool({
|
||||||
<div id="window-container" />
|
<div id="window-container" />
|
||||||
|
|
||||||
{oomExhausted && (
|
{oomExhausted && (
|
||||||
<MemoryExhaustedDialog onReload={() => window.location.reload()} />
|
<MemoryExhaustedDialog
|
||||||
|
onOpenNewTab={() => respawnInNewTab()}
|
||||||
|
onReload={() => window.location.reload()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status && (
|
{status && (
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,38 @@ import { BlockingDialog } from "@/preflight/BlockingDialog";
|
||||||
* Terminal "not enough memory" UI for feature 0002 — shown after the OOM retry
|
* Terminal "not enough memory" UI for feature 0002 — shown after the OOM retry
|
||||||
* chain hits MAX_RETRIES. Reuses 0001's blocking dialog for visual consistency.
|
* chain hits MAX_RETRIES. Reuses 0001's blocking dialog for visual consistency.
|
||||||
*
|
*
|
||||||
* The copy is honest about the two limitations the spec calls out: a respawn
|
* Offers two manual actions:
|
||||||
* re-opens the same file but loses in-tab edits not yet pushed to the backend /
|
* - "Open in a new tab" (primary): a user-gesture respawn in a fresh browsing
|
||||||
* Y.Doc, and `window.close()` may not close the very first (user-opened) tab.
|
* context. This sometimes succeeds where the automatic in-place reload did
|
||||||
|
* not, because a brand-new tab is the most reliable way to actually release
|
||||||
|
* the OOM'd wasm heap (an in-place reload doesn't, reliably, in every
|
||||||
|
* browser — notably Firefox).
|
||||||
|
* - "Reload this tab" (secondary): the simple in-place retry.
|
||||||
|
*
|
||||||
|
* Copy is honest about lost unsaved edits (a respawn re-opens the same file but
|
||||||
|
* loses in-tab edits not yet synced to the backend / Y.Doc).
|
||||||
*/
|
*/
|
||||||
export function MemoryExhaustedDialog({ onReload }: { onReload?: () => void }) {
|
export function MemoryExhaustedDialog({
|
||||||
|
onOpenNewTab,
|
||||||
|
onReload,
|
||||||
|
}: {
|
||||||
|
onOpenNewTab?: () => void;
|
||||||
|
onReload?: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<BlockingDialog
|
<BlockingDialog
|
||||||
title="Your device ran out of memory"
|
title="Your device ran out of memory"
|
||||||
description="The editor tried to recover a few times but kept running out of memory, so it stopped to avoid looping. This design is likely too large for this device's available memory."
|
description="The editor tried to recover a few times but kept running out of memory, so it stopped to avoid looping. This design is likely too large for this device's available memory."
|
||||||
reasons={[
|
reasons={[
|
||||||
{
|
{
|
||||||
title: "What you can try",
|
title: "Try a fresh tab",
|
||||||
detail:
|
detail:
|
||||||
"Close other tabs and applications to free memory, use a desktop machine with more RAM, or open a smaller design.",
|
"Opening the editor in a brand-new browser tab (and closing this one) sometimes frees enough memory to continue — especially in Firefox, where reloading in place may not fully release the previous session's memory.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Free up memory",
|
||||||
|
detail:
|
||||||
|
"Close other tabs and applications, use a desktop machine with more RAM, or open a smaller design.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Unsaved changes",
|
title: "Unsaved changes",
|
||||||
|
|
@ -26,10 +44,13 @@ export function MemoryExhaustedDialog({ onReload }: { onReload?: () => void }) {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
primary={
|
primary={
|
||||||
onReload
|
onOpenNewTab
|
||||||
? { label: "Reload and try again", onClick: onReload }
|
? { label: "Open in a new tab", onClick: onOpenNewTab }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
secondary={
|
||||||
|
onReload ? { label: "Reload this tab", onClick: onReload } : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,33 @@ export function createOomWatch(opts: OomWatchOptions): OomWatch {
|
||||||
return { start, stop, onAbort };
|
return { start, stop, onAbort };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-gesture escape hatch for the terminal dialog: open the editor in a brand
|
||||||
|
* new tab with a clean retry chain, then close this one.
|
||||||
|
*
|
||||||
|
* This is deliberately NOT the automatic recovery path: it runs from a real
|
||||||
|
* click, so `window.open` is not popup-blocked, and a fresh top-level browsing
|
||||||
|
* context is the most reliable way to actually drop the OOM'd wasm heap — an
|
||||||
|
* in-place `location.replace` reload does not reliably release it in every
|
||||||
|
* browser (notably Firefox). `window.close()` may still be refused on the
|
||||||
|
* original, user-opened tab; that's fine — the new tab is the working one and
|
||||||
|
* the user can close the old one. Returns whether a new tab was opened.
|
||||||
|
*/
|
||||||
|
export function respawnInNewTab(win: Window = window): boolean {
|
||||||
|
const url = new URL(win.location.href);
|
||||||
|
url.searchParams.delete(RETRY_PARAM); // fresh chain — this is a manual retry
|
||||||
|
const opened = win.open(url.toString(), "_blank");
|
||||||
|
if (opened) {
|
||||||
|
try {
|
||||||
|
win.close();
|
||||||
|
} catch {
|
||||||
|
/* original tab may refuse to close — new tab is the survivor anyway */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function safeLocalStorage(win: Window): Storage | null {
|
function safeLocalStorage(win: Window): Storage | null {
|
||||||
try {
|
try {
|
||||||
return win.localStorage;
|
return win.localStorage;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue