Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fd9c0a3
feat(aborter): adds fetcher installation functionality
TENSIILE Mar 20, 2026
43228d8
feat(aborter): adds the abortSignalAny lib function
TENSIILE Mar 20, 2026
ccc18dc
feat: fetcherFactory was moved to a separate module
TENSIILE Mar 20, 2026
7236db3
feat: adds a utility for generating a unique id (#59)
TENSIILE Mar 24, 2026
67f2bf3
adds exceptions to cspell (#59)
TENSIILE Mar 24, 2026
5f3fca2
feat: adds the abortSignalAny function (#59)
TENSIILE Mar 25, 2026
8fb7e67
feat: adds fetcher factory functionality (#59)
TENSIILE Mar 25, 2026
7acab09
chore: adds a dot-notation rule (#59)
TENSIILE Mar 25, 2026
81b90ed
adds a new word to the exception (#59)
TENSIILE Mar 25, 2026
78f2d3c
feat: integrates the fetcher factory into Aborter (#59)
TENSIILE Mar 25, 2026
7a954b6
fix: fixes fetcher factory tests (#59)
TENSIILE Mar 25, 2026
3473f9b
test: renames the test (#59)
TENSIILE Mar 25, 2026
e156035
chore: add badges for test coverage (#59)
TENSIILE Mar 26, 2026
05e3dea
Merge branch develop into feature/2.x.x-fetcher
TENSIILE Mar 26, 2026
9e76cd6
docs(readme): fixes badge in testing coverage
TENSIILE Mar 26, 2026
0cc158a
refactor: removes the fetcher factory implementation (#59)
TENSIILE Mar 26, 2026
5dcd0db
feat: adds server breaker functionality (#59)
TENSIILE Mar 27, 2026
aee9f62
refactor: simplifies API interaction with the @saborter/server packag…
TENSIILE Mar 27, 2026
d30468f
refactor: simplifies the server breaker in Aborter (#59)
TENSIILE Mar 27, 2026
15fb63c
fix: fixes the test with Aborter's headings (#59)
TENSIILE Mar 27, 2026
0618946
Merge pull request #59 from TENSIILE/feature/2.x.x-fetcher
TENSIILE Mar 27, 2026
5534e7b
docs(lib): adds documentation to the abortSignalAny lib function (#60)
TENSIILE Mar 29, 2026
e5264b9
fix(AbortError): fixes a bug with inheritance of the AbortError error…
TENSIILE Mar 29, 2026
7487700
test(AbortError): adds a test for copyAbortError (#60)
TENSIILE Mar 31, 2026
3ed41dc
feat: adds a runtime check for what cannot be overridden in AbortErro…
TENSIILE Mar 31, 2026
23bc30b
test(AbortError): renames tests (#60)
TENSIILE Mar 31, 2026
3c6a9fd
docs(readme): adds an example to the documentation about interrupting…
TENSIILE Mar 31, 2026
cf658ad
docs(changelog): updates the changelog (#60)
TENSIILE Mar 31, 2026
7988be6
raises the package version (#60)
TENSIILE Mar 31, 2026
934020c
test(abortError|debounce): fixes tests related to fixing the error ov…
TENSIILE Mar 31, 2026
c7d908f
Merge pull request #60 from TENSIILE/release/v2.2.0
TENSIILE Mar 31, 2026
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
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"import/no-extraneous-dependencies": ["off"],
"@typescript-eslint/no-unused-vars": ["off", {}],
"no-unused-vars": ["off"],
"dot-notation": "off",
"prettier/prettier": [
"error",
{
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Tests

on:
pull_request:
push:
branches:
- master
- develop
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test -- --coverage --coverageReporters=lcov
- uses: coverallsapp/github-action@v2
with:
file: coverage/lcov.info
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Saborter Changelog

## v2.2.0 (Match 31th, 2026)

### New Features

- Added the `abortSignalAny` utility function [#60](https://github.com/TENSIILE/saborter/pull/60)
- Added integration functionality with the `@saborter/server` package [#60](https://github.com/TENSIILE/saborter/pull/60)
- Improved JSdoc documentation for `Aborter` [#60](https://github.com/TENSIILE/saborter/pull/60)

### Bug Fixes

- Fixed a bug in the `debounce` and `setTimeoutAsync` utilities with overriding the `initiator` field in the `AbortError` error [#60](https://github.com/TENSIILE/saborter/pull/60)

## v2.1.0 (March 18th, 2026)

### New Features
Expand Down
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "0.2",
"language": "en,ru",
"words": ["Saborter", "saborter", "Laptev", "Vladislav", "tgz", "Сalls"],
"words": ["Сalls", "Laptev", "saborter", "Saborter", "tgz", "Vladislav", "yxxx", "TENSIILE"],
"flagWords": [],
"ignorePaths": [
"node_modules/**",
Expand Down
87 changes: 87 additions & 0 deletions docs/libs.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,93 @@ const processItems = (items: unknown[], signal: AbortSignal) => {
};
```

### `abortSignalAny`

Combines multiple abort signals into a single signal that aborts when any of the input signals aborts. This is useful when you need to cancel an operation if any of several independent signals (e.g., from different sources) become aborted.

**Signature:**

```typescript
export const abortSignalAny = <T extends AbortSignalLike | AbortSignalLike[]>(...args: T[]): AbortSignal
```

Where `AbortSignalLike = AbortSignal | null | undefined`.

**Parameters:**

- `...args` – A rest parameter that accepts any number of arguments. Each argument can be:
- A single `AbortSignal` (or `null`/`undefined` – these are ignored).
- An array of `AbortSignalLike`.

This function accepts either an unlimited number of signals or an array of signals.

**Returns:**

A new `AbortController.signal` that will be aborted when `any` of the input signals aborts. If any input signal is already aborted when the function is called, the returned signal is immediately aborted.

**Description:**

The function works as follows:

1. Flattens the provided arguments into a single array of signals (ignoring `null` or `undefined`).
2. Creates a new `AbortController`.
3. For each signal in the flattened list:

- If the signal is already aborted, the controller is aborted immediately with a custom `AbortError` (see error handling below) and no further listeners are attached.
- Otherwise, it attaches a one‑time `'abort'` event listener to that signal. When the signal aborts, the handler is called, which:
- Aborts the controller using the same `AbortError` created from the aborting signal.
- Cleans up all listeners from all other signals (removes the `'abort'` event handlers) to prevent memory leaks.

The function ensures that the controller is aborted only once, even if multiple signals abort simultaneously or in quick succession.

**Error Handling:**

The function creates a consistent `AbortError` object when aborting the controller. It uses the helper `createAbortError`, which:

- If the signal’s `reason` is already an `AbortError`, that error is reused.
- If the signal’s `reason` is a `DOMException` with the name `'AbortError'` (as in browser‑native abort), it stores the original reason under a `cause` property.
- Otherwise, it creates a new `AbortError` with the message `'The operation was aborted'` and attaches the original reason under a `reason` property.

In all cases, the resulting error has an `initiator` property set to `'abortSignalAny'` to help trace the source of the abort.

**Examples:**

#### Basic usage with two signals:

```typescript
import { abortSignalAny } from '.saborter/lib';

const ac1 = new AbortController();
const ac2 = new AbortController();
const combined = abortSignalAny(ac1.signal, ac2.signal);

combined.addEventListener('abort', () => console.log('Combined signal aborted!'));
ac1.abort(); // triggers combined abort
```

#### Using arrays:

```typescript
const signals = [ac1.signal, ac2.signal];
const combined = abortSignalAny(signals);
```

#### Handling already aborted signals:

```typescript
const ac = new AbortController();
ac.abort(); // signal is already aborted

const combined = abortSignalAny(ac.signal); // returns an already aborted signal
combined.aborted; // true
```

#### Ignoring `null` or `undefined`:

```typescript
const combined = abortSignalAny(null, undefined, ac.signal); // only ac.signal is considered
```

### `timeInMilliseconds`

Converts a configuration object containing time components (hours, minutes, seconds, milliseconds) into a total number of milliseconds. All components are optional and default to `0` if not provided.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "saborter",
"version": "2.1.0",
"version": "2.2.0",
"description": "A simple and efficient library for canceling asynchronous requests using AbortController",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
Expand Down
55 changes: 47 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
<img src="https://github.com/TENSIILE/saborter/actions/workflows/publish.yml/badge.svg" /></a>
<a href="https://github.com/TENSIILE/saborter/actions/workflows/ci.yml" alt="CI">
<img src="https://github.com/TENSIILE/saborter/actions/workflows/ci.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/saborter" alt="Tests">
<img src="https://img.shields.io/badge/coverage-90%25-green" /></a>
<a href="https://github.com/TENSIILE/saborter/actions/workflows/tests.yml" alt="Tests">
<img src="https://github.com/TENSIILE/saborter/actions/workflows/tests.yml/badge.svg" /></a>
<a href='https://coveralls.io/github/TENSIILE/saborter?branch=master'><img src='https://coveralls.io/repos/github/TENSIILE/saborter/badge.svg?branch=master' alt='Coverage Status' /></a>
<a href="https://bundlejs.com/?q=saborter#sharing" alt="Size">
<img src="https://deno.bundlejs.com/badge?q=saborter" /></a>
<a href="https://github.com/TENSIILE/saborter/blob/develop/LICENSE" alt="License">
<img src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://github.com/TENSIILE/saborter" alt="Github">
Expand All @@ -24,6 +27,8 @@
**Saborter** is a lightweight, dependency-free, simple, yet incredibly powerful JavaScript/TypeScript library for managing asynchronous cancellation.
It builds on top of its own [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) but fully exploits its shortcomings, providing a clean, inexpensive, and convenient API.

Add a 🌟 and follow me to support the project!

## 📚 Documentation

The documentation is divided into several sections:
Expand Down Expand Up @@ -123,7 +128,7 @@ The `Aborter` class makes it easy to cancel running requests after a period of t
const aborter = new Aborter();

// Start a long-running request and cancel the request after 2 seconds
const results = aborter.try(
const results = await aborter.try(
(signal) => {
return fetch('/api/long-task', { signal });
},
Expand All @@ -142,14 +147,29 @@ import { debounce } from 'saborter/lib';
const aborter = new Aborter();

// The request will be delayed for 2 seconds and then executed.
const results = aborter.try(
const results = await aborter.try(
debounce((signal) => {
return fetch('/api/long-task', { signal });
}, 2000)
);
```

### 4. Interrupting promises without a signal
### 4. Canceling a request on the server

For the server to support interrupts via the `saborter`, it is necessary to use the [@saborter/server](https://github.com/TENSIILE/saborter-server) package on the server side:

```javascript
// Create an Aborter instance
const aborter = new Aborter();

// The request will be cancelled on the server side
// if the request fails to complete either successfully or with an error.
const results = await aborter.try((signal, { headers }) => {
return fetch('/api/posts', { signal, headers });
});
```

### 5. Interrupting promises without a signal

If you want to cancel a task with a promise:

Expand All @@ -164,15 +184,15 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

// The callback function can be restarted each time (by calling .try() again), which will interrupt the previous call and start it again.
// Or the `.abort()` method can be used to abort the callback function entirely.
const results = aborter.try(
const results = await aborter.try(
async () => {
await delay(2000);
return Promise.resolve({ done: true });
}
);
```

### 5. Multiple request aborts through a single `ReusableAborter` instance
### 6. Multiple request aborts through a single `ReusableAborter` instance

The `ReusableAborter` class allows you to easily cancel requests an unlimited number of times while preserving all listeners:

Expand All @@ -193,7 +213,7 @@ reusableAborter.abort(); // call of the listener -> console.log('aborted', e)
reusableAborter.abort(); // listener recall -> console.log('aborted', e)
```

### 6. Working with Multiple Requests
### 7. Working with Multiple Requests

You can create separate instances for different groups of requests:

Expand Down Expand Up @@ -402,6 +422,24 @@ try {
> [!NOTE]
> In this case, the wait for the request to be executed will be interrupted, but the request itself will still be executed.

**Canceling a request on the server:**

To automatically abort a server-side operation when a client aborts a request,
you must use [@saborter/server](https://github.com/TENSIILE/saborter-server) on the server and pass headers on the client.

```typescript
try {
const users = await aborter.try(async (signal, { headers }) => {
const response = await fetch('/api/users', { signal, headers });
return response.json();
});
} catch (error) {
if (error instanceof AbortError) {
console.log('interrupt error handling');
}
}
```

**Examples using automatic cancellation after a time:**

```javascript
Expand Down Expand Up @@ -541,6 +579,7 @@ fetchData();
The `saborter` package contains additional features out of the box that can help you:

- [**@saborter/react**](https://github.com/TENSIILE/saborter-react) - a standalone library with `Saborter` and `React` integration.
- [**@saborter/server**](https://github.com/TENSIILE/saborter-server) - library that automatically cancels server-side operations when the client aborts a request.
- [**saborter/lib**](./docs/libs.md) - auxiliary functions.
- [**saborter/errors**](./docs/errors.md) - package errors.
- [**AbortError**](./docs/errors.md#aborterror) - custom error for working with Aborter.
Expand Down
41 changes: 41 additions & 0 deletions src/features/abort-error/abort-error.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,44 @@ export const isAbortError = (error: any): error is Error => {

return !!checkErrorCause(error);
};

const CANNOT_BE_OVERRIDDEN = ['cause', 'timestamp', 'stack', 'name'] satisfies Array<keyof AbortError>;

/**
* Creates a new `AbortError` instance that is a copy of the original,
* allowing selective override of its properties.
*
* The original error is set as the `cause` of the new error, preserving
* the error chain. This is useful when you need to augment or transform
* an abort error without losing the original context.
*
* @param abortError - The original `AbortError` to copy.
* @param override - An object with properties to override on the new error.
* The following properties cannot be overridden:
* - `cause` – always set to the original error.
* - `timestamp` – always the creation time of the new error.
* - `stack` – automatically generated.
* - `name` – always `'AbortError'`.
* All other properties of `AbortError` can be overridden.
* @returns A new `AbortError` instance with the same properties as the original,
* except those specified in `override`.
*
* @example
* const original = new AbortError('Operation aborted', { type: 'cancelled', initiator: 'user' });
* const copy = copyAbortError(original, { message: 'Custom message', metadata: { retry: false } });
* console.log(copy.message); // 'Custom message'
* console.log(copy.type); // 'cancelled' (from original)
* console.log(copy.cause); // original (the original error)
*/
export const copyAbortError = (
abortError: AbortError,
override: Omit<{ [key in keyof AbortError]?: AbortError[key] }, (typeof CANNOT_BE_OVERRIDDEN)[any]> = {}
) => {
const foundOverriddenField = CANNOT_BE_OVERRIDDEN.find((key) => Object.prototype.hasOwnProperty.call(override, key));

if (foundOverriddenField) {
throw new TypeError(`The '${foundOverriddenField}' field cannot be overridden!`);
}

return new AbortError(override?.message ?? abortError.message, { ...abortError, ...override, cause: abortError });
};
Loading
Loading