@ethlete/query 2.1.0 → 2.2.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.
@@ -71,7 +71,7 @@ const queryStateDoesNotContainKeyError = (data) => new QueryError('005', 'The qu
71
71
  const queryStateCannotTransformError = (data) => new QueryError('006', 'The query state cannot be transformed because it is not loading', data);
72
72
  const invalidBodyError = (data) => new QueryError('007', 'The body is not valid or upsupported', data);
73
73
 
74
- const isRequestError = (error) => error instanceof Object && 'code' in error && 'message' in error;
74
+ const isRequestError = (error) => error instanceof Object && 'status' in error && 'statusText' in error && 'url' in error;
75
75
  const buildRoute = (options) => {
76
76
  if (options.base.endsWith('/')) {
77
77
  throw invalidBaseRouteError(options.base);
@@ -163,6 +163,9 @@ const extractExpiresInSeconds = (headers) => {
163
163
  const ageSeconds = parseInt(age);
164
164
  expiresIn = maxAge - ageSeconds;
165
165
  }
166
+ else if (maxAge) {
167
+ expiresIn = maxAge / 2; // We assume the response is half way to its expiration
168
+ }
166
169
  else if (expires) {
167
170
  // Used by some apis to tell the response will never expire
168
171
  // In this case we let the response expire after 1 hour
@@ -268,6 +271,37 @@ const getResponseUrl = (xhr) => {
268
271
  }
269
272
  return null;
270
273
  };
274
+ const shouldRetryRequest = (config) => {
275
+ const defaultRetryDelay = 1000 + 1000 * config.currentRetryCount;
276
+ if (config.currentRetryCount > 3) {
277
+ return { retry: false };
278
+ }
279
+ if (!isRequestError(config.error)) {
280
+ return { retry: false };
281
+ }
282
+ const { status } = config.error;
283
+ // Retry on 5xx errors
284
+ if (status >= 500) {
285
+ return { retry: true, delay: defaultRetryDelay };
286
+ }
287
+ // Retry on 408 or 425 errors
288
+ if (status === 408 /* HttpStatusCode.RequestTimeout */ || status === 425 /* HttpStatusCode.TooEarly */) {
289
+ return { retry: true, delay: defaultRetryDelay };
290
+ }
291
+ // Retry on 429 errors
292
+ if (status === 429 /* HttpStatusCode.TooManyRequests */) {
293
+ const retryAfter = config.headers['retry-after'] ||
294
+ config.headers['Retry-After'] ||
295
+ config.headers['x-retry-after'] ||
296
+ config.headers['X-Retry-After'];
297
+ if (retryAfter) {
298
+ const delay = parseInt(retryAfter) * 1000;
299
+ return { retry: true, delay: Number.isNaN(delay) ? defaultRetryDelay : delay };
300
+ }
301
+ return { retry: true, delay: defaultRetryDelay };
302
+ }
303
+ return { retry: false };
304
+ };
271
305
 
272
306
  const XSSI_PREFIX = /^\)\]\}',?\n/;
