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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Vladislav Laptev
3
+ Copyright (c) 2026 Vladislav Laptev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 EMIT_METHOD_SYMBOL = /* @__PURE__ */ Symbol("STATE-OBSERVER:EMIT_METHOD");
4
- const CLEAR_METHOD_SYMBOL$1 = /* @__PURE__ */ Symbol("STATE-OBSERVER:CLEAR_METHOD");
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 = EMIT_METHOD_SYMBOL, _a$1 = CLEAR_METHOD_SYMBOL$1;
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 = /* @__PURE__ */ new Set();
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[EMIT_METHOD_SYMBOL](requestState);
33
+ instance[emitMethodSymbol](requestState);
34
34
  };
35
35
  const clearStateListeners = (instance) => {
36
- instance[CLEAR_METHOD_SYMBOL$1]();
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 Error {
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
- class Timeout {
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 = CLEAR_METHOD_SYMBOL;
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
- this.getListenersByType(type).add(listener);
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).delete(listener);
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).forEach((listener) => listener(event));
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[CLEAR_METHOD_SYMBOL]();
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<EventListenerOptions_2, 'onAbort' | 'onStateChange'> {
53
+ declare interface AborterOptions extends Pick<EventListenerConstructorOptions, 'onAbort' | 'onStateChange'> {
49
54
  }
50
55
 
51
- export declare class AbortError extends Error {
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?: any;
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 CLEAR_METHOD_SYMBOL: unique symbol;
108
+ declare const clearMethodSymbol: unique symbol;
102
109
 
103
- declare const CLEAR_METHOD_SYMBOL_2: unique symbol;
110
+ declare const clearMethodSymbol_2: unique symbol;
104
111
 
105
112
  declare namespace Constants {
106
113
  export {
107
- EMIT_METHOD_SYMBOL,
108
- CLEAR_METHOD_SYMBOL
114
+ emitMethodSymbol,
115
+ clearMethodSymbol
109
116
  }
110
117
  }
111
118
 
112
- declare const EMIT_METHOD_SYMBOL: unique symbol;
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.EventListenerOptions);
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, L extends Types_2.EventCallback<T>>(type: T, listener: L) => VoidFunction;
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, L extends Types_2.EventCallback<T>>(type: T, listener: L) => void;
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, E extends Types_2.EventMap[T]>(type: T, event: E) => void;
140
- /* Excluded from this release type: [CLEAR_METHOD_SYMBOL] */
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 EventListenerOptions_2 {
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.EMIT_METHOD_SYMBOL] */
204
- /* Excluded from this release type: [Constants.CLEAR_METHOD_SYMBOL] */
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 Error {
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
- EventListenerOptions_2 as EventListenerOptions
288
+ EventListenerConstructorOptions,
289
+ ListenerWrapper
255
290
  }
256
291
  }
257
292
 
package/dist/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
- const EMIT_METHOD_SYMBOL = /* @__PURE__ */ Symbol("STATE-OBSERVER:EMIT_METHOD");
2
- const CLEAR_METHOD_SYMBOL$1 = /* @__PURE__ */ Symbol("STATE-OBSERVER:CLEAR_METHOD");
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 = EMIT_METHOD_SYMBOL, _a$1 = CLEAR_METHOD_SYMBOL$1;
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 = /* @__PURE__ */ new Set();
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[EMIT_METHOD_SYMBOL](requestState);
31
+ instance[emitMethodSymbol](requestState);
32
32
  };
33
33
  const clearStateListeners = (instance) => {
34
- instance[CLEAR_METHOD_SYMBOL$1]();
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 Error {
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
- class Timeout {
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 = CLEAR_METHOD_SYMBOL;
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
- this.getListenersByType(type).add(listener);
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).delete(listener);
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).forEach((listener) => listener(event));
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[CLEAR_METHOD_SYMBOL]();
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.4.2",
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
  ![Logo](./assets/logo.png)
2
2
 
3
- [![Npm package](https://img.shields.io/badge/npm%20package-1.4.2-red)](https://www.npmjs.com/package/saborter)
3
+ [![Npm package](https://img.shields.io/npm/v/saborter?color=red&label=npm%20package)](https://www.npmjs.com/package/saborter)
4
4
  ![Static Badge](https://img.shields.io/badge/coverage-90%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)
@@ -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 function fetchData() {
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 function handleSearch(query) {
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 function fetchUser(id) {
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 function fetchData(params) {
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
- function cancelUserRequests() {
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 function fetchData() {
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 = useRef(new 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.current.signal.aborted) {
518
+ if (abortController.signal.aborted) {
492
519
  setLoading(false);
493
520
  }
494
521
  }
495
522
  };
496
523
 
497
- const abortLoad = () => abortController.current.abort();
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 aborterRef = useRef(new Aborter());
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 aborterRef.current.try(getUsers);
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: Autocomplete
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 search(query) {
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 2: File Upload with Cancellation
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 uploadFile(file) {
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
- async (signal) => {
574
- const response = await fetch('/api/upload', {
575
- method: 'POST',
576
- body: formData,
577
- signal
578
- });
579
-
580
- // Track progress
581
- const reader = response.body.getReader();
582
- let receivedLength = 0;
583
- const contentLength = +response.headers.get('Content-Length');
584
-
585
- while (true) {
586
- const { done, value } = await reader.read();
587
- if (done) break;
588
-
589
- receivedLength += value.length;
590
- this.progress = Math.round((receivedLength / contentLength) * 100);
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 3: Integration with UI Frameworks
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
- function DataFetcher({ url }) {
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
- if (aborterRef.current) {
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.