fix: isolate concurrent OpenCode chat runs
This commit is contained in:
107
electron/opencode/runtime-config-readiness.ts
Normal file
107
electron/opencode/runtime-config-readiness.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export interface RuntimeConfigGenerationManager {
|
||||
getRuntimeGeneration?: () => number;
|
||||
getRuntimeGenerationProvenance?: () => 'unknown' | 'starting' | 'fresh' | 'attached';
|
||||
}
|
||||
|
||||
interface RuntimeConfigPendingLatch {
|
||||
runtimeGeneration: number;
|
||||
clearOnFreshGeneration: boolean;
|
||||
}
|
||||
|
||||
const pendingLatches = new WeakMap<object, RuntimeConfigPendingLatch>();
|
||||
const coordinatorTails = new WeakMap<object, Promise<void>>();
|
||||
|
||||
export interface RuntimeConfigCoordinatorLease {
|
||||
isActive: () => boolean;
|
||||
isRefreshPending: () => boolean;
|
||||
markRefreshPending: () => number;
|
||||
retainRefreshPending: () => number;
|
||||
}
|
||||
|
||||
function pendingForCurrentGeneration(manager: RuntimeConfigGenerationManager): boolean {
|
||||
const latch = pendingLatches.get(manager as object);
|
||||
if (!latch) return false;
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
const provenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown';
|
||||
if (
|
||||
latch.clearOnFreshGeneration
|
||||
&& runtimeGeneration !== latch.runtimeGeneration
|
||||
&& provenance === 'fresh'
|
||||
) {
|
||||
pendingLatches.delete(manager as object);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function withRuntimeConfigCoordinator<T>(
|
||||
manager: RuntimeConfigGenerationManager,
|
||||
operation: (lease: RuntimeConfigCoordinatorLease) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const key = manager as object;
|
||||
const previous = coordinatorTails.get(key) ?? Promise.resolve();
|
||||
let release!: () => void;
|
||||
const current = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const queued = previous.then(() => current);
|
||||
coordinatorTails.set(key, queued);
|
||||
await previous;
|
||||
let active = true;
|
||||
try {
|
||||
return await operation({
|
||||
isActive: () => active,
|
||||
isRefreshPending: () => active ? pendingForCurrentGeneration(manager) : true,
|
||||
markRefreshPending: () => {
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
if (active) pendingLatches.set(key, { runtimeGeneration, clearOnFreshGeneration: true });
|
||||
return runtimeGeneration;
|
||||
},
|
||||
retainRefreshPending: () => {
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
if (active) pendingLatches.set(key, { runtimeGeneration, clearOnFreshGeneration: false });
|
||||
return runtimeGeneration;
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
active = false;
|
||||
release();
|
||||
if (coordinatorTails.get(key) === queued) coordinatorTails.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markRuntimeConfigRefreshPending(
|
||||
manager: RuntimeConfigGenerationManager,
|
||||
): Promise<number> {
|
||||
return await withRuntimeConfigCoordinator(manager, async (lease) => lease.markRefreshPending());
|
||||
}
|
||||
|
||||
export async function isRuntimeConfigRefreshPending(
|
||||
manager: RuntimeConfigGenerationManager,
|
||||
): Promise<boolean> {
|
||||
return await withRuntimeConfigCoordinator(manager, async (lease) => lease.isRefreshPending());
|
||||
}
|
||||
|
||||
export async function withRuntimeAcceptanceTimeout<T>(
|
||||
operation: (signal: AbortSignal) => Promise<T>,
|
||||
timeoutMs = 10_000,
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
let timeout!: ReturnType<typeof setTimeout>;
|
||||
let timeoutFallback: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeoutError = new Error('OpenCode runtime acceptance timed out');
|
||||
const timedOut = new Promise<never>((_resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
controller.abort(timeoutError);
|
||||
timeoutFallback = setTimeout(() => reject(timeoutError), 0);
|
||||
}, timeoutMs);
|
||||
});
|
||||
try {
|
||||
controller.signal.throwIfAborted();
|
||||
const accepted = operation(controller.signal);
|
||||
return await Promise.race([accepted, timedOut]);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
if (timeoutFallback) clearTimeout(timeoutFallback);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user