mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 19:47:26 +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 { createMainWindowSizePersistence } from './utils/mainWindowState';
|
||||||
import type { MainWindowSize } from './bindings/MainWindowSize';
|
import type { MainWindowSize } from './bindings/MainWindowSize';
|
||||||
import { beginSchedulerControl, isSchedulerControlCurrent } from './utils/schedulerControl';
|
import { beginSchedulerControl, isSchedulerControlCurrent } from './utils/schedulerControl';
|
||||||
|
import { createSerialTaskQueue } from './utils/serialTaskQueue';
|
||||||
|
|
||||||
const loadSettingsView = () => import('./components/SettingsView');
|
const loadSettingsView = () => import('./components/SettingsView');
|
||||||
const loadSchedulerView = () => import('./components/SchedulerView');
|
const loadSchedulerView = () => import('./components/SchedulerView');
|
||||||
@@ -254,6 +255,7 @@ function App() {
|
|||||||
const pendingPostActionTimer = useRef<number | null>(null);
|
const pendingPostActionTimer = useRef<number | null>(null);
|
||||||
const startupResumeStarted = useRef(false);
|
const startupResumeStarted = useRef(false);
|
||||||
const startupInputReady = useRef(false);
|
const startupInputReady = useRef(false);
|
||||||
|
const extensionProcessing = useRef(createSerialTaskQueue());
|
||||||
const frontendReadyUpdate = useRef<Promise<void>>(Promise.resolve());
|
const frontendReadyUpdate = useRef<Promise<void>>(Promise.resolve());
|
||||||
const pendingStartupInputs = useRef<Array<
|
const pendingStartupInputs = useRef<Array<
|
||||||
| { type: 'extension'; payload: ExtensionDownloadRequest }
|
| { type: 'extension'; payload: ExtensionDownloadRequest }
|
||||||
@@ -307,6 +309,24 @@ function App() {
|
|||||||
return update;
|
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'>) => {
|
const schedulePostQueueAction = useCallback((action: Exclude<PostQueueAction, 'none'>) => {
|
||||||
clearPendingPostActionTimer();
|
clearPendingPostActionTimer();
|
||||||
|
|
||||||
@@ -646,16 +666,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
unlistenExtension = await listen('extension-add-download', (event) => {
|
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) {
|
if (!startupInputReady.current || useSettingsStore.getState().showKeychainModal) {
|
||||||
pendingStartupInputs.current.push({ type: 'extension', payload: event.payload });
|
pendingStartupInputs.current.push({ type: 'extension', payload: event.payload });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
useDownloadStore.getState().handleExtensionDownload(event.payload).catch(error => {
|
enqueueAddInput(() => processExtensionDownload(event.payload)).catch(error => {
|
||||||
console.error('Failed to handle browser extension download:', 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 });
|
pendingStartupInputs.current.push({ type: 'deep-link', payload: event.payload });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
useDownloadStore.getState().openAddModalWithUrls(event.payload);
|
enqueueAddInput(() => useDownloadStore.getState().openAddModalWithUrls(event.payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupListeners = disposeListeners;
|
cleanupListeners = disposeListeners;
|
||||||
@@ -718,7 +733,7 @@ function App() {
|
|||||||
mainWindowSizePersistence.dispose();
|
mainWindowSizePersistence.dispose();
|
||||||
disposePersistence();
|
disposePersistence();
|
||||||
};
|
};
|
||||||
}, [addToast, queueFrontendReadyUpdate]);
|
}, [addToast, enqueueAddInput, processExtensionDownload, queueFrontendReadyUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!coreReady) return;
|
if (!coreReady) return;
|
||||||
@@ -740,14 +755,14 @@ function App() {
|
|||||||
const pendingInputs = pendingStartupInputs.current.splice(0);
|
const pendingInputs = pendingStartupInputs.current.splice(0);
|
||||||
for (const input of pendingInputs) {
|
for (const input of pendingInputs) {
|
||||||
if (input.type === 'extension') {
|
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);
|
console.error('Failed to handle queued browser extension download:', error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
useDownloadStore.getState().openAddModalWithUrls(input.payload);
|
enqueueAddInput(() => useDownloadStore.getState().openAddModalWithUrls(input.payload));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [coreReady, showKeychainModal]);
|
}, [coreReady, enqueueAddInput, processExtensionDownload, showKeychainModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!coreReady || showKeychainModal || startupResumeStarted.current) return;
|
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