refactor(autosave): rename flushViaBeacon → flushOnUnload; add void to fire-and-forget fetch
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m27s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m48s
CI / Unit & Component Tests (pull_request) Failing after 2m44s
CI / OCR Service Tests (pull_request) Successful in 29s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m27s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m48s
CI / Unit & Component Tests (pull_request) Failing after 2m44s
CI / OCR Service Tests (pull_request) Successful in 29s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,7 @@ $effect(() => {
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
function onBeforeUnload() {
|
function onBeforeUnload() {
|
||||||
autoSave.flushViaBeacon();
|
autoSave.flushOnUnload();
|
||||||
}
|
}
|
||||||
window.addEventListener('beforeunload', onBeforeUnload);
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ describe('createBlockAutoSave', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('flushViaBeacon', () => {
|
describe('flushOnUnload', () => {
|
||||||
let mockFetch: Mock;
|
let mockFetch: Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -103,7 +103,7 @@ describe('flushViaBeacon', () => {
|
|||||||
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
||||||
as.handleTextChange('block-1', 'hello');
|
as.handleTextChange('block-1', 'hello');
|
||||||
as.handleTextChange('block-2', 'world');
|
as.handleTextChange('block-2', 'world');
|
||||||
as.flushViaBeacon();
|
as.flushOnUnload();
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
@@ -128,14 +128,14 @@ describe('flushViaBeacon', () => {
|
|||||||
const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
|
const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
|
||||||
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
||||||
as.handleTextChange('block-1', 'text');
|
as.handleTextChange('block-1', 'text');
|
||||||
as.flushViaBeacon();
|
as.flushOnUnload();
|
||||||
|
|
||||||
expect(sendBeaconSpy).not.toHaveBeenCalled();
|
expect(sendBeaconSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does nothing when there are no pending edits', () => {
|
it('does nothing when there are no pending edits', () => {
|
||||||
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
||||||
as.flushViaBeacon();
|
as.flushOnUnload();
|
||||||
|
|
||||||
expect(mockFetch).not.toHaveBeenCalled();
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -143,9 +143,21 @@ describe('flushViaBeacon', () => {
|
|||||||
it('cancels the debounce timer so saveFn is not also called', async () => {
|
it('cancels the debounce timer so saveFn is not also called', async () => {
|
||||||
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' });
|
||||||
as.handleTextChange('block-1', 'text');
|
as.handleTextChange('block-1', 'text');
|
||||||
as.flushViaBeacon();
|
as.flushOnUnload();
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(2000);
|
await vi.advanceTimersByTimeAsync(2000);
|
||||||
expect(mockSaveFn).not.toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) {
|
|||||||
saveStates.delete(blockId);
|
saveStates.delete(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushViaBeacon(): void {
|
function flushOnUnload(): void {
|
||||||
for (const [blockId, text] of pendingTexts) {
|
for (const [blockId, text] of pendingTexts) {
|
||||||
clearDebounce(blockId);
|
clearDebounce(blockId);
|
||||||
fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, {
|
void fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text }),
|
body: JSON.stringify({ text }),
|
||||||
@@ -124,7 +124,7 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) {
|
|||||||
handleBlur,
|
handleBlur,
|
||||||
handleRetry,
|
handleRetry,
|
||||||
clearBlock,
|
clearBlock,
|
||||||
flushViaBeacon,
|
flushOnUnload,
|
||||||
destroy
|
destroy
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user