fix(transcription): replace sendBeacon with fetch keepalive; add catch-all API proxy #304

Merged
marcel merged 7 commits from fix/204-transcription-beacon-proxy into main 2026-04-23 07:12:23 +02:00
3 changed files with 21 additions and 9 deletions
Showing only changes of commit 61b89ac9e4 - Show all commits

View File

@@ -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 () => {

View File

@@ -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();
});
}); });

View File

@@ -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
}; };
} }