keq 5.0.0-alpha.6 → 5.0.0-alpha.7

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 (74) hide show
  1. package/CHANGELOG.md +187 -240
  2. package/jest.browser.config.ts +22 -0
  3. package/jest.config.cts +22 -0
  4. package/package.json +21 -24
  5. package/dist/index.cjs.js +0 -1115
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.esm.js +0 -1082
  8. package/dist/package.json +0 -65
  9. package/dist/src/constant.d.ts +0 -2
  10. package/dist/src/core.d.ts +0 -36
  11. package/dist/src/create-request.d.ts +0 -9
  12. package/dist/src/exception/exception.d.ts +0 -4
  13. package/dist/src/exception/invalid-arguments.exception.d.ts +0 -4
  14. package/dist/src/index.d.ts +0 -23
  15. package/dist/src/is/is-array-buffer.d.ts +0 -1
  16. package/dist/src/is/is-blob.d.ts +0 -4
  17. package/dist/src/is/is-browser.d.ts +0 -1
  18. package/dist/src/is/is-buffer.d.ts +0 -1
  19. package/dist/src/is/is-file.d.ts +0 -1
  20. package/dist/src/is/is-form-data.d.ts +0 -1
  21. package/dist/src/is/is-function.d.ts +0 -1
  22. package/dist/src/is/is-headers.d.ts +0 -1
  23. package/dist/src/is/is-object.d.ts +0 -1
  24. package/dist/src/is/is-readable-stream.d.ts +0 -1
  25. package/dist/src/is/is-string.d.ts +0 -1
  26. package/dist/src/is/is-url-search-params.d.ts +0 -1
  27. package/dist/src/keq.d.ts +0 -93
  28. package/dist/src/middlewares/abort-flow-control-middleware.d.ts +0 -2
  29. package/dist/src/middlewares/fetch-arguments-middleware.d.ts +0 -2
  30. package/dist/src/middlewares/fetch-middleware.d.ts +0 -5
  31. package/dist/src/middlewares/proxy-response-middleware.d.ts +0 -2
  32. package/dist/src/middlewares/retry-middleware.d.ts +0 -2
  33. package/dist/src/middlewares/serial-flow-control-middleware.d.ts +0 -2
  34. package/dist/src/middlewares/timeout-middleware.d.ts +0 -2
  35. package/dist/src/request.d.ts +0 -1
  36. package/dist/src/router/keq-host-route.d.ts +0 -2
  37. package/dist/src/router/keq-location-route.d.ts +0 -2
  38. package/dist/src/router/keq-method-route.d.ts +0 -2
  39. package/dist/src/router/keq-module-route.d.ts +0 -2
  40. package/dist/src/router/keq-pathname-route.d.ts +0 -2
  41. package/dist/src/router/keq-router.d.ts +0 -12
  42. package/dist/src/types/content-type.d.ts +0 -2
  43. package/dist/src/types/exclude-property.d.ts +0 -3
  44. package/dist/src/types/extract-property.d.ts +0 -3
  45. package/dist/src/types/keq-context-request.d.ts +0 -18
  46. package/dist/src/types/keq-context.d.ts +0 -62
  47. package/dist/src/types/keq-events.d.ts +0 -8
  48. package/dist/src/types/keq-flow-control.d.ts +0 -7
  49. package/dist/src/types/keq-global.d.ts +0 -6
  50. package/dist/src/types/keq-init.d.ts +0 -2
  51. package/dist/src/types/keq-middleware.d.ts +0 -3
  52. package/dist/src/types/keq-next.d.ts +0 -1
  53. package/dist/src/types/keq-operation.d.ts +0 -44
  54. package/dist/src/types/keq-options.d.ts +0 -55
  55. package/dist/src/types/keq-query-value.d.ts +0 -6
  56. package/dist/src/types/keq-request.d.ts +0 -32
  57. package/dist/src/types/keq-resolve-with-mode.d.ts +0 -1
  58. package/dist/src/types/keq-retry.d.ts +0 -3
  59. package/dist/src/types/keq-route.d.ts +0 -2
  60. package/dist/src/types/keq-timeout.d.ts +0 -3
  61. package/dist/src/util/base64.d.ts +0 -2
  62. package/dist/src/util/clone-body.d.ts +0 -1
  63. package/dist/src/util/compile-url.d.ts +0 -1
  64. package/dist/src/util/compose-middleware.d.ts +0 -2
  65. package/dist/src/util/compose-route.d.ts +0 -2
  66. package/dist/src/util/create-response-proxy.d.ts +0 -1
  67. package/dist/src/util/fix-content-type.d.ts +0 -3
  68. package/dist/src/util/get-unique-code-identifier.d.ts +0 -1
  69. package/dist/src/util/is-valid-header-value.d.ts +0 -1
  70. package/dist/src/util/merge-keq-request-body.d.ts +0 -2
  71. package/dist/src/util/shallow-clone.d.ts +0 -4
  72. package/project.json +0 -16
  73. package/tsconfig.json +0 -28
  74. package/tsconfig.lib.json +0 -15
