@naturalcycles/js-lib 14.147.0 → 14.148.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.
@@ -51,6 +51,7 @@ export declare class Fetcher {
51
51
  callNativeFetch(url: string, init: RequestInit): Promise<Response>;
52
52
  private onNotOkResponse;
53
53
  private processRetry;
54
+ private getRetryTimeout;
54
55
  /**
55
56
  * Default is yes,
56
57
  * unless there's reason not to (e.g method is POST).
@@ -151,6 +151,8 @@ class Fetcher {
151
151
  try {
152
152
  res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
153
153
  res.ok = res.fetchResponse.ok;
154
+ // important to set it to undefined, otherwise it can keep the previous value (from previous try)
155
+ res.err = undefined;
154
156
  }
155
157
  catch (err) {
156
158
  // For example, CORS error would result in "TypeError: failed to fetch" here
@@ -308,12 +310,11 @@ class Fetcher {
308
310
  // but we should log all previous errors, otherwise they are lost.
309
311
  // Here is the right place where we know it's not the "last error"
310
312
  if (res.err) {
311
- const { retryAttempt } = retryStatus;
312
313
  this.cfg.logger.error([
313
314
  ' <<',
314
315
  res.fetchResponse?.status || 0,
315
316
  res.signature,
316
- `try#${retryAttempt + 1}/${count + 1}`,
317
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
317
318
  (0, time_util_1._since)(res.req.started),
318
319
  ]
319
320
  .filter(Boolean)
@@ -321,8 +322,36 @@ class Fetcher {
321
322
  }
322
323
  retryStatus.retryAttempt++;
323
324
  retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
324
- const noise = Math.random() * 500;
325
- await (0, pDelay_1.pDelay)(retryStatus.retryTimeout + noise);
325
+ await (0, pDelay_1.pDelay)(this.getRetryTimeout(res));
326
+ }
327
+ getRetryTimeout(res) {
328
+ let timeout = 0;
329
+ // Handling http 429 with specific retry headers
330
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
331
+ if (res.fetchResponse && [429, 503].includes(res.fetchResponse.status)) {
332
+ const retryAfterStr = res.fetchResponse.headers.get('retry-after') ??
333
+ res.fetchResponse.headers.get('x-ratelimit-reset');
334
+ if (retryAfterStr) {
335
+ if (Number(retryAfterStr)) {
336
+ timeout = Number(retryAfterStr) * 1000;
337
+ }
338
+ else {
339
+ const date = new Date(retryAfterStr);
340
+ if (!isNaN(date)) {
341
+ timeout = Number(date) - Date.now();
342
+ }
343
+ }
344
+ this.cfg.logger.log(`retry-after: ${retryAfterStr}`);
345
+ if (!timeout) {
346
+ this.cfg.logger.warn(`retry-after could not be parsed`);
347
+ }
348
+ }
349
+ }
350
+ if (!timeout) {
351
+ const noise = Math.random() * 500;
352
+ timeout = res.retryStatus.retryTimeout + noise;
353
+ }
354
+ return timeout;
326
355
  }
327
356
  /**
328
357
  * Default is yes,
@@ -136,6 +136,8 @@ export class Fetcher {
136
136
  try {
137
137
  res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
138
138
  res.ok = res.fetchResponse.ok;
139
+ // important to set it to undefined, otherwise it can keep the previous value (from previous try)
140
+ res.err = undefined;
139
141
  }
140
142
  catch (err) {
141
143
  // For example, CORS error would result in "TypeError: failed to fetch" here
@@ -295,12 +297,11 @@ export class Fetcher {
295
297
  // but we should log all previous errors, otherwise they are lost.
296
298
  // Here is the right place where we know it's not the "last error"
297
299
  if (res.err) {
298
- const { retryAttempt } = retryStatus;
299
300
  this.cfg.logger.error([
300
301
  ' <<',
301
302
  ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0,
302
303
  res.signature,
303
- `try#${retryAttempt + 1}/${count + 1}`,
304
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
304
305
  _since(res.req.started),
305
306
  ]
306
307
  .filter(Boolean)
@@ -308,8 +309,36 @@ export class Fetcher {
308
309
  }
309
310
  retryStatus.retryAttempt++;
310
311
  retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
311
- const noise = Math.random() * 500;
312
- await pDelay(retryStatus.retryTimeout + noise);
312
+ await pDelay(this.getRetryTimeout(res));
313
+ }
314
+ getRetryTimeout(res) {
315
+ var _a;
316
+ let timeout = 0;
317
+ // Handling http 429 with specific retry headers
318
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
319
+ if (res.fetchResponse && [429, 503].includes(res.fetchResponse.status)) {
320
+ const retryAfterStr = (_a = res.fetchResponse.headers.get('retry-after')) !== null && _a !== void 0 ? _a : res.fetchResponse.headers.get('x-ratelimit-reset');
321
+ if (retryAfterStr) {
322
+ if (Number(retryAfterStr)) {
323
+ timeout = Number(retryAfterStr) * 1000;
324
+ }
325
+ else {
326
+ const date = new Date(retryAfterStr);
327
+ if (!isNaN(date)) {
328
+ timeout = Number(date) - Date.now();
329
+ }
330
+ }
331
+ this.cfg.logger.log(`retry-after: ${retryAfterStr}`);
332
+ if (!timeout) {
333
+ this.cfg.logger.warn(`retry-after could not be parsed`);
334
+ }
335
+ }
336
+ }
337
+ if (!timeout) {
338
+ const noise = Math.random() * 500;
339
+ timeout = res.retryStatus.retryTimeout + noise;
340
+ }
341
+ return timeout;
313
342
  }
314
343
  /**
315
344
  * Default is yes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.147.0",
3
+ "version": "14.148.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -15,6 +15,7 @@ import {
15
15
  import { pDelay } from '../promise/pDelay'
16
16
  import { _jsonParse, _jsonParseIfPossible } from '../string/json.util'
17
17
  import { _since } from '../time/time.util'
18
+ import { NumberOfMilliseconds } from '../types'
18
19
  import type {
19
20
  FetcherAfterResponseHook,
20
21
  FetcherBeforeRequestHook,
@@ -224,6 +225,8 @@ export class Fetcher {
224
225
  try {
225
226
  res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init)
226
227
  res.ok = res.fetchResponse.ok
228
+ // important to set it to undefined, otherwise it can keep the previous value (from previous try)
229
+ res.err = undefined
227
230
  } catch (err) {
228
231
  // For example, CORS error would result in "TypeError: failed to fetch" here
229
232
  res.err = err as Error
@@ -405,13 +408,12 @@ export class Fetcher {
405
408
  // but we should log all previous errors, otherwise they are lost.
406
409
  // Here is the right place where we know it's not the "last error"
407
410
  if (res.err) {
408
- const { retryAttempt } = retryStatus
409
411
  this.cfg.logger.error(
410
412
  [
411
413
  ' <<',
412
414
  res.fetchResponse?.status || 0,
413
415
  res.signature,
414
- `try#${retryAttempt + 1}/${count + 1}`,
416
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
415
417
  _since(res.req.started),
416
418
  ]
417
419
  .filter(Boolean)
@@ -423,8 +425,41 @@ export class Fetcher {
423
425
  retryStatus.retryAttempt++
424
426
  retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
425
427
 
426
- const noise = Math.random() * 500
427
- await pDelay(retryStatus.retryTimeout + noise)
428
+ await pDelay(this.getRetryTimeout(res))
429
+ }
430
+
431
+ private getRetryTimeout(res: FetcherResponse): NumberOfMilliseconds {
432
+ let timeout: NumberOfMilliseconds = 0
433
+
434
+ // Handling http 429 with specific retry headers
435
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
436
+ if (res.fetchResponse && [429, 503].includes(res.fetchResponse.status)) {
437
+ const retryAfterStr =
438
+ res.fetchResponse.headers.get('retry-after') ??
439
+ res.fetchResponse.headers.get('x-ratelimit-reset')
440
+ if (retryAfterStr) {
441
+ if (Number(retryAfterStr)) {
442
+ timeout = Number(retryAfterStr) * 1000
443
+ } else {
444
+ const date = new Date(retryAfterStr)
445
+ if (!isNaN(date as any)) {
446
+ timeout = Number(date) - Date.now()
447
+ }
448
+ }
449
+
450
+ this.cfg.logger.log(`retry-after: ${retryAfterStr}`)
451
+ if (!timeout) {
452
+ this.cfg.logger.warn(`retry-after could not be parsed`)
453
+ }
454
+ }
455
+ }
456
+
457
+ if (!timeout) {
458
+ const noise = Math.random() * 500
459
+ timeout = res.retryStatus.retryTimeout + noise
460
+ }
461
+
462
+ return timeout
428
463
  }
429
464
 
430
465
  /**