saborter 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -5,41 +5,91 @@ const ABORT_ERROR_WITHOUT_REASON_MESSAGE = "signal is aborted without reason";
5
5
  const ABORT_ERROR_NAME = "AbortError";
6
6
  const ERROR_CAUSE_PATH_NAME = "cause.name";
7
7
  const ERROR_CAUSE_PATH_MESSAGE = "cause.message";
8
- const ABORT_ERROR_MESSAGES = [
9
- ABORT_ERROR_MESSAGE,
10
- ABORT_ERROR_WITHOUT_REASON_MESSAGE
11
- ];
12
- const get = (object, path) => path.split(".").reduce(
13
- (acc, key) => acc && acc[key],
14
- object
15
- );
16
- const isError = (error) => ABORT_ERROR_MESSAGES.includes(
17
- error?.message ?? ""
18
- ) || get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(
19
- get(error, ERROR_CAUSE_PATH_MESSAGE)
20
- );
8
+ const ABORT_ERROR_MESSAGES = [ABORT_ERROR_MESSAGE, ABORT_ERROR_WITHOUT_REASON_MESSAGE];
9
+ class AbortError extends Error {
10
+ constructor(message, options) {
11
+ super(message);
12
+ this.code = 20;
13
+ this.timestamp = Date.now();
14
+ this.name = ABORT_ERROR_NAME;
15
+ this.type = options?.type || "aborted";
16
+ this.reason = options?.reason;
17
+ this.signal = options?.signal;
18
+ }
19
+ }
20
+ const get = (object, path) => path.split(".").reduce((acc, key) => acc && acc[key], object);
21
+ const checkErrorCause = (error) => get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(get(error, ERROR_CAUSE_PATH_MESSAGE));
22
+ const isError = (error) => error instanceof AbortError || "name" in error && error.name === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(error?.message ?? "") || checkErrorCause(error);
23
+ const getCauseMessage = (error) => {
24
+ return get(error, ERROR_CAUSE_PATH_MESSAGE);
25
+ };
26
+ class EventListener {
27
+ constructor(options) {
28
+ this.listeners = {
29
+ aborted: /* @__PURE__ */ new Set(),
30
+ cancelled: /* @__PURE__ */ new Set()
31
+ };
32
+ this.getListenersByType = (type) => {
33
+ return this.listeners[type];
34
+ };
35
+ this.addEventListener = (type, listener) => {
36
+ this.getListenersByType(type).add(listener);
37
+ return () => this.removeEventListener(type, listener);
38
+ };
39
+ this.removeEventListener = (type, listener) => {
40
+ this.getListenersByType(type).delete(listener);
41
+ };
42
+ this.dispatchEvent = (type, event) => {
43
+ if (type === "aborted" || type === "cancelled") {
44
+ this.onabort?.(event);
45
+ }
46
+ this.getListenersByType(type).forEach((listener) => listener(event));
47
+ };
48
+ this.onabort = options?.onAbort;
49
+ }
50
+ }
21
51
  const _Aborter = class _Aborter {
22
- constructor() {
52
+ constructor(options) {
23
53
  this.abortController = new AbortController();
24
54
  this.try = (request, { isErrorNativeBehavior = false } = {}) => {
25
55
  let promise = new Promise((resolve, reject) => {
26
- this.abort();
27
- this.abortController = new AbortController();
28
- const { signal } = this.abortController;
56
+ const cancelledAbortError = new AbortError("cancellation of the previous AbortController", {
57
+ type: "cancelled",
58
+ signal: this.signal
59
+ });
60
+ this.listeners.dispatchEvent("cancelled", cancelledAbortError);
61
+ const { signal } = this.abortWithRecovery(cancelledAbortError);
29
62
  request(signal).then(resolve).catch((err) => {
30
63
  const error = {
31
64
  ...err,
32
- message: err?.message || get(err, ERROR_CAUSE_PATH_MESSAGE) || ""
65
+ message: err?.message || getCauseMessage(err) || ""
33
66
  };
34
67
  if (isErrorNativeBehavior || !_Aborter.isError(err)) {
35
68
  return reject(error);
36
69
  }
70
+ if (error?.type !== "cancelled") {
71
+ this.listeners.dispatchEvent(
72
+ "aborted",
73
+ new AbortError(error.message, {
74
+ signal: this.signal,
75
+ reason: get(error, "reason") || this.signal.reason
76
+ })
77
+ );
78
+ }
37
79
  promise = null;
38
80
  });
39
81
  });
40
82
  return promise;
41
83
  };
42
- this.abort = (reason) => this.abortController.abort(reason);
84
+ this.abort = (reason) => {
85
+ this.abortController.abort(reason);
86
+ };
87
+ this.abortWithRecovery = (reason) => {
88
+ this.abort(reason);
89
+ this.abortController = new AbortController();
90
+ return this.abortController;
91
+ };
92
+ this.listeners = new EventListener({ onAbort: options?.onAbort });
43
93
  }
44
94
  /**
45
95
  * Returns the AbortSignal object associated with this object.
@@ -51,5 +101,5 @@ const _Aborter = class _Aborter {
51
101
  _Aborter.errorName = ABORT_ERROR_NAME;
52
102
  _Aborter.isError = isError;
53
103
  let Aborter = _Aborter;
104
+ exports.AbortError = AbortError;
54
105
  exports.Aborter = Aborter;
55
- //# sourceMappingURL=index.cjs.js.map
package/dist/index.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  export declare class Aborter {
2
2
  protected abortController: AbortController;
3
+ /**
4
+ * Returns an `EventListener` instance to listen for `Aborter` events.
5
+ */
6
+ listeners: EventListener_2;
7
+ constructor(options?: Types_2.AborterOptions);
3
8
  /**
4
9
  * The name of the error instance thrown by the AbortSignal.
5
10
  * @readonly
11
+ * @deprecated use AbortError.name
6
12
  */
7
13
  static readonly errorName = "AbortError";
8
14
  /**
9
15
  * Method of checking whether an error is an error AbortError.
10
16
  * @returns boolean
11
17
  */
12
- static isError: (error: unknown) => error is Error;
18
+ static isError: (error: any) => error is Error;
13
19
  /**
14
20
  * Returns the AbortSignal object associated with this object.
15
21
  */
@@ -20,15 +26,92 @@ export declare class Aborter {
20
26
  * @param options an object that receives a set of settings for performing a request attempt
21
27
  * @returns Promise
22
28
  */
23
- try: <R>(request: Types.AbortRequest<R>, { isErrorNativeBehavior }?: Types.FnTryOptions) => Promise<R>;
29
+ try: <R>(request: Types_2.AbortRequest<R>, { isErrorNativeBehavior }?: Types_2.FnTryOptions) => Promise<R>;
24
30
  /**
25
31
  * Calling this method sets the AbortSignal flag of this object and signals all observers that the associated action should be aborted.
26
32
  */
27
33
  abort: (reason?: any) => void;
34
+ /**
35
+ * Calling this method sets the AbortSignal flag of this object and signals all observers that the associated action should be aborted.
36
+ * After aborting, it restores the AbortSignal, resetting the isAborted property, and interaction with the signal property becomes available again.
37
+ */
38
+ abortWithRecovery: (reason?: any) => AbortController;
39
+ }
40
+
41
+ declare interface AborterOptions extends Pick<EventListenerOptions_2, 'onAbort'> {
42
+ }
43
+
44
+ export declare class AbortError extends Error {
45
+ /**
46
+ * Interrupt error code.
47
+ * @readonly
48
+ */
49
+ readonly code: number;
50
+ /**
51
+ * Interrupt type 'cancelled' | 'aborted'.
52
+ * @default `aborted`
53
+ */
54
+ type: AbortErrorOptions['type'];
55
+ /**
56
+ *The timestamp in milliseconds when the error was created.
57
+ @readonly
58
+ @returns Date.now();
59
+ */
60
+ readonly timestamp: number;
61
+ /**
62
+ * Additional reason or data associated with the interrupt.
63
+ */
64
+ reason?: any;
65
+ /**
66
+ * AbortSignal that was just interrupted.
67
+ */
68
+ signal?: AbortSignal;
69
+ constructor(message: string, options?: AbortErrorOptions);
70
+ }
71
+
72
+ declare interface AbortErrorOptions {
73
+ type?: 'cancelled' | 'aborted';
74
+ reason?: any;
75
+ signal?: AbortSignal;
28
76
  }
29
77
 
30
78
  declare type AbortRequest<T> = (signal: AbortSignal) => Promise<T>;
31
79
 
80
+ declare type EventCallback<T extends EventListenerType> = EventMap[T] extends undefined ? () => void : (event: EventMap[T]) => void;
81
+
82
+ declare class EventListener_2 {
83
+ private listeners;
84
+ /**
85
+ * Method called when an Aborter request is cancelled
86
+ */
87
+ onabort?: Types.OnAbortCallback;
88
+ constructor(options?: Types.EventListenerOptions);
89
+ private getListenersByType;
90
+ /**
91
+ * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
92
+ */
93
+ addEventListener: <T extends Types.EventListenerType, L extends Types.EventCallback<T>>(type: T, listener: L) => VoidFunction;
94
+ /**
95
+ * Removes the event listener in target's event listener list with the same type and callback.
96
+ */
97
+ removeEventListener: <T extends Types.EventListenerType, L extends Types.EventCallback<T>>(type: T, listener: L) => void;
98
+ /**
99
+ * Dispatches a synthetic event event to target
100
+ */
101
+ dispatchEvent: <T extends Types.EventListenerType, E extends Types.EventMap[T]>(type: T, event: E) => void;
102
+ }
103
+
104
+ declare interface EventListenerOptions_2 {
105
+ onAbort?: OnAbortCallback;
106
+ }
107
+
108
+ declare type EventListenerType = keyof EventMap;
109
+
110
+ declare interface EventMap {
111
+ aborted: AbortError;
112
+ cancelled: AbortError;
113
+ }
114
+
32
115
  declare interface FnTryOptions {
33
116
  /**
34
117
  * Returns the ability to catch a canceled request error in a catch block.
@@ -37,10 +120,23 @@ declare interface FnTryOptions {
37
120
  isErrorNativeBehavior?: boolean;
38
121
  }
39
122
 
123
+ declare type OnAbortCallback = (error: AbortError) => void;
124
+
40
125
  declare namespace Types {
126
+ export {
127
+ EventMap,
128
+ EventListenerType,
129
+ EventCallback,
130
+ OnAbortCallback,
131
+ EventListenerOptions_2 as EventListenerOptions
132
+ }
133
+ }
134
+
135
+ declare namespace Types_2 {
41
136
  export {
42
137
  AbortRequest,
43
- FnTryOptions
138
+ FnTryOptions,
139
+ AborterOptions
44
140
  }
45
141
  }
46
142
 
package/dist/index.es.js CHANGED
@@ -3,41 +3,91 @@ const ABORT_ERROR_WITHOUT_REASON_MESSAGE = "signal is aborted without reason";
3
3
  const ABORT_ERROR_NAME = "AbortError";
4
4
  const ERROR_CAUSE_PATH_NAME = "cause.name";
5
5
  const ERROR_CAUSE_PATH_MESSAGE = "cause.message";
6
- const ABORT_ERROR_MESSAGES = [
7
- ABORT_ERROR_MESSAGE,
8
- ABORT_ERROR_WITHOUT_REASON_MESSAGE
9
- ];
10
- const get = (object, path) => path.split(".").reduce(
11
- (acc, key) => acc && acc[key],
12
- object
13
- );
14
- const isError = (error) => ABORT_ERROR_MESSAGES.includes(
15
- error?.message ?? ""
16
- ) || get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(
17
- get(error, ERROR_CAUSE_PATH_MESSAGE)
18
- );
6
+ const ABORT_ERROR_MESSAGES = [ABORT_ERROR_MESSAGE, ABORT_ERROR_WITHOUT_REASON_MESSAGE];
7
+ class AbortError extends Error {
8
+ constructor(message, options) {
9
+ super(message);
10
+ this.code = 20;
11
+ this.timestamp = Date.now();
12
+ this.name = ABORT_ERROR_NAME;
13
+ this.type = options?.type || "aborted";
14
+ this.reason = options?.reason;
15
+ this.signal = options?.signal;
16
+ }
17
+ }
18
+ const get = (object, path) => path.split(".").reduce((acc, key) => acc && acc[key], object);
19
+ const checkErrorCause = (error) => get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(get(error, ERROR_CAUSE_PATH_MESSAGE));
20
+ const isError = (error) => error instanceof AbortError || "name" in error && error.name === ABORT_ERROR_NAME || ABORT_ERROR_MESSAGES.includes(error?.message ?? "") || checkErrorCause(error);
21
+ const getCauseMessage = (error) => {
22
+ return get(error, ERROR_CAUSE_PATH_MESSAGE);
23
+ };
24
+ class EventListener {
25
+ constructor(options) {
26
+ this.listeners = {
27
+ aborted: /* @__PURE__ */ new Set(),
28
+ cancelled: /* @__PURE__ */ new Set()
29
+ };
30
+ this.getListenersByType = (type) => {
31
+ return this.listeners[type];
32
+ };
33
+ this.addEventListener = (type, listener) => {
34
+ this.getListenersByType(type).add(listener);
35
+ return () => this.removeEventListener(type, listener);
36
+ };
37
+ this.removeEventListener = (type, listener) => {
38
+ this.getListenersByType(type).delete(listener);
39
+ };
40
+ this.dispatchEvent = (type, event) => {
41
+ if (type === "aborted" || type === "cancelled") {
42
+ this.onabort?.(event);
43
+ }
44
+ this.getListenersByType(type).forEach((listener) => listener(event));
45
+ };
46
+ this.onabort = options?.onAbort;
47
+ }
48
+ }
19
49
  const _Aborter = class _Aborter {
20
- constructor() {
50
+ constructor(options) {
21
51
  this.abortController = new AbortController();
22
52
  this.try = (request, { isErrorNativeBehavior = false } = {}) => {
23
53
  let promise = new Promise((resolve, reject) => {
24
- this.abort();
25
- this.abortController = new AbortController();
26
- const { signal } = this.abortController;
54
+ const cancelledAbortError = new AbortError("cancellation of the previous AbortController", {
55
+ type: "cancelled",
56
+ signal: this.signal
57
+ });
58
+ this.listeners.dispatchEvent("cancelled", cancelledAbortError);
59
+ const { signal } = this.abortWithRecovery(cancelledAbortError);
27
60
  request(signal).then(resolve).catch((err) => {
28
61
  const error = {
29
62
  ...err,
30
- message: err?.message || get(err, ERROR_CAUSE_PATH_MESSAGE) || ""
63
+ message: err?.message || getCauseMessage(err) || ""
31
64
  };
32
65
  if (isErrorNativeBehavior || !_Aborter.isError(err)) {
33
66
  return reject(error);
34
67
  }
68
+ if (error?.type !== "cancelled") {
69
+ this.listeners.dispatchEvent(
70
+ "aborted",
71
+ new AbortError(error.message, {
72
+ signal: this.signal,
73
+ reason: get(error, "reason") || this.signal.reason
74
+ })
75
+ );
76
+ }
35
77
  promise = null;
36
78
  });
37
79
  });
38
80
  return promise;
39
81
  };
40
- this.abort = (reason) => this.abortController.abort(reason);
82
+ this.abort = (reason) => {
83
+ this.abortController.abort(reason);
84
+ };
85
+ this.abortWithRecovery = (reason) => {
86
+ this.abort(reason);
87
+ this.abortController = new AbortController();
88
+ return this.abortController;
89
+ };
90
+ this.listeners = new EventListener({ onAbort: options?.onAbort });
41
91
  }
42
92
  /**
43
93
  * Returns the AbortSignal object associated with this object.
@@ -50,6 +100,6 @@ _Aborter.errorName = ABORT_ERROR_NAME;
50
100
  _Aborter.isError = isError;
51
101
  let Aborter = _Aborter;
52
102
  export {
103
+ AbortError,
53
104
  Aborter
54
105
  };
55
- //# sourceMappingURL=index.es.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saborter",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A simple and efficient library for canceling asynchronous requests using AbortController",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -37,23 +37,48 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "prepublishOnly": "npm run build",
40
- "build": "tsc && vite build",
41
- "test": "jest --colors --coverage test --passWithNoTests"
40
+ "build": "npm run clean:dist && tsc && vite build",
41
+ "clean:dist": "rimraf dist",
42
+ "typecheck": "tsc --pretty --noEmit --skipLibCheck",
43
+ "verify:prettier": "npx prettier . --check",
44
+ "verify:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --max-warnings=0",
45
+ "verify": "npm run typecheck && npm run verify:prettier && npm run verify:eslint",
46
+ "fix:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --fix",
47
+ "fix:prettier": "npx prettier --write .",
48
+ "fix": "npm run fix:eslint && npm run fix:prettier",
49
+ "test": "jest --colors --coverage test --passWithNoTests",
50
+ "prepare": "husky"
42
51
  },
43
52
  "devDependencies": {
44
53
  "@types/jest": "^30.0.0",
45
54
  "@types/node": "^25.0.3",
46
55
  "@typescript-eslint/eslint-plugin": "^8.50.1",
47
56
  "@typescript-eslint/parser": "^8.50.1",
48
- "eslint": "^9.39.2",
57
+ "eslint": "^8.11.0",
58
+ "eslint-config-airbnb": "^19.0.4",
59
+ "eslint-config-prettier": "^10.1.8",
60
+ "eslint-plugin-import": "^2.32.0",
61
+ "eslint-plugin-prettier": "^5.5.4",
62
+ "husky": "^9.1.7",
49
63
  "jest": "^30.2.0",
50
64
  "jest-environment-jsdom": "^30.2.0",
65
+ "lint-staged": "^16.2.7",
51
66
  "prettier": "^3.7.4",
67
+ "rimraf": "^6.1.2",
52
68
  "ts-jest": "^29.4.6",
53
69
  "ts-node": "^10.9.2",
54
70
  "typescript": "^5.9.3",
55
71
  "vite": "^7.3.0",
72
+ "vite-plugin-circular-dependency": "^0.5.0",
56
73
  "vite-plugin-clean": "^2.0.1",
57
74
  "vite-plugin-dts": "^4.5.4"
75
+ },
76
+ "lint-staged": {
77
+ "*.{js,jsx,ts,tsx}": [
78
+ "npm run fix:prettier"
79
+ ],
80
+ "*.{json,md}": [
81
+ "npm run fix:prettier"
82
+ ]
58
83
  }
59
84
  }
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
- # Saborter
1
+ ![Logo](./assets/logo.png)
2
2
 
3
- [![Npm package](https://img.shields.io/badge/npm%20package-1.2.0-red)](https://www.npmjs.com/package/saborter)
3
+ [![Npm package](https://img.shields.io/badge/npm%20package-1.3.0-red)](https://www.npmjs.com/package/saborter)
4
4
  ![Static Badge](https://img.shields.io/badge/coverage-100%25-orange)
5
5
  ![Static Badge](https://img.shields.io/badge/license-MIT-blue)
6
6
  [![Github](https://img.shields.io/badge/repository-github-color)](https://github.com/TENSIILE/saborter)
@@ -28,14 +28,10 @@ const aborter = new Aborter();
28
28
  // Use for the request
29
29
  async function fetchData() {
30
30
  try {
31
- const result = await aborter.try(signal => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
31
+ const result = await aborter.try((signal) => fetch('/api/data', { signal }));
32
32
  console.log('Data received:', result);
33
33
  } catch (error) {
34
- if (error.name === 'AbortError') {
35
- console.log('The request was canceled');
36
- } else {
37
- console.error('Request error:', error);
38
- }
34
+ console.error('Request error:', error);
39
35
  }
40
36
  }
41
37
  ```
@@ -50,7 +46,7 @@ The `Aborter` class allows you to easily cancel ongoing requests:
50
46
  const aborter = new Aborter();
51
47
 
52
48
  // Start a long-running request
53
- const longRequest = aborter.try(signal => fetch('/api/long-task', { signal }));
49
+ const longRequest = aborter.try((signal) => fetch('/api/long-task', { signal }));
54
50
 
55
51
  // Cancel the request after 2 seconds
56
52
  setTimeout(() => {
@@ -61,13 +57,13 @@ setTimeout(() => {
61
57
 
62
58
  ### 2. Automatically canceling previous requests
63
59
 
64
- Each time **try** is called, the previous request is automatically canceled:
60
+ Each time `try()` is called, the previous request is automatically canceled:
65
61
 
66
62
  ```javascript
67
63
  // When searching with autocomplete
68
64
  async function handleSearch(query) {
69
65
  // The previous request is automatically canceled
70
- const results = await aborter.try(signal => fetch(`/api/search?q=${query}`, { signal }));
66
+ const results = await aborter.try((signal) => fetch(`/api/search?q=${query}`, { signal }));
71
67
  return results;
72
68
  }
73
69
 
@@ -88,12 +84,12 @@ const dataAborter = new Aborter();
88
84
 
89
85
  // Manage user requests separately
90
86
  async function fetchUser(id) {
91
- return userAborter.try(signal => fetch(`/api/users/${id}`, { signal }));
87
+ return userAborter.try((signal) => fetch(`/api/users/${id}`, { signal }));
92
88
  }
93
89
 
94
90
  // And manage data separately
95
91
  async function fetchData(params) {
96
- return dataAborter.try(signal => fetch('/api/data', { signal, ...params }));
92
+ return dataAborter.try((signal) => fetch('/api/data', { signal, ...params }));
97
93
  }
98
94
 
99
95
  // Cancel only user requests
@@ -106,11 +102,28 @@ function cancelUserRequests() {
106
102
 
107
103
  ### Constructor
108
104
 
109
- ```javascript
110
- new Aborter();
105
+ ```typescript
106
+ const aborter = new Aborter(options?: AborterOptions);
111
107
  ```
112
108
 
113
- Creates a new `Aborter` instance. Takes no parameters.
109
+ ### Constructor Parameters
110
+
111
+ | Parameter | Type | Description | Required |
112
+ | --------- | ---------------- | ----------------------------- | -------- |
113
+ | `options` | `AborterOptions` | Aborter configuration options | No |
114
+
115
+ **AborterOptions:**
116
+
117
+ ```typescript
118
+ {
119
+ /*
120
+ Callback function for abort events.
121
+ Associated with EventListener.onabort.
122
+ It can be overridden via `aborter.listeners.onabort`
123
+ */
124
+ onAbort?: OnAbortCallback;
125
+ }
126
+ ```
114
127
 
115
128
  ### Properties
116
129
 
@@ -123,10 +136,32 @@ const aborter = new Aborter();
123
136
 
124
137
  // Using signal in the request
125
138
  fetch('/api/data', {
126
- signal: aborter.signal,
139
+ signal: aborter.signal
127
140
  });
128
141
  ```
129
142
 
143
+ `listeners`
144
+
145
+ Returns an `EventListener` object to listen for `Aborter` events.
146
+
147
+ [Detailed documentation here](./docs/event-listener.md)
148
+
149
+ ⚠️ `static errorName`
150
+
151
+ ⚠️ `[DEPRECATED]:` Use `AbortError.name`.
152
+
153
+ Name of the `AbortError` error instance thrown by AbortSignal.
154
+
155
+ ```javascript
156
+ const result = await aborter
157
+ .try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
158
+ .catch((error) => {
159
+ if (error.name === AbortError.name) {
160
+ console.log('Canceled');
161
+ }
162
+ });
163
+ ```
164
+
130
165
  ### Methods
131
166
 
132
167
  `try(request, options?)`
@@ -137,7 +172,7 @@ Executes an asynchronous request with the ability to cancel.
137
172
 
138
173
  - `request: (signal: AbortSignal) => Promise<T>` - the function that fulfills the request
139
174
  - `options?: Object` (optional)
140
- - `isErrorNativeBehavior: boolean` - a flag for controlling error handling
175
+ - `isErrorNativeBehavior?: boolean` - a flag for controlling error handling. Default is `false`
141
176
 
142
177
  **Returns:** `Promise<T>`
143
178
 
@@ -145,12 +180,12 @@ Executes an asynchronous request with the ability to cancel.
145
180
 
146
181
  ```javascript
147
182
  // Simple request
148
- const result = await aborter.try(signal => {
149
- return fetch('/api/data', { signal }).then(response => response.json());
183
+ const result = await aborter.try((signal) => {
184
+ return fetch('/api/data', { signal }).then((response) => response.json());
150
185
  });
151
186
 
152
187
  // With custom request logic
153
- const result = await aborter.try(async signal => {
188
+ const result = await aborter.try(async (signal) => {
154
189
  const response = await fetch('/api/data', { signal });
155
190
  if (!response.ok) {
156
191
  throw new Error('Server Error');
@@ -169,26 +204,61 @@ Immediately cancels the currently executing request.
169
204
 
170
205
  ```javascript
171
206
  // Start the request
172
- const requestPromise = aborter.try(signal => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
207
+ const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
173
208
 
174
209
  // Cancel
175
210
  aborter.abort();
176
211
 
177
212
  // Handle cancellation
178
- requestPromise.catch(error => {
213
+ requestPromise.catch((error) => {
179
214
  if (error.name === 'AbortError') {
180
215
  console.log('Request canceled');
181
216
  }
182
217
  });
183
218
  ```
184
219
 
220
+ `abortWithRecovery(reason?)`
221
+
222
+ Immediately cancels the currently executing request.
223
+ After aborting, it restores the `AbortSignal`, resetting the `isAborted` property, and interaction with the `signal` property becomes available again.
224
+
225
+ **Parameters:**
226
+
227
+ - `reason?: any` - the reason for aborting the request (optional)
228
+
229
+ **Returns:** `AbortController`
230
+
231
+ ```javascript
232
+ // Create an Aborter instance
233
+ const aborter = new Aborter();
234
+
235
+ // Data retrieval function
236
+ async function fetchData() {
237
+ try {
238
+ const data = await fetch('/api/data', { signal: aborter.signal });
239
+ } catch (error) {
240
+ // Any error, except AbortError, will go here
241
+ console.log(error);
242
+ }
243
+ }
244
+
245
+ // Calling a function with a request
246
+ fetchData();
247
+
248
+ // We interrupt the request and then restore the signal
249
+ aborter.abortWithRecovery();
250
+
251
+ // Call the function again
252
+ fetchData();
253
+ ```
254
+
185
255
  `static isError(error)`
186
256
 
187
257
  Static method for checking if an object is an `AbortError` error.
188
258
 
189
259
  ```javascript
190
260
  try {
191
- await aborter.try(signal => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
261
+ await aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
192
262
  } catch (error) {
193
263
  if (Aborter.isError(error)) {
194
264
  console.log('This is a cancellation error');
@@ -198,20 +268,80 @@ try {
198
268
  }
199
269
  ```
200
270
 
201
- `static errorName`
271
+ ## 🔌 Additional APIs
202
272
 
203
- Name of the `AbortError` error instance thrown by AbortSignal.
273
+ - [**AbortError**](./docs/abort-error.md) - Custom error for working with Aborter.
274
+
275
+ ## ⚠️ Important Features
276
+
277
+ ### Error Handling
278
+
279
+ By default, the `try()` method does not reject the promise on `AbortError` (cancellation error). This prevents the `catch` block from being called when the request is canceled.
280
+
281
+ If you want the default behavior (the promise to be rejected on any error), use the `isErrorNativeBehavior` option:
204
282
 
205
283
  ```javascript
284
+ // The promise will be rejected even if an AbortError occurs
206
285
  const result = await aborter
207
- .try(signal => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
208
- .catch(error => {
209
- if (error.name === Aborter.errorName) {
210
- console.log('Canceled');
286
+ .try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
287
+ .catch((error) => {
288
+ // ALL errors, including cancellations, will go here
289
+ if (error.name === 'AbortError') {
290
+ console.log('Cancelled');
211
291
  }
212
292
  });
213
293
  ```
214
294
 
295
+ ### Finally block
296
+
297
+ By ignoring `AbortError` errors, the `finally` block will only be executed if other errors are received or if the request is successful.
298
+
299
+ ```javascript
300
+ const result = await aborter
301
+ .try((signal) => fetch('/api/data', { signal }))
302
+ .catch((error) => {
303
+ // Any error, except AbortError, will go here
304
+ console.log(error);
305
+ })
306
+ .finally(() => {
307
+ // The request was successfully completed or we caught a "throw"
308
+ });
309
+ ```
310
+
311
+ Everything will also work if you use the `try-catch` syntax.
312
+
313
+ ```javascript
314
+ try {
315
+ const result = await aborter.try((signal) => fetch('/api/data', { signal }));
316
+ } catch (error) {
317
+ // Any error, except AbortError, will go here
318
+ console.log(error);
319
+ } finally {
320
+ // The request was successfully completed or we caught a "throw"
321
+ }
322
+ ```
323
+
324
+ #### ⚠️ Attention!
325
+
326
+ With the `isErrorNativeBehavior` flag enabled, the `finally` block will also be executed.
327
+
328
+ ### Resource Cleanup
329
+
330
+ Always abort requests when unmounting components or closing pages:
331
+
332
+ ```javascript
333
+ // In React
334
+ useEffect(() => {
335
+ const aborter = new Aborter();
336
+
337
+ // Make requests
338
+
339
+ return () => {
340
+ aborter.abort(); // Clean up on unmount
341
+ };
342
+ }, []);
343
+ ```
344
+
215
345
  ## 🎯 Usage Examples
216
346
 
217
347
  ### Example 1: Autocomplete
@@ -224,7 +354,7 @@ class SearchAutocomplete {
224
354
  if (!query.trim()) return [];
225
355
 
226
356
  try {
227
- const results = await this.aborter.try(async signal => {
357
+ const results = await this.aborter.try(async (signal) => {
228
358
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
229
359
 
230
360
  return response.json();
@@ -258,11 +388,11 @@ class FileUploader {
258
388
 
259
389
  try {
260
390
  await this.aborter.try(
261
- async signal => {
391
+ async (signal) => {
262
392
  const response = await fetch('/api/upload', {
263
393
  method: 'POST',
264
394
  body: formData,
265
- signal,
395
+ signal
266
396
  });
267
397
 
268
398
  // Track progress
@@ -278,7 +408,7 @@ class FileUploader {
278
408
  this.progress = Math.round((receivedLength / contentLength) * 100);
279
409
  }
280
410
  },
281
- { isErrorNativeBehavior: true },
411
+ { isErrorNativeBehavior: true }
282
412
  );
283
413
 
284
414
  console.log('File uploaded successfully');
@@ -319,7 +449,7 @@ function DataFetcher({ url }) {
319
449
  const fetchData = async () => {
320
450
  setLoading(true);
321
451
  try {
322
- const result = await aborterRef.current.try(async signal => {
452
+ const result = await aborterRef.current.try(async (signal) => {
323
453
  const response = await fetch(url, { signal });
324
454
  return response.json();
325
455
  });
@@ -361,7 +491,7 @@ export default {
361
491
  return {
362
492
  aborter: null,
363
493
  data: null,
364
- loading: false,
494
+ loading: false
365
495
  };
366
496
  },
367
497
  created() {
@@ -374,7 +504,7 @@ export default {
374
504
  async fetchData() {
375
505
  this.loading = true;
376
506
  try {
377
- this.data = await this.aborter.try(async signal => {
507
+ this.data = await this.aborter.try(async (signal) => {
378
508
  const response = await fetch(this.url, { signal });
379
509
  return response.json();
380
510
  });
@@ -386,46 +516,9 @@ export default {
386
516
  },
387
517
  cancelRequest() {
388
518
  this.aborter.abort();
389
- },
390
- },
391
- };
392
- ```
393
-
394
- ## ⚠️ Important Features
395
-
396
- ### Error Handling
397
-
398
- By default, the `try()` method does not reject the promise on `AbortError` (cancellation error). This prevents the `catch` block from being called when the request is canceled.
399
-
400
- If you want the default behavior (the promise to be rejected on any error), use the `isErrorNativeBehavior` option:
401
-
402
- ```javascript
403
- // The promise will be rejected even if an AbortError occurs
404
- const result = await aborter
405
- .try(signal => fetch('/api/data', { signal }), { isErrorNativeBehavior: true })
406
- .catch(error => {
407
- // ALL errors, including cancellations, will go here
408
- if (error.name === 'AbortError') {
409
- console.log('Cancelled');
410
519
  }
411
- });
412
- ```
413
-
414
- ### Resource Cleanup
415
-
416
- Always abort requests when unmounting components or closing pages:
417
-
418
- ```javascript
419
- // In React
420
- useEffect(() => {
421
- const aborter = new Aborter();
422
-
423
- // Make requests
424
-
425
- return () => {
426
- aborter.abort(); // Clean up on unmount
427
- };
428
- }, []);
520
+ }
521
+ };
429
522
  ```
430
523
 
431
524
  ## 💻 Compatibility
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../src/constants/constants.ts","../src/utils/utils.ts","../src/aborter.ts"],"sourcesContent":["const ABORT_ERROR_MESSAGE = 'The user aborted a request.';\nconst ABORT_ERROR_WITHOUT_REASON_MESSAGE = 'signal is aborted without reason';\nexport const ABORT_ERROR_NAME = 'AbortError';\nexport const ERROR_CAUSE_PATH_NAME = 'cause.name';\nexport const ERROR_CAUSE_PATH_MESSAGE = 'cause.message';\nexport const ABORT_ERROR_MESSAGES = [\n ABORT_ERROR_MESSAGE,\n ABORT_ERROR_WITHOUT_REASON_MESSAGE,\n];\n","import * as Constants from '../constants';\n\nexport const get = <T, R>(object: T, path: string) =>\n path\n .split('.')\n .reduce(\n (acc, key) => acc && (acc as Record<string, any>)[key],\n object\n ) as unknown as R;\n\nexport const isError = (error: unknown): error is Error =>\n Constants.ABORT_ERROR_MESSAGES.includes(\n (error as Error | undefined)?.message ?? ''\n ) ||\n get(error, Constants.ERROR_CAUSE_PATH_NAME) === Constants.ABORT_ERROR_NAME ||\n Constants.ABORT_ERROR_MESSAGES.includes(\n get(error, Constants.ERROR_CAUSE_PATH_MESSAGE)\n );\n","import * as Utils from './utils';\nimport * as Constants from './constants';\nimport * as Types from './types';\n\nexport class Aborter {\n protected abortController = new AbortController();\n\n /**\n * The name of the error instance thrown by the AbortSignal.\n * @readonly\n */\n public static readonly errorName = Constants.ABORT_ERROR_NAME;\n /**\n * Method of checking whether an error is an error AbortError.\n * @returns boolean\n */\n public static isError = Utils.isError;\n\n /**\n * Returns the AbortSignal object associated with this object.\n */\n public get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /**\n * Performs an asynchronous request with cancellation of the previous request, preventing the call of the catch block when the request is canceled and the subsequent finally block.\n * @param request callback function\n * @param options an object that receives a set of settings for performing a request attempt\n * @returns Promise\n */\n public try = <R>(\n request: Types.AbortRequest<R>,\n { isErrorNativeBehavior = false }: Types.FnTryOptions = {},\n ): Promise<R> => {\n let promise: Promise<R> | null = new Promise<R>((resolve, reject) => {\n this.abort();\n\n this.abortController = new AbortController();\n\n const { signal } = this.abortController;\n\n request(signal)\n .then(resolve)\n .catch((err: Error) => {\n const error: Error = {\n ...err,\n message: err?.message || Utils.get(err, Constants.ERROR_CAUSE_PATH_MESSAGE) || '',\n };\n\n if (isErrorNativeBehavior || !Aborter.isError(err)) {\n return reject(error);\n }\n\n promise = null;\n });\n });\n\n return promise;\n };\n\n /**\n * Calling this method sets the AbortSignal flag of this object and signals all observers that the associated action should be aborted.\n */\n public abort = (reason?: any) => this.abortController.abort(reason);\n}\n"],"names":["Constants.ABORT_ERROR_MESSAGES","Constants.ERROR_CAUSE_PATH_NAME","Constants.ABORT_ERROR_NAME","Constants.ERROR_CAUSE_PATH_MESSAGE","Utils.get","Utils.isError"],"mappings":";;AAAA,MAAM,sBAAsB;AAC5B,MAAM,qCAAqC;AACpC,MAAM,mBAAmB;AACzB,MAAM,wBAAwB;AAC9B,MAAM,2BAA2B;AACjC,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AACF;ACNO,MAAM,MAAM,CAAO,QAAW,SACnC,KACG,MAAM,GAAG,EACT;AAAA,EACC,CAAC,KAAK,QAAQ,OAAQ,IAA4B,GAAG;AAAA,EACrD;AACF;AAEG,MAAM,UAAU,CAAC,UACtBA,qBAA+B;AAAA,EAC5B,OAA6B,WAAW;AAC3C,KACA,IAAI,OAAOC,qBAA+B,MAAMC,oBAChDF,qBAA+B;AAAA,EAC7B,IAAI,OAAOG,wBAAkC;AAC/C;ACbK,MAAM,WAAN,MAAM,SAAQ;AAAA,EAAd,cAAA;AACL,SAAU,kBAAkB,IAAI,gBAAA;AA0BhC,SAAO,MAAM,CACX,SACA,EAAE,wBAAwB,MAAA,IAA8B,OACzC;AACf,UAAI,UAA6B,IAAI,QAAW,CAAC,SAAS,WAAW;AACnE,aAAK,MAAA;AAEL,aAAK,kBAAkB,IAAI,gBAAA;AAE3B,cAAM,EAAE,WAAW,KAAK;AAExB,gBAAQ,MAAM,EACX,KAAK,OAAO,EACZ,MAAM,CAAC,QAAe;AACrB,gBAAM,QAAe;AAAA,YACnB,GAAG;AAAA,YACH,SAAS,KAAK,WAAWC,IAAU,KAAKD,wBAAkC,KAAK;AAAA,UAAA;AAGjF,cAAI,yBAAyB,CAAC,SAAQ,QAAQ,GAAG,GAAG;AAClD,mBAAO,OAAO,KAAK;AAAA,UACrB;AAEA,oBAAU;AAAA,QACZ,CAAC;AAAA,MACL,CAAC;AAED,aAAO;AAAA,IACT;AAKA,SAAO,QAAQ,CAAC,WAAiB,KAAK,gBAAgB,MAAM,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EA3ClE,IAAW,SAAsB;AAC/B,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AA0CF;AAtDE,SAAuB,YAAYD;AAKnC,SAAc,UAAUG;AAZnB,IAAM,UAAN;;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.es.js","sources":["../src/constants/constants.ts","../src/utils/utils.ts","../src/aborter.ts"],"sourcesContent":["const ABORT_ERROR_MESSAGE = 'The user aborted a request.';\nconst ABORT_ERROR_WITHOUT_REASON_MESSAGE = 'signal is aborted without reason';\nexport const ABORT_ERROR_NAME = 'AbortError';\nexport const ERROR_CAUSE_PATH_NAME = 'cause.name';\nexport const ERROR_CAUSE_PATH_MESSAGE = 'cause.message';\nexport const ABORT_ERROR_MESSAGES = [\n ABORT_ERROR_MESSAGE,\n ABORT_ERROR_WITHOUT_REASON_MESSAGE,\n];\n","import * as Constants from '../constants';\n\nexport const get = <T, R>(object: T, path: string) =>\n path\n .split('.')\n .reduce(\n (acc, key) => acc && (acc as Record<string, any>)[key],\n object\n ) as unknown as R;\n\nexport const isError = (error: unknown): error is Error =>\n Constants.ABORT_ERROR_MESSAGES.includes(\n (error as Error | undefined)?.message ?? ''\n ) ||\n get(error, Constants.ERROR_CAUSE_PATH_NAME) === Constants.ABORT_ERROR_NAME ||\n Constants.ABORT_ERROR_MESSAGES.includes(\n get(error, Constants.ERROR_CAUSE_PATH_MESSAGE)\n );\n","import * as Utils from './utils';\nimport * as Constants from './constants';\nimport * as Types from './types';\n\nexport class Aborter {\n protected abortController = new AbortController();\n\n /**\n * The name of the error instance thrown by the AbortSignal.\n * @readonly\n */\n public static readonly errorName = Constants.ABORT_ERROR_NAME;\n /**\n * Method of checking whether an error is an error AbortError.\n * @returns boolean\n */\n public static isError = Utils.isError;\n\n /**\n * Returns the AbortSignal object associated with this object.\n */\n public get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /**\n * Performs an asynchronous request with cancellation of the previous request, preventing the call of the catch block when the request is canceled and the subsequent finally block.\n * @param request callback function\n * @param options an object that receives a set of settings for performing a request attempt\n * @returns Promise\n */\n public try = <R>(\n request: Types.AbortRequest<R>,\n { isErrorNativeBehavior = false }: Types.FnTryOptions = {},\n ): Promise<R> => {\n let promise: Promise<R> | null = new Promise<R>((resolve, reject) => {\n this.abort();\n\n this.abortController = new AbortController();\n\n const { signal } = this.abortController;\n\n request(signal)\n .then(resolve)\n .catch((err: Error) => {\n const error: Error = {\n ...err,\n message: err?.message || Utils.get(err, Constants.ERROR_CAUSE_PATH_MESSAGE) || '',\n };\n\n if (isErrorNativeBehavior || !Aborter.isError(err)) {\n return reject(error);\n }\n\n promise = null;\n });\n });\n\n return promise;\n };\n\n /**\n * Calling this method sets the AbortSignal flag of this object and signals all observers that the associated action should be aborted.\n */\n public abort = (reason?: any) => this.abortController.abort(reason);\n}\n"],"names":["Constants.ABORT_ERROR_MESSAGES","Constants.ERROR_CAUSE_PATH_NAME","Constants.ABORT_ERROR_NAME","Constants.ERROR_CAUSE_PATH_MESSAGE","Utils.get","Utils.isError"],"mappings":"AAAA,MAAM,sBAAsB;AAC5B,MAAM,qCAAqC;AACpC,MAAM,mBAAmB;AACzB,MAAM,wBAAwB;AAC9B,MAAM,2BAA2B;AACjC,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AACF;ACNO,MAAM,MAAM,CAAO,QAAW,SACnC,KACG,MAAM,GAAG,EACT;AAAA,EACC,CAAC,KAAK,QAAQ,OAAQ,IAA4B,GAAG;AAAA,EACrD;AACF;AAEG,MAAM,UAAU,CAAC,UACtBA,qBAA+B;AAAA,EAC5B,OAA6B,WAAW;AAC3C,KACA,IAAI,OAAOC,qBAA+B,MAAMC,oBAChDF,qBAA+B;AAAA,EAC7B,IAAI,OAAOG,wBAAkC;AAC/C;ACbK,MAAM,WAAN,MAAM,SAAQ;AAAA,EAAd,cAAA;AACL,SAAU,kBAAkB,IAAI,gBAAA;AA0BhC,SAAO,MAAM,CACX,SACA,EAAE,wBAAwB,MAAA,IAA8B,OACzC;AACf,UAAI,UAA6B,IAAI,QAAW,CAAC,SAAS,WAAW;AACnE,aAAK,MAAA;AAEL,aAAK,kBAAkB,IAAI,gBAAA;AAE3B,cAAM,EAAE,WAAW,KAAK;AAExB,gBAAQ,MAAM,EACX,KAAK,OAAO,EACZ,MAAM,CAAC,QAAe;AACrB,gBAAM,QAAe;AAAA,YACnB,GAAG;AAAA,YACH,SAAS,KAAK,WAAWC,IAAU,KAAKD,wBAAkC,KAAK;AAAA,UAAA;AAGjF,cAAI,yBAAyB,CAAC,SAAQ,QAAQ,GAAG,GAAG;AAClD,mBAAO,OAAO,KAAK;AAAA,UACrB;AAEA,oBAAU;AAAA,QACZ,CAAC;AAAA,MACL,CAAC;AAED,aAAO;AAAA,IACT;AAKA,SAAO,QAAQ,CAAC,WAAiB,KAAK,gBAAgB,MAAM,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EA3ClE,IAAW,SAAsB;AAC/B,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AA0CF;AAtDE,SAAuB,YAAYD;AAKnC,SAAc,UAAUG;AAZnB,IAAM,UAAN;"}