1- import { default as FormData } from "form-data" ;
21import qs from "qs" ;
32import { RUNTIME } from "../runtime" ;
43import { APIResponse } from "./APIResponse" ;
@@ -16,6 +15,7 @@ export declare namespace Fetcher {
1615 timeoutMs ?: number ;
1716 maxRetries ?: number ;
1817 withCredentials ?: boolean ;
18+ abortSignal ?: AbortSignal ;
1919 responseType ?: "json" | "blob" | "streaming" | "text" ;
2020 }
2121
@@ -67,13 +67,28 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
6767 : args . url ;
6868
6969 let body : BodyInit | undefined = undefined ;
70- if ( args . body instanceof FormData ) {
71- // @ts -expect-error
72- body = args . body ;
73- } else if ( args . body instanceof Uint8Array ) {
74- body = args . body ;
70+ const maybeStringifyBody = ( body : any ) => {
71+ if ( body instanceof Uint8Array ) {
72+ return body ;
73+ } else {
74+ return JSON . stringify ( body ) ;
75+ }
76+ } ;
77+
78+ if ( RUNTIME . type === "node" ) {
79+ if ( args . body instanceof ( await import ( "formdata-node" ) ) . FormData ) {
80+ // @ts -expect-error
81+ body = args . body ;
82+ } else {
83+ body = maybeStringifyBody ( args . body ) ;
84+ }
7585 } else {
76- body = JSON . stringify ( args . body ) ;
86+ if ( args . body instanceof ( await import ( "form-data" ) ) . default ) {
87+ // @ts -expect-error
88+ body = args . body ;
89+ } else {
90+ body = maybeStringifyBody ( args . body ) ;
91+ }
7792 }
7893
7994 // In Node.js environments, the SDK always uses`node-fetch`.
@@ -89,21 +104,33 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
89104 : ( ( await import ( "node-fetch" ) ) . default as any ) ;
90105
91106 const makeRequest = async ( ) : Promise < Response > => {
92- const controller = new AbortController ( ) ;
93- let abortId = undefined ;
107+ const signals : AbortSignal [ ] = [ ] ;
108+
109+ // Add timeout signal
110+ let timeoutAbortId : NodeJS . Timeout | undefined = undefined ;
94111 if ( args . timeoutMs != null ) {
95- abortId = setTimeout ( ( ) => controller . abort ( ) , args . timeoutMs ) ;
112+ const { signal, abortId } = getTimeoutSignal ( args . timeoutMs ) ;
113+ timeoutAbortId = abortId ;
114+ signals . push ( signal ) ;
96115 }
116+
117+ // Add arbitrary signal
118+ if ( args . abortSignal != null ) {
119+ signals . push ( args . abortSignal ) ;
120+ }
121+
97122 const response = await fetchFn ( url , {
98123 method : args . method ,
99124 headers,
100125 body,
101- signal : controller . signal ,
126+ signal : anySignal ( signals ) ,
102127 credentials : args . withCredentials ? "include" : undefined ,
103128 } ) ;
104- if ( abortId != null ) {
105- clearTimeout ( abortId ) ;
129+
130+ if ( timeoutAbortId != null ) {
131+ clearTimeout ( timeoutAbortId ) ;
106132 }
133+
107134 return response ;
108135 } ;
109136
@@ -167,7 +194,15 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
167194 } ;
168195 }
169196 } catch ( error ) {
170- if ( error instanceof Error && error . name === "AbortError" ) {
197+ if ( args . abortSignal != null && args . abortSignal . aborted ) {
198+ return {
199+ ok : false ,
200+ error : {
201+ reason : "unknown" ,
202+ errorMessage : "The user aborted a request" ,
203+ } ,
204+ } ;
205+ } else if ( error instanceof Error && error . name === "AbortError" ) {
171206 return {
172207 ok : false ,
173208 error : {
@@ -194,4 +229,43 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
194229 }
195230}
196231
232+ const TIMEOUT = "timeout" ;
233+
234+ function getTimeoutSignal ( timeoutMs : number ) : { signal : AbortSignal ; abortId : NodeJS . Timeout } {
235+ const controller = new AbortController ( ) ;
236+ const abortId = setTimeout ( ( ) => controller . abort ( TIMEOUT ) , timeoutMs ) ;
237+ return { signal : controller . signal , abortId } ;
238+ }
239+
240+ /**
241+ * Returns an abort signal that is getting aborted when
242+ * at least one of the specified abort signals is aborted.
243+ *
244+ * Requires at least node.js 18.
245+ */
246+ function anySignal ( ...args : AbortSignal [ ] | [ AbortSignal [ ] ] ) : AbortSignal {
247+ // Allowing signals to be passed either as array
248+ // of signals or as multiple arguments.
249+ const signals = < AbortSignal [ ] > ( args . length === 1 && Array . isArray ( args [ 0 ] ) ? args [ 0 ] : args ) ;
250+
251+ const controller = new AbortController ( ) ;
252+
253+ for ( const signal of signals ) {
254+ if ( signal . aborted ) {
255+ // Exiting early if one of the signals
256+ // is already aborted.
257+ controller . abort ( ( signal as any ) ?. reason ) ;
258+ break ;
259+ }
260+
261+ // Listening for signals and removing the listeners
262+ // when at least one symbol is aborted.
263+ signal . addEventListener ( "abort" , ( ) => controller . abort ( ( signal as any ) ?. reason ) , {
264+ signal : controller . signal ,
265+ } ) ;
266+ }
267+
268+ return controller . signal ;
269+ }
270+
197271export const fetcher : FetchFunction = fetcherImpl ;
0 commit comments