From 7cbe28679a01aca9ea87bbd30f9842a2fe3b6452 Mon Sep 17 00:00:00 2001 From: OrbisK Date: Mon, 4 Aug 2025 12:28:57 +0200 Subject: [PATCH 1/9] feat: merge timeout and custom signal --- .gitignore | 1 + src/fetch.ts | 23 +++++------------------ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 96537549..a6fcea55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode +.idea node_modules *.log .DS_Store diff --git a/src/fetch.ts b/src/fetch.ts index 3655a0fa..ef225ad3 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -166,21 +166,12 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { } } - let abortTimeout: NodeJS.Timeout | undefined; + const mergedSignal = AbortSignal.any([ + typeof context.options.timeout === "number" ? AbortSignal.timeout(context.options.timeout) : undefined, + context.options.signal, + ].filter((s): s is NonNullable => Boolean(s))) - // TODO: Can we merge signals? - if (!context.options.signal && context.options.timeout) { - const controller = new AbortController(); - abortTimeout = setTimeout(() => { - const error = new Error( - "[TimeoutError]: The operation was aborted due to timeout" - ); - error.name = "TimeoutError"; - (error as any).code = 23; // DOMException.TIMEOUT_ERR - controller.abort(error); - }, context.options.timeout); - context.options.signal = controller.signal; - } + context.options.signal = mergedSignal try { context.response = await fetch( @@ -196,10 +187,6 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { ); } return await onError(context); - } finally { - if (abortTimeout) { - clearTimeout(abortTimeout); - } } const hasBody = From c580df631778e709f8a8a212dde6a85ed9d5cf37 Mon Sep 17 00:00:00 2001 From: OrbisK Date: Mon, 4 Aug 2025 12:32:37 +0200 Subject: [PATCH 2/9] chore: linting + tests --- src/fetch.ts | 22 ++++++++++++---------- test/index.test.ts | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index ef225ad3..61019457 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -35,11 +35,8 @@ const retryStatusCodes = new Set([ const nullBodyResponses = new Set([101, 204, 205, 304]); export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { - const { - fetch = globalThis.fetch, - Headers = globalThis.Headers, - AbortController = globalThis.AbortController, - } = globalOptions; + const { fetch = globalThis.fetch, Headers = globalThis.Headers } = + globalOptions; async function onError(context: FetchContext): Promise> { // Is Abort @@ -166,12 +163,17 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { } } - const mergedSignal = AbortSignal.any([ - typeof context.options.timeout === "number" ? AbortSignal.timeout(context.options.timeout) : undefined, - context.options.signal, - ].filter((s): s is NonNullable => Boolean(s))) + const mergedSignal = AbortSignal.any( + [ + typeof context.options.timeout === "number" + ? AbortSignal.timeout(context.options.timeout) + : undefined, + context.options.signal, + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + ].filter((s): s is NonNullable => Boolean(s)) + ); - context.options.signal = mergedSignal + context.options.signal = mergedSignal; try { context.response = await fetch( diff --git a/test/index.test.ts b/test/index.test.ts index f432e2d7..1cfa10bf 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -531,6 +531,7 @@ describe("ofetch", () => { const options = fetch.mock.calls[0][1]; expect(options).toStrictEqual({ headers: expect.any(Headers), + signal: expect.any(AbortSignal), }); }); }); From ce1e902a860b5a26fc3ec96f378fbf65b1ba43a0 Mon Sep 17 00:00:00 2001 From: OrbisK Date: Mon, 4 Aug 2025 12:40:29 +0200 Subject: [PATCH 3/9] chore: readd AbortController to not break existing api --- src/fetch.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 61019457..3d682dd9 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -35,8 +35,11 @@ const retryStatusCodes = new Set([ const nullBodyResponses = new Set([101, 204, 205, 304]); export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { - const { fetch = globalThis.fetch, Headers = globalThis.Headers } = - globalOptions; + const { + fetch = globalThis.fetch, + Headers = globalThis.Headers, + AbortController = globalThis.AbortController, + } = globalOptions; async function onError(context: FetchContext): Promise> { // Is Abort @@ -169,6 +172,7 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { ? AbortSignal.timeout(context.options.timeout) : undefined, context.options.signal, + AbortController.signal, // eslint-disable-next-line unicorn/prefer-native-coercion-functions ].filter((s): s is NonNullable => Boolean(s)) ); From 457632d18b86599d13cfc0317afd62df4f2117bd Mon Sep 17 00:00:00 2001 From: OrbisK Date: Sat, 30 Aug 2025 13:51:43 +0200 Subject: [PATCH 4/9] chore: refactoring --- src/fetch.ts | 29 +++++++++++------------------ src/types.ts | 3 +++ test/index.test.ts | 10 ++++++++++ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 3d682dd9..b07230b7 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -35,11 +35,8 @@ const retryStatusCodes = new Set([ const nullBodyResponses = new Set([101, 204, 205, 304]); export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { - const { - fetch = globalThis.fetch, - Headers = globalThis.Headers, - AbortController = globalThis.AbortController, - } = globalOptions; + const { fetch = globalThis.fetch, Headers = globalThis.Headers } = + globalOptions; async function onError(context: FetchContext): Promise> { // Is Abort @@ -165,19 +162,15 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { } } } - - const mergedSignal = AbortSignal.any( - [ - typeof context.options.timeout === "number" - ? AbortSignal.timeout(context.options.timeout) - : undefined, - context.options.signal, - AbortController.signal, - // eslint-disable-next-line unicorn/prefer-native-coercion-functions - ].filter((s): s is NonNullable => Boolean(s)) - ); - - context.options.signal = mergedSignal; + if (typeof context.options.timeout === "number") { + context.options.signal = AbortSignal.any( + [ + AbortSignal.timeout(context.options.timeout), + context.options.signal, + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + ].filter((s): s is NonNullable => Boolean(s)) + ); + } try { context.response = await fetch( diff --git a/src/types.ts b/src/types.ts index 5f2e8187..e1de88fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,6 +78,9 @@ export interface CreateFetchOptions { defaults?: FetchOptions; fetch?: Fetch; Headers?: typeof Headers; + /** + * @deprecated AbortController is not used internally anymore and will be removed in future versions. + */ AbortController?: typeof AbortController; } diff --git a/test/index.test.ts b/test/index.test.ts index 1cfa10bf..b9fe29ea 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -531,7 +531,17 @@ describe("ofetch", () => { const options = fetch.mock.calls[0][1]; expect(options).toStrictEqual({ headers: expect.any(Headers), + }); + fetch.mockReset(); + await $fetch("https://jsonplaceholder.typicode.com/todos/1", { + timeout: 10_000, + }); + expect(fetch).toHaveBeenCalledOnce(); + const options2 = fetch.mock.calls[0][1]; + expect(options2).toStrictEqual({ + headers: expect.any(Headers), signal: expect.any(AbortSignal), + timeout: 10_000, }); }); }); From 0d33c9e8b0ec0bc7da5d5ceb5b856ed4e988a4f8 Mon Sep 17 00:00:00 2001 From: OrbisK Date: Wed, 10 Sep 2025 10:45:54 +0200 Subject: [PATCH 5/9] refactor: only use `AbortSignal.timeout` if `context.options.signal` is passed --- src/fetch.ts | 43 +++++++++++++++++++++++++++++++++---------- src/types.ts | 3 --- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index b07230b7..f0a7de17 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -35,8 +35,11 @@ const retryStatusCodes = new Set([ const nullBodyResponses = new Set([101, 204, 205, 304]); export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { - const { fetch = globalThis.fetch, Headers = globalThis.Headers } = - globalOptions; + const { + fetch = globalThis.fetch, + Headers = globalThis.Headers, + AbortController = globalThis.AbortController, + } = globalOptions; async function onError(context: FetchContext): Promise> { // Is Abort @@ -162,14 +165,30 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { } } } - if (typeof context.options.timeout === "number") { - context.options.signal = AbortSignal.any( - [ - AbortSignal.timeout(context.options.timeout), - context.options.signal, - // eslint-disable-next-line unicorn/prefer-native-coercion-functions - ].filter((s): s is NonNullable => Boolean(s)) - ); + + let abortTimeout: NodeJS.Timeout | undefined; + + if (context.options.timeout) { + if (context.options.signal) { + context.options.signal = AbortSignal.any( + [ + AbortSignal.timeout(context.options.timeout), + context.options.signal, + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + ].filter((s): s is NonNullable => Boolean(s)) + ); + } else { + const controller = new AbortController(); + abortTimeout = setTimeout(() => { + const error = new Error( + "[TimeoutError]: The operation was aborted due to timeout" + ); + error.name = "TimeoutError"; + (error as any).code = 23; // DOMException.TIMEOUT_ERR + controller.abort(error); + }, context.options.timeout); + context.options.signal = controller.signal; + } } try { @@ -186,6 +205,10 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { ); } return await onError(context); + } finally { + if (abortTimeout) { + clearTimeout(abortTimeout); + } } const hasBody = diff --git a/src/types.ts b/src/types.ts index e1de88fd..5f2e8187 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,9 +78,6 @@ export interface CreateFetchOptions { defaults?: FetchOptions; fetch?: Fetch; Headers?: typeof Headers; - /** - * @deprecated AbortController is not used internally anymore and will be removed in future versions. - */ AbortController?: typeof AbortController; } From 89e3a3af77e826cb2809d4e4db76e39f5d295f65 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 10 Sep 2025 12:09:00 +0200 Subject: [PATCH 6/9] Update src/fetch.ts --- src/fetch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fetch.ts b/src/fetch.ts index f0a7de17..a9f7159a 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -178,6 +178,7 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { ].filter((s): s is NonNullable => Boolean(s)) ); } else { + // TODO: Use AbortSignal.timeout in next major const controller = new AbortController(); abortTimeout = setTimeout(() => { const error = new Error( From 0852a282f2216ddefbb51d7612e3f8f8d35301c0 Mon Sep 17 00:00:00 2001 From: OrbisK Date: Wed, 10 Sep 2025 13:04:22 +0200 Subject: [PATCH 7/9] chore: remove unused filter --- src/fetch.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index a9f7159a..46013ce5 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -174,8 +174,7 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { [ AbortSignal.timeout(context.options.timeout), context.options.signal, - // eslint-disable-next-line unicorn/prefer-native-coercion-functions - ].filter((s): s is NonNullable => Boolean(s)) + ] ); } else { // TODO: Use AbortSignal.timeout in next major From 6cb1b31b603410a857ba5106849f8b06dbb1b917 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:04:52 +0000 Subject: [PATCH 8/9] chore: apply automated updates --- src/fetch.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 46013ce5..356af978 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -170,12 +170,10 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { if (context.options.timeout) { if (context.options.signal) { - context.options.signal = AbortSignal.any( - [ - AbortSignal.timeout(context.options.timeout), - context.options.signal, - ] - ); + context.options.signal = AbortSignal.any([ + AbortSignal.timeout(context.options.timeout), + context.options.signal, + ]); } else { // TODO: Use AbortSignal.timeout in next major const controller = new AbortController(); From 5be353afca4975a2e05ba3591c79a12dac56c297 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 28 Oct 2025 12:11:24 +0100 Subject: [PATCH 9/9] use AbortSignal.timeout as targetting v2 --- src/fetch.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 216a6129..7745b69d 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -170,24 +170,12 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { let abortTimeout: NodeJS.Timeout | undefined; if (context.options.timeout) { - if (context.options.signal) { - context.options.signal = AbortSignal.any([ - AbortSignal.timeout(context.options.timeout), - context.options.signal, - ]); - } else { - // TODO: Use AbortSignal.timeout in next major - const controller = new AbortController(); - abortTimeout = setTimeout(() => { - const error = new Error( - "[TimeoutError]: The operation was aborted due to timeout" - ); - error.name = "TimeoutError"; - (error as any).code = 23; // DOMException.TIMEOUT_ERR - controller.abort(error); - }, context.options.timeout); - context.options.signal = controller.signal; - } + context.options.signal = context.options.signal + ? AbortSignal.any([ + AbortSignal.timeout(context.options.timeout), + context.options.signal, + ]) + : AbortSignal.timeout(context.options.timeout); } try {