blob: 0a50d09517a529cdc1fb240cb0b130d5d70f359c [file]
interface ParkedRequest<TValue, TMetadata> {
metadata: TMetadata;
resolve(value: TValue): void;
reject(error: Error): void;
}
/**
* Pure turn-scoped ownership for requests that park tool execution while a
* host waits for user input. Product policy stays with the owning caller.
*/
export class TurnScopedAwaitRegistry<TValue, TMetadata> {
private readonly turns = new Map<string, Map<string, ParkedRequest<TValue, TMetadata>>>();
beginTurn(turnId: string): void {
if (!this.turns.has(turnId)) this.turns.set(turnId, new Map());
}
park(turnId: string, requestId: string, metadata: TMetadata): Promise<TValue> {
const requests = this.requireTurn(turnId);
if (requests.has(requestId)) throw new Error(`Request ${requestId} is already parked`);
return new Promise<TValue>((resolve, reject) => {
requests.set(requestId, { metadata, resolve, reject });
});
}
resolve(turnId: string, requestId: string, value: TValue): TMetadata | null {
return this.resolveWith(turnId, requestId, () => value);
}
resolveWith(
turnId: string,
requestId: string,
valueFor: (metadata: TMetadata) => TValue,
): TMetadata | null {
const request = this.take(turnId, requestId);
if (!request) return null;
request.resolve(valueFor(request.metadata));
return request.metadata;
}
reject(turnId: string, requestId: string, error: Error): TMetadata | null {
const request = this.take(turnId, requestId);
if (!request) return null;
request.reject(error);
return request.metadata;
}
endTurn(turnId: string, errorFor: (requestId: string, metadata: TMetadata) => Error): void {
const requests = this.turns.get(turnId);
if (!requests) return;
this.turns.delete(turnId);
for (const [requestId, request] of requests) {
request.reject(errorFor(requestId, request.metadata));
}
}
entries(turnId: string): ReadonlyArray<readonly [string, TMetadata]> {
return [...(this.turns.get(turnId) ?? [])].map(
([requestId, request]) => [requestId, request.metadata] as const,
);
}
findTurn(requestId: string): string | undefined {
for (const [turnId, requests] of this.turns) {
if (requests.has(requestId)) return turnId;
}
return undefined;
}
pendingCount(turnId: string): number {
return this.turns.get(turnId)?.size ?? 0;
}
private take(turnId: string, requestId: string): ParkedRequest<TValue, TMetadata> | null {
const requests = this.turns.get(turnId);
const request = requests?.get(requestId);
if (!request) return null;
requests?.delete(requestId);
return request;
}
private requireTurn(turnId: string): Map<string, ParkedRequest<TValue, TMetadata>> {
const turn = this.turns.get(turnId);
if (!turn) throw new Error(`Turn ${turnId} is not accepting requests`);
return turn;
}
}