@@ -190,8 +190,14 @@ interface PendingExecution {
190190 timeout : ReturnType < typeof setTimeout >
191191 ownerKey : string
192192 brokers ?: Record < string , IsolatedVMBrokerHandler >
193- /** Set when the caller aborts. Broker dispatches and the final result stop resolving the promise. */
194- cancelled : boolean
193+ /**
194+ * Aborted once the pool gives up on the execution (caller abort, timeout, worker loss). Broker
195+ * dispatches and the final result stop resolving the promise, and in-flight `fetch` calls are
196+ * dropped, since their responses can no longer reach the isolate.
197+ */
198+ abort : AbortController
199+ /** How long the pool waits for a result before giving up; also the longest any one `fetch` may run. */
200+ hostTimeoutMs : number
195201 /** Number of broker calls made so far for this execution. */
196202 brokerCallCount : number
197203}
@@ -387,7 +393,8 @@ function normalizeFetchOptions(options?: IsolatedFetchOptions): SecureFetchOptio
387393async function secureFetch (
388394 requestId : string ,
389395 url : string ,
390- options ?: IsolatedFetchOptions
396+ options : IsolatedFetchOptions | undefined ,
397+ signal : AbortSignal
391398) : Promise < string > {
392399 if ( url . length > MAX_FETCH_URL_LENGTH ) {
393400 return JSON . stringify ( {
@@ -398,7 +405,7 @@ async function secureFetch(
398405 try {
399406 const response = await secureFetchWithValidation (
400407 url ,
401- normalizeFetchOptions ( options ) ,
408+ { ... normalizeFetchOptions ( options ) , signal } ,
402409 'fetchUrl'
403410 )
404411 const bodyResult = truncateString ( await response . text ( ) , MAX_FETCH_RESPONSE_CHARS )
@@ -758,7 +765,7 @@ function handleBrokerMessage(
758765 return
759766 }
760767
761- if ( pending . cancelled ) {
768+ if ( pending . abort . signal . aborted ) {
762769 sendResponse ( { error : 'Execution cancelled' } )
763770 return
764771 }
@@ -801,7 +808,7 @@ function handleBrokerMessage(
801808 Promise . resolve ( )
802809 . then ( ( ) => pending . runInOutboundScope ( ( ) => handler ( args , brokerId ) ) )
803810 . then ( ( resultValue ) => {
804- if ( pending . cancelled ) {
811+ if ( pending . abort . signal . aborted ) {
805812 sendResponse ( { error : 'Execution cancelled' } )
806813 return
807814 }
@@ -864,7 +871,7 @@ function handleWorkerMessage(workerId: number, message: unknown) {
864871 // If the caller aborted, the outer Promise is already resolved with
865872 // AbortError. Still run all the bookkeeping above so pool counters stay
866873 // accurate; just skip re-resolving.
867- if ( ! pending . cancelled ) {
874+ if ( ! pending . abort . signal . aborted ) {
868875 pending . resolve ( msg . result as IsolatedVMExecutionResult )
869876 }
870877 drainQueue ( )
@@ -895,7 +902,7 @@ function handleWorkerMessage(workerId: number, message: unknown) {
895902 typeof msg . executionId === 'number'
896903 ? workerInfo ?. pendingExecutions . get ( msg . executionId )
897904 : undefined
898- if ( ! pending || pending . cancelled ) {
905+ if ( ! pending || pending . abort . signal . aborted ) {
899906 sendFetchResponse ( JSON . stringify ( { error : 'Execution no longer active' } ) )
900907 return
901908 }
@@ -926,8 +933,14 @@ function handleWorkerMessage(workerId: number, message: unknown) {
926933 return
927934 }
928935 }
936+ // A fetch the script left running after returning is cut off once the execution's own time
937+ // budget has passed: nothing can read its response.
938+ const signal = AbortSignal . any ( [
939+ pending . abort . signal ,
940+ AbortSignal . timeout ( pending . hostTimeoutMs ) ,
941+ ] )
929942 pending
930- . runInOutboundScope ( ( ) => secureFetch ( requestId , url , options ) )
943+ . runInOutboundScope ( ( ) => secureFetch ( requestId , url , options , signal ) )
931944 . then ( sendFetchResponse )
932945 . catch ( ( err ) => {
933946 sendFetchResponse ( JSON . stringify ( { error : getErrorMessage ( err , 'Fetch failed' ) } ) )
@@ -971,6 +984,7 @@ function cleanupWorker(workerId: number, interruptedMessage?: string) {
971984
972985 for ( const [ id , pending ] of workerInfo . pendingExecutions ) {
973986 clearTimeout ( pending . timeout )
987+ pending . abort . abort ( )
974988 totalActiveExecutions --
975989 const owner = ownerStates . get ( pending . ownerKey )
976990 if ( owner ) {
@@ -1316,8 +1330,11 @@ function dispatchToWorker(
13161330 workerInfo . idleTimeout = null
13171331 }
13181332
1333+ const abort = new AbortController ( )
1334+ const hostTimeoutMs = req . timeoutMs + 1000
13191335 const timeout = setTimeout ( ( ) => {
13201336 workerInfo . pendingExecutions . delete ( execId )
1337+ abort . abort ( )
13211338 // A never-settling promise escapes the isolate's own script timeout, so the worker
13221339 // would otherwise keep the isolate and its timers alive until the worker retires.
13231340 sendCancel ( workerInfo , execId )
@@ -1345,15 +1362,16 @@ function dispatchToWorker(
13451362 resetWorkerIdleTimeout ( workerInfo . id )
13461363 }
13471364 drainQueue ( )
1348- } , req . timeoutMs + 1000 )
1365+ } , hostTimeoutMs )
13491366
13501367 workerInfo . pendingExecutions . set ( execId , {
13511368 runInOutboundScope : state . runInOutboundScope ,
13521369 resolve,
13531370 timeout,
13541371 ownerKey : ownerState . ownerKey ,
13551372 brokers,
1356- cancelled : false ,
1373+ abort,
1374+ hostTimeoutMs,
13571375 brokerCallCount : 0 ,
13581376 } )
13591377 workerInfo . activeExecutions ++
@@ -1687,13 +1705,13 @@ export async function executeInIsolatedVM(
16871705 if ( removed ) clearTimeout ( removed . queueTimeout )
16881706 state . queueId = undefined
16891707 }
1690- // If dispatched, mark the pending entry cancelled and ask the worker to
1708+ // If dispatched, abort the pending entry and ask the worker to
16911709 // dispose its isolate so the pool slot can be released. The worker will
16921710 // emit a `result` shortly after, which runs the normal counter cleanup.
16931711 if ( state . workerId !== undefined && state . execId !== undefined ) {
16941712 const wi = workers . get ( state . workerId )
16951713 const pending = wi ?. pendingExecutions . get ( state . execId )
1696- if ( pending ) pending . cancelled = true
1714+ pending ?. abort . abort ( )
16971715 if ( wi ) sendCancel ( wi , state . execId )
16981716 }
16991717 resolveWithRelease ( {
0 commit comments