@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.
- package/esm2020/lib/query/query.mjs +13 -5
- package/esm2020/lib/query/query.types.mjs +1 -1
- package/esm2020/lib/query-client/query-client.types.mjs +1 -1
- package/esm2020/lib/request/request.mjs +56 -38
- package/esm2020/lib/request/request.types.mjs +1 -1
- package/esm2020/lib/request/request.util.mjs +36 -2
- package/fesm2015/ethlete-query.mjs +98 -43
- package/fesm2015/ethlete-query.mjs.map +1 -1
- package/fesm2020/ethlete-query.mjs +102 -42
- package/fesm2020/ethlete-query.mjs.map +1 -1
- package/lib/query/query.types.d.ts +3 -0
- package/lib/query-client/query-client.types.d.ts +6 -1
- package/lib/request/request.types.d.ts +19 -0
- package/lib/request/request.util.d.ts +3 -1
- package/package.json +2 -2
|
@@ -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 && '
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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:
|
|
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
|