timeout.ts
1,183 bytes
| 1 | /** |
|---|---|
| 2 | * Race a promise-returning function against a wall-clock deadline, wiring an |
| 3 | * AbortSignal through so the underlying LLM call is actually cancelled (not just |
| 4 | * abandoned). Also forwards a parent signal so a client disconnect / debate |
| 5 | * abort propagates down into in-flight model calls. |
| 6 | */ |
| 7 | export class TimeoutError extends Error { |
| 8 | readonly timeoutMs: number; |
| 9 | constructor(ms: number) { |
| 10 | super(`Operation timed out after ${ms}ms`); |
| 11 | this.name = 'TimeoutError'; |
| 12 | this.timeoutMs = ms; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | export async function withTimeout<T>( |
| 17 | fn: (signal: AbortSignal) => Promise<T>, |
| 18 | ms: number, |
| 19 | parentSignal?: AbortSignal, |
| 20 | ): Promise<T> { |
| 21 | const controller = new AbortController(); |
| 22 | const abortFromParent = () => controller.abort(parentSignal?.reason); |
| 23 | |
| 24 | if (parentSignal) { |
| 25 | if (parentSignal.aborted) controller.abort(parentSignal.reason); |
| 26 | else parentSignal.addEventListener('abort', abortFromParent, { once: true }); |
| 27 | } |
| 28 | |
| 29 | const timer = setTimeout(() => controller.abort(new TimeoutError(ms)), ms); |
| 30 | |
| 31 | try { |
| 32 | return await fn(controller.signal); |
| 33 | } finally { |
| 34 | clearTimeout(timer); |
| 35 | parentSignal?.removeEventListener('abort', abortFromParent); |
| 36 | } |
| 37 | } |
| 38 | |