perf: make OpenCode config defaults non-blocking

Removes startup blocking on OpenCode config defaults
Preserves manual and directory-specific model selections
Adds regression coverage for config races
This commit is contained in:
Bohdan Triapitsyn
2026-06-17 11:21:43 +03:00
parent 6b11211968
commit 077a766f94
6 changed files with 940 additions and 33 deletions
+55 -5
View File
@@ -30,6 +30,7 @@ import {
// Use relative path by default (works with both dev and nginx proxy server)
// Can be overridden with VITE_OPENCODE_URL for absolute URLs in special deployments
const DEFAULT_BASE_URL = import.meta.env.VITE_OPENCODE_URL || "/api";
const CONFIG_CACHE_TTL_MS = 10_000;
/**
* Render an SDK error payload into a short string for Error messages.
@@ -231,6 +232,9 @@ class OpencodeService {
private listDirectoryInFlight: Map<string, Promise<FilesystemEntry[]>> = new Map();
private configProvidersInFlight: Map<string, Promise<{ providers: Provider[]; default: { [key: string]: string } }>> = new Map();
private listAgentsInFlight: Map<string, Promise<Agent[]>> = new Map();
private configInFlight: Map<string, Promise<Config>> = new Map();
private configCache: Map<string, { config: Config; expiresAt: number }> = new Map();
private configCacheGeneration = 0;
private listDirectoryCache: Map<string, { entries: FilesystemEntry[]; expiresAt: number }> = new Map();
constructor(baseUrl: string = DEFAULT_BASE_URL) {
@@ -256,6 +260,7 @@ class OpencodeService {
this.listDirectoryInFlight.clear();
this.configProvidersInFlight.clear();
this.listAgentsInFlight.clear();
this.clearConfigCache();
this.listDirectoryCache.clear();
}
@@ -1232,17 +1237,62 @@ class OpencodeService {
}
// Configuration
async getConfig(): Promise<Config> {
const response = await this.client.config.get();
if (!response.data) throw new Error('Failed to get config');
return response.data;
clearConfigCache(): void {
this.configCacheGeneration += 1;
this.configInFlight.clear();
this.configCache.clear();
}
async getConfig(directory?: string | null): Promise<Config> {
const effectiveDirectory = this.normalizeCandidatePath(directory) ?? directory ?? this.currentDirectory ?? undefined;
const key = effectiveDirectory ?? '';
const cached = this.configCache.get(key);
if (cached && cached.expiresAt > Date.now()) {
markStartupTrace('opencodeClient.getConfig:cacheHit', { directory: effectiveDirectory ?? null });
return cached.config;
}
const existing = this.configInFlight.get(key);
if (existing) {
markStartupTrace('opencodeClient.getConfig:deduped', { directory: effectiveDirectory ?? null });
return existing;
}
const generation = this.configCacheGeneration;
const request = (async () => {
markStartupTrace('opencodeClient.getConfig:start', { directory: effectiveDirectory ?? null });
const started = typeof performance !== 'undefined' ? performance.now() : Date.now();
const scopedClient = effectiveDirectory ? this.getScopedApiClient(effectiveDirectory) : this.client;
const response = await scopedClient.config.get();
if (!response.data) throw new Error('Failed to get config');
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
markStartupTrace('opencodeClient.getConfig:end', {
directory: effectiveDirectory ?? null,
durationMs: Math.round(ended - started),
});
if (generation === this.configCacheGeneration) {
this.configCache.set(key, { config: response.data, expiresAt: Date.now() + CONFIG_CACHE_TTL_MS });
}
return response.data;
})();
this.configInFlight.set(key, request);
try {
return await request;
} finally {
if (this.configInFlight.get(key) === request) {
this.configInFlight.delete(key);
}
}
}
async updateConfig(config: Record<string, unknown>): Promise<Config> {
// IMPORTANT: Do NOT pass directory parameter for config updates
// The config should be global, not directory-specific
const response = await this.client.config.update({ config: config as Config });
return unwrapSdkData(response, 'global.config.update');
const data = unwrapSdkData(response, 'global.config.update');
this.clearConfigCache();
return data;
}
/**