Files
OpenFrame/lib/client/unload-guard.ts
T
yusufipek cba8163286 fix(download): warn before the tab closes mid-download
Bunny and direct downloads are pulled through fetch() so we can save them
under our own filename. The browser does not treat that as a download, so
closing the tab discarded everything received so far without a word.

Register a reference counted beforeunload guard while those transfers are
in flight, and while a project manifest is being pulled file by file.
Browser owned downloads (same-origin proxy, the over-10GB fallback, asset
downloads) survive a tab close on their own and stay unguarded.
2026-08-22 12:50:45 +03:00

49 lines
1.6 KiB
TypeScript

'use client';
/**
* Downloads that we pull through fetch() live inside the page: closing the tab
* (or reloading) throws away every byte received so far and the browser gives no
* warning, because as far as it knows nothing is downloading. While one of those
* is in flight we register a beforeunload handler so the user gets the native
* "leave site?" dialog instead of silently losing the transfer.
*
* Plain navigation downloads (the `download` attribute / a redirect to the CDN)
* are owned by the browser and survive a tab close, so they must NOT be guarded.
*/
let activeCount = 0;
function handleBeforeUnload(event: BeforeUnloadEvent) {
event.preventDefault();
// Legacy browsers only show the dialog when returnValue is set; the string
// itself is ignored, every browser shows its own wording.
event.returnValue = '';
}
/** Registers the guard and returns a release function. Safe to call again while
* another download is already guarded — the listener is reference counted and
* only detaches once the last one releases. Releasing twice is a no-op. */
export function beginUnloadGuard(): () => void {
if (typeof window === 'undefined') return () => {};
if (activeCount === 0) {
window.addEventListener('beforeunload', handleBeforeUnload);
}
activeCount += 1;
let released = false;
return () => {
if (released) return;
released = true;
activeCount -= 1;
if (activeCount === 0) {
window.removeEventListener('beforeunload', handleBeforeUnload);
}
};
}
/** Test helper: number of downloads currently holding the guard. */
export function unloadGuardCount(): number {
return activeCount;
}