@@ -181,6 +181,7 @@ export interface ReconnectOptions {
181181 maxRetries ?: number ;
182182 retryDelay ?: number ;
183183 maxWait ?: number ;
184+ signal ?: AbortSignal ;
184185}
185186
186187export async function runWithReconnect < TFilter , TBlock > (
@@ -193,10 +194,18 @@ export async function runWithReconnect<TFilter, TBlock>(
193194 const maxRetries = options . maxRetries ?? 10 ;
194195 const retryDelay = options . retryDelay ?? 1_000 ;
195196 const maxWait = options . maxWait ?? 30_000 ;
197+ const { signal } = options ;
196198
197199 while ( true ) {
200+ if ( signal ?. aborted ) return ;
201+
198202 const abortController = new AbortController ( ) ;
199203
204+ // Forward external abort into the per-run internal controller so that
205+ // run() sees the abort signal and can break its loop cleanly.
206+ const forwardAbort = ( ) => abortController . abort ( signal ?. reason ) ;
207+ signal ?. addEventListener ( "abort" , forwardAbort , { once : true } ) ;
208+
200209 const runOptions : RunOptions = {
201210 onConnect ( ) {
202211 retryCount = 0 ;
@@ -206,15 +215,14 @@ export async function runWithReconnect<TFilter, TBlock>(
206215
207216 try {
208217 await run ( client , indexer , runOptions ) ;
209- abortController . abort ( ) ;
210218 return ;
211219 } catch ( error ) {
212220 // Only reconnect on internal/server errors.
213221 // All other errors should be rethrown.
214222
215223 retryCount ++ ;
216224
217- abortController . abort ( ) ;
225+ if ( signal ?. aborted ) return ;
218226
219227 if ( error instanceof ClientError || error instanceof ServerError ) {
220228 const isServerError = error instanceof ServerError ;
@@ -231,15 +239,29 @@ export async function runWithReconnect<TFilter, TBlock>(
231239
232240 // Add jitter to the retry delay to avoid all clients retrying at the same time.
233241 const delay = Math . random ( ) * ( retryDelay * 0.2 ) + retryDelay ;
234- await new Promise ( ( resolve ) =>
235- setTimeout ( resolve , Math . min ( retryCount * delay , maxWait ) ) ,
236- ) ;
242+ await new Promise < void > ( ( resolve ) => {
243+ const t = setTimeout (
244+ resolve ,
245+ Math . min ( retryCount * delay , maxWait ) ,
246+ ) ;
247+ // wake up immediately if externally aborted during the delay.
248+ signal ?. addEventListener (
249+ "abort" ,
250+ ( ) => {
251+ clearTimeout ( t ) ;
252+ resolve ( ) ;
253+ } ,
254+ { once : true } ,
255+ ) ;
256+ } ) ;
237257
238258 continue ;
239259 }
240260 }
241261 }
242262 throw error ;
263+ } finally {
264+ signal ?. removeEventListener ( "abort" , forwardAbort ) ;
243265 }
244266 }
245267}
@@ -322,8 +344,29 @@ export async function run<TFilter, TBlock>(
322344
323345 let onConnectCalled = false ;
324346
347+ const abortPromise = abortSignal ? waitForAbort ( abortSignal ) : null ;
348+
325349 while ( true ) {
326- const { value : message , done } = await stream . next ( ) ;
350+ let result : IteratorResult <
351+ StreamDataResponse < TBlock > ,
352+ StreamDataResponse < TBlock >
353+ > ;
354+
355+ try {
356+ result = abortPromise
357+ ? await Promise . race ( [ stream . next ( ) , abortPromise ] )
358+ : await stream . next ( ) ;
359+ } catch ( e ) {
360+ if ( abortSignal ?. aborted ) {
361+ // cancel the underlying gRPC stream and exit the loop cleanly so
362+ // that run:after hooks are still called below.
363+ await stream . return ?.( ) ;
364+ break ;
365+ }
366+ throw e ;
367+ }
368+
369+ const { value : message , done } = result ;
327370
328371 if ( done ) {
329372 break ;
@@ -527,6 +570,22 @@ export async function run<TFilter, TBlock>(
527570 } ) ;
528571}
529572
573+ /**
574+ * Returns a promise that rejects as soon as the given AbortSignal fires.
575+ * Used to race against stream.next() so the loop can break on abort.
576+ */
577+ function waitForAbort ( signal : AbortSignal ) : Promise < never > {
578+ return new Promise < never > ( ( _ , reject ) => {
579+ if ( signal . aborted ) {
580+ reject ( signal . reason ) ;
581+ return ;
582+ }
583+ signal . addEventListener ( "abort" , ( ) => reject ( signal . reason ) , {
584+ once : true ,
585+ } ) ;
586+ } ) ;
587+ }
588+
530589async function registerMiddleware < TFilter , TBlock > (
531590 indexer : Indexer < TFilter , TBlock > ,
532591 abortSignal ?: AbortSignal ,
0 commit comments