Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
17 changes: 16 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions src/features/abort-error/abort-error.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = <T extends 'strict' | 'soft' = 'strict'>(
error: any
): error is T extends 'strict' ? AbortError : Error => {
if (error instanceof AbortError) {
return true;
}
Expand Down
23 changes: 23 additions & 0 deletions src/features/lib/fetch/fetch.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
};
1 change: 1 addition & 0 deletions src/features/lib/fetch/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Fetch lib', () => {

injectAborterContextIntoHttpRequest = module.injectAborterContextIntoHttpRequest;
internalFetch = module.internalFetch;
module.setAborterContextProvisionMode(true);
});

beforeEach(() => {
Expand Down
9 changes: 5 additions & 4 deletions src/modules/aborter/aborter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -127,6 +127,8 @@ export class Aborter implements Types.AborterType {
request: Types.AbortableRequest<any>,
{ isErrorNativeBehavior = false, timeout, unpackData = true, provision = true }: Types.FnTryOptions = {}
): Promise<R> {
setAborterContextProvisionMode(provision);

if (this.isRequestInProgress) {
const cancelledAbortError = new AbortError(ErrorMessage.CancelRequest, {
type: 'cancelled',
Expand Down Expand Up @@ -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),
Expand All @@ -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) => {
Expand Down
Loading