saborter 1.4.2 → 1.5.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/LICENSE +1 -1
- package/dist/index.cjs.js +105 -45
- package/dist/index.d.ts +53 -18
- package/dist/index.es.js +105 -45
- package/package.json +4 -2
- package/readme.md +163 -70
package/LICENSE
CHANGED
package/dist/index.cjs.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const emitMethodSymbol = /* @__PURE__ */ Symbol("state-observer::emit()");
|
|
4
|
+
const clearMethodSymbol$1 = /* @__PURE__ */ Symbol("state-observer::clear()");
|
|
5
5
|
var _a$1, _b;
|
|
6
|
-
_b =
|
|
6
|
+
_b = emitMethodSymbol, _a$1 = clearMethodSymbol$1;
|
|
7
7
|
class StateObserver {
|
|
8
8
|
constructor(options) {
|
|
9
9
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
@@ -22,7 +22,7 @@ class StateObserver {
|
|
|
22
22
|
this.onstatechange?.(state);
|
|
23
23
|
};
|
|
24
24
|
this[_a$1] = () => {
|
|
25
|
-
this.subscribers
|
|
25
|
+
this.subscribers.clear();
|
|
26
26
|
this.onstatechange = void 0;
|
|
27
27
|
this.value = void 0;
|
|
28
28
|
};
|
|
@@ -30,15 +30,31 @@ class StateObserver {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
const emitRequestState = (instance, requestState) => {
|
|
33
|
-
instance[
|
|
33
|
+
instance[emitMethodSymbol](requestState);
|
|
34
34
|
};
|
|
35
35
|
const clearStateListeners = (instance) => {
|
|
36
|
-
instance[
|
|
36
|
+
instance[clearMethodSymbol$1]();
|
|
37
37
|
};
|
|
38
|
+
const EXTENDED_STACK_ADDITIONAL_INFO_TEXT = "Debug Info";
|
|
39
|
+
const getDebugStackInfo = (info) => {
|
|
40
|
+
return `
|
|
41
|
+
|
|
42
|
+
${EXTENDED_STACK_ADDITIONAL_INFO_TEXT}: ${JSON.stringify(info, null, 2)}
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
};
|
|
46
|
+
class ExtendedStackError extends Error {
|
|
47
|
+
constructor() {
|
|
48
|
+
super(...arguments);
|
|
49
|
+
this.expandStack = () => {
|
|
50
|
+
this.stack += getDebugStackInfo(this.debugStackInfo);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
38
54
|
const ABORT_ERROR_NAME = "AbortError";
|
|
39
55
|
const ERROR_CAUSE_PATH_NAME = "cause.name";
|
|
40
56
|
const ERROR_CAUSE_PATH_MESSAGE = "cause.message";
|
|
41
|
-
class AbortError extends
|
|
57
|
+
class AbortError extends ExtendedStackError {
|
|
42
58
|
constructor(message, options) {
|
|
43
59
|
super(message);
|
|
44
60
|
this.code = 20;
|
|
@@ -49,6 +65,15 @@ class AbortError extends Error {
|
|
|
49
65
|
this.signal = options?.signal;
|
|
50
66
|
this.cause = options?.cause;
|
|
51
67
|
this.initiator = options?.initiator || "user";
|
|
68
|
+
this.expandStack();
|
|
69
|
+
}
|
|
70
|
+
get debugStackInfo() {
|
|
71
|
+
return {
|
|
72
|
+
createdAt: new Date(this.timestamp).toISOString(),
|
|
73
|
+
reason: this.reason,
|
|
74
|
+
type: this.type,
|
|
75
|
+
initiator: this.initiator
|
|
76
|
+
};
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
79
|
const get = (object, path) => path.split(".").reduce((acc, key) => acc && acc[key], object);
|
|
@@ -57,37 +82,9 @@ const isObject = (value) => {
|
|
|
57
82
|
};
|
|
58
83
|
const checkErrorCause = (error) => get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || "abort".includes(get(error, ERROR_CAUSE_PATH_MESSAGE));
|
|
59
84
|
const isError = (error) => error instanceof AbortError || isObject(error) && "name" in error && error.name === ABORT_ERROR_NAME || "abort".includes(error?.message ?? "") || checkErrorCause(error);
|
|
60
|
-
|
|
61
|
-
constructor() {
|
|
62
|
-
this.setTimeout = (timeout, onAbort) => {
|
|
63
|
-
this.clearTimeout();
|
|
64
|
-
if (!timeout || timeout <= 0) return;
|
|
65
|
-
this.timeoutId = setTimeout(onAbort, timeout);
|
|
66
|
-
};
|
|
67
|
-
this.clearTimeout = () => {
|
|
68
|
-
if (this.timeoutId) {
|
|
69
|
-
clearTimeout(this.timeoutId);
|
|
70
|
-
this.timeoutId = void 0;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
class TimeoutError extends Error {
|
|
76
|
-
constructor(message, options) {
|
|
77
|
-
super(message);
|
|
78
|
-
this.timestamp = Date.now();
|
|
79
|
-
this.ms = options?.ms;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
var ErrorMessage = /* @__PURE__ */ ((ErrorMessage2) => {
|
|
83
|
-
ErrorMessage2["AbortedSignalWithoutMessage"] = "signal is aborted without message";
|
|
84
|
-
ErrorMessage2["RequestTimedout"] = "the request timed out and an automatic abort occurred";
|
|
85
|
-
ErrorMessage2["CancelRequest"] = "cancellation of the previous AbortController";
|
|
86
|
-
return ErrorMessage2;
|
|
87
|
-
})(ErrorMessage || {});
|
|
88
|
-
const CLEAR_METHOD_SYMBOL = /* @__PURE__ */ Symbol("EVENT_LISTENER:CLEAR_METHOD");
|
|
85
|
+
const clearMethodSymbol = /* @__PURE__ */ Symbol("event-listener::clear()");
|
|
89
86
|
var _a;
|
|
90
|
-
_a =
|
|
87
|
+
_a = clearMethodSymbol;
|
|
91
88
|
class EventListener {
|
|
92
89
|
constructor(options) {
|
|
93
90
|
this.listeners = {};
|
|
@@ -97,21 +94,41 @@ class EventListener {
|
|
|
97
94
|
(_a2 = this.listeners)[type] ?? (_a2[type] = /* @__PURE__ */ new Set());
|
|
98
95
|
return this.listeners[type];
|
|
99
96
|
};
|
|
100
|
-
this.addEventListener = (type, listener) => {
|
|
101
|
-
|
|
97
|
+
this.addEventListener = (type, listener, options2) => {
|
|
98
|
+
const wrapper = {
|
|
99
|
+
originalListener: listener
|
|
100
|
+
};
|
|
101
|
+
if (options2?.once) {
|
|
102
|
+
const onceWrapper = (event) => {
|
|
103
|
+
listener(event);
|
|
104
|
+
this.getListenersByType(type).delete(wrapper);
|
|
105
|
+
};
|
|
106
|
+
wrapper.wrappedListener = onceWrapper;
|
|
107
|
+
}
|
|
108
|
+
this.getListenersByType(type).add(wrapper);
|
|
102
109
|
return () => this.removeEventListener(type, listener);
|
|
103
110
|
};
|
|
104
111
|
this.removeEventListener = (type, listener) => {
|
|
105
|
-
this.getListenersByType(type)
|
|
112
|
+
const listeners = this.getListenersByType(type);
|
|
113
|
+
for (const wrapper of listeners) {
|
|
114
|
+
if (wrapper.originalListener === listener) {
|
|
115
|
+
this.getListenersByType(type).delete(wrapper);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
106
119
|
};
|
|
107
120
|
this.dispatchEvent = (type, event) => {
|
|
108
121
|
if (type === "aborted" || type === "cancelled") {
|
|
109
122
|
this.onabort?.(event);
|
|
110
123
|
}
|
|
111
|
-
this.getListenersByType(type)
|
|
124
|
+
const listeners = [...this.getListenersByType(type)];
|
|
125
|
+
listeners.forEach((wrapper) => {
|
|
126
|
+
const listener = wrapper.wrappedListener || wrapper.originalListener;
|
|
127
|
+
listener(event);
|
|
128
|
+
});
|
|
112
129
|
};
|
|
113
130
|
this[_a] = () => {
|
|
114
|
-
this.listeners
|
|
131
|
+
Object.values(this.listeners).forEach((listeners) => listeners.clear());
|
|
115
132
|
this.onabort = void 0;
|
|
116
133
|
clearStateListeners(this.state);
|
|
117
134
|
};
|
|
@@ -120,8 +137,45 @@ class EventListener {
|
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
const clearEventListeners = (instance) => {
|
|
123
|
-
instance[
|
|
140
|
+
instance[clearMethodSymbol]();
|
|
124
141
|
};
|
|
142
|
+
class Timeout {
|
|
143
|
+
constructor() {
|
|
144
|
+
this.setTimeout = (timeout, onAbort) => {
|
|
145
|
+
this.clearTimeout();
|
|
146
|
+
if (!timeout || timeout <= 0) return;
|
|
147
|
+
this.timeoutId = setTimeout(onAbort, timeout);
|
|
148
|
+
};
|
|
149
|
+
this.clearTimeout = () => {
|
|
150
|
+
if (this.timeoutId) {
|
|
151
|
+
clearTimeout(this.timeoutId);
|
|
152
|
+
this.timeoutId = void 0;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
class TimeoutError extends ExtendedStackError {
|
|
158
|
+
constructor(message, options) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.timestamp = Date.now();
|
|
161
|
+
this.ms = options?.ms;
|
|
162
|
+
this.reason = options?.reason;
|
|
163
|
+
this.expandStack();
|
|
164
|
+
}
|
|
165
|
+
get debugStackInfo() {
|
|
166
|
+
return {
|
|
167
|
+
createdAt: new Date(this.timestamp).toISOString(),
|
|
168
|
+
ms: this.ms,
|
|
169
|
+
reason: this.reason
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
var ErrorMessage = /* @__PURE__ */ ((ErrorMessage2) => {
|
|
174
|
+
ErrorMessage2["AbortedSignalWithoutMessage"] = "signal is aborted without message";
|
|
175
|
+
ErrorMessage2["RequestTimedout"] = "the request timed out and an automatic abort occurred";
|
|
176
|
+
ErrorMessage2["CancelRequest"] = "cancellation of the previous AbortController";
|
|
177
|
+
return ErrorMessage2;
|
|
178
|
+
})(ErrorMessage || {});
|
|
125
179
|
const getAbortErrorByReason = (reason) => {
|
|
126
180
|
if (reason instanceof AbortError) {
|
|
127
181
|
return reason;
|
|
@@ -151,7 +205,6 @@ const _Aborter = class _Aborter {
|
|
|
151
205
|
signal: this.signal,
|
|
152
206
|
initiator: "system"
|
|
153
207
|
});
|
|
154
|
-
this.setRequestState("cancelled");
|
|
155
208
|
this.abort(cancelledAbortError);
|
|
156
209
|
}
|
|
157
210
|
let promise = new Promise((resolve, reject) => {
|
|
@@ -184,10 +237,10 @@ const _Aborter = class _Aborter {
|
|
|
184
237
|
};
|
|
185
238
|
this.abort = (reason) => {
|
|
186
239
|
if (!this.isRequestInProgress) return;
|
|
187
|
-
this.setRequestState("aborted");
|
|
188
240
|
const error = getAbortErrorByReason(reason);
|
|
189
241
|
this.listeners.dispatchEvent(error.type, error);
|
|
190
242
|
this.abortController.abort(error);
|
|
243
|
+
this.setRequestState(error.type);
|
|
191
244
|
};
|
|
192
245
|
this.abortWithRecovery = (reason) => {
|
|
193
246
|
this.abort(reason);
|
|
@@ -200,8 +253,15 @@ const _Aborter = class _Aborter {
|
|
|
200
253
|
};
|
|
201
254
|
this.listeners = new EventListener(options);
|
|
202
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Returns true if Aborter has signaled to abort, and false otherwise.
|
|
258
|
+
*/
|
|
259
|
+
get isAborted() {
|
|
260
|
+
return this.signal.aborted && this.listeners.state.value === "aborted";
|
|
261
|
+
}
|
|
203
262
|
/**
|
|
204
263
|
* Returns the AbortSignal object associated with this object.
|
|
264
|
+
* @deprecated
|
|
205
265
|
*/
|
|
206
266
|
get signal() {
|
|
207
267
|
return this.abortController?.signal;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,13 @@ export declare class Aborter {
|
|
|
18
18
|
* @returns boolean
|
|
19
19
|
*/
|
|
20
20
|
static isError: (error: any) => error is Error;
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if Aborter has signaled to abort, and false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
get isAborted(): boolean;
|
|
21
25
|
/**
|
|
22
26
|
* Returns the AbortSignal object associated with this object.
|
|
27
|
+
* @deprecated
|
|
23
28
|
*/
|
|
24
29
|
get signal(): AbortSignal;
|
|
25
30
|
private setRequestState;
|
|
@@ -45,10 +50,10 @@ export declare class Aborter {
|
|
|
45
50
|
dispose: () => void;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
declare interface AborterOptions extends Pick<
|
|
53
|
+
declare interface AborterOptions extends Pick<EventListenerConstructorOptions, 'onAbort' | 'onStateChange'> {
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
export declare class AbortError extends
|
|
56
|
+
export declare class AbortError extends ExtendedStackError {
|
|
52
57
|
/**
|
|
53
58
|
* Interrupt error code.
|
|
54
59
|
* @readonly
|
|
@@ -79,15 +84,17 @@ export declare class AbortError extends Error {
|
|
|
79
84
|
cause?: Error;
|
|
80
85
|
/**
|
|
81
86
|
* field with the name of the error initiator.
|
|
87
|
+
* @default `user`
|
|
82
88
|
*/
|
|
83
89
|
initiator?: Types.AbortInitiator;
|
|
84
90
|
constructor(message: string, options?: Types.AbortErrorOptions);
|
|
91
|
+
protected get debugStackInfo(): Record<string, any>;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
declare interface AbortErrorOptions {
|
|
88
95
|
type?: AbortType;
|
|
89
96
|
reason?: any;
|
|
90
|
-
cause?:
|
|
97
|
+
cause?: Error;
|
|
91
98
|
signal?: AbortSignal;
|
|
92
99
|
initiator?: AbortInitiator;
|
|
93
100
|
}
|
|
@@ -98,18 +105,18 @@ declare type AbortRequest<T> = (signal: AbortSignal) => Promise<T>;
|
|
|
98
105
|
|
|
99
106
|
export declare type AbortType = 'cancelled' | 'aborted';
|
|
100
107
|
|
|
101
|
-
declare const
|
|
108
|
+
declare const clearMethodSymbol: unique symbol;
|
|
102
109
|
|
|
103
|
-
declare const
|
|
110
|
+
declare const clearMethodSymbol_2: unique symbol;
|
|
104
111
|
|
|
105
112
|
declare namespace Constants {
|
|
106
113
|
export {
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
emitMethodSymbol,
|
|
115
|
+
clearMethodSymbol
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
declare const
|
|
119
|
+
declare const emitMethodSymbol: unique symbol;
|
|
113
120
|
|
|
114
121
|
declare type EventCallback<T extends EventListenerType> = EventMap[T] extends undefined ? () => void : (event: EventMap[T]) => void;
|
|
115
122
|
|
|
@@ -123,28 +130,32 @@ declare class EventListener_2 {
|
|
|
123
130
|
* Returns an `StateObserver` object for monitoring the status of requests.
|
|
124
131
|
*/
|
|
125
132
|
state: StateObserver;
|
|
126
|
-
constructor(options?: Types_2.
|
|
133
|
+
constructor(options?: Types_2.EventListenerConstructorOptions);
|
|
127
134
|
private getListenersByType;
|
|
128
135
|
/**
|
|
129
136
|
* 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.
|
|
130
137
|
*/
|
|
131
|
-
addEventListener: <T extends Types_2.EventListenerType,
|
|
138
|
+
addEventListener: <T extends Types_2.EventListenerType>(type: T, listener: Types_2.EventCallback<T>, options?: Types_2.EventListenerOptions) => VoidFunction;
|
|
132
139
|
/**
|
|
133
140
|
* Removes the event listener in target's event listener list with the same type and callback.
|
|
134
141
|
*/
|
|
135
|
-
removeEventListener: <T extends Types_2.EventListenerType
|
|
142
|
+
removeEventListener: <T extends Types_2.EventListenerType>(type: T, listener: Types_2.EventCallback<T>) => void;
|
|
136
143
|
/**
|
|
137
144
|
* Dispatches a synthetic event event to target
|
|
138
145
|
*/
|
|
139
|
-
dispatchEvent: <T extends Types_2.EventListenerType
|
|
140
|
-
/* Excluded from this release type: [
|
|
146
|
+
dispatchEvent: <T extends Types_2.EventListenerType>(type: T, event: Types_2.EventMap[T]) => void;
|
|
147
|
+
/* Excluded from this release type: [clearMethodSymbol] */
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
declare interface
|
|
150
|
+
declare interface EventListenerConstructorOptions {
|
|
144
151
|
onAbort?: OnAbortCallback;
|
|
145
152
|
onStateChange?: OnStateChangeCallback;
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
declare interface EventListenerOptions_2 {
|
|
156
|
+
once?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
declare type EventListenerType = keyof EventMap;
|
|
149
160
|
|
|
150
161
|
declare interface EventMap {
|
|
@@ -152,6 +163,14 @@ declare interface EventMap {
|
|
|
152
163
|
cancelled: AbortError;
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
declare abstract class ExtendedStackError extends Error {
|
|
167
|
+
protected abstract get debugStackInfo(): Record<string, any>;
|
|
168
|
+
/**
|
|
169
|
+
* Expands the stack with additional error information.
|
|
170
|
+
*/
|
|
171
|
+
protected expandStack: () => void;
|
|
172
|
+
}
|
|
173
|
+
|
|
155
174
|
declare interface FnTryOptions {
|
|
156
175
|
/**
|
|
157
176
|
* Returns the ability to catch a canceled request error in a catch block.
|
|
@@ -164,6 +183,11 @@ declare interface FnTryOptions {
|
|
|
164
183
|
timeout?: TimeoutErrorOptions;
|
|
165
184
|
}
|
|
166
185
|
|
|
186
|
+
declare interface ListenerWrapper<K extends EventListenerType> {
|
|
187
|
+
originalListener: (event: EventMap[K]) => any;
|
|
188
|
+
wrappedListener?: (event: EventMap[K]) => any;
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
export declare type OnAbortCallback = (error: AbortError) => void;
|
|
168
192
|
|
|
169
193
|
export declare type OnStateChangeCallback = (state: RequestState) => void;
|
|
@@ -200,8 +224,8 @@ declare class StateObserver {
|
|
|
200
224
|
* @returns {void}
|
|
201
225
|
*/
|
|
202
226
|
unsubscribe: (callbackfn: Types_3.OnStateChangeCallback) => void;
|
|
203
|
-
/* Excluded from this release type: [Constants.
|
|
204
|
-
/* Excluded from this release type: [Constants.
|
|
227
|
+
/* Excluded from this release type: [Constants.emitMethodSymbol] */
|
|
228
|
+
/* Excluded from this release type: [Constants.clearMethodSymbol] */
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
declare class Timeout {
|
|
@@ -216,7 +240,7 @@ declare class Timeout {
|
|
|
216
240
|
clearTimeout: () => void;
|
|
217
241
|
}
|
|
218
242
|
|
|
219
|
-
export declare class TimeoutError extends
|
|
243
|
+
export declare class TimeoutError extends ExtendedStackError {
|
|
220
244
|
/**
|
|
221
245
|
*The timestamp in milliseconds when the error was created.
|
|
222
246
|
@readonly
|
|
@@ -227,7 +251,12 @@ export declare class TimeoutError extends Error {
|
|
|
227
251
|
* A field displaying the time in milliseconds after which the request was interrupted.
|
|
228
252
|
*/
|
|
229
253
|
ms?: number;
|
|
254
|
+
/**
|
|
255
|
+
* A field storing the error reason. Can contain any metadata.
|
|
256
|
+
*/
|
|
257
|
+
reason?: any;
|
|
230
258
|
constructor(message: string, options?: TimeoutErrorOptions);
|
|
259
|
+
protected get debugStackInfo(): Record<string, any>;
|
|
231
260
|
}
|
|
232
261
|
|
|
233
262
|
declare interface TimeoutErrorOptions {
|
|
@@ -235,6 +264,10 @@ declare interface TimeoutErrorOptions {
|
|
|
235
264
|
* Time in milliseconds after which interrupts should be started.
|
|
236
265
|
*/
|
|
237
266
|
ms: number;
|
|
267
|
+
/**
|
|
268
|
+
* A field that stores any metadata passed into the error.
|
|
269
|
+
*/
|
|
270
|
+
reason?: any;
|
|
238
271
|
}
|
|
239
272
|
|
|
240
273
|
declare namespace Types {
|
|
@@ -250,8 +283,10 @@ declare namespace Types_2 {
|
|
|
250
283
|
EventMap,
|
|
251
284
|
EventListenerType,
|
|
252
285
|
EventCallback,
|
|
286
|
+
EventListenerOptions_2 as EventListenerOptions,
|
|
253
287
|
OnAbortCallback,
|
|
254
|
-
|
|
288
|
+
EventListenerConstructorOptions,
|
|
289
|
+
ListenerWrapper
|
|
255
290
|
}
|
|
256
291
|
}
|
|
257
292
|
|
package/dist/index.es.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const emitMethodSymbol = /* @__PURE__ */ Symbol("state-observer::emit()");
|
|
2
|
+
const clearMethodSymbol$1 = /* @__PURE__ */ Symbol("state-observer::clear()");
|
|
3
3
|
var _a$1, _b;
|
|
4
|
-
_b =
|
|
4
|
+
_b = emitMethodSymbol, _a$1 = clearMethodSymbol$1;
|
|
5
5
|
class StateObserver {
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
@@ -20,7 +20,7 @@ class StateObserver {
|
|
|
20
20
|
this.onstatechange?.(state);
|
|
21
21
|
};
|
|
22
22
|
this[_a$1] = () => {
|
|
23
|
-
this.subscribers
|
|
23
|
+
this.subscribers.clear();
|
|
24
24
|
this.onstatechange = void 0;
|
|
25
25
|
this.value = void 0;
|
|
26
26
|
};
|
|
@@ -28,15 +28,31 @@ class StateObserver {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
const emitRequestState = (instance, requestState) => {
|
|
31
|
-
instance[
|
|
31
|
+
instance[emitMethodSymbol](requestState);
|
|
32
32
|
};
|
|
33
33
|
const clearStateListeners = (instance) => {
|
|
34
|
-
instance[
|
|
34
|
+
instance[clearMethodSymbol$1]();
|
|
35
35
|
};
|
|
36
|
+
const EXTENDED_STACK_ADDITIONAL_INFO_TEXT = "Debug Info";
|
|
37
|
+
const getDebugStackInfo = (info) => {
|
|
38
|
+
return `
|
|
39
|
+
|
|
40
|
+
${EXTENDED_STACK_ADDITIONAL_INFO_TEXT}: ${JSON.stringify(info, null, 2)}
|
|
41
|
+
|
|
42
|
+
`;
|
|
43
|
+
};
|
|
44
|
+
class ExtendedStackError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super(...arguments);
|
|
47
|
+
this.expandStack = () => {
|
|
48
|
+
this.stack += getDebugStackInfo(this.debugStackInfo);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
36
52
|
const ABORT_ERROR_NAME = "AbortError";
|
|
37
53
|
const ERROR_CAUSE_PATH_NAME = "cause.name";
|
|
38
54
|
const ERROR_CAUSE_PATH_MESSAGE = "cause.message";
|
|
39
|
-
class AbortError extends
|
|
55
|
+
class AbortError extends ExtendedStackError {
|
|
40
56
|
constructor(message, options) {
|
|
41
57
|
super(message);
|
|
42
58
|
this.code = 20;
|
|
@@ -47,6 +63,15 @@ class AbortError extends Error {
|
|
|
47
63
|
this.signal = options?.signal;
|
|
48
64
|
this.cause = options?.cause;
|
|
49
65
|
this.initiator = options?.initiator || "user";
|
|
66
|
+
this.expandStack();
|
|
67
|
+
}
|
|
68
|
+
get debugStackInfo() {
|
|
69
|
+
return {
|
|
70
|
+
createdAt: new Date(this.timestamp).toISOString(),
|
|
71
|
+
reason: this.reason,
|
|
72
|
+
type: this.type,
|
|
73
|
+
initiator: this.initiator
|
|
74
|
+
};
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
const get = (object, path) => path.split(".").reduce((acc, key) => acc && acc[key], object);
|
|
@@ -55,37 +80,9 @@ const isObject = (value) => {
|
|
|
55
80
|
};
|
|
56
81
|
const checkErrorCause = (error) => get(error, ERROR_CAUSE_PATH_NAME) === ABORT_ERROR_NAME || "abort".includes(get(error, ERROR_CAUSE_PATH_MESSAGE));
|
|
57
82
|
const isError = (error) => error instanceof AbortError || isObject(error) && "name" in error && error.name === ABORT_ERROR_NAME || "abort".includes(error?.message ?? "") || checkErrorCause(error);
|
|
58
|
-
|
|
59
|
-
constructor() {
|
|
60
|
-
this.setTimeout = (timeout, onAbort) => {
|
|
61
|
-
this.clearTimeout();
|
|
62
|
-
if (!timeout || timeout <= 0) return;
|
|
63
|
-
this.timeoutId = setTimeout(onAbort, timeout);
|
|
64
|
-
};
|
|
65
|
-
this.clearTimeout = () => {
|
|
66
|
-
if (this.timeoutId) {
|
|
67
|
-
clearTimeout(this.timeoutId);
|
|
68
|
-
this.timeoutId = void 0;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
class TimeoutError extends Error {
|
|
74
|
-
constructor(message, options) {
|
|
75
|
-
super(message);
|
|
76
|
-
this.timestamp = Date.now();
|
|
77
|
-
this.ms = options?.ms;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
var ErrorMessage = /* @__PURE__ */ ((ErrorMessage2) => {
|
|
81
|
-
ErrorMessage2["AbortedSignalWithoutMessage"] = "signal is aborted without message";
|
|
82
|
-
ErrorMessage2["RequestTimedout"] = "the request timed out and an automatic abort occurred";
|
|
83
|
-
ErrorMessage2["CancelRequest"] = "cancellation of the previous AbortController";
|
|
84
|
-
return ErrorMessage2;
|
|
85
|
-
})(ErrorMessage || {});
|
|
86
|
-
const CLEAR_METHOD_SYMBOL = /* @__PURE__ */ Symbol("EVENT_LISTENER:CLEAR_METHOD");
|
|
83
|
+
const clearMethodSymbol = /* @__PURE__ */ Symbol("event-listener::clear()");
|
|
87
84
|
var _a;
|
|
88
|
-
_a =
|
|
85
|
+
_a = clearMethodSymbol;
|
|
89
86
|
class EventListener {
|
|
90
87
|
constructor(options) {
|
|
91
88
|
this.listeners = {};
|
|
@@ -95,21 +92,41 @@ class EventListener {
|
|
|
95
92
|
(_a2 = this.listeners)[type] ?? (_a2[type] = /* @__PURE__ */ new Set());
|
|
96
93
|
return this.listeners[type];
|
|
97
94
|
};
|
|
98
|
-
this.addEventListener = (type, listener) => {
|
|
99
|
-
|
|
95
|
+
this.addEventListener = (type, listener, options2) => {
|
|
96
|
+
const wrapper = {
|
|
97
|
+
originalListener: listener
|
|
98
|
+
};
|
|
99
|
+
if (options2?.once) {
|
|
100
|
+
const onceWrapper = (event) => {
|
|
101
|
+
listener(event);
|
|
102
|
+
this.getListenersByType(type).delete(wrapper);
|
|
103
|
+
};
|
|
104
|
+
wrapper.wrappedListener = onceWrapper;
|
|
105
|
+
}
|
|
106
|
+
this.getListenersByType(type).add(wrapper);
|
|
100
107
|
return () => this.removeEventListener(type, listener);
|
|
101
108
|
};
|
|
102
109
|
this.removeEventListener = (type, listener) => {
|
|
103
|
-
this.getListenersByType(type)
|
|
110
|
+
const listeners = this.getListenersByType(type);
|
|
111
|
+
for (const wrapper of listeners) {
|
|
112
|
+
if (wrapper.originalListener === listener) {
|
|
113
|
+
this.getListenersByType(type).delete(wrapper);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
};
|
|
105
118
|
this.dispatchEvent = (type, event) => {
|
|
106
119
|
if (type === "aborted" || type === "cancelled") {
|
|
107
120
|
this.onabort?.(event);
|
|
108
121
|
}
|
|
109
|
-
this.getListenersByType(type)
|
|
122
|
+
const listeners = [...this.getListenersByType(type)];
|
|
123
|
+
listeners.forEach((wrapper) => {
|
|
124
|
+
const listener = wrapper.wrappedListener || wrapper.originalListener;
|
|
125
|
+
listener(event);
|
|
126
|
+
});
|
|
110
127
|
};
|
|
111
128
|
this[_a] = () => {
|
|
112
|
-
this.listeners
|
|
129
|
+
Object.values(this.listeners).forEach((listeners) => listeners.clear());
|
|
113
130
|
this.onabort = void 0;
|
|
114
131
|
clearStateListeners(this.state);
|
|
115
132
|
};
|
|
@@ -118,8 +135,45 @@ class EventListener {
|
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
const clearEventListeners = (instance) => {
|
|
121
|
-
instance[
|
|
138
|
+
instance[clearMethodSymbol]();
|
|
122
139
|
};
|
|
140
|
+
class Timeout {
|
|
141
|
+
constructor() {
|
|
142
|
+
this.setTimeout = (timeout, onAbort) => {
|
|
143
|
+
this.clearTimeout();
|
|
144
|
+
if (!timeout || timeout <= 0) return;
|
|
145
|
+
this.timeoutId = setTimeout(onAbort, timeout);
|
|
146
|
+
};
|
|
147
|
+
this.clearTimeout = () => {
|
|
148
|
+
if (this.timeoutId) {
|
|
149
|
+
clearTimeout(this.timeoutId);
|
|
150
|
+
this.timeoutId = void 0;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
class TimeoutError extends ExtendedStackError {
|
|
156
|
+
constructor(message, options) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.timestamp = Date.now();
|
|
159
|
+
this.ms = options?.ms;
|
|
160
|
+
this.reason = options?.reason;
|
|
161
|
+
this.expandStack();
|
|
162
|
+
}
|
|
163
|
+
get debugStackInfo() {
|
|
164
|
+
return {
|
|
165
|
+
createdAt: new Date(this.timestamp).toISOString(),
|
|
166
|
+
ms: this.ms,
|
|
167
|
+
reason: this.reason
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
var ErrorMessage = /* @__PURE__ */ ((ErrorMessage2) => {
|
|
172
|
+
ErrorMessage2["AbortedSignalWithoutMessage"] = "signal is aborted without message";
|
|
173
|
+
ErrorMessage2["RequestTimedout"] = "the request timed out and an automatic abort occurred";
|
|
174
|
+
ErrorMessage2["CancelRequest"] = "cancellation of the previous AbortController";
|
|
175
|
+
return ErrorMessage2;
|
|
176
|
+
})(ErrorMessage || {});
|
|
123
177
|
const getAbortErrorByReason = (reason) => {
|
|
124
178
|
if (reason instanceof AbortError) {
|
|
125
179
|
return reason;
|
|
@@ -149,7 +203,6 @@ const _Aborter = class _Aborter {
|
|
|
149
203
|
signal: this.signal,
|
|
150
204
|
initiator: "system"
|
|
151
205
|
});
|
|
152
|
-
this.setRequestState("cancelled");
|
|
153
206
|
this.abort(cancelledAbortError);
|
|
154
207
|
}
|
|
155
208
|
let promise = new Promise((resolve, reject) => {
|
|
@@ -182,10 +235,10 @@ const _Aborter = class _Aborter {
|
|
|
182
235
|
};
|
|
183
236
|
this.abort = (reason) => {
|
|
184
237
|
if (!this.isRequestInProgress) return;
|
|
185
|
-
this.setRequestState("aborted");
|
|
186
238
|
const error = getAbortErrorByReason(reason);
|
|
187
239
|
this.listeners.dispatchEvent(error.type, error);
|
|
188
240
|
this.abortController.abort(error);
|
|
241
|
+
this.setRequestState(error.type);
|
|
189
242
|
};
|
|
190
243
|
this.abortWithRecovery = (reason) => {
|
|
191
244
|
this.abort(reason);
|
|
@@ -198,8 +251,15 @@ const _Aborter = class _Aborter {
|
|
|
198
251
|
};
|
|
199
252
|
this.listeners = new EventListener(options);
|
|
200
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Returns true if Aborter has signaled to abort, and false otherwise.
|
|
256
|
+
*/
|
|
257
|
+
get isAborted() {
|
|
258
|
+
return this.signal.aborted && this.listeners.state.value === "aborted";
|
|
259
|
+
}
|
|
201
260
|
/**
|
|
202
261
|
* Returns the AbortSignal object associated with this object.
|
|
262
|
+
* @deprecated
|
|
203
263
|
*/
|
|
204
264
|
get signal() {
|
|
205
265
|
return this.abortController?.signal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saborter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
"typecheck": "tsc --pretty --noEmit --skipLibCheck",
|
|
43
43
|
"verify:prettier": "npx prettier . --check",
|
|
44
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",
|
|
45
|
+
"verify": "npm run typecheck && npm run verify:prettier && npm run verify:eslint && npm run spellcheck",
|
|
46
46
|
"fix:eslint": "npx eslint \"./**/*.{js,jsx,ts,tsx}\" --fix",
|
|
47
47
|
"fix:prettier": "npx prettier --write .",
|
|
48
48
|
"fix": "npm run fix:eslint && npm run fix:prettier",
|
|
49
49
|
"test": "jest --colors --coverage test --passWithNoTests",
|
|
50
|
+
"spellcheck": "cspell \"**/*.{js,ts,jsx,tsx,md,json}\"",
|
|
50
51
|
"prepare": "husky"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"@types/node": "^25.0.3",
|
|
55
56
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
56
57
|
"@typescript-eslint/parser": "^8.50.1",
|
|
58
|
+
"cspell": "^9.6.2",
|
|
57
59
|
"eslint": "^8.11.0",
|
|
58
60
|
"eslint-config-airbnb": "^19.0.4",
|
|
59
61
|
"eslint-config-prettier": "^10.1.8",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/saborter)
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
[](https://github.com/TENSIILE/saborter)
|
|
@@ -12,6 +12,7 @@ A simple and effective library for canceling asynchronous requests using AbortCo
|
|
|
12
12
|
The documentation is divided into several sections:
|
|
13
13
|
|
|
14
14
|
- [Installation](#📦-installation)
|
|
15
|
+
- [Why Saborter?](#📈-why-saborter)
|
|
15
16
|
- [Quick Start](#🚀-quick-start)
|
|
16
17
|
- [Key Features](#📖-key-features)
|
|
17
18
|
- [API](#🔧-api)
|
|
@@ -20,6 +21,7 @@ The documentation is divided into several sections:
|
|
|
20
21
|
- [Troubleshooting](#🐜-troubleshooting)
|
|
21
22
|
- [Usage Examples](#🎯-usage-examples)
|
|
22
23
|
- [Compatibility](#💻-compatibility)
|
|
24
|
+
- [License](#📋-license)
|
|
23
25
|
|
|
24
26
|
## 📦 Installation
|
|
25
27
|
|
|
@@ -29,6 +31,20 @@ npm install saborter
|
|
|
29
31
|
yarn add saborter
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
### Related libraries
|
|
35
|
+
|
|
36
|
+
- [React](https://github.com/TENSIILE/saborter-react) - a standalone library with `Saborter` and `React` integration.
|
|
37
|
+
|
|
38
|
+
## 📈 Why Saborter ?
|
|
39
|
+
|
|
40
|
+
| Function/Characteristic | Saborter | AbortController |
|
|
41
|
+
| ------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------- |
|
|
42
|
+
| Eliminated race condition when speed typing. | ✔️ | ✖️ |
|
|
43
|
+
| The signal is created anew, there is no need to recreate it yourself. After `abort()` you can "reset" and use it again. | ✔️ | ✖️ |
|
|
44
|
+
| Legible error handling across all browsers. | ✔️ | ✖️ |
|
|
45
|
+
| There is extended information about request interruptions: who cancelled, when, and the reason. | ✔️ | ✖️ |
|
|
46
|
+
| The signal will always be new. It's no coincidence that a previously disabled signal can appear from outside, which breaks all logic. | ✔️ | ✖️ |
|
|
47
|
+
|
|
32
48
|
## 🚀 Quick Start
|
|
33
49
|
|
|
34
50
|
### Basic Usage
|
|
@@ -40,14 +56,14 @@ import { Aborter } from 'saborter';
|
|
|
40
56
|
const aborter = new Aborter();
|
|
41
57
|
|
|
42
58
|
// Use for the request
|
|
43
|
-
async
|
|
59
|
+
const fetchData = async () => {
|
|
44
60
|
try {
|
|
45
61
|
const result = await aborter.try((signal) => fetch('/api/data', { signal }));
|
|
46
62
|
console.log('Data received:', result);
|
|
47
63
|
} catch (error) {
|
|
48
64
|
console.error('Request error:', error);
|
|
49
65
|
}
|
|
50
|
-
}
|
|
66
|
+
};
|
|
51
67
|
```
|
|
52
68
|
|
|
53
69
|
## 📖 Key Features
|
|
@@ -58,11 +74,11 @@ Each time `try()` is called, the previous request is automatically canceled:
|
|
|
58
74
|
|
|
59
75
|
```javascript
|
|
60
76
|
// When searching with autocomplete
|
|
61
|
-
async
|
|
77
|
+
const handleSearch = async (query) => {
|
|
62
78
|
// The previous request is automatically canceled
|
|
63
79
|
const results = await aborter.try((signal) => fetch(`/api/search?q=${query}`, { signal }));
|
|
64
80
|
return results;
|
|
65
|
-
}
|
|
81
|
+
};
|
|
66
82
|
|
|
67
83
|
// When the user quickly types:
|
|
68
84
|
handleSearch('a'); // Starts
|
|
@@ -93,19 +109,19 @@ const userAborter = new Aborter();
|
|
|
93
109
|
const dataAborter = new Aborter();
|
|
94
110
|
|
|
95
111
|
// Manage user requests separately
|
|
96
|
-
async
|
|
112
|
+
const fetchUser = async (id) => {
|
|
97
113
|
return userAborter.try((signal) => fetch(`/api/users/${id}`, { signal }));
|
|
98
|
-
}
|
|
114
|
+
};
|
|
99
115
|
|
|
100
116
|
// And manage data separately
|
|
101
|
-
async
|
|
117
|
+
const fetchData = async (params) => {
|
|
102
118
|
return dataAborter.try((signal) => fetch('/api/data', { signal, ...params }));
|
|
103
|
-
}
|
|
119
|
+
};
|
|
104
120
|
|
|
105
121
|
// Cancel only user requests
|
|
106
|
-
|
|
122
|
+
const cancelUserRequests = () => {
|
|
107
123
|
userAborter.abort();
|
|
108
|
-
}
|
|
124
|
+
};
|
|
109
125
|
```
|
|
110
126
|
|
|
111
127
|
## 🔧 API
|
|
@@ -144,10 +160,14 @@ const aborter = new Aborter(options?: AborterOptions);
|
|
|
144
160
|
|
|
145
161
|
### Properties
|
|
146
162
|
|
|
147
|
-
`signal`
|
|
163
|
+
⚠️ `[DEPRECATED] signal`
|
|
148
164
|
|
|
149
165
|
Returns the `AbortSignal` associated with the current controller.
|
|
150
166
|
|
|
167
|
+
> [!WARNING]
|
|
168
|
+
> It's best not to use a signal to subscribe to interrupts or check whether a request has been interrupted.
|
|
169
|
+
> The signal is updated on every attempt, and your subscriptions will be lost, causing a memory leak.
|
|
170
|
+
|
|
151
171
|
```javascript
|
|
152
172
|
const aborter = new Aborter();
|
|
153
173
|
|
|
@@ -157,6 +177,10 @@ fetch('/api/data', {
|
|
|
157
177
|
});
|
|
158
178
|
```
|
|
159
179
|
|
|
180
|
+
`isAborted`
|
|
181
|
+
|
|
182
|
+
Returns a `boolean` value indicating whether the request was aborted or not.
|
|
183
|
+
|
|
160
184
|
`listeners`
|
|
161
185
|
|
|
162
186
|
Returns an `EventListener` object to listen for `Aborter` events.
|
|
@@ -192,6 +216,7 @@ Executes an asynchronous request with the ability to cancel.
|
|
|
192
216
|
- `isErrorNativeBehavior?: boolean` - a flag for controlling error handling. Default is `false`
|
|
193
217
|
- `timeout?: Object`
|
|
194
218
|
- `ms: number` - Time in milliseconds after which interrupts should be started
|
|
219
|
+
- `reason?: any` - A field storing the error reason. Can contain any metadata
|
|
195
220
|
|
|
196
221
|
**Returns:** `Promise<T>`
|
|
197
222
|
|
|
@@ -262,15 +287,15 @@ Immediately cancels the currently executing request.
|
|
|
262
287
|
// Start the request
|
|
263
288
|
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }), { isErrorNativeBehavior: true });
|
|
264
289
|
|
|
265
|
-
// Cancel
|
|
266
|
-
aborter.abort();
|
|
267
|
-
|
|
268
290
|
// Handle cancellation
|
|
269
291
|
requestPromise.catch((error) => {
|
|
270
292
|
if (error.name === 'AbortError') {
|
|
271
293
|
console.log('Request canceled');
|
|
272
294
|
}
|
|
273
295
|
});
|
|
296
|
+
|
|
297
|
+
// Cancel
|
|
298
|
+
aborter.abort();
|
|
274
299
|
```
|
|
275
300
|
|
|
276
301
|
You can specify any data as the `reason`.
|
|
@@ -280,9 +305,6 @@ If we want to pass an `object`, we can put the error message in the `message` fi
|
|
|
280
305
|
// Start the request
|
|
281
306
|
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }));
|
|
282
307
|
|
|
283
|
-
// Cancel
|
|
284
|
-
aborter.abort({ message: 'Hello', data: [] });
|
|
285
|
-
|
|
286
308
|
// Handle cancellation
|
|
287
309
|
requestPromise.catch((error) => {
|
|
288
310
|
if (error instanceof AbortError) {
|
|
@@ -290,6 +312,9 @@ requestPromise.catch((error) => {
|
|
|
290
312
|
console.log(error.reason); // { message: 'Hello', data: [] }
|
|
291
313
|
}
|
|
292
314
|
});
|
|
315
|
+
|
|
316
|
+
// Cancel
|
|
317
|
+
aborter.abort({ message: 'Hello', data: [] });
|
|
293
318
|
```
|
|
294
319
|
|
|
295
320
|
You can also submit your own `AbortError` with your own settings.
|
|
@@ -301,16 +326,16 @@ You can also submit your own `AbortError` with your own settings.
|
|
|
301
326
|
// Start the request
|
|
302
327
|
const requestPromise = aborter.try((signal) => fetch('/api/data', { signal }));
|
|
303
328
|
|
|
304
|
-
// Cancel
|
|
305
|
-
aborter.abort(new AbortError('Custom AbortError message', { reason: 1 }));
|
|
306
|
-
|
|
307
329
|
// Handle cancellation
|
|
308
330
|
requestPromise.catch((error) => {
|
|
309
331
|
if (error instanceof AbortError) {
|
|
310
|
-
console.log(error.message); // Custom AbortError message
|
|
332
|
+
console.log(error.message); // 'Custom AbortError message'
|
|
311
333
|
console.log(error.reason); // 1
|
|
312
334
|
}
|
|
313
335
|
});
|
|
336
|
+
|
|
337
|
+
// Cancel
|
|
338
|
+
aborter.abort(new AbortError('Custom AbortError message', { reason: 1 }));
|
|
314
339
|
```
|
|
315
340
|
|
|
316
341
|
`abortWithRecovery(reason?)`
|
|
@@ -331,14 +356,14 @@ After aborting, it restores the `AbortSignal`, resetting the `isAborted` propert
|
|
|
331
356
|
const aborter = new Aborter();
|
|
332
357
|
|
|
333
358
|
// Data retrieval function
|
|
334
|
-
async
|
|
359
|
+
const fetchData = async () => {
|
|
335
360
|
try {
|
|
336
361
|
const data = await fetch('/api/data', { signal: aborter.signal });
|
|
337
362
|
} catch (error) {
|
|
338
363
|
// ALL errors, including cancellations, will go here
|
|
339
364
|
console.log(error);
|
|
340
365
|
}
|
|
341
|
-
}
|
|
366
|
+
};
|
|
342
367
|
|
|
343
368
|
// Calling a function with a request
|
|
344
369
|
fetchData();
|
|
@@ -468,12 +493,14 @@ try {
|
|
|
468
493
|
|
|
469
494
|
## 🐜 Troubleshooting
|
|
470
495
|
|
|
496
|
+
### Finally block
|
|
497
|
+
|
|
471
498
|
Many people have probably encountered the problem with the `finally` block and the classic `AbortController`. When a request is canceled, the `catch` block is called. Why would `finally` block be called? This behavior only gets in the way and causes problems.
|
|
472
499
|
|
|
473
500
|
**Example:**
|
|
474
501
|
|
|
475
502
|
```javascript
|
|
476
|
-
const abortController =
|
|
503
|
+
const abortController = new AbortController();
|
|
477
504
|
|
|
478
505
|
const handleLoad = async () => {
|
|
479
506
|
try {
|
|
@@ -488,35 +515,60 @@ const handleLoad = async () => {
|
|
|
488
515
|
}
|
|
489
516
|
console.log(error);
|
|
490
517
|
} finally {
|
|
491
|
-
if (abortController.
|
|
518
|
+
if (abortController.signal.aborted) {
|
|
492
519
|
setLoading(false);
|
|
493
520
|
}
|
|
494
521
|
}
|
|
495
522
|
};
|
|
496
523
|
|
|
497
|
-
const abortLoad = () => abortController.
|
|
524
|
+
const abortLoad = () => abortController.abort();
|
|
498
525
|
```
|
|
499
526
|
|
|
500
527
|
The problem is obvious: checking the error by name, checking the condition to see if the `AbortController` was actually terminated in the `finally` block—it's all rather inconvenient.
|
|
501
528
|
|
|
502
529
|
How `Aborter` solves these problems:
|
|
503
530
|
|
|
531
|
+
```javascript
|
|
532
|
+
const aborter = new Aborter();
|
|
533
|
+
|
|
534
|
+
const handleLoad = async () => {
|
|
535
|
+
try {
|
|
536
|
+
setLoading(true);
|
|
537
|
+
|
|
538
|
+
const users = await aborter.try(getUsers);
|
|
539
|
+
|
|
540
|
+
setUsers(users);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (error instanceof AbortError) return;
|
|
543
|
+
|
|
544
|
+
console.log(error);
|
|
545
|
+
} finally {
|
|
546
|
+
setLoading(false);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const abortLoad = () => aborter.abort();
|
|
551
|
+
```
|
|
552
|
+
|
|
504
553
|
The name check is gone, replaced by an instance check. It's easy to make a typo in the error name and not be able to fix it. With `instanceof` this problem disappears.
|
|
505
554
|
With the `finally` block, everything has become even simpler. The condition that checked for termination is completely gone.
|
|
506
555
|
|
|
556
|
+
> [!NOTE]
|
|
557
|
+
> If you do not use the `abort()` method to terminate a request, then the check for `AbortError` in the `catch` block can be excluded.
|
|
558
|
+
|
|
559
|
+
**Example:**
|
|
560
|
+
|
|
507
561
|
```javascript
|
|
508
|
-
const
|
|
562
|
+
const aborter = new Aborter();
|
|
509
563
|
|
|
510
564
|
const handleLoad = async () => {
|
|
511
565
|
try {
|
|
512
566
|
setLoading(true);
|
|
513
567
|
|
|
514
|
-
const users = await
|
|
568
|
+
const users = await aborter.try(getUsers);
|
|
515
569
|
|
|
516
570
|
setUsers(users);
|
|
517
571
|
} catch (error) {
|
|
518
|
-
if (error instanceof AbortError) return;
|
|
519
|
-
|
|
520
572
|
console.log(error);
|
|
521
573
|
} finally {
|
|
522
574
|
setLoading(false);
|
|
@@ -524,15 +576,57 @@ const handleLoad = async () => {
|
|
|
524
576
|
};
|
|
525
577
|
```
|
|
526
578
|
|
|
579
|
+
### Subsequent calls to the `try` method
|
|
580
|
+
|
|
581
|
+
If you want to cancel a group of requests combined in `Promise.all` or `Promise.allSettled` from a single `Aborter` instance, do not use multiple sequentially called `try` methods:
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
// ✖️ Bad solution
|
|
585
|
+
const fetchData = async () => {
|
|
586
|
+
const [users, posts] = await Promise.all([
|
|
587
|
+
aborter.try((signal) => axios.get('/api/users', { signal })),
|
|
588
|
+
aborter.try((signal) => axios.get('/api/posts', { signal }))
|
|
589
|
+
]);
|
|
590
|
+
};
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```javascript
|
|
594
|
+
// ✔️ Good solution
|
|
595
|
+
const fetchData = async () => {
|
|
596
|
+
const [users, posts] = await aborter.try((signal) => {
|
|
597
|
+
return Promise.all([axios.get('/api/users', { signal }), axios.get('/api/posts', { signal })]);
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
In the case of the first solution, the second call to the `try` method will cancel the request of the first call, which will break your logic.
|
|
603
|
+
|
|
527
604
|
## 🎯 Usage Examples
|
|
528
605
|
|
|
529
|
-
### Example 1:
|
|
606
|
+
### Example 1: Canceling multiple simultaneous requests
|
|
607
|
+
|
|
608
|
+
```javascript
|
|
609
|
+
const aborter = new Aborter();
|
|
610
|
+
|
|
611
|
+
const getCategoriesByUserId = async (userId) => {
|
|
612
|
+
const data = await aborter.try(async (signal) => {
|
|
613
|
+
const user = await fetch(`/api/users/${userId}`, { signal });
|
|
614
|
+
const categories = await fetch(`/api/categories/${user.categoryId}`, { signal });
|
|
615
|
+
|
|
616
|
+
return [await user.json(), await categories.json()];
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return data;
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Example 2: Autocomplete
|
|
530
624
|
|
|
531
625
|
```javascript
|
|
532
626
|
class SearchAutocomplete {
|
|
533
627
|
aborter = new Aborter();
|
|
534
628
|
|
|
535
|
-
async
|
|
629
|
+
search = async (query) => {
|
|
536
630
|
if (!query.trim()) return [];
|
|
537
631
|
|
|
538
632
|
try {
|
|
@@ -547,15 +641,15 @@ class SearchAutocomplete {
|
|
|
547
641
|
// Get any error except AbortError
|
|
548
642
|
console.error('Search error:', error);
|
|
549
643
|
}
|
|
550
|
-
}
|
|
644
|
+
};
|
|
551
645
|
|
|
552
|
-
displayResults(results) {
|
|
646
|
+
displayResults = (results) => {
|
|
553
647
|
// Display the results
|
|
554
|
-
}
|
|
648
|
+
};
|
|
555
649
|
}
|
|
556
650
|
```
|
|
557
651
|
|
|
558
|
-
### Example
|
|
652
|
+
### Example 3: File Upload with Cancellation
|
|
559
653
|
|
|
560
654
|
```javascript
|
|
561
655
|
class FileUploader {
|
|
@@ -564,34 +658,31 @@ class FileUploader {
|
|
|
564
658
|
this.progress = 0;
|
|
565
659
|
}
|
|
566
660
|
|
|
567
|
-
async
|
|
661
|
+
uploadFile = async (file) => {
|
|
568
662
|
const formData = new FormData();
|
|
569
663
|
formData.append('file', file);
|
|
570
664
|
|
|
571
665
|
try {
|
|
572
|
-
await this.aborter.try(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
},
|
|
593
|
-
{ isErrorNativeBehavior: true }
|
|
594
|
-
);
|
|
666
|
+
await this.aborter.try(async (signal) => {
|
|
667
|
+
const response = await fetch('/api/upload', {
|
|
668
|
+
method: 'POST',
|
|
669
|
+
body: formData,
|
|
670
|
+
signal
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Track progress
|
|
674
|
+
const reader = response.body.getReader();
|
|
675
|
+
let receivedLength = 0;
|
|
676
|
+
const contentLength = +response.headers.get('Content-Length');
|
|
677
|
+
|
|
678
|
+
while (true) {
|
|
679
|
+
const { done, value } = await reader.read();
|
|
680
|
+
if (done) break;
|
|
681
|
+
|
|
682
|
+
receivedLength += value.length;
|
|
683
|
+
this.progress = Math.round((receivedLength / contentLength) * 100);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
595
686
|
|
|
596
687
|
console.log('File uploaded successfully');
|
|
597
688
|
} catch (error) {
|
|
@@ -601,15 +692,15 @@ class FileUploader {
|
|
|
601
692
|
console.error('Upload error:', error);
|
|
602
693
|
}
|
|
603
694
|
}
|
|
604
|
-
}
|
|
695
|
+
};
|
|
605
696
|
|
|
606
|
-
cancelUpload() {
|
|
697
|
+
cancelUpload = () => {
|
|
607
698
|
this.aborter.abort();
|
|
608
|
-
}
|
|
699
|
+
};
|
|
609
700
|
}
|
|
610
701
|
```
|
|
611
702
|
|
|
612
|
-
### Example
|
|
703
|
+
### Example 4: Integration with UI Frameworks
|
|
613
704
|
|
|
614
705
|
**React**
|
|
615
706
|
|
|
@@ -617,7 +708,7 @@ class FileUploader {
|
|
|
617
708
|
import React, { useState, useEffect, useRef } from 'react';
|
|
618
709
|
import { Aborter } from 'saborter';
|
|
619
710
|
|
|
620
|
-
|
|
711
|
+
const DataFetcher = ({ url }) => {
|
|
621
712
|
const [data, setData] = useState(null);
|
|
622
713
|
const [loading, setLoading] = useState(false);
|
|
623
714
|
const aborterRef = useRef(new Aborter());
|
|
@@ -644,9 +735,7 @@ function DataFetcher({ url }) {
|
|
|
644
735
|
};
|
|
645
736
|
|
|
646
737
|
const cancelRequest = () => {
|
|
647
|
-
|
|
648
|
-
aborterRef.current.abort();
|
|
649
|
-
}
|
|
738
|
+
aborterRef.current?.abort();
|
|
650
739
|
};
|
|
651
740
|
|
|
652
741
|
return (
|
|
@@ -660,7 +749,7 @@ function DataFetcher({ url }) {
|
|
|
660
749
|
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
|
661
750
|
</div>
|
|
662
751
|
);
|
|
663
|
-
}
|
|
752
|
+
};
|
|
664
753
|
```
|
|
665
754
|
|
|
666
755
|
**Vue.js**
|
|
@@ -708,3 +797,7 @@ export default {
|
|
|
708
797
|
- **Browsers:** All modern browsers that support AbortController
|
|
709
798
|
- **Node.js:** Requires a polyfill for AbortController (version 16+ has built-in support)
|
|
710
799
|
- **TypeScript:** Full type support
|
|
800
|
+
|
|
801
|
+
## 📋 License
|
|
802
|
+
|
|
803
|
+
MIT License - see [LICENSE](./LICENSE) for details.
|