mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 15:46:17 +00:00
fix(integration): serialize browser add inputs
- serialize extension and deep-link Add-window events - acknowledge successful extension handling at the frontend boundary - add regression coverage for ordered input processing
This commit is contained in:
+1
-1
Submodule Extensions/Browser updated: 64a8c3c5a2...f08ebb5d0f
+26
-11
@@ -43,6 +43,7 @@ import { synchronizeDocumentAppearance } from './utils/documentAppearance';
|
||||
import { createMainWindowSizePersistence } from './utils/mainWindowState';
|
||||
import type { MainWindowSize } from './bindings/MainWindowSize';
|
||||
import { beginSchedulerControl, isSchedulerControlCurrent } from './utils/schedulerControl';
|
||||
import { createSerialTaskQueue } from './utils/serialTaskQueue';
|
||||
|
||||
const loadSettingsView = () => import('./components/SettingsView');
|
||||
const loadSchedulerView = () => import('./components/SchedulerView');
|
||||
@@ -254,6 +255,7 @@ function App() {
|
||||
const pendingPostActionTimer = useRef<number | null>(null);
|
||||
const startupResumeStarted = useRef(false);
|
||||
const startupInputReady = useRef(false);
|
||||
const extensionProcessing = useRef(createSerialTaskQueue());
|
||||
const frontendReadyUpdate = useRef<Promise<void>>(Promise.resolve());
|
||||
const pendingStartupInputs = useRef<Array<
|
||||
| { type: 'extension'; payload: ExtensionDownloadRequest }
|
||||
@@ -307,6 +309,24 @@ function App() {
|
||||
return update;
|
||||
}, []);
|
||||
|
||||
const acknowledgeExtensionDownload = useCallback(async (requestId?: string) => {
|
||||
if (!requestId) return;
|
||||
try {
|
||||
await invoke('ack_extension_download', { requestId });
|
||||
} catch (error) {
|
||||
console.error('Failed to acknowledge browser extension download:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const processExtensionDownload = useCallback(async (payload: ExtensionDownloadRequest) => {
|
||||
await useDownloadStore.getState().handleExtensionDownload(payload);
|
||||
await acknowledgeExtensionDownload(payload.request_id);
|
||||
}, [acknowledgeExtensionDownload]);
|
||||
|
||||
const enqueueAddInput = useCallback((task: () => void | Promise<void>) => {
|
||||
return extensionProcessing.current(task);
|
||||
}, []);
|
||||
|
||||
const schedulePostQueueAction = useCallback((action: Exclude<PostQueueAction, 'none'>) => {
|
||||
clearPendingPostActionTimer();
|
||||
|
||||
@@ -646,16 +666,11 @@ function App() {
|
||||
}
|
||||
});
|
||||
unlistenExtension = await listen('extension-add-download', (event) => {
|
||||
if (event.payload.request_id) {
|
||||
void invoke('ack_extension_download', { requestId: event.payload.request_id }).catch(error => {
|
||||
console.error('Failed to acknowledge browser extension download:', error);
|
||||
});
|
||||
}
|
||||
if (!startupInputReady.current || useSettingsStore.getState().showKeychainModal) {
|
||||
pendingStartupInputs.current.push({ type: 'extension', payload: event.payload });
|
||||
return;
|
||||
}
|
||||
useDownloadStore.getState().handleExtensionDownload(event.payload).catch(error => {
|
||||
enqueueAddInput(() => processExtensionDownload(event.payload)).catch(error => {
|
||||
console.error('Failed to handle browser extension download:', error);
|
||||
});
|
||||
});
|
||||
@@ -664,7 +679,7 @@ function App() {
|
||||
pendingStartupInputs.current.push({ type: 'deep-link', payload: event.payload });
|
||||
return;
|
||||
}
|
||||
useDownloadStore.getState().openAddModalWithUrls(event.payload);
|
||||
enqueueAddInput(() => useDownloadStore.getState().openAddModalWithUrls(event.payload));
|
||||
});
|
||||
|
||||
cleanupListeners = disposeListeners;
|
||||
@@ -718,7 +733,7 @@ function App() {
|
||||
mainWindowSizePersistence.dispose();
|
||||
disposePersistence();
|
||||
};
|
||||
}, [addToast, queueFrontendReadyUpdate]);
|
||||
}, [addToast, enqueueAddInput, processExtensionDownload, queueFrontendReadyUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coreReady) return;
|
||||
@@ -740,14 +755,14 @@ function App() {
|
||||
const pendingInputs = pendingStartupInputs.current.splice(0);
|
||||
for (const input of pendingInputs) {
|
||||
if (input.type === 'extension') {
|
||||
useDownloadStore.getState().handleExtensionDownload(input.payload).catch(error => {
|
||||
enqueueAddInput(() => processExtensionDownload(input.payload)).catch(error => {
|
||||
console.error('Failed to handle queued browser extension download:', error);
|
||||
});
|
||||
} else {
|
||||
useDownloadStore.getState().openAddModalWithUrls(input.payload);
|
||||
enqueueAddInput(() => useDownloadStore.getState().openAddModalWithUrls(input.payload));
|
||||
}
|
||||
}
|
||||
}, [coreReady, showKeychainModal]);
|
||||
}, [coreReady, enqueueAddInput, processExtensionDownload, showKeychainModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coreReady || showKeychainModal || startupResumeStarted.current) return;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createSerialTaskQueue } from './serialTaskQueue';
|
||||
|
||||
describe('createSerialTaskQueue', () => {
|
||||
it('runs tasks in enqueue order and waits for each previous task', async () => {
|
||||
const queue = createSerialTaskQueue();
|
||||
const events: string[] = [];
|
||||
let releaseFirst!: () => void;
|
||||
const firstReleased = new Promise<void>(resolve => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
|
||||
const first = queue(async () => {
|
||||
events.push('first-start');
|
||||
await firstReleased;
|
||||
events.push('first-end');
|
||||
});
|
||||
const second = queue(async () => {
|
||||
events.push('second');
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||||
expect(events).toEqual(['first-start']);
|
||||
releaseFirst();
|
||||
await Promise.all([first, second]);
|
||||
expect(events).toEqual(['first-start', 'first-end', 'second']);
|
||||
});
|
||||
|
||||
it('continues with later tasks after a failed task', async () => {
|
||||
const queue = createSerialTaskQueue();
|
||||
const events: string[] = [];
|
||||
|
||||
const failed = queue(async () => {
|
||||
events.push('failed');
|
||||
throw new Error('expected failure');
|
||||
});
|
||||
const continued = queue(async () => {
|
||||
events.push('continued');
|
||||
});
|
||||
|
||||
await expect(failed).rejects.toThrow('expected failure');
|
||||
await expect(continued).resolves.toBeUndefined();
|
||||
expect(events).toEqual(['failed', 'continued']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export type SerialTask = () => void | Promise<void>;
|
||||
|
||||
export type SerialTaskQueue = (task: SerialTask) => Promise<void>;
|
||||
|
||||
export const createSerialTaskQueue = (): SerialTaskQueue => {
|
||||
let tail: Promise<void> = Promise.resolve();
|
||||
|
||||
return task => {
|
||||
const next = tail.catch(() => undefined).then(task);
|
||||
tail = next.catch(() => undefined);
|
||||
return next;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user