273
307
  const request = (config) => {
@@ -275,25 +309,32 @@ const request = (config) => {
275
309
  const responseType = config.responseType || 'json';
276
310
  const body = config.body || null;
277
311
  const url = config.urlWithParams.split('?')[0];
312
+ const retryFn = config.retryFn || shouldRetryRequest;
313
+ let currentRetryCount = 0;
314
+ let retryTimeout = null;
278
315
  return new Observable((observer) => {
279
316
  const xhr = new XMLHttpRequest();
280
- xhr.open(config.method, config.urlWithParams);
281
- if (config.withCredentials) {
282
- xhr.withCredentials = true;
283
- }
284
- forEachHeader(headers, (name, value) => xhr.setRequestHeader(name, value));
285
- if (!hasHeader(headers, 'Accept')) {
286
- xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
287
- }
288
- if (!hasHeader(headers, 'Content-Type')) {
289
- const detectedType = detectContentTypeHeader(body);
290
- if (detectedType) {
291
- xhr.setRequestHeader('Content-Type', detectedType);
292
- }
293
- }
294
- xhr.responseType = responseType !== 'json' ? responseType : 'text';
295
317
  const reqBody = serializeBody(body);
296
318
  let headerResponse = null;
319
+ const setupXhr = () => {
320
+ headerResponse = null;
321
+ xhr.open(config.method, config.urlWithParams);
322
+ if (config.withCredentials) {
323
+ xhr.withCredentials = true;
324
+ }
325
+ forEachHeader(headers, (name, value) => xhr.setRequestHeader(name, value));
326
+ if (!hasHeader(headers, 'Accept')) {
327
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
328
+ }
329
+ if (!hasHeader(headers, 'Content-Type')) {
330
+ const detectedType = detectContentTypeHeader(body);
331
+ if (detectedType) {
332
+ xhr.setRequestHeader('Content-Type', detectedType);
333
+ }
334
+ }
335
+ xhr.responseType = responseType !== 'json' ? responseType : 'text';
336
+ };
337
+ setupXhr();
297
338
  const partialFromXhr = () => {
298
339
  if (headerResponse !== null) {
299
340
  return headerResponse;
@@ -304,6 +345,24 @@ const request = (config) => {
304
345
  headerResponse = { headers, status: xhr.status, statusText, url: _url };
305
346
  return headerResponse;
306
347
  };
348
+ const handleRetry = (error) => {
349
+ const newRetryCount = currentRetryCount + 1;
350
+ const { headers } = partialFromXhr();
351
+ const { retry, delay } = retryFn({ currentRetryCount: newRetryCount, headers, error });
352
+ if (!retry) {
353
+ observer.next({ type: 'failure', headers, error });
354
+ observer.complete();
355
+ return;
356
+ }
357
+ const _delay = delay ?? 1000;
358
+ observer.next({ type: 'delay-retry', headers, retryNumber: newRetryCount, retryDelay: _delay });
359
+ retryTimeout = window.setTimeout(() => {
360
+ setupXhr();
361
+ xhr.send(reqBody);
362
+ currentRetryCount = newRetryCount;
363
+ observer.next({ type: 'start', headers, isRetry: true, retryNumber: currentRetryCount, retryDelay: _delay });
364
+ }, _delay);
365
+ };
307
366
  const onLoad = () => {
308
367
  const { headers, statusText, url } = partialFromXhr();
309
368
  let { status } = partialFromXhr();
@@ -344,32 +403,22 @@ const request = (config) => {
344
403
  observer.complete();
345
404
  }
346
405
  else {
347
- observer.next({
348
- type: 'failure',
349
- headers,
350
- error: {
351
- url,
352
- detail: body,
353
- status,
354
- statusText,
355
- },
406
+ handleRetry({
407
+ url,
408
+ detail: body,
409
+ status,
410
+ statusText,
356
411
  });
357
- observer.complete();
358
412
  }
359
413
  };
360
414
  const onError = (error) => {
361
- const { url, headers } = partialFromXhr();
362
- observer.next({
363
- type: 'failure',
364
- headers,
365
- error: {
366
- url,
367
- detail: error,
368
- status: xhr.status || 0,
369
- statusText: xhr.statusText || 'Unknown Error',
370
- },
415
+ const { url } = partialFromXhr();
416
+ handleRetry({
417
+ url,
418
+ detail: error,
419
+ status: xhr.status || 0,
420
+ statusText: xhr.statusText || 'Unknown Error',
371
421
  });
372
- observer.complete();
373
422
  };
374
423
  const onDownProgress = (event) => {
375
424
  const { headers } = partialFromXhr();
@@ -423,6 +472,9 @@ const request = (config) => {
423
472
  xhr.removeEventListener('abort', onError);
424
473
  xhr.removeEventListener('load', onLoad);
425
474
  xhr.removeEventListener('timeout', onError);
475
+ if (retryTimeout) {
476
+ window.clearTimeout(retryTimeout);
477
+ }
426
478
  if (config.reportProgress) {
427
479
  xhr.removeEventListener('progress', onDownProgress);
428
480
  if (reqBody !== null && xhr.upload) {
@@ -625,23 +677,31 @@ class Query {
625
677
  authHeader = this._client.authProvider.header;
626
678
  }
627
679
  }
680
+ const mergedHeaders = mergeHeaders(getDefaultHeaders(this._client.config.request?.headers, this._queryConfig.method), authHeader, this._args?.headers) || undefined;
628
681
  request({
629
682
  urlWithParams: this._routeWithParams,
630
683
  method: transformMethod(this._queryConfig.method),
631
684
  body,
632
- headers: mergeHeaders(getDefaultHeaders(this._client.config.request?.headers, this._queryConfig.method), authHeader, this._args?.headers) || undefined,
633
- cacheAdapter: this._client.config.request?.cacheAdapter,
685
+ headers: mergedHeaders,
634
686
  reportProgress: this._queryConfig.reportProgress,
635
687
  responseType: this._queryConfig.responseType,
636
688
  withCredentials: this._queryConfig.withCredentials,
689
+ cacheAdapter: this._client.config.request?.cacheAdapter,
690
+ retryFn: this._client.config.request?.retryFn,
637
691
  })
638
692
  .pipe(takeUntil(this._onAbort$))
639
693
  .subscribe({
640
694
  next: (state) => {
641
- if (state.type === 'start') {
695
+ if (state.type === 'start' || state.type === 'delay-retry') {
696
+ const newMeta = {
697
+ ...meta,
698
+ isWaitingForRetry: state.type === 'delay-retry',
699
+ retryDelay: state.retryDelay,
700
+ retryNumber: state.retryNumber,
701
+ };
642
702
  this._state$.next({
643
703
  type: "LOADING" /* QueryStateType.Loading */,
644
- meta,
704
+ meta: newMeta,
645
705
  });
646
706
  }
647
707
  else if (state.type === 'download-progress') {
@@ -1866,5 +1926,5 @@ const def = () => null;
1866
1926
  * Generated bundle index. Do not edit.
1867
1927
  */
1868
1928
 
1869
- export { BasicAuthProvider, BearerAuthProvider, CustomHeaderAuthProvider, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, Query, QueryClient, QueryDirective, QueryError, QueryField, QueryForm, QueryStore, buildErrorMessage, buildGqlCacheKey, buildQueryArrayString, buildQueryString, buildRoute, buildTimestampFromSeconds, createInfinityQueryConfig, createQueryCollection, decryptBearer, deepFreeze, def, detectContentTypeHeader, extractExpiresInSeconds, extractQuery, filterFailure, filterInvalidParams, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getResponseUrl, gql, hasHeader, invalidBaseRouteError, invalidBodyError, invalidRouteError, isBasicAuthProvider, isBearerAuthProvider, isCustomHeaderAuthProvider, isFetchResponse, isGqlQueryConfig, isParamValid, isQuery, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, mergeHeaders, parseAllXhrResponseHeaders, pathParamsMissingInRouteFunctionError, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, request, serializeBody, shouldCacheQuery, skipPaginationPageParamCalculator, switchQueryCollectionState, switchQueryState, takeUntilResponse, transformGql, transformMethod, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray };
1929
+ export { BasicAuthProvider, BearerAuthProvider, CustomHeaderAuthProvider, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, Query, QueryClient, QueryDirective, QueryError, QueryField, QueryForm, QueryStore, buildErrorMessage, buildGqlCacheKey, buildQueryArrayString, buildQueryString, buildRoute, buildTimestampFromSeconds, createInfinityQueryConfig, createQueryCollection, decryptBearer, deepFreeze, def, detectContentTypeHeader, extractExpiresInSeconds, extractQuery, filterFailure, filterInvalidParams, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getResponseUrl, gql, hasHeader, invalidBaseRouteError, invalidBodyError, invalidRouteError, isBasicAuthProvider, isBearerAuthProvider, isCustomHeaderAuthProvider, isFetchResponse, isGqlQueryConfig, isParamValid, isQuery, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, mergeHeaders, parseAllXhrResponseHeaders, pathParamsMissingInRouteFunctionError, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, request, serializeBody, shouldCacheQuery, shouldRetryRequest, skipPaginationPageParamCalculator, switchQueryCollectionState, switchQueryState, takeUntilResponse, transformGql, transformMethod, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray };
1870
1930
  //# sourceMappingURL=ethlete-query.mjs.map