feat(fp): flush pending autosave before navigation/unload
Close the last-mile autosave gap: a debounced edit made in the final <600ms before leaving a page was lost — the wizard draft-sync timer is cleared on destroy without flushing, and root stores keep an armed timer the teardown ignores. New `shared/application/pending-saves.ts`: a root `PendingSaves` registry every autosave owner joins (BriefStore, OrgTemplateStore, each createDraftSync). Two seams flush through it — `flushPendingGuard` (CanDeactivate, on the five autosave routes) awaits the pending write before an in-app route change; a `beforeunload` handler (provideUnloadFlush) fires it best-effort and raises the browser's native unsaved-changes prompt. ponytail: the HTTP seam is Angular HttpClient (no keepalive/sendBeacon), so a hard-close flush can't be guaranteed — hence the prompt; upgrade path noted in a comment. Each owner now nulls its timer handle on fire so `hasPendingSave()` is accurate, and exposes `flushPending()`. Verified live against the running stack: navigating away 91ms after a keystroke (well inside the debounce) fires one PUT /brief before the route changes; a dirty reload raises the prompt, a clean reload does not. FE lint / check:tokens / 299 tests (+11) / build / build-storybook green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -301,3 +301,28 @@ describe('BriefStore.revealBigNummer (PRD-0002 §5c)', () => {
|
||||
expect(store.lastError()).toBe('geweigerd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BriefStore.flushPending (CanDeactivate guard / beforeunload)', () => {
|
||||
const okSave = () =>
|
||||
vi.fn(() => Promise.resolve({ ok: true, value: filledView } as Result<string, BriefView>));
|
||||
|
||||
it('flushes a pending debounced edit immediately and clears the pending flag', async () => {
|
||||
const save = okSave();
|
||||
const store = await loadedStore({ save });
|
||||
expect(store.hasPendingSave()).toBe(false);
|
||||
|
||||
store.edit({ tag: 'FreeTextBlockAdded', sectionKey: 'kern' });
|
||||
expect(store.hasPendingSave()).toBe(true); // 600ms debounce armed, not yet fired
|
||||
|
||||
await store.flushPending();
|
||||
expect(save).toHaveBeenCalledTimes(1); // no timer wait needed
|
||||
expect(store.hasPendingSave()).toBe(false); // timer consumed
|
||||
});
|
||||
|
||||
it('is a no-op when no edit is pending', async () => {
|
||||
const save = okSave();
|
||||
const store = await loadedStore({ save });
|
||||
await store.flushPending();
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user