11import { aborted } from "util" ;
2- import type { BodyParserFn , CloneOptions , FetchFn , FetchFnInit , FetchFnUrl , FetchResult , StatusCode } from "./types.js" ;
2+ import type { AutoAbortKey , BodyParserFn , CloneOptions , FetchFn , FetchFnInit , FetchFnUrl , FetchResult , StatusCode } from "./types.js" ;
33import { hasHeader , setHeaders } from "./headers.js" ;
44
55/**
@@ -109,8 +109,8 @@ function textParser(response: Response) {
109109export class DrFetch < TStatusCode extends number = StatusCode , T = unknown , Abortable extends boolean = false > {
110110 #fetchFn: FetchFn ;
111111 #customProcessors: [ string | RegExp , ( response : Response , stockParsers : { json : BodyParserFn < any > ; text : BodyParserFn < string > ; } ) => Promise < any > ] [ ] = [ ] ;
112- #fetchImpl: Function ;
113- #isAbortable: boolean = false ;
112+ #fetchImpl: ( url : FetchFnUrl , init ?: FetchFnInit ) => Promise < any > ;
113+ #autoAbortMap: Map < AutoAbortKey , AbortController > | undefined ;
114114
115115 async #abortableFetch( url : FetchFnUrl , init ?: FetchFnInit ) {
116116 try {
@@ -178,6 +178,15 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
178178 this . #fetchImpl = this . #simpleFetch. bind ( this ) ;
179179 }
180180
181+ /**
182+ * Gets a Boolean value indicating whether this fetcher object is in abortable mode or not.
183+ *
184+ * **NOTE**: Once in abortable mode, the fetcher object cannot be reverted to non-abortable mode.
185+ */
186+ get isAbortable ( ) {
187+ return ! ! this . #autoAbortMap;
188+ }
189+
181190 /**
182191 * Clones this fetcher object by creating a new fetcher object with the same data-fetching function, custom
183192 * body processors, and data typing unless specified otherwise via the options parameter.
@@ -198,7 +207,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
198207 if ( opts . includeProcessors ) {
199208 newClone . #customProcessors = [ ...this . #customProcessors] ;
200209 }
201- if ( opts . preserveAbortable && this . # isAbortable) {
210+ if ( opts . preserveAbortable && this . isAbortable ) {
202211 newClone . abortable ( ) ;
203212 }
204213 return newClone as DrFetch < TStatusCode , TInherit extends true ? T : unknown , CloneAbortable > ;
@@ -281,7 +290,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
281290
282291 abortable ( ) {
283292 this . #fetchImpl = this . #abortableFetch. bind ( this ) ;
284- this . #isAbortable = true ;
293+ this . #autoAbortMap ??= new Map < AutoAbortKey , AbortController > ( ) ;
285294 return this as DrFetch < TStatusCode , T , true > ;
286295 }
287296
@@ -292,11 +301,38 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
292301 * @param init Options for the data-fetching function.
293302 * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
294303 */
295- fetch ( url : FetchFnUrl , init ?: FetchFnInit ) {
296- return this . #fetchImpl( url , init ) as ( Abortable extends true ? Promise < {
297- aborted : true ;
298- error : DOMException ;
299- } | T > : Promise < T > ) ;
304+ async fetch ( url : FetchFnUrl , init ?: FetchFnInit ) : Promise < ( Abortable extends true ? {
305+ aborted : true ;
306+ error : DOMException ;
307+ } | T : T ) > {
308+ if ( ! this . #autoAbortMap && init ?. autoAbort ) {
309+ throw new Error ( 'Cannot use autoAbort if the fetcher is not in abortable mode. Call "abortable()" first.' ) ;
310+ }
311+ const autoAbort = {
312+ key : typeof init ?. autoAbort === 'object' ? init . autoAbort . key : init ?. autoAbort ,
313+ delay : typeof init ?. autoAbort === 'object' ? init . autoAbort . delay : undefined ,
314+ } ;
315+ if ( autoAbort . key ) {
316+ this . #autoAbortMap?. get ( autoAbort . key ) ?. abort ( ) ;
317+ const ac = new AbortController ( ) ;
318+ this . #autoAbortMap! . set ( autoAbort . key , ac ) ;
319+ init ??= { } ;
320+ init . signal = ac . signal ;
321+ if ( autoAbort . delay !== undefined ) {
322+ const aborted = await new Promise < boolean > ( ( rs ) => {
323+ setTimeout ( ( ) => rs ( ac . signal . aborted ) , autoAbort . delay ) ;
324+ } ) ;
325+ if ( aborted ) {
326+ // @ts -expect-error TS2322: A runtime check is in place to ensure that the type is correct.
327+ return {
328+ aborted : true ,
329+ error : new DOMException ( 'Aborted while delayed.' , 'AbortError' )
330+ } ;
331+ }
332+ }
333+ }
334+ return await this . #fetchImpl( url , init )
335+ . finally ( ( ) => autoAbort . key && this . #autoAbortMap?. delete ( autoAbort . key ) ) ;
300336 }
301337
302338 #createInit( body : BodyInit | null | Record < string , any > | undefined , init ?: FetchFnInit ) {
@@ -319,8 +355,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
319355 * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
320356 */
321357 get ( url : URL | string , init ?: Omit < FetchFnInit , 'method' | 'body' > ) {
322- init = { ...init , method : 'GET' } ;
323- return this . fetch ( url , init ) ;
358+ return this . fetch ( url , { ...init , method : 'GET' } ) ;
324359 }
325360
326361 /**
@@ -329,8 +364,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
329364 * @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
330365 */
331366 head ( url : URL | string , init ?: Omit < FetchFnInit , 'method' | 'body' > ) {
332- init = { ...init , method : 'HEAD' } ;
333- return this . fetch ( url , init ) ;
367+ return this . fetch ( url , { ...init , method : 'HEAD' } ) ;
334368 }
335369
336370 /**
0 commit comments