fix(autosave): start late-installed impl for already-mounted hooks

The overlay's auto-save implementation arrives via a dynamic import that
races the first React commit. A hook whose mount effect ran before the
overlay chunk evaluated saw installedImpl === null and stayed idle for
the whole life of the tab: no debounced saves, no beforeunload flush,
with no visible symptom. Any tab that hard-loaded straight into the
editor and never remounted it silently lost every edit made after the
project was bound.

installAutoSaveImpl now wakes hooks that mounted before it ran, so the
implementation starts as soon as it exists. Swapping a live impl at
runtime remains unsupported.
This commit is contained in:
David Montero 2026-07-31 08:24:44 +02:00
parent a79d8fd563
commit 96c1b3323c
1 changed files with 22 additions and 4 deletions

View File

@ -30,18 +30,36 @@ const IDLE: AutoSaveState = { status: 'idle', lastSavedAt: null, errorMessage: n
let installedImpl: AutoSaveImpl | null = null;
/** Hooks that mounted before an impl was installed, waiting to start it. */
const installWaiters = new Set<() => void>();
export function installAutoSaveImpl(impl: AutoSaveImpl | null): void {
installedImpl = impl;
// Overlays load through a dynamic import that races the first React
// commit: a hook whose mount effect ran before the overlay chunk
// evaluated used to see `installedImpl === null` and stay idle for the
// whole life of the tab — no auto-save, no unload flush. Start those
// already-mounted hooks now that the impl exists.
if (impl) installWaiters.forEach((start) => start());
}
export function useAutoSaveProject(): AutoSaveState {
const [state, setState] = useState<AutoSaveState>(IDLE);
useEffect(() => {
if (!installedImpl) return;
return installedImpl(setState);
// Mount-only — impl is installed at module load, swapping at runtime is unsupported.
// eslint-disable-next-line react-hooks/exhaustive-deps
let cleanup: (() => void) | null = null;
const start = () => {
if (installedImpl && !cleanup) cleanup = installedImpl(setState);
};
start();
// Late-install support only — swapping a live impl at runtime is still
// unsupported (the first installed impl keeps running until unmount).
installWaiters.add(start);
return () => {
installWaiters.delete(start);
cleanup?.();
cleanup = null;
};
}, []);
return state;