@@ -10,6 +10,8 @@ export interface PollingOptions<T> {
1010 maxAttempts ?: number ;
1111 /** Optional timeout for the entire polling operation (in milliseconds) */
1212 timeoutMs ?: number ;
13+ /** Optional AbortSignal to cancel polling and in-flight delays between attempts. */
14+ signal ?: AbortSignal | null | undefined ;
1315 /**
1416 * Condition to check if polling should stop
1517 * Return true when the condition is met and polling should stop
@@ -196,9 +198,29 @@ export class LongPollAbortError extends Error {
196198}
197199
198200/**
199- * Delay execution for specified milliseconds
201+ * Delay execution for specified milliseconds, aborting with { @link LongPollAbortError} when `signal` is aborted.
200202 */
201- const delay = ( ms : number ) : Promise < void > => new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
203+ function abortableDelay (
204+ ms : number ,
205+ signal : AbortSignal | null | undefined ,
206+ lastResult : unknown ,
207+ ) : Promise < void > {
208+ return new Promise ( ( resolve , reject ) => {
209+ if ( signal ?. aborted ) {
210+ reject ( new LongPollAbortError ( 'Polling aborted' , lastResult ) ) ;
211+ return ;
212+ }
213+ const timer = setTimeout ( ( ) => {
214+ signal ?. removeEventListener ( 'abort' , onAbort ) ;
215+ resolve ( ) ;
216+ } , ms ) ;
217+ const onAbort = ( ) => {
218+ clearTimeout ( timer ) ;
219+ reject ( new LongPollAbortError ( 'Polling aborted' , lastResult ) ) ;
220+ } ;
221+ signal ?. addEventListener ( 'abort' , onAbort , { once : true } ) ;
222+ } ) ;
223+ }
202224
203225/**
204226 * Generic polling function that handles polling logic with configurable options
@@ -212,11 +234,23 @@ export async function poll<T>(
212234 pollingRequest : ( ) => Promise < T > ,
213235 options : PollingOptions < T > = { } ,
214236) : Promise < T > {
215- const { initialDelayMs, pollingIntervalMs, maxAttempts, timeoutMs, shouldStop, onPollingAttempt, onError } =
216- {
217- ...DEFAULT_OPTIONS ,
218- ...options ,
219- } ;
237+ const {
238+ initialDelayMs,
239+ pollingIntervalMs,
240+ maxAttempts,
241+ timeoutMs,
242+ shouldStop,
243+ onPollingAttempt,
244+ onError,
245+ signal,
246+ } = {
247+ ...DEFAULT_OPTIONS ,
248+ ...options ,
249+ } ;
250+
251+ if ( signal ?. aborted ) {
252+ throw new LongPollAbortError ( 'Polling aborted' , undefined ) ;
253+ }
220254
221255 if ( initialDelayMs !== undefined && initialDelayMs < 0 ) {
222256 throw new Error ( 'initialDelayMs must be non-negative' ) ;
@@ -253,10 +287,35 @@ export async function poll<T>(
253287 const raceTimeout = < R > ( promise : Promise < R > ) : Promise < R > =>
254288 timeoutPromise ? Promise . race ( [ promise , timeoutPromise ] ) : promise ;
255289
290+ const raceAbort = < R > ( promise : Promise < R > ) : Promise < R > => {
291+ if ( ! signal ) return promise ;
292+ const abortSignal = signal ;
293+ if ( abortSignal . aborted ) {
294+ return Promise . reject ( new LongPollAbortError ( 'Polling aborted' , lastResult ) ) ;
295+ }
296+ return new Promise ( ( resolve , reject ) => {
297+ const onAbort = ( ) => {
298+ abortSignal . removeEventListener ( 'abort' , onAbort ) ;
299+ reject ( new LongPollAbortError ( 'Polling aborted' , lastResult ) ) ;
300+ } ;
301+ abortSignal . addEventListener ( 'abort' , onAbort ) ;
302+ promise . then (
303+ ( value ) => {
304+ abortSignal . removeEventListener ( 'abort' , onAbort ) ;
305+ resolve ( value ) ;
306+ } ,
307+ ( err ) => {
308+ abortSignal . removeEventListener ( 'abort' , onAbort ) ;
309+ reject ( err ) ;
310+ } ,
311+ ) ;
312+ } ) ;
313+ } ;
314+
256315 try {
257316 let result : T ;
258317 try {
259- result = await raceTimeout ( initialRequest ( ) ) ;
318+ result = await raceAbort ( raceTimeout ( initialRequest ( ) ) ) ;
260319 } catch ( error ) {
261320 if ( onError && error instanceof APIError ) {
262321 result = onError ( error ) ;
@@ -275,7 +334,7 @@ export async function poll<T>(
275334 return result ;
276335 }
277336
278- await raceTimeout ( delay ( initialDelayMs ! ) ) ;
337+ await raceAbort ( raceTimeout ( abortableDelay ( initialDelayMs ! , signal , lastResult ) ) ) ;
279338
280339 let attempts = 0 ;
281340
@@ -284,7 +343,7 @@ export async function poll<T>(
284343 ++ attempts ;
285344
286345 try {
287- result = await raceTimeout ( pollingRequest ( ) ) ;
346+ result = await raceAbort ( raceTimeout ( pollingRequest ( ) ) ) ;
288347 } catch ( error ) {
289348 if ( onError && error instanceof APIError ) {
290349 result = onError ( error ) ;
@@ -304,7 +363,7 @@ export async function poll<T>(
304363 throw new MaxAttemptsExceededError ( `Polling exceeded maximum attempts (${ maxAttempts } )` , result ) ;
305364 }
306365
307- await raceTimeout ( delay ( pollingIntervalMs ! ) ) ;
366+ await raceAbort ( raceTimeout ( abortableDelay ( pollingIntervalMs ! , signal , lastResult ) ) ) ;
308367 }
309368
310369 // This should only be reachable if maxAttempts is defined
0 commit comments