diff --git a/CHANGELOG.md b/CHANGELOG.md index 95585f0..84e3144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,16 @@ ## v2.5.0 -- Added separate handler methods `onCancel` and `onInterrupt` for interrupting and canceling a request -- Added a new event `settled` to `EventListener` +### New Features + +- Added separate handler methods `onCancel` and `onInterrupt` for interrupting and canceling a request [#84](https://github.com/TENSIILE/saborter/pull/84/changes/d959429fca49fd4fb5c4c0058e117a3d4f1ba6ca) +- Added a new event `settled` to `EventListener` [#84](https://github.com/TENSIILE/saborter/pull/84/changes/34843174ea21c79f83142c328e326cd3dffff3ee) +- Added strict typing for the `isAbortError` function where the typeguard targets `AbortError` [#85](https://github.com/TENSIILE/saborter/pull/85/changes/efb8d5faa5029e580127b447c26ec860284f2fde) +- Added `Response` exception to the `catch` block when `response.ok` is `false` when using the short `fetch` format [#85](https://github.com/TENSIILE/saborter/pull/85/changes/d096d569aadd3ad6c7aa9e1b08a679e41fb0fe49) + +### Bug Fixes + +- Fixed unnecessary calls to the Aborter context inject in Http Request when the provider is disabled [#85](https://github.com/TENSIILE/saborter/pull/85/changes/fa7b1a192e5f94a9caa2b124efdce62e0f619a66) ## v2.4.0 (April 23th, 2026) diff --git a/readme.md b/readme.md index 7c8c3f2..9b062dc 100644 --- a/readme.md +++ b/readme.md @@ -115,7 +115,10 @@ const aborter = new Aborter(); // Use for the request const fetchData = async () => { try { - const result = await aborter.try((signal) => fetch('/api/data', { signal })); + const result = await aborter.try(async (signal) => { + const response = await fetch('/api/data', { signal }); + return response.json(); + }); console.log('Data received:', result); } catch (error) { console.error('Request error:', error); @@ -478,6 +481,18 @@ const response = await aborter.try( const data = await response.json(); ``` +When using `fetch` briefly, `Response.OK` is also processed. `Aborter` throws the response itself into the `catch` block: + +```typescript +try { + const data = await aborter.try(() => fetch('/api/data')); +} catch (error) { + if (error instanceof Response) { + // Processing a case when response.ok is false + } +} +``` + **Calling a method without a signal:** The `.try()` method can be called without removing the `signal` from the callback argument if you don't need it. diff --git a/src/features/abort-error/abort-error.lib.ts b/src/features/abort-error/abort-error.lib.ts index d7300f1..6599a7d 100644 --- a/src/features/abort-error/abort-error.lib.ts +++ b/src/features/abort-error/abort-error.lib.ts @@ -15,7 +15,9 @@ const checkErrorCause = (error: unknown) => * Determines whether a given error is an AbortError. * * @param {any} error - The value to check. - * @returns {error is Error} `true` if the error is identified as an AbortError, otherwise `false`. + * @returns {error is AbortError | Error} `true` if the error is identified as an AbortError, otherwise `false`. + * + * The function returns the typeguard to `AbortError` by default. If you want the typeguard to return `Error`, pass the `'soft'` generic. * * @example * // Direct instance @@ -38,7 +40,9 @@ const checkErrorCause = (error: unknown) => * const outer = new Error('Wrapper', { cause: inner }); * isAbortError(outer); // true */ -export const isAbortError = (error: any): error is Error => { +export const isAbortError = ( + error: any +): error is T extends 'strict' ? AbortError : Error => { if (error instanceof AbortError) { return true; } diff --git a/src/features/lib/fetch/fetch.lib.ts b/src/features/lib/fetch/fetch.lib.ts index 3c96c73..64c214b 100644 --- a/src/features/lib/fetch/fetch.lib.ts +++ b/src/features/lib/fetch/fetch.lib.ts @@ -4,6 +4,8 @@ import { AborterType } from '../../../modules/aborter/aborter.types'; let executableAborter: AborterType | null = null; +let isAborterCtxProvisionEnabled = false; + const originalFetch = globalThis.fetch; const OriginalXHR = globalThis.XMLHttpRequest; @@ -25,6 +27,10 @@ const OriginalXHR = globalThis.XMLHttpRequest; * // All subsequent `fetch` calls will use the aborter's signal and headers. */ export function injectAborterContextIntoHttpRequest(aborter: AborterType | null): void { + if (!isAborterCtxProvisionEnabled) { + return; + } + if (!aborter) { if (globalThis.fetch !== originalFetch) { globalThis.fetch = originalFetch; @@ -160,3 +166,20 @@ export function internalFetch(url: RequestInfo | URL, init?: RequestInit): Promi headers: { ...init?.headers, ...headers } }); } + +/** + +* Enables or disables automatic provisioning of the active `Aborter` context for `fetch | XMLHttpRequest` calls. +* If enabled, `Aborter.try` calls will override the global `fetch | XMLHttpRequest` only at the time of the call and +* if the user chooses to pass the context automatically. +* +* After the `fetch | XMLHttpRequest` call, the context is immediately restored to the original one. +* The `fetch | XMLHttpRequest` override occurs only in the scope of the `Aborter.try` method. +* +* If disabled, the original `fetch | XMLHttpRequest` is always used, and interception does not occur. + +* @param enabled - `true` to enable context provisioning, `false` to disable. +*/ +export const setAborterContextProvisionMode = (enabled: boolean): void => { + isAborterCtxProvisionEnabled = enabled; +}; diff --git a/src/features/lib/fetch/fetch.test.ts b/src/features/lib/fetch/fetch.test.ts index 6c21e76..648d3f3 100644 --- a/src/features/lib/fetch/fetch.test.ts +++ b/src/features/lib/fetch/fetch.test.ts @@ -21,6 +21,7 @@ describe('Fetch lib', () => { injectAborterContextIntoHttpRequest = module.injectAborterContextIntoHttpRequest; internalFetch = module.internalFetch; + module.setAborterContextProvisionMode(true); }); beforeEach(() => { diff --git a/src/modules/aborter/aborter.ts b/src/modules/aborter/aborter.ts index 6a4daaa..9661a6f 100644 --- a/src/modules/aborter/aborter.ts +++ b/src/modules/aborter/aborter.ts @@ -2,7 +2,7 @@ import { RequestState, emitRequestState } from '../../features/state-observer'; import { AbortError, isAbortError } from '../../features/abort-error'; import { EventListener, clearEventListeners } from '../../features/event-listener'; -import { injectAborterContextIntoHttpRequest } from '../../features/lib/fetch'; +import { injectAborterContextIntoHttpRequest, setAborterContextProvisionMode } from '../../features/lib/fetch'; import { ServerBreaker } from '../../features/server-breaker'; import { Timeout, TimeoutError } from '../../features/timeout'; import { ErrorMessage, disposeSymbol } from './aborter.constants'; @@ -127,6 +127,8 @@ export class Aborter implements Types.AborterType { request: Types.AbortableRequest, { isErrorNativeBehavior = false, timeout, unpackData = true, provision = true }: Types.FnTryOptions = {} ): Promise { + setAborterContextProvisionMode(provision); + if (this.isRequestInProgress) { const cancelledAbortError = new AbortError(ErrorMessage.CancelRequest, { type: 'cancelled', @@ -156,9 +158,7 @@ export class Aborter implements Types.AborterType { queueMicrotask(() => this.setRequestState('pending')); - if (provision) { - injectAborterContextIntoHttpRequest(this); - } + injectAborterContextIntoHttpRequest(this); Promise.race([ request(this.abortController.signal, this.requestOptions), @@ -173,6 +173,7 @@ export class Aborter implements Types.AborterType { if (unpackData && response instanceof Response) { if (!response.ok) { logger.warn('Request failed, something went wrong', response); + throw response; } return response.json().then((data) => {