@microlink/mql 0.13.6 → 0.13.8

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/README.md CHANGED
@@ -10,4 +10,4 @@
10
10
  **microlink** © [Microlink](https://microlink.io), Released under the [MIT](https://github.com/microlinkhq/sdk/blob/master/LICENSE.md) License.<br>
11
11
  Authored and maintained by Kiko Beats with help from [contributors](https://github.com/microlinkhq/sdk/contributors).
12
12
 
13
- > [microlink.io](https://microlink.io) · GitHub [@MicrolinkHQ](https://github.com/microlinkhq) · Twitter [@microlinkhq](https://twitter.com/microlinkhq)
13
+ > [microlink.io](https://microlink.io) · GitHub [@MicrolinkHQ](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
@@ -27,18 +27,7 @@ function getAugmentedNamespace(n) {
27
27
  return a;
28
28
  }
29
29
 
30
- var lightweight$2 = {exports: {}};
31
-
32
- const REGEX_HTTP_PROTOCOL = /^https?:\/\//i;
33
-
34
- var lightweight$1 = url => {
35
- try {
36
- const { href } = new URL(url);
37
- return REGEX_HTTP_PROTOCOL.test(href) && href
38
- } catch (err) {
39
- return false
40
- }
41
- };
30
+ var lightweight$1 = {exports: {}};
42
31
 
43
32
  var dist = {};
44
33
 
@@ -70,32 +59,16 @@ function flattie(input, glue, toNull) {
70
59
 
71
60
  dist.flattie = flattie;
72
61
 
73
- // eslint-lint-disable-next-line @typescript-eslint/naming-convention
74
62
  class HTTPError extends Error {
63
+ response;
64
+ request;
65
+ options;
75
66
  constructor(response, request, options) {
76
67
  const code = (response.status || response.status === 0) ? response.status : '';
77
68
  const title = response.statusText || '';
78
69
  const status = `${code} ${title}`.trim();
79
70
  const reason = status ? `status code ${status}` : 'an unknown error';
80
- super(`Request failed with ${reason}`);
81
- Object.defineProperty(this, "response", {
82
- enumerable: true,
83
- configurable: true,
84
- writable: true,
85
- value: void 0
86
- });
87
- Object.defineProperty(this, "request", {
88
- enumerable: true,
89
- configurable: true,
90
- writable: true,
91
- value: void 0
92
- });
93
- Object.defineProperty(this, "options", {
94
- enumerable: true,
95
- configurable: true,
96
- writable: true,
97
- value: void 0
98
- });
71
+ super(`Request failed with ${reason}: ${request.method} ${request.url}`);
99
72
  this.name = 'HTTPError';
100
73
  this.response = response;
101
74
  this.request = request;
@@ -104,14 +77,9 @@ class HTTPError extends Error {
104
77
  }
105
78
 
106
79
  class TimeoutError extends Error {
80
+ request;
107
81
  constructor(request) {
108
- super('Request timed out');
109
- Object.defineProperty(this, "request", {
110
- enumerable: true,
111
- configurable: true,
112
- writable: true,
113
- value: void 0
114
- });
82
+ super(`Request timed out: ${request.method} ${request.url}`);
115
83
  this.name = 'TimeoutError';
116
84
  this.request = request;
117
85
  }
@@ -142,10 +110,22 @@ const mergeHeaders = (source1 = {}, source2 = {}) => {
142
110
  }
143
111
  return result;
144
112
  };
113
+ function newHookValue(original, incoming, property) {
114
+ return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
115
+ ? []
116
+ : deepMerge(original[property] ?? [], incoming[property] ?? []);
117
+ }
118
+ const mergeHooks = (original = {}, incoming = {}) => ({
119
+ beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
120
+ beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
121
+ afterResponse: newHookValue(original, incoming, 'afterResponse'),
122
+ beforeError: newHookValue(original, incoming, 'beforeError'),
123
+ });
145
124
  // TODO: Make this strongly-typed (no `any`).
146
125
  const deepMerge = (...sources) => {
147
126
  let returnValue = {};
148
127
  let headers = {};
128
+ let hooks = {};
149
129
  for (const source of sources) {
150
130
  if (Array.isArray(source)) {
151
131
  if (!Array.isArray(returnValue)) {
@@ -160,6 +140,10 @@ const deepMerge = (...sources) => {
160
140
  }
161
141
  returnValue = { ...returnValue, [key]: value };
162
142
  }
143
+ if (isObject$1(source.hooks)) {
144
+ hooks = mergeHooks(hooks, source.hooks);
145
+ returnValue.hooks = hooks;
146
+ }
163
147
  if (isObject$1(source.headers)) {
164
148
  headers = mergeHeaders(headers, source.headers);
165
149
  returnValue.headers = headers;
@@ -175,15 +159,24 @@ const supportsRequestStreams = (() => {
175
159
  const supportsReadableStream = typeof globalThis.ReadableStream === 'function';
176
160
  const supportsRequest = typeof globalThis.Request === 'function';
177
161
  if (supportsReadableStream && supportsRequest) {
178
- hasContentType = new globalThis.Request('https://empty.invalid', {
179
- body: new globalThis.ReadableStream(),
180
- method: 'POST',
181
- // @ts-expect-error - Types are outdated.
182
- get duplex() {
183
- duplexAccessed = true;
184
- return 'half';
185
- },
186
- }).headers.has('Content-Type');
162
+ try {
163
+ hasContentType = new globalThis.Request('https://empty.invalid', {
164
+ body: new globalThis.ReadableStream(),
165
+ method: 'POST',
166
+ // @ts-expect-error - Types are outdated.
167
+ get duplex() {
168
+ duplexAccessed = true;
169
+ return 'half';
170
+ },
171
+ }).headers.has('Content-Type');
172
+ }
173
+ catch (error) {
174
+ // QQBrowser on iOS throws "unsupported BodyInit type" error (see issue #581)
175
+ if (error instanceof Error && error.message === 'unsupported BodyInit type') {
176
+ return false;
177
+ }
178
+ throw error;
179
+ }
187
180
  }
188
181
  return duplexAccessed && !hasContentType;
189
182
  })();
@@ -204,6 +197,7 @@ const stop = Symbol('stop');
204
197
  const kyOptionKeys = {
205
198
  json: true,
206
199
  parseJson: true,
200
+ stringifyJson: true,
207
201
  searchParams: true,
208
202
  prefixUrl: true,
209
203
  retry: true,
@@ -261,7 +255,6 @@ const normalizeRetryOptions = (retry = {}) => {
261
255
  return {
262
256
  ...defaultRetryOptions,
263
257
  ...retry,
264
- afterStatusCodes: retryAfterStatusCodes,
265
258
  };
266
259
  };
267
260
 
@@ -377,47 +370,18 @@ class Ky {
377
370
  }
378
371
  return result;
379
372
  }
373
+ request;
374
+ abortController;
375
+ _retryCount = 0;
376
+ _input;
377
+ _options;
380
378
  // eslint-disable-next-line complexity
381
379
  constructor(input, options = {}) {
382
- Object.defineProperty(this, "request", {
383
- enumerable: true,
384
- configurable: true,
385
- writable: true,
386
- value: void 0
387
- });
388
- Object.defineProperty(this, "abortController", {
389
- enumerable: true,
390
- configurable: true,
391
- writable: true,
392
- value: void 0
393
- });
394
- Object.defineProperty(this, "_retryCount", {
395
- enumerable: true,
396
- configurable: true,
397
- writable: true,
398
- value: 0
399
- });
400
- Object.defineProperty(this, "_input", {
401
- enumerable: true,
402
- configurable: true,
403
- writable: true,
404
- value: void 0
405
- });
406
- Object.defineProperty(this, "_options", {
407
- enumerable: true,
408
- configurable: true,
409
- writable: true,
410
- value: void 0
411
- });
412
380
  this._input = input;
413
- const credentials = this._input instanceof Request && 'credentials' in Request.prototype
414
- ? this._input.credentials
415
- : undefined;
416
381
  this._options = {
417
- ...(credentials && { credentials }), // For exactOptionalPropertyTypes
418
382
  ...options,
419
383
  headers: mergeHeaders(this._input.headers, options.headers),
420
- hooks: deepMerge({
384
+ hooks: mergeHooks({
421
385
  beforeRequest: [],
422
386
  beforeRetry: [],
423
387
  beforeError: [],
@@ -445,18 +409,20 @@ class Ky {
445
409
  }
446
410
  if (supportsAbortController) {
447
411
  this.abortController = new globalThis.AbortController();
448
- if (this._options.signal) {
449
- const originalSignal = this._options.signal;
450
- this._options.signal.addEventListener('abort', () => {
451
- this.abortController.abort(originalSignal.reason);
452
- });
453
- }
412
+ const originalSignal = this._options.signal ?? this._input.signal;
413
+ originalSignal?.addEventListener('abort', () => {
414
+ this.abortController.abort(originalSignal.reason);
415
+ });
454
416
  this._options.signal = this.abortController.signal;
455
417
  }
456
418
  if (supportsRequestStreams) {
457
419
  // @ts-expect-error - Types are outdated.
458
420
  this._options.duplex = 'half';
459
421
  }
422
+ if (this._options.json !== undefined) {
423
+ this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json);
424
+ this._options.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json');
425
+ }
460
426
  this.request = new globalThis.Request(this._input, this._options);
461
427
  if (this._options.searchParams) {
462
428
  // eslint-disable-next-line unicorn/prevent-abbreviations
@@ -474,41 +440,38 @@ class Ky {
474
440
  // The spread of `this.request` is required as otherwise it misses the `duplex` option for some reason and throws.
475
441
  this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
476
442
  }
477
- if (this._options.json !== undefined) {
478
- this._options.body = JSON.stringify(this._options.json);
479
- this.request.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json');
480
- this.request = new globalThis.Request(this.request, { body: this._options.body });
481
- }
482
443
  }
483
444
  _calculateRetryDelay(error) {
484
445
  this._retryCount++;
485
- if (this._retryCount <= this._options.retry.limit && !(error instanceof TimeoutError)) {
486
- if (error instanceof HTTPError) {
487
- if (!this._options.retry.statusCodes.includes(error.response.status)) {
488
- return 0;
489
- }
490
- const retryAfter = error.response.headers.get('Retry-After');
491
- if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
492
- let after = Number(retryAfter);
493
- if (Number.isNaN(after)) {
494
- after = Date.parse(retryAfter) - Date.now();
495
- }
496
- else {
497
- after *= 1000;
498
- }
499
- if (this._options.retry.maxRetryAfter !== undefined && after > this._options.retry.maxRetryAfter) {
500
- return 0;
501
- }
502
- return after;
446
+ if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
447
+ throw error;
448
+ }
449
+ if (error instanceof HTTPError) {
450
+ if (!this._options.retry.statusCodes.includes(error.response.status)) {
451
+ throw error;
452
+ }
453
+ const retryAfter = error.response.headers.get('Retry-After')
454
+ ?? error.response.headers.get('RateLimit-Reset')
455
+ ?? error.response.headers.get('X-RateLimit-Reset') // GitHub
456
+ ?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter
457
+ if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
458
+ let after = Number(retryAfter) * 1000;
459
+ if (Number.isNaN(after)) {
460
+ after = Date.parse(retryAfter) - Date.now();
503
461
  }
504
- if (error.response.status === 413) {
505
- return 0;
462
+ else if (after >= Date.parse('2024-01-01')) {
463
+ // A large number is treated as a timestamp (fixed threshold protects against clock skew)
464
+ after -= Date.now();
506
465
  }
466
+ const max = this._options.retry.maxRetryAfter ?? after;
467
+ return after < max ? after : max;
468
+ }
469
+ if (error.response.status === 413) {
470
+ throw error;
507
471
  }
508
- const retryDelay = this._options.retry.delay(this._retryCount);
509
- return Math.min(this._options.retry.backoffLimit, retryDelay);
510
472
  }
511
- return 0;
473
+ const retryDelay = this._options.retry.delay(this._retryCount);
474
+ return Math.min(this._options.retry.backoffLimit, retryDelay);
512
475
  }
513
476
  _decorateResponse(response) {
514
477
  if (this._options.parseJson) {
@@ -522,24 +485,24 @@ class Ky {
522
485
  }
523
486
  catch (error) {
524
487
  const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
525
- if (ms !== 0 && this._retryCount > 0) {
526
- await delay(ms, { signal: this._options.signal });
527
- for (const hook of this._options.hooks.beforeRetry) {
528
- // eslint-disable-next-line no-await-in-loop
529
- const hookResult = await hook({
530
- request: this.request,
531
- options: this._options,
532
- error: error,
533
- retryCount: this._retryCount,
534
- });
535
- // If `stop` is returned from the hook, the retry process is stopped
536
- if (hookResult === stop) {
537
- return;
538
- }
488
+ if (this._retryCount < 1) {
489
+ throw error;
490
+ }
491
+ await delay(ms, { signal: this._options.signal });
492
+ for (const hook of this._options.hooks.beforeRetry) {
493
+ // eslint-disable-next-line no-await-in-loop
494
+ const hookResult = await hook({
495
+ request: this.request,
496
+ options: this._options,
497
+ error: error,
498
+ retryCount: this._retryCount,
499
+ });
500
+ // If `stop` is returned from the hook, the retry process is stopped
501
+ if (hookResult === stop) {
502
+ return;
539
503
  }
540
- return this._retry(function_);
541
504
  }
542
- throw error;
505
+ return this._retry(function_);
543
506
  }
544
507
  }
545
508
  async _fetch() {
@@ -555,10 +518,13 @@ class Ky {
555
518
  }
556
519
  }
557
520
  const nonRequestOptions = findUnknownOptions(this.request, this._options);
521
+ // Cloning is done here to prepare in advance for retries
522
+ const mainRequest = this.request;
523
+ this.request = mainRequest.clone();
558
524
  if (this._options.timeout === false) {
559
- return this._options.fetch(this.request.clone(), nonRequestOptions);
525
+ return this._options.fetch(mainRequest, nonRequestOptions);
560
526
  }
561
- return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options);
527
+ return timeout(mainRequest, nonRequestOptions, this.abortController, this._options);
562
528
  }
563
529
  /* istanbul ignore next */
564
530
  _stream(response, onDownloadProgress) {
@@ -613,7 +579,12 @@ const createInstance = (defaults) => {
613
579
  ky[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method }));
614
580
  }
615
581
  ky.create = (newDefaults) => createInstance(validateAndMerge(newDefaults));
616
- ky.extend = (newDefaults) => createInstance(validateAndMerge(defaults, newDefaults));
582
+ ky.extend = (newDefaults) => {
583
+ if (typeof newDefaults === 'function') {
584
+ newDefaults = newDefaults(defaults ?? {});
585
+ }
586
+ return createInstance(validateAndMerge(defaults, newDefaults));
587
+ };
617
588
  ky.stop = stop;
618
589
  return ky;
619
590
  };
@@ -626,7 +597,7 @@ var distribution = /*#__PURE__*/Object.freeze({
626
597
  default: ky$1
627
598
  });
628
599
 
629
- var require$$2 = /*@__PURE__*/getAugmentedNamespace(distribution);
600
+ var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution);
630
601
 
631
602
  const ENDPOINT = {
632
603
  FREE: 'https://api.microlink.io/',
@@ -658,15 +629,22 @@ const parseBody = (input, error, url) => {
658
629
  }
659
630
  };
660
631
 
632
+ const isURL = url => {
633
+ try {
634
+ return /^https?:\/\//i.test(new URL(url).href)
635
+ } catch (_) {
636
+ return false
637
+ }
638
+ };
639
+
661
640
  const factory$1 = streamResponseType => ({
662
641
  VERSION,
663
642
  MicrolinkError,
664
- urlHttp,
665
643
  got,
666
644
  flatten
667
645
  }) => {
668
646
  const assertUrl = (url = '') => {
669
- if (!urlHttp(url)) {
647
+ if (!isURL(url)) {
670
648
  const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`;
671
649
  throw new MicrolinkError({
672
650
  status: 'fail',
@@ -766,9 +744,8 @@ const factory$1 = streamResponseType => ({
766
744
 
767
745
  var factory_1 = factory$1;
768
746
 
769
- const urlHttp = lightweight$1;
770
747
  const { flattie: flatten } = dist;
771
- const { default: ky } = require$$2;
748
+ const { default: ky } = require$$1;
772
749
 
773
750
  const factory = factory_1('arrayBuffer');
774
751
 
@@ -815,22 +792,21 @@ got.stream = (...args) => ky(...args).then(res => res.body);
815
792
 
816
793
  const mql = factory({
817
794
  MicrolinkError,
818
- urlHttp,
819
795
  got,
820
796
  flatten,
821
- VERSION: '0.13.6'
797
+ VERSION: '0.13.8'
822
798
  });
823
799
 
824
- lightweight$2.exports = mql;
825
- var arrayBuffer = lightweight$2.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
826
- var extend = lightweight$2.exports.extend = mql.extend;
827
- var fetchFromApi = lightweight$2.exports.fetchFromApi = mql.fetchFromApi;
828
- var getApiUrl = lightweight$2.exports.getApiUrl = mql.getApiUrl;
829
- var mapRules = lightweight$2.exports.mapRules = mql.mapRules;
830
- var MicrolinkError_1 = lightweight$2.exports.MicrolinkError = mql.MicrolinkError;
831
- var version = lightweight$2.exports.version = mql.version;
800
+ lightweight$1.exports = mql;
801
+ var arrayBuffer = lightweight$1.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
802
+ var extend = lightweight$1.exports.extend = mql.extend;
803
+ var fetchFromApi = lightweight$1.exports.fetchFromApi = mql.fetchFromApi;
804
+ var getApiUrl = lightweight$1.exports.getApiUrl = mql.getApiUrl;
805
+ var mapRules = lightweight$1.exports.mapRules = mql.mapRules;
806
+ var MicrolinkError_1 = lightweight$1.exports.MicrolinkError = mql.MicrolinkError;
807
+ var version = lightweight$1.exports.version = mql.version;
832
808
 
833
- var lightweightExports = lightweight$2.exports;
809
+ var lightweightExports = lightweight$1.exports;
834
810
  var lightweight = /*@__PURE__*/getDefaultExportFromCjs(lightweightExports);
835
811
 
836
812
  export { MicrolinkError_1 as MicrolinkError, arrayBuffer, lightweight as default, extend, fetchFromApi, getApiUrl, mapRules, version };
@@ -33,18 +33,7 @@
33
33
  return a;
34
34
  }
35
35
 
36
- var lightweight$2 = {exports: {}};
37
-
38
- const REGEX_HTTP_PROTOCOL = /^https?:\/\//i;
39
-
40
- var lightweight$1 = url => {
41
- try {
42
- const { href } = new URL(url);
43
- return REGEX_HTTP_PROTOCOL.test(href) && href
44
- } catch (err) {
45
- return false
46
- }
47
- };
36
+ var lightweight$1 = {exports: {}};
48
37
 
49
38
  var dist = {};
50
39
 
@@ -76,32 +65,16 @@
76
65
 
77
66
  dist.flattie = flattie;
78
67
 
79
- // eslint-lint-disable-next-line @typescript-eslint/naming-convention
80
68
  class HTTPError extends Error {
69
+ response;
70
+ request;
71
+ options;
81
72
  constructor(response, request, options) {
82
73
  const code = (response.status || response.status === 0) ? response.status : '';
83
74
  const title = response.statusText || '';
84
75
  const status = `${code} ${title}`.trim();
85
76
  const reason = status ? `status code ${status}` : 'an unknown error';
86
- super(`Request failed with ${reason}`);
87
- Object.defineProperty(this, "response", {
88
- enumerable: true,
89
- configurable: true,
90
- writable: true,
91
- value: void 0
92
- });
93
- Object.defineProperty(this, "request", {
94
- enumerable: true,
95
- configurable: true,
96
- writable: true,
97
- value: void 0
98
- });
99
- Object.defineProperty(this, "options", {
100
- enumerable: true,
101
- configurable: true,
102
- writable: true,
103
- value: void 0
104
- });
77
+ super(`Request failed with ${reason}: ${request.method} ${request.url}`);
105
78
  this.name = 'HTTPError';
106
79
  this.response = response;
107
80
  this.request = request;
@@ -110,14 +83,9 @@
110
83
  }
111
84
 
112
85
  class TimeoutError extends Error {
86
+ request;
113
87
  constructor(request) {
114
- super('Request timed out');
115
- Object.defineProperty(this, "request", {
116
- enumerable: true,
117
- configurable: true,
118
- writable: true,
119
- value: void 0
120
- });
88
+ super(`Request timed out: ${request.method} ${request.url}`);
121
89
  this.name = 'TimeoutError';
122
90
  this.request = request;
123
91
  }
@@ -148,10 +116,22 @@
148
116
  }
149
117
  return result;
150
118
  };
119
+ function newHookValue(original, incoming, property) {
120
+ return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
121
+ ? []
122
+ : deepMerge(original[property] ?? [], incoming[property] ?? []);
123
+ }
124
+ const mergeHooks = (original = {}, incoming = {}) => ({
125
+ beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
126
+ beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
127
+ afterResponse: newHookValue(original, incoming, 'afterResponse'),
128
+ beforeError: newHookValue(original, incoming, 'beforeError'),
129
+ });
151
130
  // TODO: Make this strongly-typed (no `any`).
152
131
  const deepMerge = (...sources) => {
153
132
  let returnValue = {};
154
133
  let headers = {};
134
+ let hooks = {};
155
135
  for (const source of sources) {
156
136
  if (Array.isArray(source)) {
157
137
  if (!Array.isArray(returnValue)) {
@@ -166,6 +146,10 @@
166
146
  }
167
147
  returnValue = { ...returnValue, [key]: value };
168
148
  }
149
+ if (isObject$1(source.hooks)) {
150
+ hooks = mergeHooks(hooks, source.hooks);
151
+ returnValue.hooks = hooks;
152
+ }
169
153
  if (isObject$1(source.headers)) {
170
154
  headers = mergeHeaders(headers, source.headers);
171
155
  returnValue.headers = headers;
@@ -181,15 +165,24 @@
181
165
  const supportsReadableStream = typeof globalThis.ReadableStream === 'function';
182
166
  const supportsRequest = typeof globalThis.Request === 'function';
183
167
  if (supportsReadableStream && supportsRequest) {
184
- hasContentType = new globalThis.Request('https://empty.invalid', {
185
- body: new globalThis.ReadableStream(),
186
- method: 'POST',
187
- // @ts-expect-error - Types are outdated.
188
- get duplex() {
189
- duplexAccessed = true;
190
- return 'half';
191
- },
192
- }).headers.has('Content-Type');
168
+ try {
169
+ hasContentType = new globalThis.Request('https://empty.invalid', {
170
+ body: new globalThis.ReadableStream(),
171
+ method: 'POST',
172
+ // @ts-expect-error - Types are outdated.
173
+ get duplex() {
174
+ duplexAccessed = true;
175
+ return 'half';
176
+ },
177
+ }).headers.has('Content-Type');
178
+ }
179
+ catch (error) {
180
+ // QQBrowser on iOS throws "unsupported BodyInit type" error (see issue #581)
181
+ if (error instanceof Error && error.message === 'unsupported BodyInit type') {
182
+ return false;
183
+ }
184
+ throw error;
185
+ }
193
186
  }
194
187
  return duplexAccessed && !hasContentType;
195
188
  })();
@@ -210,6 +203,7 @@
210
203
  const kyOptionKeys = {
211
204
  json: true,
212
205
  parseJson: true,
206
+ stringifyJson: true,
213
207
  searchParams: true,
214
208
  prefixUrl: true,
215
209
  retry: true,
@@ -267,7 +261,6 @@
267
261
  return {
268
262
  ...defaultRetryOptions,
269
263
  ...retry,
270
- afterStatusCodes: retryAfterStatusCodes,
271
264
  };
272
265
  };
273
266
 
@@ -383,47 +376,18 @@
383
376
  }
384
377
  return result;
385
378
  }
379
+ request;
380
+ abortController;
381
+ _retryCount = 0;
382
+ _input;
383
+ _options;
386
384
  // eslint-disable-next-line complexity
387
385
  constructor(input, options = {}) {
388
- Object.defineProperty(this, "request", {
389
- enumerable: true,
390
- configurable: true,
391
- writable: true,
392
- value: void 0
393
- });
394
- Object.defineProperty(this, "abortController", {
395
- enumerable: true,
396
- configurable: true,
397
- writable: true,
398
- value: void 0
399
- });
400
- Object.defineProperty(this, "_retryCount", {
401
- enumerable: true,
402
- configurable: true,
403
- writable: true,
404
- value: 0
405
- });
406
- Object.defineProperty(this, "_input", {
407
- enumerable: true,
408
- configurable: true,
409
- writable: true,
410
- value: void 0
411
- });
412
- Object.defineProperty(this, "_options", {
413
- enumerable: true,
414
- configurable: true,
415
- writable: true,
416
- value: void 0
417
- });
418
386
  this._input = input;
419
- const credentials = this._input instanceof Request && 'credentials' in Request.prototype
420
- ? this._input.credentials
421
- : undefined;
422
387
  this._options = {
423
- ...(credentials && { credentials }), // For exactOptionalPropertyTypes
424
388
  ...options,
425
389
  headers: mergeHeaders(this._input.headers, options.headers),
426
- hooks: deepMerge({
390
+ hooks: mergeHooks({
427
391
  beforeRequest: [],
428
392
  beforeRetry: [],
429
393
  beforeError: [],
@@ -451,18 +415,20 @@
451
415
  }
452
416
  if (supportsAbortController) {
453
417
  this.abortController = new globalThis.AbortController();
454
- if (this._options.signal) {
455
- const originalSignal = this._options.signal;
456
- this._options.signal.addEventListener('abort', () => {
457
- this.abortController.abort(originalSignal.reason);
458
- });
459
- }
418
+ const originalSignal = this._options.signal ?? this._input.signal;
419
+ originalSignal?.addEventListener('abort', () => {
420
+ this.abortController.abort(originalSignal.reason);
421
+ });
460
422
  this._options.signal = this.abortController.signal;
461
423
  }
462
424
  if (supportsRequestStreams) {
463
425
  // @ts-expect-error - Types are outdated.
464
426
  this._options.duplex = 'half';
465
427
  }
428
+ if (this._options.json !== undefined) {
429
+ this._options.body = this._options.stringifyJson?.(this._options.json) ?? JSON.stringify(this._options.json);
430
+ this._options.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json');
431
+ }
466
432
  this.request = new globalThis.Request(this._input, this._options);
467
433
  if (this._options.searchParams) {
468
434
  // eslint-disable-next-line unicorn/prevent-abbreviations
@@ -480,41 +446,38 @@
480
446
  // The spread of `this.request` is required as otherwise it misses the `duplex` option for some reason and throws.
481
447
  this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
482
448
  }
483
- if (this._options.json !== undefined) {
484
- this._options.body = JSON.stringify(this._options.json);
485
- this.request.headers.set('content-type', this._options.headers.get('content-type') ?? 'application/json');
486
- this.request = new globalThis.Request(this.request, { body: this._options.body });
487
- }
488
449
  }
489
450
  _calculateRetryDelay(error) {
490
451
  this._retryCount++;
491
- if (this._retryCount <= this._options.retry.limit && !(error instanceof TimeoutError)) {
492
- if (error instanceof HTTPError) {
493
- if (!this._options.retry.statusCodes.includes(error.response.status)) {
494
- return 0;
495
- }
496
- const retryAfter = error.response.headers.get('Retry-After');
497
- if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
498
- let after = Number(retryAfter);
499
- if (Number.isNaN(after)) {
500
- after = Date.parse(retryAfter) - Date.now();
501
- }
502
- else {
503
- after *= 1000;
504
- }
505
- if (this._options.retry.maxRetryAfter !== undefined && after > this._options.retry.maxRetryAfter) {
506
- return 0;
507
- }
508
- return after;
452
+ if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) {
453
+ throw error;
454
+ }
455
+ if (error instanceof HTTPError) {
456
+ if (!this._options.retry.statusCodes.includes(error.response.status)) {
457
+ throw error;
458
+ }
459
+ const retryAfter = error.response.headers.get('Retry-After')
460
+ ?? error.response.headers.get('RateLimit-Reset')
461
+ ?? error.response.headers.get('X-RateLimit-Reset') // GitHub
462
+ ?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter
463
+ if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) {
464
+ let after = Number(retryAfter) * 1000;
465
+ if (Number.isNaN(after)) {
466
+ after = Date.parse(retryAfter) - Date.now();
509
467
  }
510
- if (error.response.status === 413) {
511
- return 0;
468
+ else if (after >= Date.parse('2024-01-01')) {
469
+ // A large number is treated as a timestamp (fixed threshold protects against clock skew)
470
+ after -= Date.now();
512
471
  }
472
+ const max = this._options.retry.maxRetryAfter ?? after;
473
+ return after < max ? after : max;
474
+ }
475
+ if (error.response.status === 413) {
476
+ throw error;
513
477
  }
514
- const retryDelay = this._options.retry.delay(this._retryCount);
515
- return Math.min(this._options.retry.backoffLimit, retryDelay);
516
478
  }
517
- return 0;
479
+ const retryDelay = this._options.retry.delay(this._retryCount);
480
+ return Math.min(this._options.retry.backoffLimit, retryDelay);
518
481
  }
519
482
  _decorateResponse(response) {
520
483
  if (this._options.parseJson) {
@@ -528,24 +491,24 @@
528
491
  }
529
492
  catch (error) {
530
493
  const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout);
531
- if (ms !== 0 && this._retryCount > 0) {
532
- await delay(ms, { signal: this._options.signal });
533
- for (const hook of this._options.hooks.beforeRetry) {
534
- // eslint-disable-next-line no-await-in-loop
535
- const hookResult = await hook({
536
- request: this.request,
537
- options: this._options,
538
- error: error,
539
- retryCount: this._retryCount,
540
- });
541
- // If `stop` is returned from the hook, the retry process is stopped
542
- if (hookResult === stop) {
543
- return;
544
- }
494
+ if (this._retryCount < 1) {
495
+ throw error;
496
+ }
497
+ await delay(ms, { signal: this._options.signal });
498
+ for (const hook of this._options.hooks.beforeRetry) {
499
+ // eslint-disable-next-line no-await-in-loop
500
+ const hookResult = await hook({
501
+ request: this.request,
502
+ options: this._options,
503
+ error: error,
504
+ retryCount: this._retryCount,
505
+ });
506
+ // If `stop` is returned from the hook, the retry process is stopped
507
+ if (hookResult === stop) {
508
+ return;
545
509
  }
546
- return this._retry(function_);
547
510
  }
548
- throw error;
511
+ return this._retry(function_);
549
512
  }
550
513
  }
551
514
  async _fetch() {
@@ -561,10 +524,13 @@
561
524
  }
562
525
  }
563
526
  const nonRequestOptions = findUnknownOptions(this.request, this._options);
527
+ // Cloning is done here to prepare in advance for retries
528
+ const mainRequest = this.request;
529
+ this.request = mainRequest.clone();
564
530
  if (this._options.timeout === false) {
565
- return this._options.fetch(this.request.clone(), nonRequestOptions);
531
+ return this._options.fetch(mainRequest, nonRequestOptions);
566
532
  }
567
- return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options);
533
+ return timeout(mainRequest, nonRequestOptions, this.abortController, this._options);
568
534
  }
569
535
  /* istanbul ignore next */
570
536
  _stream(response, onDownloadProgress) {
@@ -619,7 +585,12 @@
619
585
  ky[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method }));
620
586
  }
621
587
  ky.create = (newDefaults) => createInstance(validateAndMerge(newDefaults));
622
- ky.extend = (newDefaults) => createInstance(validateAndMerge(defaults, newDefaults));
588
+ ky.extend = (newDefaults) => {
589
+ if (typeof newDefaults === 'function') {
590
+ newDefaults = newDefaults(defaults ?? {});
591
+ }
592
+ return createInstance(validateAndMerge(defaults, newDefaults));
593
+ };
623
594
  ky.stop = stop;
624
595
  return ky;
625
596
  };
@@ -632,7 +603,7 @@
632
603
  default: ky$1
633
604
  });
634
605
 
635
- var require$$2 = /*@__PURE__*/getAugmentedNamespace(distribution);
606
+ var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution);
636
607
 
637
608
  const ENDPOINT = {
638
609
  FREE: 'https://api.microlink.io/',
@@ -664,15 +635,22 @@
664
635
  }
665
636
  };
666
637
 
638
+ const isURL = url => {
639
+ try {
640
+ return /^https?:\/\//i.test(new URL(url).href)
641
+ } catch (_) {
642
+ return false
643
+ }
644
+ };
645
+
667
646
  const factory$1 = streamResponseType => ({
668
647
  VERSION,
669
648
  MicrolinkError,
670
- urlHttp,
671
649
  got,
672
650
  flatten
673
651
  }) => {
674
652
  const assertUrl = (url = '') => {
675
- if (!urlHttp(url)) {
653
+ if (!isURL(url)) {
676
654
  const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`;
677
655
  throw new MicrolinkError({
678
656
  status: 'fail',
@@ -772,9 +750,8 @@
772
750
 
773
751
  var factory_1 = factory$1;
774
752
 
775
- const urlHttp = lightweight$1;
776
753
  const { flattie: flatten } = dist;
777
- const { default: ky } = require$$2;
754
+ const { default: ky } = require$$1;
778
755
 
779
756
  const factory = factory_1('arrayBuffer');
780
757
 
@@ -821,22 +798,21 @@
821
798
 
822
799
  const mql = factory({
823
800
  MicrolinkError,
824
- urlHttp,
825
801
  got,
826
802
  flatten,
827
- VERSION: '0.13.6'
803
+ VERSION: '0.13.8'
828
804
  });
829
805
 
830
- lightweight$2.exports = mql;
831
- var arrayBuffer = lightweight$2.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
832
- var extend = lightweight$2.exports.extend = mql.extend;
833
- var fetchFromApi = lightweight$2.exports.fetchFromApi = mql.fetchFromApi;
834
- var getApiUrl = lightweight$2.exports.getApiUrl = mql.getApiUrl;
835
- var mapRules = lightweight$2.exports.mapRules = mql.mapRules;
836
- var MicrolinkError_1 = lightweight$2.exports.MicrolinkError = mql.MicrolinkError;
837
- var version = lightweight$2.exports.version = mql.version;
806
+ lightweight$1.exports = mql;
807
+ var arrayBuffer = lightweight$1.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
808
+ var extend = lightweight$1.exports.extend = mql.extend;
809
+ var fetchFromApi = lightweight$1.exports.fetchFromApi = mql.fetchFromApi;
810
+ var getApiUrl = lightweight$1.exports.getApiUrl = mql.getApiUrl;
811
+ var mapRules = lightweight$1.exports.mapRules = mql.mapRules;
812
+ var MicrolinkError_1 = lightweight$1.exports.MicrolinkError = mql.MicrolinkError;
813
+ var version = lightweight$1.exports.version = mql.version;
838
814
 
839
- var lightweightExports = lightweight$2.exports;
815
+ var lightweightExports = lightweight$1.exports;
840
816
  var lightweight = /*@__PURE__*/getDefaultExportFromCjs(lightweightExports);
841
817
 
842
818
  exports.MicrolinkError = MicrolinkError_1;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@microlink/mql",
3
3
  "description": "Microlink Query Language. The official HTTP client to interact with Microlink API for Node.js, browsers & Deno.",
4
4
  "homepage": "https://microlink.io/mql",
5
- "version": "0.13.6",
5
+ "version": "0.13.8",
6
6
  "types": "lightweight/index.d.ts",
7
7
  "exports": {
8
8
  "require": "./src/node.js",
@@ -48,14 +48,13 @@
48
48
  "dependencies": {
49
49
  "flattie": "~1.1.0",
50
50
  "got": "~11.8.6",
51
- "url-http": "~1.2.0",
52
51
  "whoops": "~4.1.7"
53
52
  },
54
53
  "devDependencies": {
55
54
  "@commitlint/cli": "latest",
56
55
  "@commitlint/config-conventional": "latest",
57
56
  "@ksmithut/prettier-standard": "latest",
58
- "@rollup/plugin-commonjs": "latest",
57
+ "@rollup/plugin-commonjs": "26",
59
58
  "@rollup/plugin-node-resolve": "latest",
60
59
  "@rollup/plugin-replace": "latest",
61
60
  "@rollup/plugin-terser": "latest",
@@ -67,7 +66,6 @@
67
66
  "github-generate-release": "latest",
68
67
  "ky": "latest",
69
68
  "nano-staged": "latest",
70
- "npm-check-updates": "latest",
71
69
  "prettier-standard": "latest",
72
70
  "rollup": "latest",
73
71
  "rollup-plugin-filesize": "latest",
@@ -98,14 +96,11 @@
98
96
  "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
99
97
  "prebuild": "npm run clean:build",
100
98
  "prepublishOnly": "npm run build",
101
- "prerelease": "npm run update:check",
102
99
  "pretest": "npm run lint && npm run build",
103
100
  "release": "standard-version -a",
104
101
  "release:github": "github-generate-release",
105
102
  "release:tags": "git push --follow-tags origin HEAD:master",
106
- "test": "c8 ava --verbose",
107
- "update": "ncu -u",
108
- "update:check": "ncu -- --error-level 2"
103
+ "test": "c8 ava --verbose"
109
104
  },
110
105
  "license": "MIT",
111
106
  "ava": {
@@ -118,7 +113,12 @@
118
113
  "commitlint": {
119
114
  "extends": [
120
115
  "@commitlint/config-conventional"
121
- ]
116
+ ],
117
+ "rules": {
118
+ "body-max-line-length": [
119
+ 0
120
+ ]
121
+ }
122
122
  },
123
123
  "nano-staged": {
124
124
  "*.js": [
package/src/factory.js CHANGED
@@ -28,15 +28,22 @@ const parseBody = (input, error, url) => {
28
28
  }
29
29
  }
30
30
 
31
+ const isURL = url => {
32
+ try {
33
+ return /^https?:\/\//i.test(new URL(url).href)
34
+ } catch (_) {
35
+ return false
36
+ }
37
+ }
38
+
31
39
  const factory = streamResponseType => ({
32
40
  VERSION,
33
41
  MicrolinkError,
34
- urlHttp,
35
42
  got,
36
43
  flatten
37
44
  }) => {
38
45
  const assertUrl = (url = '') => {
39
- if (!urlHttp(url)) {
46
+ if (!isURL(url)) {
40
47
  const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`
41
48
  throw new MicrolinkError({
42
49
  status: 'fail',
package/src/node.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const mql = require('./factory')('buffer')({
2
2
  MicrolinkError: require('whoops')('MicrolinkError'),
3
- urlHttp: require('url-http/lightweight'),
4
3
  got: require('got').extend({ headers: { 'user-agent': undefined } }),
5
4
  flatten: require('flattie').flattie,
6
5
  VERSION: require('../package.json').version