@ethlete/query 2.1.1 → 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);
@@ -271,6 +271,37 @@ const getResponseUrl = (xhr) => {
271
271
  }
272
272
  return null;
273
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
+ };
274
305
 
275
306
  const XSSI_PREFIX = /^\)\]\}',?\n/;
276
307
  const request = (config) => {
@@ -278,25 +309,32 @@ const request = (config) => {
278
309
  const responseType = config.responseType || 'json';
279
310
  const body = config.body || null;
280
311
  const url = config.urlWithParams.split('?')[0];
312
+ const retryFn = config.retryFn || shouldRetryRequest;
313
+ let currentRetryCount = 0;
314
+ let retryTimeout = null;
281
315
  return new Observable((observer) => {
282
316
  const xhr = new XMLHttpRequest();
283
- xhr.open(config.method, config.urlWithParams);
284
- if (config.withCredentials) {
285
- xhr.withCredentials = true;
286
- }
287
- forEachHeader(headers, (name, value) => xhr.setRequestHeader(name, value));
288
- if (!hasHeader(headers, 'Accept')) {
289
- xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
290
- }
291
- if (!hasHeader(headers, 'Content-Type')) {
292
- const detectedType = detectContentTypeHeader(body);
293
- if (detectedType) {
294
- xhr.setRequestHeader('Content-Type', detectedType);
295
- }
296
- }
297
- xhr.responseType = responseType !== 'json' ? responseType : 'text';
298
317
  const reqBody = serializeBody(body);
299
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();
300
338
  const partialFromXhr = () => {
301
339
  if (headerResponse !== null) {
302
340
  return headerResponse;
@@ -307,6 +345,24 @@ const request = (config) => {
307
345
  headerResponse = { headers, status: xhr.status, statusText, url: _url };
308
346
  return headerResponse;
309
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
+ };
310
366
  const onLoad = () => {
311
367
  const { headers, statusText, url } = partialFromXhr();
312
368
  let { status } = partialFromXhr();
@@ -347,32 +403,22 @@ const request = (config) => {
347
403
  observer.complete();
348
404
  }
349
405
  else {
350
- observer.next({
351
- type: 'failure',
352
- headers,
353
- error: {
354
- url,
355
- detail: body,
356
- status,
357
- statusText,
358
- },
406
+ handleRetry({
407
+ url,
408
+ detail: body,
409
+ status,
410
+ statusText,
359
411
  });
360
- observer.complete();
361
412
  }
362
413
  };
363
414
  const onError = (error) => {
364
- const { url, headers } = partialFromXhr();
365
- observer.next({
366
- type: 'failure',
367
- headers,
368
- error: {
369
- url,
370
- detail: error,
371
- status: xhr.status || 0,
372
- statusText: xhr.statusText || 'Unknown Error',
373
- },
415
+ const { url } = partialFromXhr();
416
+ handleRetry({
417
+ url,
418
+ detail: error,
419
+ status: xhr.status || 0,
420
+ statusText: xhr.statusText || 'Unknown Error',
374
421
  });
375
- observer.complete();
376
422
  };
377
423
  const onDownProgress = (event) => {
378
424
  const { headers } = partialFromXhr();
@@ -426,6 +472,9 @@ const request = (config) => {
426
472
  xhr.removeEventListener('abort', onError);
427
473
  xhr.removeEventListener('load', onLoad);
428
474
  xhr.removeEventListener('timeout', onError);
475
+ if (retryTimeout) {
476
+ window.clearTimeout(retryTimeout);
477
+ }
429
478
  if (config.reportProgress) {
430
479
  xhr.removeEventListener('progress', onDownProgress);
431
480
  if (reqBody !== null && xhr.upload) {
@@ -628,23 +677,31 @@ class Query {
628
677
  authHeader = this._client.authProvider.header;
629
678
  }
630
679
  }
680
+ const mergedHeaders = mergeHeaders(getDefaultHeaders(this._client.config.request?.headers, this._queryConfig.method), authHeader, this._args?.headers) || undefined;
631
681
  request({
632
682
  urlWithParams: this._routeWithParams,
633
683
  method: transformMethod(this._queryConfig.method),
634
684
  body,
635
- headers: mergeHeaders(getDefaultHeaders(this._client.config.request?.headers, this._queryConfig.method), authHeader, this._args?.headers) || undefined,
636
- cacheAdapter: this._client.config.request?.cacheAdapter,
685
+ headers: mergedHeaders,
637
686
  reportProgress: this._queryConfig.reportProgress,
638
687
  responseType: this._queryConfig.responseType,
639
688
  withCredentials: this._queryConfig.withCredentials,
689
+ cacheAdapter: this._client.config.request?.cacheAdapter,
690
+ retryFn: this._client.config.request?.retryFn,
640
691
  })
641
692
  .pipe(takeUntil(this._onAbort$))
642
693
  .subscribe({
643
694
  next: (state) => {
644
- 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
+ };
645
702
  this._state$.next({
646
703
  type: "LOADING" /* QueryStateType.Loading */,
647
- meta,
704
+ meta: newMeta,
648
705
  });
649
706
  }
650
707
  else if (state.type === 'download-progress') {
@@ -1869,5 +1926,5 @@ const def = () => null;
1869
1926
  * Generated bundle index. Do not edit.
1870
1927
  */
1871
1928
 
1872
- 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 };
1873
1930
  //# sourceMappingURL=ethlete-query.mjs.map