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:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user