fix(startup): serialize provider runtime checks

This commit is contained in:
777genius 2026-05-26 09:12:05 +03:00
parent ec5bb5d5ea
commit b88ca42fe3
2 changed files with 53 additions and 3 deletions

View file

@ -38,6 +38,7 @@ const logger = createLogger('IPC:cliInstaller');
let service: CliInstallerService;
const statusInFlight = new Map<CliInstallerProviderStatusMode, Promise<CliInstallationStatus>>();
const providerStatusInFlight = new Map<CliProviderId, Promise<CliProviderStatus | null>>();
let providerRuntimeRequestTail: Promise<void> = Promise.resolve();
const cachedStatus = new Map<
CliInstallerProviderStatusMode,
{ value: CliInstallationStatus; at: number }
@ -110,11 +111,21 @@ function canUseStatusForCacheKey(
);
}
function runProviderRuntimeRequest<T>(operation: () => Promise<T>): Promise<T> {
const request = providerRuntimeRequestTail.then(operation, operation);
providerRuntimeRequestTail = request.then(
() => undefined,
() => undefined
);
return request;
}
/**
* Initializes CLI installer handlers with the service instance.
*/
export function initializeCliInstallerHandlers(installerService: CliInstallerService): void {
service = installerService;
providerRuntimeRequestTail = Promise.resolve();
}
/**
@ -255,8 +266,7 @@ async function handleGetProviderStatus(
}
const generation = statusCacheGeneration;
const request = service
.getProviderStatus(providerId)
const request = runProviderRuntimeRequest(() => service.getProviderStatus(providerId))
.then((status) => {
if (generation === statusCacheGeneration) {
patchCachedProviderStatus(status);
@ -296,7 +306,7 @@ async function handleVerifyProviderModels(
): Promise<IpcResult<CliProviderStatus | null>> {
try {
const generation = statusCacheGeneration;
const status = await service.verifyProviderModels(providerId);
const status = await runProviderRuntimeRequest(() => service.verifyProviderModels(providerId));
if (generation === statusCacheGeneration) {
patchCachedProviderStatus(status);
}

View file

@ -221,6 +221,46 @@ describe('cliInstaller IPC handlers', () => {
expect(service.invalidateStatusCache).toHaveBeenCalledTimes(1);
});
it('serializes explicit provider runtime status requests to avoid startup memory spikes', async () => {
const codexRequest = deferred<CliProviderStatus>();
const opencodeRequest = deferred<CliProviderStatus>();
const startedProviders: CliProviderId[] = [];
service.getProviderStatus.mockImplementation((providerId: CliProviderId) => {
startedProviders.push(providerId);
return providerId === 'codex' ? codexRequest.promise : opencodeRequest.promise;
});
const codexInvoke = ipcMain.invoke(
CLI_INSTALLER_GET_PROVIDER_STATUS,
'codex'
) as Promise<IpcResult<CliProviderStatus | null>>;
await vi.waitFor(() => expect(service.getProviderStatus).toHaveBeenCalledTimes(1));
const opencodeInvoke = ipcMain.invoke(
CLI_INSTALLER_GET_PROVIDER_STATUS,
'opencode'
) as Promise<IpcResult<CliProviderStatus | null>>;
await Promise.resolve();
await Promise.resolve();
expect(startedProviders).toEqual(['codex']);
expect(service.getProviderStatus).toHaveBeenCalledTimes(1);
codexRequest.resolve(provider({ providerId: 'codex', authenticated: true }));
await expect(codexInvoke).resolves.toMatchObject({
success: true,
data: { providerId: 'codex' },
});
await vi.waitFor(() => expect(service.getProviderStatus).toHaveBeenCalledTimes(2));
expect(startedProviders).toEqual(['codex', 'opencode']);
opencodeRequest.resolve(provider({ providerId: 'opencode', authenticated: true }));
await expect(opencodeInvoke).resolves.toMatchObject({
success: true,
data: { providerId: 'opencode' },
});
});
it('does not reuse or recache a status request that was in flight before invalidation', async () => {
const staleStatus = status([
provider({