keq 2.9.0 → 2.10.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/esm/src/constant.d.ts +2 -0
  3. package/dist/esm/src/constant.js +2 -0
  4. package/dist/esm/src/core.d.ts +30 -0
  5. package/dist/esm/src/core.js +162 -53
  6. package/dist/esm/src/exception/abort.exception.d.ts +3 -0
  7. package/dist/esm/src/exception/abort.exception.js +6 -0
  8. package/dist/esm/src/index.d.ts +1 -0
  9. package/dist/esm/src/index.js +1 -0
  10. package/dist/esm/src/is/is-abort-signal.d.ts +1 -0
  11. package/dist/esm/src/is/is-abort-signal.js +8 -0
  12. package/dist/esm/src/middlewares/fetch-arguments-middleware.js +5 -1
  13. package/dist/esm/src/middlewares/retry-middleware.js +20 -3
  14. package/dist/esm/src/middlewares/timeout-middleware.js +3 -1
  15. package/dist/esm/src/types/keq-context.d.ts +10 -1
  16. package/dist/esm/src/types/keq-context.js +1 -1
  17. package/dist/umd/src/constant.d.ts +2 -0
  18. package/dist/umd/src/constant.js +3 -1
  19. package/dist/umd/src/core.d.ts +30 -0
  20. package/dist/umd/src/core.js +162 -53
  21. package/dist/umd/src/exception/abort.exception.d.ts +3 -0
  22. package/dist/umd/src/exception/abort.exception.js +20 -0
  23. package/dist/umd/src/index.d.ts +1 -0
  24. package/dist/umd/src/index.js +4 -2
  25. package/dist/umd/src/is/is-abort-signal.d.ts +1 -0
  26. package/dist/umd/src/is/is-abort-signal.js +22 -0
  27. package/dist/umd/src/middlewares/fetch-arguments-middleware.js +4 -0
  28. package/dist/umd/src/middlewares/retry-middleware.js +21 -4
  29. package/dist/umd/src/middlewares/timeout-middleware.js +4 -2
  30. package/dist/umd/src/types/keq-context.d.ts +10 -1
  31. package/package.json +10 -13
  32. package/.claude/settings.local.json +0 -26
  33. package/packages/cache/.turbo/turbo-build.log +0 -29
  34. package/packages/cache/dist/index.d.mts +0 -354
  35. package/packages/cache/dist/index.d.mts.map +0 -1
  36. package/packages/cache/dist/index.d.ts +0 -353
  37. package/packages/cache/dist/index.d.ts.map +0 -1
  38. package/packages/cache/dist/index.js +0 -1271
  39. package/packages/cache/dist/index.js.map +0 -1
  40. package/packages/cache/dist/index.mjs +0 -1237
  41. package/packages/cache/dist/index.mjs.map +0 -1
  42. package/packages/cli/.turbo/turbo-build.log +0 -91
  43. package/packages/cli/dist/cli.cjs +0 -1364
  44. package/packages/cli/dist/cli.cjs.map +0 -1
  45. package/packages/cli/dist/cli.d.cts +0 -1
  46. package/packages/cli/dist/cli.d.mts +0 -1
  47. package/packages/cli/dist/cli.mjs +0 -1359
  48. package/packages/cli/dist/cli.mjs.map +0 -1
  49. package/packages/cli/dist/compiler-0Kli6UCr.cjs +0 -738
  50. package/packages/cli/dist/compiler-0Kli6UCr.cjs.map +0 -1
  51. package/packages/cli/dist/compiler-C3AE8j54.mjs +0 -715
  52. package/packages/cli/dist/compiler-C3AE8j54.mjs.map +0 -1
  53. package/packages/cli/dist/index.cjs +0 -20
  54. package/packages/cli/dist/index.cjs.map +0 -1
  55. package/packages/cli/dist/index.d.cts +0 -17
  56. package/packages/cli/dist/index.d.cts.map +0 -1
  57. package/packages/cli/dist/index.d.mts +0 -17
  58. package/packages/cli/dist/index.d.mts.map +0 -1
  59. package/packages/cli/dist/index.mjs +0 -10
  60. package/packages/cli/dist/index.mjs.map +0 -1
  61. package/packages/cli/dist/init-BNhD2oCI.mjs +0 -168
  62. package/packages/cli/dist/init-BNhD2oCI.mjs.map +0 -1
  63. package/packages/cli/dist/init-DqxLcRSQ.cjs +0 -170
  64. package/packages/cli/dist/init-DqxLcRSQ.cjs.map +0 -1
  65. package/packages/cli/dist/logger-D7PyVZo3.cjs +0 -46
  66. package/packages/cli/dist/logger-D7PyVZo3.cjs.map +0 -1
  67. package/packages/cli/dist/logger-QLBHySQ3.mjs +0 -12
  68. package/packages/cli/dist/logger-QLBHySQ3.mjs.map +0 -1
  69. package/packages/cli/dist/micro-function.translator-BmJZzkKh.cjs +0 -16
  70. package/packages/cli/dist/micro-function.translator-BmJZzkKh.cjs.map +0 -1
  71. package/packages/cli/dist/micro-function.translator-CDfkj4Tt.mjs +0 -11
  72. package/packages/cli/dist/micro-function.translator-CDfkj4Tt.mjs.map +0 -1
  73. package/packages/cli/dist/mock-command-Bp1LxSls.mjs +0 -350
  74. package/packages/cli/dist/mock-command-Bp1LxSls.mjs.map +0 -1
  75. package/packages/cli/dist/mock-command-WZcfy5Iq.cjs +0 -353
  76. package/packages/cli/dist/mock-command-WZcfy5Iq.cjs.map +0 -1
  77. package/packages/cli/dist/plugins-CgMhaNZq.cjs +0 -3558
  78. package/packages/cli/dist/plugins-CgMhaNZq.cjs.map +0 -1
  79. package/packages/cli/dist/plugins-DCvJ8iHR.mjs +0 -3280
  80. package/packages/cli/dist/plugins-DCvJ8iHR.mjs.map +0 -1
  81. package/packages/cli/dist/plugins.cjs +0 -36
  82. package/packages/cli/dist/plugins.d.cts +0 -2
  83. package/packages/cli/dist/plugins.d.mts +0 -2
  84. package/packages/cli/dist/plugins.mjs +0 -2
  85. package/packages/cli/dist/runtime-config-BWQlPhiB.d.cts +0 -780
  86. package/packages/cli/dist/runtime-config-BWQlPhiB.d.cts.map +0 -1
  87. package/packages/cli/dist/runtime-config-CrzK-Ucf.d.mts +0 -780
  88. package/packages/cli/dist/runtime-config-CrzK-Ucf.d.mts.map +0 -1
  89. package/packages/cli/dist/translators.cjs +0 -14
  90. package/packages/cli/dist/translators.cjs.map +0 -1
  91. package/packages/cli/dist/translators.d.cts +0 -14
  92. package/packages/cli/dist/translators.d.cts.map +0 -1
  93. package/packages/cli/dist/translators.d.mts +0 -14
  94. package/packages/cli/dist/translators.d.mts.map +0 -1
  95. package/packages/cli/dist/translators.mjs +0 -12
  96. package/packages/cli/dist/translators.mjs.map +0 -1
  97. package/packages/cli/node_modules/.bin/eslint +0 -21
  98. package/packages/cli/node_modules/.bin/jiti +0 -21
  99. package/packages/cli/node_modules/.bin/js-yaml +0 -21
  100. package/packages/cli/node_modules/.bin/jsf +0 -21
  101. package/packages/cli/node_modules/.bin/jsonpath +0 -21
  102. package/packages/cli/node_modules/.bin/jsonpath-plus +0 -21
  103. package/packages/cli/node_modules/.bin/semver +0 -21
  104. package/packages/cli/node_modules/.bin/tsc +0 -21
  105. package/packages/cli/node_modules/.bin/tsserver +0 -21
  106. package/packages/exception/.turbo/turbo-build.log +0 -29
  107. package/packages/exception/dist/index.d.mts +0 -21
  108. package/packages/exception/dist/index.d.mts.map +0 -1
  109. package/packages/exception/dist/index.d.ts +0 -21
  110. package/packages/exception/dist/index.d.ts.map +0 -1
  111. package/packages/exception/dist/index.js +0 -104
  112. package/packages/exception/dist/index.js.map +0 -1
  113. package/packages/exception/dist/index.mjs +0 -99
  114. package/packages/exception/dist/index.mjs.map +0 -1
  115. package/packages/headers/.turbo/turbo-build.log +0 -29
  116. package/packages/headers/dist/index.d.mts +0 -46
  117. package/packages/headers/dist/index.d.mts.map +0 -1
  118. package/packages/headers/dist/index.d.ts +0 -46
  119. package/packages/headers/dist/index.d.ts.map +0 -1
  120. package/packages/headers/dist/index.js +0 -81
  121. package/packages/headers/dist/index.js.map +0 -1
  122. package/packages/headers/dist/index.mjs +0 -75
  123. package/packages/headers/dist/index.mjs.map +0 -1
  124. package/packages/keq/.turbo/turbo-build.log +0 -29
  125. package/packages/keq/dist/index.d.mts +0 -962
  126. package/packages/keq/dist/index.d.mts.map +0 -1
  127. package/packages/keq/dist/index.d.ts +0 -962
  128. package/packages/keq/dist/index.d.ts.map +0 -1
  129. package/packages/keq/dist/index.js +0 -1876
  130. package/packages/keq/dist/index.js.map +0 -1
  131. package/packages/keq/dist/index.mjs +0 -1814
  132. package/packages/keq/dist/index.mjs.map +0 -1
  133. package/packages/nestjs/.turbo/turbo-build.log +0 -29
  134. package/packages/nestjs/dist/index.d.mts +0 -52
  135. package/packages/nestjs/dist/index.d.mts.map +0 -1
  136. package/packages/nestjs/dist/index.d.ts +0 -52
  137. package/packages/nestjs/dist/index.d.ts.map +0 -1
  138. package/packages/nestjs/dist/index.js +0 -166
  139. package/packages/nestjs/dist/index.js.map +0 -1
  140. package/packages/nestjs/dist/index.mjs +0 -136
  141. package/packages/nestjs/dist/index.mjs.map +0 -1
  142. package/packages/test/.turbo/turbo-build.log +0 -6
  143. package/packages/url/.turbo/turbo-build.log +0 -29
  144. package/packages/url/dist/index.d.mts +0 -9
  145. package/packages/url/dist/index.d.mts.map +0 -1
  146. package/packages/url/dist/index.d.ts +0 -9
  147. package/packages/url/dist/index.d.ts.map +0 -1
  148. package/packages/url/dist/index.js +0 -43
  149. package/packages/url/dist/index.js.map +0 -1
  150. package/packages/url/dist/index.mjs +0 -40
  151. package/packages/url/dist/index.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.10.0](https://github.com/keq-request/keq/compare/v2.9.0...v2.10.0) (2026-11-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * add abort and abortWhen request methods ([29b4d9c](https://github.com/keq-request/keq/commit/29b4d9c668d0432944913e55043278efd09e05ce))
11
+
5
12
  ## [2.9.0](https://github.com/keq-request/keq/compare/v2.8.14...v2.9.0) (2026-06-17)
6
13
 
7
14
 
@@ -1,2 +1,4 @@
1
1
  export declare const OUTPUT_PROPERTY: unique symbol;
2
2
  export declare const ABORT_PROPERTY: unique symbol;
3
+ export declare const SESSION_ABORTED_PROPERTY: unique symbol;
4
+ export declare const SESSION_ABORT_CONTROLLER_PROPERTY: unique symbol;
@@ -1,2 +1,4 @@
1
1
  export const OUTPUT_PROPERTY = Symbol('outputProperty');
2
2
  export const ABORT_PROPERTY = Symbol('abortProperty');
3
+ export const SESSION_ABORTED_PROPERTY = Symbol('sessionAbortedProperty');
4
+ export const SESSION_ABORT_CONTROLLER_PROPERTY = Symbol('sessionAbortControllerProperty');
@@ -12,6 +12,18 @@ export declare class Core<OUTPUT> {
12
12
  */
13
13
  __identifier__: string;
14
14
  private requestPromise?;
15
+ /** Whether abort() has been called on this request. */
16
+ private __aborted__;
17
+ /** The reason passed to abort(). */
18
+ private __abortReason__;
19
+ /** The context of the running attempt, used to abort the in-flight fetch. */
20
+ private __activeContext__?;
21
+ /** Aborts as soon as abort() is called, used to interrupt retry delays. */
22
+ private __sessionAbortController__?;
23
+ /** Cleanup callbacks registered by abortWhen(). */
24
+ private __abortWhenCleanups__;
25
+ /** Whether the request has settled (resolved or rejected). */
26
+ private __settled__;
15
27
  protected requestContext: Omit<KeqContextRequest, 'abort' | '__url__'>;
16
28
  protected __listeners__: KeqListeners;
17
29
  protected __global__: Record<string, any>;
@@ -22,6 +34,24 @@ export declare class Core<OUTPUT> {
22
34
  prependMiddlewares(...middlewares: KeqMiddleware[]): this;
23
35
  appendMiddlewares(...middlewares: KeqMiddleware[]): this;
24
36
  on<K extends keyof KeqEvents>(event: K, listener: (data: KeqEvents[K]) => void): this;
37
+ /**
38
+ * Abort the request manually.
39
+ *
40
+ * The request rejects with the given reason (an AbortException by default),
41
+ * and no further retry is performed. It is safe to call abort() before the
42
+ * request is sent.
43
+ *
44
+ * @description 手动中止请求
45
+ */
46
+ abort(reason?: any): void;
47
+ /**
48
+ * Abort the request when the given AbortSignal aborts,
49
+ * or when the given Promise settles (resolves or rejects).
50
+ *
51
+ * @description 当 signal 中止或 promise 完成时终止请求
52
+ */
53
+ abortWhen(source: AbortSignal | PromiseLike<unknown>): this;
54
+ private __clearAbortWhenCleanups__;
25
55
  private run;
26
56
  end(): Promise<OUTPUT>;
27
57
  /**
@@ -1,8 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import mitt from 'mitt';
3
+ import { AbortException } from './exception/abort.exception.js';
3
4
  import { Exception } from './exception/exception.js';
5
+ import { InvalidArgumentsExceptions } from './exception/invalid-arguments.exception.js';
6
+ import { isAbortSignal } from './is/is-abort-signal.js';
4
7
  import { cloneBody } from './util/clone-body.js';
5
- import { ABORT_PROPERTY, OUTPUT_PROPERTY } from './constant.js';
8
+ import { ABORT_PROPERTY, OUTPUT_PROPERTY, SESSION_ABORTED_PROPERTY, SESSION_ABORT_CONTROLLER_PROPERTY } from './constant.js';
6
9
  import { composeMiddleware } from './util/compose-middleware.js';
7
10
  import { shallowClone } from './util/shallow-clone.js';
8
11
  import { compileUrl } from './util/compile-url.js';
@@ -16,6 +19,18 @@ export class Core {
16
19
  */
17
20
  __identifier__;
18
21
  requestPromise;
22
+ /** Whether abort() has been called on this request. */
23
+ __aborted__ = false;
24
+ /** The reason passed to abort(). */
25
+ __abortReason__;
26
+ /** The context of the running attempt, used to abort the in-flight fetch. */
27
+ __activeContext__;
28
+ /** Aborts as soon as abort() is called, used to interrupt retry delays. */
29
+ __sessionAbortController__;
30
+ /** Cleanup callbacks registered by abortWhen(). */
31
+ __abortWhenCleanups__ = [];
32
+ /** Whether the request has settled (resolved or rejected). */
33
+ __settled__ = false;
19
34
  requestContext;
20
35
  __listeners__ = {};
21
36
  __global__;
@@ -50,7 +65,74 @@ export class Core {
50
65
  this.__listeners__[event].push(listener);
51
66
  return this;
52
67
  }
68
+ /**
69
+ * Abort the request manually.
70
+ *
71
+ * The request rejects with the given reason (an AbortException by default),
72
+ * and no further retry is performed. It is safe to call abort() before the
73
+ * request is sent.
74
+ *
75
+ * @description 手动中止请求
76
+ */
77
+ abort(reason = new AbortException('Request aborted')) {
78
+ if (this.__aborted__ || this.__settled__)
79
+ return;
80
+ this.__aborted__ = true;
81
+ this.__abortReason__ = reason;
82
+ const ctx = this.__activeContext__;
83
+ if (ctx)
84
+ ctx.abort(reason);
85
+ this.__clearAbortWhenCleanups__();
86
+ }
87
+ /**
88
+ * Abort the request when the given AbortSignal aborts,
89
+ * or when the given Promise settles (resolves or rejects).
90
+ *
91
+ * @description 当 signal 中止或 promise 完成时终止请求
92
+ */
93
+ abortWhen(source) {
94
+ if (this.__aborted__ || this.__settled__)
95
+ return this;
96
+ if (isAbortSignal(source)) {
97
+ const reason = () => source.reason ?? new AbortException('Request aborted by AbortSignal passed to abortWhen()');
98
+ if (source.aborted) {
99
+ this.abort(reason());
100
+ return this;
101
+ }
102
+ const onAbort = () => this.abort(reason());
103
+ source.addEventListener('abort', onAbort, { once: true });
104
+ this.__abortWhenCleanups__.push(() => source.removeEventListener('abort', onAbort));
105
+ return this;
106
+ }
107
+ if (typeof source === 'object' && source !== null && typeof source.then === 'function') {
108
+ const reason = new AbortException('Request aborted by Promise passed to abortWhen()');
109
+ let cancelled = false;
110
+ this.__abortWhenCleanups__.push(() => {
111
+ cancelled = true;
112
+ });
113
+ void Promise.resolve(source).then(() => {
114
+ if (!cancelled)
115
+ this.abort(reason);
116
+ }, () => {
117
+ if (!cancelled)
118
+ this.abort(reason);
119
+ });
120
+ return this;
121
+ }
122
+ throw new InvalidArgumentsExceptions();
123
+ }
124
+ __clearAbortWhenCleanups__() {
125
+ const cleanups = this.__abortWhenCleanups__;
126
+ this.__abortWhenCleanups__ = [];
127
+ for (const cleanup of cleanups)
128
+ cleanup();
129
+ }
53
130
  async run() {
131
+ if (this.__aborted__) {
132
+ this.__settled__ = true;
133
+ this.__clearAbortWhenCleanups__();
134
+ throw this.__abortReason__;
135
+ }
54
136
  const headers = new Headers();
55
137
  for (const [key, value] of this.requestContext.headers.entries()) {
56
138
  headers.append(key, value);
@@ -100,69 +182,98 @@ export class Core {
100
182
  },
101
183
  [ABORT_PROPERTY]: undefined,
102
184
  abort(reason) {
185
+ // Mark the request as aborted, so pending retries are stopped
186
+ this[SESSION_ABORTED_PROPERTY] = reason;
187
+ this[SESSION_ABORT_CONTROLLER_PROPERTY]?.abort(reason);
103
188
  const abortController = this[ABORT_PROPERTY];
104
- if (abortController) {
189
+ if (abortController)
105
190
  abortController.abort(reason);
106
- }
107
191
  },
108
192
  };
193
+ const sessionAbortController = new AbortController();
194
+ this.__sessionAbortController__ = sessionAbortController;
195
+ this.__activeContext__ = ctx;
196
+ ctx[SESSION_ABORT_CONTROLLER_PROPERTY] = sessionAbortController;
109
197
  const middleware = composeMiddleware([...this.__prepend_middlewares__, ...this.__append_middlewares__]);
110
- await middleware(ctx, async function emptyNext() { });
111
- const output = ctx[OUTPUT_PROPERTY];
112
- if (ctx.options.resolveWithFullResponse || ctx.options.resolveWith === 'response') {
113
- return ctx.response?.clone();
114
- }
115
- const response = ctx.response;
116
- if (!response) {
117
- return (OUTPUT_PROPERTY in ctx) ? output : undefined;
118
- }
119
- if (ctx.options.resolveWith === 'text') {
120
- return await response.text();
121
- }
122
- else if (ctx.options.resolveWith === 'json') {
123
- return unwrap(await response.json());
124
- }
125
- else if (ctx.options.resolveWith === 'form-data') {
126
- return await response.formData();
127
- }
128
- else if (ctx.options.resolveWith === 'blob') {
129
- return await response.blob();
130
- }
131
- else if (ctx.options.resolveWith === 'array-buffer') {
132
- return await response.arrayBuffer();
133
- }
134
- if (OUTPUT_PROPERTY in ctx) {
135
- return output;
136
- }
137
- if (response.status === 204) {
138
- // 204: NO CONTENT
139
- return undefined;
140
- }
141
- const contentType = response.headers.get('content-type') || '';
142
198
  try {
143
- if (contentType.includes('application/json')) {
199
+ await middleware(ctx, async function emptyNext() { });
200
+ if (this.__aborted__)
201
+ throw this.__abortReason__;
202
+ const output = ctx[OUTPUT_PROPERTY];
203
+ if (ctx.options.resolveWithFullResponse || ctx.options.resolveWith === 'response') {
204
+ return ctx.response?.clone();
205
+ }
206
+ const response = ctx.response;
207
+ if (!response) {
208
+ return (OUTPUT_PROPERTY in ctx) ? output : undefined;
209
+ }
210
+ if (ctx.options.resolveWith === 'text') {
211
+ return await response.text();
212
+ }
213
+ else if (ctx.options.resolveWith === 'json') {
144
214
  return unwrap(await response.json());
145
215
  }
146
- else if (contentType.includes('multipart/form-data')) {
216
+ else if (ctx.options.resolveWith === 'form-data') {
147
217
  return await response.formData();
148
218
  }
149
- else if (contentType.includes('plain/text')) {
150
- return await response.text();
219
+ else if (ctx.options.resolveWith === 'blob') {
220
+ return await response.blob();
221
+ }
222
+ else if (ctx.options.resolveWith === 'array-buffer') {
223
+ return await response.arrayBuffer();
224
+ }
225
+ if (OUTPUT_PROPERTY in ctx) {
226
+ return output;
151
227
  }
228
+ if (response.status === 204) {
229
+ // 204: NO CONTENT
230
+ return undefined;
231
+ }
232
+ const contentType = response.headers.get('content-type') || '';
233
+ try {
234
+ if (contentType.includes('application/json')) {
235
+ return unwrap(await response.json());
236
+ }
237
+ else if (contentType.includes('multipart/form-data')) {
238
+ return await response.formData();
239
+ }
240
+ else if (contentType.includes('plain/text')) {
241
+ return await response.text();
242
+ }
243
+ }
244
+ catch (e) {
245
+ console.warn('Failed to auto parse response body', e);
246
+ }
247
+ /**
248
+ * Unable to parse response body
249
+ * Return undefined
250
+ * Enable users to discover the problem
251
+ * And modify the method of parsing response
252
+ */
253
+ return undefined;
152
254
  }
153
- catch (e) {
154
- console.warn('Failed to auto parse response body', e);
255
+ catch (error) {
256
+ if (this.__aborted__)
257
+ throw this.__abortReason__;
258
+ throw error;
259
+ }
260
+ finally {
261
+ if (this.__activeContext__ === ctx)
262
+ this.__activeContext__ = undefined;
263
+ if (this.__sessionAbortController__ === sessionAbortController)
264
+ this.__sessionAbortController__ = undefined;
265
+ this.__settled__ = true;
266
+ this.__clearAbortWhenCleanups__();
155
267
  }
156
- /**
157
- * Unable to parse response body
158
- * Return undefined
159
- * Enable users to discover the problem
160
- * And modify the method of parsing response
161
- */
162
- return undefined;
163
268
  }
164
- async end() {
165
- return this.run();
269
+ end() {
270
+ if (!this.requestPromise) {
271
+ this.requestPromise = this.run();
272
+ // `end()` can be used to send the request without awaiting the result,
273
+ // mark the rejection as handled to avoid an unhandled rejection
274
+ void this.requestPromise.catch(() => { });
275
+ }
276
+ return this.requestPromise;
166
277
  }
167
278
  /**
168
279
  * Attaches callbacks for the resolution and/or rejection of the Promise.
@@ -171,9 +282,7 @@ export class Core {
171
282
  * @returns A Promise for the completion of which ever callback is executed.
172
283
  */
173
284
  then(onfulfilled, onrejected) {
174
- if (!this.requestPromise)
175
- this.requestPromise = this.end();
176
- return this.requestPromise.then(onfulfilled, onrejected);
285
+ return this.end().then(onfulfilled, onrejected);
177
286
  }
178
287
  catch(onrejected) {
179
288
  return this.end().catch(onrejected);
@@ -0,0 +1,3 @@
1
+ export declare class AbortException extends DOMException {
2
+ constructor(message?: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class AbortException extends DOMException {
2
+ constructor(message = 'Request aborted') {
3
+ super(message, 'AbortError');
4
+ Object.setPrototypeOf(this, AbortException.prototype);
5
+ }
6
+ }
@@ -1,3 +1,4 @@
1
+ export { AbortException } from './exception/abort.exception.js';
1
2
  export { createRequest } from './create-request.js';
2
3
  export { Keq } from './keq.js';
3
4
  export { request } from './request.js';
@@ -1,3 +1,4 @@
1
+ export { AbortException } from './exception/abort.exception.js';
1
2
  export { createRequest } from './create-request.js';
2
3
  export { Keq } from './keq.js';
3
4
  export { request } from './request.js';
@@ -0,0 +1 @@
1
+ export declare function isAbortSignal(value: unknown): value is AbortSignal;
@@ -0,0 +1,8 @@
1
+ export function isAbortSignal(value) {
2
+ if (typeof AbortSignal !== 'undefined' && value instanceof AbortSignal)
3
+ return true;
4
+ return typeof value === 'object' && value !== null
5
+ && typeof value.aborted === 'boolean'
6
+ && typeof value.addEventListener === 'function'
7
+ && typeof value.removeEventListener === 'function';
8
+ }
@@ -1,5 +1,5 @@
1
1
  import { Exception } from '../exception/exception.js';
2
- import { ABORT_PROPERTY } from '../constant.js';
2
+ import { ABORT_PROPERTY, SESSION_ABORTED_PROPERTY } from '../constant.js';
3
3
  import { isBuffer } from "../is/is-buffer.js";
4
4
  import { isBlob } from "../is/is-blob.js";
5
5
  import { isArrayBuffer } from "../is/is-array-buffer.js";
@@ -81,6 +81,10 @@ export function fetchArgumentsMiddleware() {
81
81
  }
82
82
  const abortController = new AbortController();
83
83
  ctx[ABORT_PROPERTY] = abortController;
84
+ // The request may have been aborted before the abort controller was created
85
+ const sessionAbortReason = ctx[SESSION_ABORTED_PROPERTY];
86
+ if (sessionAbortReason)
87
+ abortController.abort(sessionAbortReason);
84
88
  const requestInit = {
85
89
  method: request.method.toUpperCase(),
86
90
  headers: request.headers,
@@ -1,5 +1,16 @@
1
- function sleep(ms) {
2
- return new Promise((resolve) => setTimeout(resolve, ms));
1
+ import { SESSION_ABORTED_PROPERTY, SESSION_ABORT_CONTROLLER_PROPERTY } from '../constant.js';
2
+ function sleep(ms, signal) {
3
+ if (signal?.aborted)
4
+ return Promise.resolve();
5
+ return new Promise((resolve) => {
6
+ const timer = setTimeout(() => finish(), ms);
7
+ function finish() {
8
+ clearTimeout(timer);
9
+ signal?.removeEventListener('abort', finish);
10
+ resolve();
11
+ }
12
+ signal?.addEventListener('abort', finish, { once: true });
13
+ });
3
14
  }
4
15
  export function retryMiddleware() {
5
16
  return async function retryMiddleware(ctx, next) {
@@ -27,6 +38,9 @@ export function retryMiddleware() {
27
38
  retryOn: undefined,
28
39
  };
29
40
  for (let i = 0; i < retryTimes; i++) {
41
+ // The request has been aborted by user, stop retrying
42
+ if (ctx[SESSION_ABORTED_PROPERTY])
43
+ throw ctx[SESSION_ABORTED_PROPERTY];
30
44
  let err = null;
31
45
  try {
32
46
  ctx.fetchArguments = undefined;
@@ -39,6 +53,9 @@ export function retryMiddleware() {
39
53
  catch (e) {
40
54
  err = e;
41
55
  }
56
+ // The request has been aborted by user, stop retrying
57
+ if (ctx[SESSION_ABORTED_PROPERTY])
58
+ throw ctx[SESSION_ABORTED_PROPERTY];
42
59
  if (i === retryTimes - 1) {
43
60
  if (err)
44
61
  throw err;
@@ -53,7 +70,7 @@ export function retryMiddleware() {
53
70
  ctx.retry = { attempt: i, error: err, delay };
54
71
  ctx.emitter.emit('retry', ctx);
55
72
  if (delay > 0)
56
- await sleep(delay);
73
+ await sleep(delay, ctx[SESSION_ABORT_CONTROLLER_PROPERTY]?.signal);
57
74
  }
58
75
  };
59
76
  }
@@ -1,3 +1,4 @@
1
+ import { ABORT_PROPERTY } from '../constant.js';
1
2
  export function timeoutMiddleware() {
2
3
  return async function timeoutMiddleware(ctx, next) {
3
4
  if (!ctx.options.timeout || ctx.options.timeout.millisecond <= 0) {
@@ -8,7 +9,8 @@ export function timeoutMiddleware() {
8
9
  ctx.emitter.on('fetch', (ctx) => {
9
10
  setTimeout(() => {
10
11
  const err = new DOMException(`keq request timeout(${millisecond}ms)`, 'AbortError');
11
- ctx.abort(err);
12
+ // Only abort the current attempt, so that the retry middleware can retry
13
+ ctx[ABORT_PROPERTY]?.abort(err);
12
14
  }, millisecond);
13
15
  });
14
16
  await next();
@@ -1,4 +1,4 @@
1
- import { ABORT_PROPERTY, OUTPUT_PROPERTY } from '../constant.js';
1
+ import { ABORT_PROPERTY, OUTPUT_PROPERTY, SESSION_ABORTED_PROPERTY, SESSION_ABORT_CONTROLLER_PROPERTY } from '../constant.js';
2
2
  import type { Emitter } from 'mitt';
3
3
  import type { KeqOptionsParameter } from './keq-options.js';
4
4
  import type { KeqEvents } from './keq-events.js';
@@ -23,6 +23,15 @@ export interface KeqContext {
23
23
  };
24
24
  [ABORT_PROPERTY]?: AbortController;
25
25
  abort: (reason: any) => void;
26
+ /**
27
+ * The reason of the request being aborted by user (abort/abortWhen)
28
+ */
29
+ [SESSION_ABORTED_PROPERTY]?: any;
30
+ /**
31
+ * Aborts when the request is aborted (abort/abortWhen or context.abort()),
32
+ * used by middlewares (e.g. retry) to interrupt pending work
33
+ */
34
+ [SESSION_ABORT_CONTROLLER_PROPERTY]?: AbortController;
26
35
  /**
27
36
  * The unique identifier of the request's location in the code
28
37
  * @readonly
@@ -1,2 +1,2 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { ABORT_PROPERTY, OUTPUT_PROPERTY } from '../constant.js';
2
+ import { ABORT_PROPERTY, OUTPUT_PROPERTY, SESSION_ABORTED_PROPERTY, SESSION_ABORT_CONTROLLER_PROPERTY } from '../constant.js';
@@ -1,2 +1,4 @@
1
1
  export declare const OUTPUT_PROPERTY: unique symbol;
2
2
  export declare const ABORT_PROPERTY: unique symbol;
3
+ export declare const SESSION_ABORTED_PROPERTY: unique symbol;
4
+ export declare const SESSION_ABORT_CONTROLLER_PROPERTY: unique symbol;
@@ -9,7 +9,9 @@
9
9
  })(function (require, exports) {
10
10
  "use strict";
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ABORT_PROPERTY = exports.OUTPUT_PROPERTY = void 0;
12
+ exports.SESSION_ABORT_CONTROLLER_PROPERTY = exports.SESSION_ABORTED_PROPERTY = exports.ABORT_PROPERTY = exports.OUTPUT_PROPERTY = void 0;
13
13
  exports.OUTPUT_PROPERTY = Symbol('outputProperty');
14
14
  exports.ABORT_PROPERTY = Symbol('abortProperty');
15
+ exports.SESSION_ABORTED_PROPERTY = Symbol('sessionAbortedProperty');
16
+ exports.SESSION_ABORT_CONTROLLER_PROPERTY = Symbol('sessionAbortControllerProperty');
15
17
  });
@@ -12,6 +12,18 @@ export declare class Core<OUTPUT> {
12
12
  */
13
13
  __identifier__: string;
14
14
  private requestPromise?;
15
+ /** Whether abort() has been called on this request. */
16
+ private __aborted__;
17
+ /** The reason passed to abort(). */
18
+ private __abortReason__;
19
+ /** The context of the running attempt, used to abort the in-flight fetch. */
20
+ private __activeContext__?;
21
+ /** Aborts as soon as abort() is called, used to interrupt retry delays. */
22
+ private __sessionAbortController__?;
23
+ /** Cleanup callbacks registered by abortWhen(). */
24
+ private __abortWhenCleanups__;
25
+ /** Whether the request has settled (resolved or rejected). */
26
+ private __settled__;
15
27
  protected requestContext: Omit<KeqContextRequest, 'abort' | '__url__'>;
16
28
  protected __listeners__: KeqListeners;
17
29
  protected __global__: Record<string, any>;
@@ -22,6 +34,24 @@ export declare class Core<OUTPUT> {
22
34
  prependMiddlewares(...middlewares: KeqMiddleware[]): this;
23
35
  appendMiddlewares(...middlewares: KeqMiddleware[]): this;
24
36
  on<K extends keyof KeqEvents>(event: K, listener: (data: KeqEvents[K]) => void): this;
37
+ /**
38
+ * Abort the request manually.
39
+ *
40
+ * The request rejects with the given reason (an AbortException by default),
41
+ * and no further retry is performed. It is safe to call abort() before the
42
+ * request is sent.
43
+ *
44
+ * @description 手动中止请求
45
+ */
46
+ abort(reason?: any): void;
47
+ /**
48
+ * Abort the request when the given AbortSignal aborts,
49
+ * or when the given Promise settles (resolves or rejects).
50
+ *
51
+ * @description 当 signal 中止或 promise 完成时终止请求
52
+ */
53
+ abortWhen(source: AbortSignal | PromiseLike<unknown>): this;
54
+ private __clearAbortWhenCleanups__;
25
55
  private run;
26
56
  end(): Promise<OUTPUT>;
27
57
  /**