package/dist/index.esm.js DELETED
@@ -1,1082 +0,0 @@
1
- import qs from 'qs';
2
- import mitt from 'mitt';
3
- import { CustomError } from 'ts-custom-error';
4
- import * as fastq from 'fastq';
5
- import * as m from 'minimatch';
6
-
7
- function isBrowser() {
8
- return typeof window !== 'undefined';
9
- }
10
-
11
- class Exception extends CustomError {
12
- constructor(message) {
13
- super(message);
14
- Object.defineProperty(this, 'name', {
15
- value: 'KeqError'
16
- });
17
- }
18
- }
19
-
20
- /* eslint-disable @typescript-eslint/no-explicit-any */
21
- function isBuffer(obj) {
22
- return isBrowser() ? false : Buffer.isBuffer(obj);
23
- }
24
-
25
- /* eslint-disable @typescript-eslint/no-explicit-any */
26
- function isFunction(value) {
27
- return typeof value === 'function';
28
- }
29
-
30
- /* eslint-disable @typescript-eslint/no-explicit-any */
31
- function isObject(value) {
32
- return typeof value === 'object' && value !== null;
33
- }
34
-
35
- function isString(value) {
36
- return typeof value === 'string';
37
- }
38
-
39
- /* eslint-disable @typescript-eslint/no-explicit-any */
40
- const names = ['Blob', 'File'];
41
- /**
42
- * Check if given valie is Blob or File -like object
43
- */
44
- function isBlob(value) {
45
- if (value instanceof Blob) return true;
46
- return isObject(value) && isString(value.type) && isFunction(value.arrayBuffer) && isFunction(value.stream) && isFunction(value.constructor) && names.includes(value.constructor.name) && 'size' in value;
47
- }
48
-
49
- /* eslint-disable @typescript-eslint/no-explicit-any */
50
- function isFile(object) {
51
- if (isBrowser()) return object instanceof Blob;
52
- return isBlob(object);
53
- }
54
-
55
- /* eslint-disable @typescript-eslint/no-explicit-any */
56
- function isFormData(object) {
57
- if (object instanceof FormData) return true;
58
- return isObject(object) && isFunction(object.append) && isFunction(object.delete) && isFunction(object.get) && isFunction(object.getAll) && isFunction(object.has) && isFunction(object.set) && isFunction(object.entries) && isFunction(object.keys) && isFunction(object.values);
59
- }
60
-
61
- /* eslint-disable @typescript-eslint/no-explicit-any */
62
- function isUrlSearchParams(obj) {
63
- if (obj instanceof URLSearchParams) return true;
64
- return isObject(obj) && isFunction(obj.append) && isFunction(obj.delete) && isFunction(obj.entries) && isFunction(obj.forEac) && isFunction(obj.get) && isFunction(obj.getAll) && isFunction(obj.has) && isFunction(obj.keys) && isFunction(obj.set) && isFunction(obj.values) && isFunction(obj.sort) && isFunction(obj.toString);
65
- }
66
-
67
- /* eslint-disable @typescript-eslint/no-explicit-any */
68
- function cloneBody(obj) {
69
- if (isFormData(obj)) {
70
- const formData = new FormData();
71
- for (const [key, value] of obj.entries()) {
72
- formData.append(key, value);
73
- }
74
- return formData;
75
- }
76
- if (isUrlSearchParams(obj)) {
77
- const urlSearchParams = new URLSearchParams();
78
- for (const [key, value] of obj.entries()) {
79
- urlSearchParams.append(key, value);
80
- }
81
- return urlSearchParams;
82
- }
83
- if (isFile(obj)) {
84
- return obj;
85
- }
86
- if (isBlob(obj)) {
87
- return obj;
88
- }
89
- if (isBuffer(obj)) {
90
- return obj;
91
- }
92
- if (obj === null) {
93
- return null;
94
- }
95
- if (Array.isArray(obj)) {
96
- return obj.map(cloneBody);
97
- }
98
- if (isObject(obj)) {
99
- const cloned = {};
100
- for (const key in obj) {
101
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
102
- cloned[key] = cloneBody(obj[key]);
103
- }
104
- }
105
- return cloned;
106
- }
107
- return obj;
108
- }
109
-
110
- const OUTPUT_PROPERTY = Symbol('outputProperty');
111
- const ABORT_PROPERTY = Symbol('abortProperty');
112
-
113
- function composeMiddleware(middlewares) {
114
- if (!middlewares.length) {
115
- throw new Exception('At least one middleware');
116
- }
117
- const middleware = [...middlewares].reverse().reduce(function (prev, curr) {
118
- return async (ctx, next) => {
119
- const metadata = {
120
- finished: false,
121
- entryNextTimes: 0,
122
- outNextTimes: 0
123
- };
124
- const context = new Proxy(ctx, {
125
- get(target, property) {
126
- if (property === 'metadata') return metadata;
127
- // @ts-ignore
128
- return target[property];
129
- }
130
- });
131
- await curr(context, async () => {
132
- if (metadata.finished) {
133
- throw new Exception([`next() should not invoke after ${curr.toString()} middleware finished.`].join(''));
134
- }
135
- if (metadata.entryNextTimes > 1) {
136
- console.warn(`next() had be invoke multiple times at ${curr.toString()} middleware`);
137
- }
138
- metadata.entryNextTimes += 1;
139
- await prev(ctx, next);
140
- metadata.outNextTimes += 1;
141
- });
142
- metadata.finished = true;
143
- if (metadata.entryNextTimes === 0) {
144
- console.warn(`next() is not invoked at ${curr.toString()}.`);
145
- }
146
- if (metadata.entryNextTimes !== metadata.outNextTimes) {
147
- throw new Exception([`next() should be invoke before ${curr.toString()} middleware finish.`, 'Maybe you forgot to await when calling next().'].join(''));
148
- }
149
- };
150
- });
151
- return middleware;
152
- }
153
-
154
- /* eslint-disable @typescript-eslint/no-explicit-any */
155
- /**
156
- * @description 浅拷贝
157
- */
158
- function shallowClone(obj) {
159
- if (Array.isArray(obj)) {
160
- return [...obj];
161
- }
162
- if (isObject(obj)) {
163
- return {
164
- ...obj
165
- };
166
- }
167
- return obj;
168
- }
169
-
170
- function compilePathnameTemplate(template, params) {
171
- return template.replace(/(^|\/)(?::([^/]+)|{([^/]+)}|%7B([^/]+)%7D)(?=$|\/)/g, (_, prefix, group1, group2, group3) => {
172
- if (group1 && params[group1]) {
173
- return `${prefix}${params[group1]}`;
174
- } else if (group2 && params[group2]) {
175
- return `${prefix}${params[group2]}`;
176
- } else if (group3 && params[group3]) {
177
- return `${prefix}${params[group3]}`;
178
- }
179
- return '';
180
- });
181
- }
182
- function compileUrl(obj, routeParams) {
183
- const url = new URL(typeof obj === 'string' ? obj : obj.href);
184
- url.pathname = compilePathnameTemplate(url.pathname, routeParams);
185
- return url;
186
- }
187
-
188
- /* eslint-disable @typescript-eslint/no-explicit-any */
189
- /**
190
- * @description Keq 核心 API,发送请求必要的原子化的API
191
- */
192
- class Core {
193
- /**
194
- * The unique identifier of the request's location in the code
195
- */
196
- __identifier__;
197
- requestPromise;
198
- requestContext;
199
- __listeners__ = {};
200
- __global__;
201
- __prepend_middlewares__ = [];
202
- __append_middlewares__ = [];
203
- __options__ = {
204
- resolveWithFullResponse: false,
205
- resolveWith: 'intelligent'
206
- };
207
- constructor(url, init, identifier, global = {}) {
208
- this.__global__ = global;
209
- this.__identifier__ = identifier;
210
- this.requestContext = {
211
- method: 'get',
212
- headers: new Headers(),
213
- routeParams: {},
214
- body: undefined,
215
- ...init,
216
- url: new URL(url.href)
217
- };
218
- }
219
- prependMiddlewares(...middlewares) {
220
- this.__prepend_middlewares__.push(...middlewares);
221
- return this;
222
- }
223
- appendMiddlewares(...middlewares) {
224
- this.__append_middlewares__.unshift(...middlewares);
225
- return this;
226
- }
227
- on(event, listener) {
228
- this.__listeners__[event] = this.__listeners__[event] || [];
229
- this.__listeners__[event].push(listener);
230
- return this;
231
- }
232
- async run() {
233
- const headers = new Headers();
234
- for (const [key, value] of this.requestContext.headers.entries()) {
235
- headers.append(key, value);
236
- }
237
- const requestContext = {
238
- url: new URL(this.requestContext.url.href),
239
- routeParams: shallowClone(this.requestContext.routeParams),
240
- get __url__() {
241
- return compileUrl(this.url, this.routeParams);
242
- },
243
- method: this.requestContext.method,
244
- headers,
245
- body: cloneBody(this.requestContext.body),
246
- cache: this.requestContext.cache,
247
- credentials: this.requestContext.credentials,
248
- integrity: this.requestContext.integrity,
249
- keepalive: this.requestContext.keepalive,
250
- mode: this.requestContext.mode,
251
- redirect: this.requestContext.redirect,
252
- referrer: this.requestContext.referrer,
253
- referrerPolicy: this.requestContext.referrerPolicy
254
- };
255
- const options = shallowClone(this.__options__);
256
- const emitter = mitt();
257
- for (const eventName in this.__listeners__) {
258
- const listeners = this.__listeners__[eventName];
259
- for (const listener of listeners) {
260
- emitter.on(eventName, listener);
261
- }
262
- }
263
- const ctx = {
264
- metadata: {
265
- finished: false,
266
- entryNextTimes: 0,
267
- outNextTimes: 0
268
- },
269
- identifier: this.__identifier__,
270
- emitter,
271
- request: requestContext,
272
- options,
273
- global: this.__global__,
274
- get output() {
275
- throw new Exception('output property is write-only');
276
- },
277
- set output(value) {
278
- this[OUTPUT_PROPERTY] = value;
279
- },
280
- [ABORT_PROPERTY]: undefined,
281
- abort(reason) {
282
- const abortController = this[ABORT_PROPERTY];
283
- if (abortController) {
284
- abortController.abort(reason);
285
- }
286
- }
287
- };
288
- Object.defineProperty(ctx, 'identifier', {
289
- value: this.__identifier__,
290
- writable: false
291
- });
292
- Object.defineProperty(ctx, 'request', {
293
- value: requestContext,
294
- writable: false
295
- });
296
- Object.defineProperty(ctx, 'emitter', {
297
- value: emitter,
298
- writable: false
299
- });
300
- Object.defineProperty(ctx, 'global', {
301
- value: this.__global__,
302
- writable: false
303
- });
304
- const middleware = composeMiddleware([...this.__prepend_middlewares__, ...this.__append_middlewares__]);
305
- await middleware(ctx, async function emptyNext() {});
306
- const output = ctx[OUTPUT_PROPERTY];
307
- if (ctx.options.resolveWithFullResponse || ctx.options.resolveWith === 'response') {
308
- return ctx.response;
309
- }
310
- const response = ctx.response;
311
- if (!response) {
312
- return OUTPUT_PROPERTY in ctx ? output : undefined;
313
- }
314
- if (ctx.options.resolveWith === 'text') {
315
- return await response.text();
316
- } else if (ctx.options.resolveWith === 'json') {
317
- return await response.json();
318
- } else if (ctx.options.resolveWith === 'form-data') {
319
- return await response.formData();
320
- } else if (ctx.options.resolveWith === 'blob') {
321
- return await response.blob();
322
- } else if (ctx.options.resolveWith === 'array-buffer') {
323
- return await response.arrayBuffer();
324
- }
325
- if (OUTPUT_PROPERTY in ctx) {
326
- return output;
327
- }
328
- if (response.status === 204) {
329
- // 204: NO CONTENT
330
- return undefined;
331
- }
332
- const contentType = response.headers.get('content-type') || '';
333
- try {
334
- if (contentType.includes('application/json')) {
335
- return await response.json();
336
- } else if (contentType.includes('multipart/form-data')) {
337
- return await response.formData();
338
- } else if (contentType.includes('plain/text')) {
339
- return await response.text();
340
- }
341
- } catch (e) {
342
- console.warn('Failed to auto parse response body', e);
343
- }
344
- /**
345
- * Unable to parse response body
346
- * Return undefined
347
- * Enable users to discover the problem
348
- * And modify the method of parsing response
349
- */
350
- return undefined;
351
- }
352
- async end() {
353
- return this.run();
354
- }
355
- /**
356
- * Attaches callbacks for the resolution and/or rejection of the Promise.
357
- * @param onfulfilled The callback to execute when the Promise is resolved.
358
- * @param onrejected The callback to execute when the Promise is rejected.
359
- * @returns A Promise for the completion of which ever callback is executed.
360
- */
361
- then(onfulfilled, onrejected) {
362
- if (!this.requestPromise) this.requestPromise = this.end();
363
- return this.requestPromise.then(onfulfilled, onrejected);
364
- }
365
- catch(onrejected) {
366
- return this.end().catch(onrejected);
367
- }
368
- finally(onfinally) {
369
- return this.end().finally(onfinally);
370
- }
371
- }
372
-
373
- class InvalidArgumentsExceptions extends Exception {
374
- constructor() {
375
- super('Invalid arguments');
376
- }
377
- }
378
-
379
- /* eslint-disable @typescript-eslint/no-explicit-any */
380
- function isHeaders(obj) {
381
- if (obj instanceof Headers) return true;
382
- if (isObject(obj) && isFunction(obj.forEach) && isFunction(obj.get) && isFunction(obj.has) && isFunction(obj.set) && isFunction(obj.append) && isFunction(obj.delete) && isFunction(obj.entries) && isFunction(obj.keys) && isFunction(obj.values)) {
383
- return true;
384
- }
385
- return false;
386
- }
387
-
388
- function isArrayBuffer(body) {
389
- return body instanceof ArrayBuffer;
390
- }
391
-
392
- function isReadableStream(body) {
393
- return body instanceof ReadableStream;
394
- }
395
-
396
- function mergeKeqRequestBody(left, right) {
397
- if (right === undefined) return left;
398
- if (typeof right === 'number') {
399
- throw new TypeError('Not support number type');
400
- }
401
- if (left === null || right === null || isBuffer(right) || isArrayBuffer(right) || isBlob(right) || isReadableStream(right) || isBuffer(left) || isArrayBuffer(left) || isBlob(left) || isReadableStream(left) || Array.isArray(left) || Array.isArray(right) || typeof left !== 'object' && left !== undefined || typeof right !== 'object') {
402
- return Array.isArray(right) ? [...right] : right;
403
- }
404
- const result = left || {};
405
- if (isUrlSearchParams(right)) {
406
- const keys = right.keys();
407
- for (const key of keys) {
408
- const values = right.getAll(key);
409
- if (values.length === 1) {
410
- result[key] = values[0];
411
- } else if (values.length > 1) {
412
- result[key] = values;
413
- }
414
- }
415
- } else if (isFormData(right)) {
416
- const keys = right.keys();
417
- for (const key of keys) {
418
- const values = right.getAll(key);
419
- if (values.length === 1) {
420
- result[key] = values[0];
421
- } else if (values.length > 1) {
422
- result[key] = values;
423
- }
424
- }
425
- } else if (typeof right === 'object') {
426
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
427
- for (const key in right) {
428
- result[key] = right[key];
429
- }
430
- } else {
431
- throw new Exception(`Not support request body type: ${typeof right}`);
432
- }
433
- return result;
434
- }
435
-
436
- const base64Encode = globalThis.btoa || (str => Buffer.from(str).toString('base64'));
437
-
438
- function fixContentType(contentType) {
439
- if (['json', 'xml'].includes(contentType)) {
440
- return `application/${contentType}`;
441
- } else if (['html', 'css'].includes(contentType)) {
442
- return `text/${contentType}`;
443
- } else if (['form-data'].includes(contentType)) {
444
- return `multipart/${contentType}`;
445
- } else if (['jpeg', 'bmp', 'apng', 'gif', 'x-icon', 'png', 'webp', 'tiff'].includes(contentType)) {
446
- return `image/${contentType}`;
447
- } else if (contentType === 'form') {
448
- return 'application/x-www-form-urlencoded';
449
- } else if (contentType === 'svg') {
450
- return 'image/svg+xml';
451
- }
452
- return contentType;
453
- }
454
-
455
- function isValidHeaderValue(str) {
456
- const regex = /^[\t\x20-\x7E\x80-\xFF]*$/;
457
- return regex.test(str);
458
- }
459
-
460
- /* eslint-disable @typescript-eslint/no-unused-vars */
461
- /* eslint-disable @typescript-eslint/no-explicit-any */
462
- /**
463
- * @description Keq 扩展 API,人性化的常用的API
464
- */
465
- class Keq extends Core {
466
- use(...middlewares) {
467
- return this.prependMiddlewares(...middlewares);
468
- }
469
- option(key, value = true) {
470
- this.__options__[key] = value;
471
- return this;
472
- }
473
- options(opts) {
474
- for (const [key, value] of Object.entries(opts)) {
475
- this.__options__[key] = value;
476
- }
477
- return this;
478
- }
479
- set(headersOrName, value) {
480
- if (isHeaders(headersOrName)) {
481
- headersOrName.forEach((value, key) => {
482
- this.requestContext.headers.set(key, value);
483
- });
484
- } else if (typeof headersOrName === 'string' && value) {
485
- if (!isValidHeaderValue(value)) {
486
- throw new Exception(`[Invalid header] Key: ${headersOrName} Value: ${value}`);
487
- }
488
- this.requestContext.headers.set(headersOrName, String(value));
489
- } else if (typeof headersOrName === 'object') {
490
- for (const [key, value] of Object.entries(headersOrName)) {
491
- if (!isValidHeaderValue(value)) {
492
- throw new Exception(`[Invalid header] Key: ${key} Value: ${value}`);
493
- }
494
- this.requestContext.headers.set(key, String(value));
495
- }
496
- }
497
- return this;
498
- }
499
- query(key, value) {
500
- if (isObject(key)) {
501
- const obj = qs.parse(qs.stringify(key, {
502
- encode: false,
503
- arrayFormat: 'brackets'
504
- }), {
505
- depth: 0
506
- });
507
- for (const [k, v] of Object.entries(obj)) {
508
- for (const item of Array.isArray(v) ? v : [v]) {
509
- this.requestContext.url.searchParams.append(k, item);
510
- }
511
- }
512
- return this;
513
- }
514
- if (typeof key === 'string') {
515
- this.query({
516
- [key]: value
517
- });
518
- return this;
519
- }
520
- throw new TypeError('typeof key is invalid');
521
- }
522
- params(key, value) {
523
- if (typeof key === 'string') {
524
- this.requestContext.routeParams[key] = String(value);
525
- } else if (typeof key === 'object') {
526
- for (const [k, v] of Object.entries(key)) {
527
- this.requestContext.routeParams[k] = String(v);
528
- }
529
- } else {
530
- throw new TypeError('Invalid Arguments for .params()');
531
- }
532
- return this;
533
- }
534
- /**
535
- * Set request body
536
- */
537
- body(value) {
538
- this.requestContext.body = value;
539
- return this;
540
- }
541
- type(contentType) {
542
- const type = fixContentType(contentType);
543
- this.set('Content-Type', type);
544
- return this;
545
- }
546
- /**
547
- * Http Basic Authentication
548
- */
549
- auth(username, password) {
550
- this.set('Authorization', `Basic ${base64Encode(`${username}:${password}`)}`);
551
- return this;
552
- }
553
- setTypeIfEmpty(contentType) {
554
- if (!this.requestContext.headers.has('Content-Type')) void this.type(contentType);
555
- }
556
- send(value) {
557
- this.requestContext.body = mergeKeqRequestBody(this.requestContext.body, value);
558
- if (isUrlSearchParams(value)) {
559
- this.setTypeIfEmpty('form');
560
- } else if (isFormData(value)) {
561
- this.setTypeIfEmpty('form-data');
562
- } else if (isBlob(value) || isReadableStream(value) || isArrayBuffer(value)) ; else if (typeof value === 'object') {
563
- this.setTypeIfEmpty('json');
564
- }
565
- return this;
566
- }
567
- field(arg1, arg2) {
568
- if (typeof arg1 === 'object') {
569
- this.requestContext.body = mergeKeqRequestBody(this.requestContext.body, arg1);
570
- } else if (arg2) {
571
- this.requestContext.body = mergeKeqRequestBody(this.requestContext.body, {
572
- [arg1]: arg2
573
- });
574
- } else {
575
- throw new InvalidArgumentsExceptions();
576
- }
577
- this.setTypeIfEmpty('form-data');
578
- return this;
579
- }
580
- attach(key, value, arg3 = 'file') {
581
- let file;
582
- if (isBlob(value)) {
583
- const formData = new FormData();
584
- formData.set(key, value, arg3);
585
- file = formData.get(key);
586
- } else if (isFile(value)) {
587
- file = value;
588
- } else if (isBuffer(value)) {
589
- const formData = new FormData();
590
- formData.set(key, new Blob([value]), arg3);
591
- file = formData.get(key);
592
- } else {
593
- throw new InvalidArgumentsExceptions();
594
- }
595
- this.requestContext.body = mergeKeqRequestBody(this.requestContext.body, {
596
- [key]: file
597
- });
598
- this.setTypeIfEmpty('form-data');
599
- return this;
600
- }
601
- /**
602
- *
603
- * @param retryTimes Max number of retries per call
604
- * @param retryDelay Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor)
605
- * @param retryCallback Will be called after request failed
606
- */
607
- retry(retryTimes, retryDelay = 0, retryOn = (attempt, error) => !!error) {
608
- this.option('retryTimes', retryTimes);
609
- this.option('retryDelay', retryDelay);
610
- this.option('retryOn', retryOn);
611
- return this;
612
- }
613
- redirect(mod) {
614
- this.requestContext.redirect = mod;
615
- return this;
616
- }
617
- credentials(mod) {
618
- this.requestContext.credentials = mod;
619
- return this;
620
- }
621
- mode(mod) {
622
- this.requestContext.mode = mod;
623
- return this;
624
- }
625
- flowControl(mode, signal) {
626
- const sig = signal ? signal : this.__identifier__;
627
- if (!sig) {
628
- throw new Exception('please set signal to .flowControl()');
629
- }
630
- const flowControl = {
631
- mode,
632
- signal: sig
633
- };
634
- this.option('flowControl', flowControl);
635
- return this;
636
- }
637
- timeout(milliseconds) {
638
- this.option('timeout', {
639
- millisecond: milliseconds
640
- });
641
- return this;
642
- }
643
- resolveWith(m) {
644
- this.option('resolveWith', m);
645
- return this;
646
- }
647
- }
648
-
649
- function abortFlowControlMiddleware() {
650
- return async function abortFlowControlMiddleware(ctx, next) {
651
- if (!ctx.options.flowControl || ctx.options.flowControl.mode !== 'abort') {
652
- await next();
653
- return;
654
- }
655
- const {
656
- signal
657
- } = ctx.options.flowControl;
658
- const key = typeof signal === 'string' ? signal : signal(ctx);
659
- if (!ctx.global.abortFlowControl) ctx.global.abortFlowControl = {};
660
- const abort = ctx.global.abortFlowControl[key];
661
- if (abort) {
662
- const reason = new DOMException('The previous request was not completed, so keq flowControl abort this request.', 'AbortError');
663
- abort(reason);
664
- }
665
- const fn = ctx.abort.bind(ctx);
666
- ctx.global.abortFlowControl[key] = fn;
667
- try {
668
- await next();
669
- } finally {
670
- if (ctx.global.abortFlowControl[key] === fn) {
671
- ctx.global.abortFlowControl[key] = undefined;
672
- }
673
- }
674
- };
675
- }
676
-
677
- function inferContentTypeByBody(body) {
678
- if (!body) return 'text/plain';
679
- if (typeof body === 'object') return 'application/json';
680
- return 'application/x-www-form-urlencoded';
681
- }
682
- function compileBody(ctx) {
683
- const request = ctx.request;
684
- const body = request.body;
685
- const contentType = request.headers.get('Content-Type');
686
- if (body === undefined) return;
687
- if (body === null) return 'null';
688
- if (typeof body === 'string') return body;
689
- if (typeof body === 'number') return String(body);
690
- if (isBuffer(body)) return body;
691
- if (isBlob(body)) return body;
692
- if (isArrayBuffer(body)) return body;
693
- if (isReadableStream(body)) return body;
694
- if (contentType === 'application/json') {
695
- return typeof body === 'object' ? JSON.stringify(body) : body;
696
- }
697
- if (contentType === 'application/x-www-form-urlencoded') {
698
- if (Array.isArray(body)) {
699
- throw new Exception('application/x-www-form-urlencoded cannot send array');
700
- }
701
- const params = new URLSearchParams();
702
- Object.entries(body).map(([key, value]) => {
703
- if (Array.isArray(value)) {
704
- for (const v of value) {
705
- params.append(key, v);
706
- }
707
- } else {
708
- params.append(key, value);
709
- }
710
- });
711
- return params;
712
- }
713
- if (contentType === 'multipart/form-data') {
714
- if (Array.isArray(body)) {
715
- throw new Exception('FormData cannot send array');
716
- }
717
- const form = new FormData();
718
- for (const [key, value] of Object.entries(body)) {
719
- if (Array.isArray(value)) {
720
- for (const v of value) {
721
- form.append(key, v);
722
- }
723
- } else {
724
- form.append(key, value);
725
- }
726
- }
727
- request.headers.delete('content-type');
728
- return form;
729
- }
730
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
731
- return body;
732
- }
733
- function fetchArgumentsMiddleware() {
734
- return async function fetchArgumentsMiddleware(ctx, next) {
735
- const request = ctx.request;
736
- const url = ctx.request.__url__.href;
737
- if (!request.headers.has('Content-Type') && request.body) {
738
- request.headers.set('Content-Type', inferContentTypeByBody(ctx.request.body));
739
- }
740
- const abortController = new AbortController();
741
- ctx[ABORT_PROPERTY] = abortController;
742
- const requestInit = {
743
- method: request.method.toUpperCase(),
744
- headers: request.headers,
745
- body: compileBody(ctx),
746
- cache: request.cache,
747
- credentials: request.credentials,
748
- integrity: request.integrity,
749
- keepalive: request.keepalive,
750
- mode: request.mode,
751
- redirect: request.redirect,
752
- referrer: request.referrer,
753
- referrerPolicy: request.referrerPolicy,
754
- signal: abortController.signal
755
- };
756
- ctx.fetchArguments = [url, requestInit];
757
- await next();
758
- };
759
- }
760
-
761
- /**
762
- * Send Request
763
- */
764
- function fetchMiddleware() {
765
- return async function fetchMiddleware(ctx) {
766
- const fetchArguments = ctx.fetchArguments;
767
- if (!fetchArguments) {
768
- throw new Exception('fetchArguments is required');
769
- }
770
- ctx.emitter.emit('fetch', ctx);
771
- const fetch = ctx.options.fetchAPI || globalThis.fetch;
772
- const response = await fetch(...fetchArguments);
773
- ctx.res = response;
774
- };
775
- }
776
-
777
- function createResponseProxy(res) {
778
- return new Proxy(res, {
779
- get(res, prop) {
780
- if (typeof prop === 'string') {
781
- if (['json', 'text', 'arrayBuffer', 'blob', 'buffer', 'formData'].includes(prop)) {
782
- /**
783
- * clone when invoking body, json, text, arrayBuffer, blob, buffer, formData
784
- * to avoid time-consuming cloning
785
- */
786
- return new Proxy(res[prop], {
787
- apply(target, thisArg, argArray) {
788
- const mirror = res.clone();
789
- return mirror[prop](...argArray);
790
- }
791
- });
792
- }
793
- if (prop === 'body') {
794
- const mirror = res.clone();
795
- return mirror.body;
796
- }
797
- }
798
- if (typeof res[prop] === 'function') {
799
- return res[prop].bind(res);
800
- }
801
- return res[prop];
802
- }
803
- });
804
- }
805
-
806
- function proxyResponseMiddleware() {
807
- return async function proxyResponseMiddleware(ctx, next) {
808
- await next();
809
- const res = ctx.res;
810
- if (res) ctx.response = createResponseProxy(res);
811
- };
812
- }
813
-
814
- function sleep(ms) {
815
- return new Promise(resolve => setTimeout(resolve, ms));
816
- }
817
- function retryMiddleware() {
818
- return async function retryMiddleware(ctx, next) {
819
- const retryTimes = Number.isInteger(ctx.options.retryTimes) ? ctx.options.retryTimes + 1 : 1;
820
- const delayOptions = ctx.options.retryDelay;
821
- const retryDelay = async (attempt, error, ctx) => {
822
- if (typeof delayOptions === 'function') {
823
- return delayOptions(attempt, error, ctx);
824
- } else if (typeof delayOptions === 'number') {
825
- return delayOptions;
826
- }
827
- return 0;
828
- };
829
- const retryOn = typeof ctx.options.retryOn === 'function' ? ctx.options.retryOn : (attempt, error) => !!error;
830
- // Avoid multiple middleware from being added repeatedly
831
- ctx.options = {
832
- ...ctx.options,
833
- retryTimes: undefined,
834
- retryDelay: undefined,
835
- retryOn: undefined
836
- };
837
- for (let i = 0; i < retryTimes; i++) {
838
- let err = null;
839
- try {
840
- ctx.fetchArguments = undefined;
841
- ctx.response = undefined;
842
- ctx.req = undefined;
843
- ctx.metadata.entryNextTimes = 0;
844
- ctx.metadata.outNextTimes = 0;
845
- await next();
846
- } catch (e) {
847
- err = e;
848
- }
849
- if (i === retryTimes - 1) {
850
- if (err) throw err;
851
- break;
852
- }
853
- if ((await retryOn(i, err, ctx)) === false) {
854
- if (err) throw err;
855
- break;
856
- }
857
- const delay = await retryDelay(i, err, ctx);
858
- ctx.retry = {
859
- attempt: i,
860
- error: err,
861
- delay
862
- };
863
- ctx.emitter.emit('retry', ctx);
864
- if (delay > 0) await sleep(delay);
865
- }
866
- };
867
- }
868
-
869
- function serialFlowControlMiddleware() {
870
- return async function serialFlowControlMiddleware(ctx, next) {
871
- if (!ctx.options.flowControl || ctx.options.flowControl.mode !== 'serial') {
872
- await next();
873
- return;
874
- }
875
- const {
876
- signal
877
- } = ctx.options.flowControl;
878
- const key = typeof signal === 'string' ? signal : signal(ctx);
879
- if (!ctx.global.serialFlowControl) ctx.global.serialFlowControl = {};
880
- if (!ctx.global.serialFlowControl[key]) {
881
- ctx.global.serialFlowControl[key] = fastq.promise(async next => {
882
- await next();
883
- }, 1);
884
- }
885
- const queue = ctx.global.serialFlowControl[key];
886
- await queue.push(next);
887
- };
888
- }
889
-
890
- function keqHostRoute(host) {
891
- return ctx => ctx.request.url.host === host;
892
- }
893
-
894
- function keqLocationRoute() {
895
- return ctx => isBrowser() && ctx.request.url.host === window.location.host;
896
- }
897
-
898
- function keqMethodRoute(method) {
899
- return ctx => ctx.request.method.toLowerCase() === method.toLowerCase();
900
- }
901
-
902
- function keqModuleRoute(moduleName) {
903
- if (!moduleName) {
904
- throw new Exception('Module name should not be empty');
905
- }
906
- return ctx => ctx.options.module?.name === moduleName;
907
- }
908
-
909
- function keqPathnameRoute(pathname) {
910
- return ctx => m.minimatch(ctx.request.url.pathname, pathname);
911
- }
912
-
913
- class KeqRouter {
914
- prependMiddlewares;
915
- constructor(prependMiddlewares = []) {
916
- this.prependMiddlewares = prependMiddlewares;
917
- }
918
- route(route, ...middlewares) {
919
- if (middlewares.length === 0) return this;
920
- const m = middlewares.length > 1 ? composeMiddleware(middlewares) : middlewares[0];
921
- this.prependMiddlewares.push(async function router(ctx, next) {
922
- if (route(ctx)) await m(ctx, next);else await next();
923
- });
924
- return this;
925
- }
926
- host(host, ...middlewares) {
927
- return this.route(keqHostRoute(host), ...middlewares);
928
- }
929
- method(method, ...middlewares) {
930
- return this.route(keqMethodRoute(method), ...middlewares);
931
- }
932
- pathname(pathname, ...middlewares) {
933
- return this.route(keqPathnameRoute(pathname), ...middlewares);
934
- }
935
- location(...middlewares) {
936
- return this.route(keqLocationRoute(), ...middlewares);
937
- }
938
- module(moduleName, ...middlewares) {
939
- return this.route(keqModuleRoute(moduleName), ...middlewares);
940
- }
941
- }
942
-
943
- function timeoutMiddleware() {
944
- return async function timeoutMiddleware(ctx, next) {
945
- if (!ctx.options.timeout || ctx.options.timeout.millisecond <= 0) {
946
- await next();
947
- return;
948
- }
949
- const millisecond = ctx.options.timeout.millisecond;
950
- ctx.emitter.on('fetch', ctx => {
951
- setTimeout(() => {
952
- const err = new DOMException(`keq request timeout(${millisecond}ms)`, 'AbortError');
953
- ctx.abort(err);
954
- }, millisecond);
955
- });
956
- await next();
957
- };
958
- }
959
-
960
- function getUniqueCodeIdentifier(depth = 0) {
961
- const err = new Error();
962
- if (!err.stack) return '';
963
- const stackLine = err.stack.split('\n')[depth + 2];
964
- return stackLine.trim();
965
- }
966
-
967
- /* eslint-disable @typescript-eslint/no-explicit-any */
968
- function createRequest(options) {
969
- let baseOrigin = options?.baseOrigin;
970
- if (!baseOrigin) {
971
- if (isBrowser()) {
972
- baseOrigin = location.origin;
973
- } else {
974
- baseOrigin = 'http://127.0.0.1';
975
- }
976
- }
977
- const appendMiddlewares = options?.initMiddlewares ? [...options.initMiddlewares] : [retryMiddleware(), serialFlowControlMiddleware(), abortFlowControlMiddleware(), timeoutMiddleware(), proxyResponseMiddleware(), fetchArgumentsMiddleware(), fetchMiddleware()];
978
- const prependMiddlewares = [];
979
- /**
980
- * share data between requests, used to implement flowControl
981
- * @description 跨请求共享数据,用于实现 flowControl的功能
982
- */
983
- const global = {};
984
- const formatUrl = url => {
985
- if (typeof url === 'string') {
986
- return new URL(url, baseOrigin);
987
- }
988
- return new URL(url.href);
989
- };
990
- const router = new KeqRouter(prependMiddlewares);
991
- const request = function (url, init) {
992
- const keq = new Keq(formatUrl(url), {
993
- ...init
994
- }, getUniqueCodeIdentifier(1), global);
995
- keq.appendMiddlewares(...appendMiddlewares);
996
- keq.prependMiddlewares(...prependMiddlewares);
997
- return keq;
998
- };
999
- request.baseOrigin = origin => {
1000
- baseOrigin = origin;
1001
- return request;
1002
- };
1003
- request.useRouter = function useRouter() {
1004
- return router;
1005
- };
1006
- request.get = function (url) {
1007
- const keq = new Keq(formatUrl(url), {
1008
- method: 'get'
1009
- }, getUniqueCodeIdentifier(1), global);
1010
- keq.appendMiddlewares(...appendMiddlewares);
1011
- keq.prependMiddlewares(...prependMiddlewares);
1012
- return keq;
1013
- };
1014
- request.put = function (url) {
1015
- const keq = new Keq(formatUrl(url), {
1016
- method: 'put'
1017
- }, getUniqueCodeIdentifier(1), global);
1018
- keq.appendMiddlewares(...appendMiddlewares);
1019
- keq.prependMiddlewares(...prependMiddlewares);
1020
- return keq;
1021
- };
1022
- request.delete = function (url) {
1023
- const keq = new Keq(formatUrl(url), {
1024
- method: 'delete'
1025
- }, getUniqueCodeIdentifier(1), global);
1026
- keq.appendMiddlewares(...appendMiddlewares);
1027
- keq.prependMiddlewares(...prependMiddlewares);
1028
- return keq;
1029
- };
1030
- request.del = request.delete;
1031
- request.post = function (url) {
1032
- const keq = new Keq(formatUrl(url), {
1033
- method: 'post'
1034
- }, getUniqueCodeIdentifier(1), global);
1035
- keq.appendMiddlewares(...appendMiddlewares);
1036
- keq.prependMiddlewares(...prependMiddlewares);
1037
- return keq;
1038
- };
1039
- request.head = function (url) {
1040
- const keq = new Keq(formatUrl(url), {
1041
- method: 'head'
1042
- }, getUniqueCodeIdentifier(1), global);
1043
- keq.appendMiddlewares(...appendMiddlewares);
1044
- keq.prependMiddlewares(...prependMiddlewares);
1045
- return keq;
1046
- };
1047
- request.patch = function (url) {
1048
- const keq = new Keq(formatUrl(url), {
1049
- method: 'patch'
1050
- }, getUniqueCodeIdentifier(1), global);
1051
- keq.appendMiddlewares(...appendMiddlewares);
1052
- keq.prependMiddlewares(...prependMiddlewares);
1053
- return keq;
1054
- };
1055
- request.options = function (url) {
1056
- const keq = new Keq(formatUrl(url), {
1057
- method: 'options'
1058
- }, getUniqueCodeIdentifier(1), global);
1059
- keq.appendMiddlewares(...appendMiddlewares);
1060
- keq.prependMiddlewares(...prependMiddlewares);
1061
- return keq;
1062
- };
1063
- request.use = function use(middleware, ...middlewares) {
1064
- prependMiddlewares.push(middleware, ...middlewares);
1065
- return request;
1066
- };
1067
- return request;
1068
- }
1069
-
1070
- const request = createRequest();
1071
-
1072
- function composeRoute(routes) {
1073
- if (!routes.length) {
1074
- throw new Exception('At least one route');
1075
- }
1076
- return async ctx => {
1077
- const results = await Promise.all(routes.map(route => route(ctx)));
1078
- return results.every(result => result === true);
1079
- };
1080
- }
1081
-
1082
- export { Keq, KeqRouter, composeMiddleware, composeRoute, createRequest, createResponseProxy, keqHostRoute, keqLocationRoute, keqMethodRoute, keqModuleRoute, keqPathnameRoute, request };