From ff0bb892978d1d5cce68f1eb5ea5d5805a2c71e5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 16:46:58 +0200 Subject: [PATCH] =?UTF-8?q?refactor(autosave):=20rename=20flushViaBeacon?= =?UTF-8?q?=20=E2=86=92=20flushOnUnload;=20add=20void=20to=20fire-and-forg?= =?UTF-8?q?et=20fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sendBeacon name was misleading after switching to keepalive fetch. Also adds a test to confirm flush is a no-op when pendingTexts is empty. Co-Authored-By: Claude Sonnet 4.6 --- .../components/TranscriptionEditView.svelte | 2 +- .../__tests__/useBlockAutoSave.svelte.test.ts | 22 ++++++++++++++----- .../src/lib/hooks/useBlockAutoSave.svelte.ts | 6 ++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/TranscriptionEditView.svelte b/frontend/src/lib/components/TranscriptionEditView.svelte index 85becbc2..c156f6c0 100644 --- a/frontend/src/lib/components/TranscriptionEditView.svelte +++ b/frontend/src/lib/components/TranscriptionEditView.svelte @@ -73,7 +73,7 @@ $effect(() => { $effect(() => { function onBeforeUnload() { - autoSave.flushViaBeacon(); + autoSave.flushOnUnload(); } window.addEventListener('beforeunload', onBeforeUnload); return () => { diff --git a/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts b/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts index 148df5de..739fb432 100644 --- a/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts +++ b/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts @@ -83,7 +83,7 @@ describe('createBlockAutoSave', () => { }); }); -describe('flushViaBeacon', () => { +describe('flushOnUnload', () => { let mockFetch: Mock; beforeEach(() => { @@ -103,7 +103,7 @@ describe('flushViaBeacon', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'hello'); as.handleTextChange('block-2', 'world'); - as.flushViaBeacon(); + as.flushOnUnload(); expect(mockFetch).toHaveBeenCalledTimes(2); expect(mockFetch).toHaveBeenCalledWith( @@ -128,14 +128,14 @@ describe('flushViaBeacon', () => { const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true); const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); - as.flushViaBeacon(); + as.flushOnUnload(); expect(sendBeaconSpy).not.toHaveBeenCalled(); }); it('does nothing when there are no pending edits', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); - as.flushViaBeacon(); + as.flushOnUnload(); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -143,9 +143,21 @@ describe('flushViaBeacon', () => { it('cancels the debounce timer so saveFn is not also called', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); - as.flushViaBeacon(); + as.flushOnUnload(); await vi.advanceTimersByTimeAsync(2000); expect(mockSaveFn).not.toHaveBeenCalled(); }); + + it('does not send fetch if debounce already fired and pendingTexts is empty', async () => { + const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); + as.handleTextChange('block-1', 'text'); + await vi.advanceTimersByTimeAsync(1500); + // debounce has fired; pendingTexts should be empty now + mockFetch.mockClear(); + + as.flushOnUnload(); + + expect(mockFetch).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts b/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts index c90b669f..07dc5692 100644 --- a/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts +++ b/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts @@ -94,10 +94,10 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) { saveStates.delete(blockId); } - function flushViaBeacon(): void { + function flushOnUnload(): void { for (const [blockId, text] of pendingTexts) { clearDebounce(blockId); - fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, { + void fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), @@ -124,7 +124,7 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) { handleBlur, handleRetry, clearBlock, - flushViaBeacon, + flushOnUnload, destroy }; }