|
8 | 8 | RunCompletionSentinelScanner, |
9 | 9 | type SentinelPiece, |
10 | 10 | } from './runCompletionSentinel.js'; |
11 | | -import { addAbortListener, makeAbortError } from '../util/abort.js'; |
| 11 | +import { waitForScopedOperation } from '../util/abort.js'; |
12 | 12 | import { invariant } from '../util/assert.js'; |
13 | 13 | import { ResourceScope } from '../util/resourceScope.js'; |
14 | 14 |
|
@@ -280,62 +280,25 @@ export class RunCompletionCoordinator { |
280 | 280 | 'timeoutMs must be a positive integer', |
281 | 281 | ); |
282 | 282 |
|
283 | | - const { signal } = options; |
284 | | - if (signal?.aborted === true) { |
| 283 | + const forgetWaiter = (): void => { |
| 284 | + // Match timeout behavior: stop waiting for a client response but keep |
| 285 | + // sentinel/postamble registrations active so eventual completion bytes |
| 286 | + // remain hidden and replayable. |
285 | 287 | this.#runCompletionWaiters.delete(marker); |
286 | | - throw makeAbortError(signal); |
287 | | - } |
288 | | - |
289 | | - const scope = new ResourceScope(); |
290 | | - const { promise, reject, resolve } = |
291 | | - Promise.withResolvers<TimedRunCompletionWaitResult>(); |
292 | | - let resolved = false; |
293 | | - |
294 | | - const rejectWithError = (error: unknown): void => { |
295 | | - if (resolved) { |
296 | | - return; |
297 | | - } |
298 | | - |
299 | | - resolved = true; |
300 | | - void scope.close().then(() => { |
301 | | - reject(error instanceof Error ? error : new Error(String(error))); |
302 | | - }, reject); |
303 | | - }; |
304 | | - |
305 | | - const resolveWithResult = (result: TimedRunCompletionWaitResult): void => { |
306 | | - if (resolved) { |
307 | | - return; |
308 | | - } |
309 | | - |
310 | | - resolved = true; |
311 | | - void scope.close().then(() => { |
312 | | - resolve(result); |
313 | | - }, reject); |
314 | 288 | }; |
315 | 289 |
|
316 | | - const timeoutHandle = setTimeout(() => { |
317 | | - // Keep sentinel/postamble registrations active after timeout so the |
318 | | - // eventual internal completion bytes are still hidden from artifacts. |
319 | | - this.#runCompletionWaiters.delete(marker); |
320 | | - resolveWithResult({ kind: 'timeout' }); |
321 | | - }, timeoutMs); |
322 | | - scope.add('run completion timeout', () => { |
323 | | - clearTimeout(timeoutHandle); |
| 290 | + return await waitForScopedOperation<TimedRunCompletionWaitResult>({ |
| 291 | + operationName: 'run completion', |
| 292 | + operation: completionPromise, |
| 293 | + scope: new ResourceScope(), |
| 294 | + ...(options.signal === undefined ? {} : { signal: options.signal }), |
| 295 | + timeoutMs, |
| 296 | + timeoutResult: () => { |
| 297 | + forgetWaiter(); |
| 298 | + return { kind: 'timeout' }; |
| 299 | + }, |
| 300 | + onAbort: forgetWaiter, |
324 | 301 | }); |
325 | | - |
326 | | - if (signal !== undefined) { |
327 | | - addAbortListener(scope, 'run completion abort listener', signal, () => { |
328 | | - // Match timeout behavior: stop waiting for a client response but keep |
329 | | - // sentinel/postamble registrations active so eventual completion bytes |
330 | | - // remain hidden and replayable. |
331 | | - this.#runCompletionWaiters.delete(marker); |
332 | | - rejectWithError(makeAbortError(signal)); |
333 | | - }); |
334 | | - } |
335 | | - |
336 | | - void completionPromise.then(resolveWithResult, rejectWithError); |
337 | | - |
338 | | - return await promise; |
339 | 302 | } |
340 | 303 |
|
341 | 304 | async #appendOutput(data: string): Promise<void> { |
|
0 commit comments