@ethlete/query 2.1.1 → 2.3.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.
@@ -1,5 +1,5 @@
1
1
  import { BehaviorSubjectWithSubscriberCount, getCookie, setCookie, deleteCookie, DestroyService, equal, clone } from '@ethlete/core';
2
- import { Observable, filter, takeWhile, switchMap, of, BehaviorSubject, Subject, takeUntil, interval, startWith, tap, timer, fromEvent, concat, take, debounceTime, combineLatest, map, distinctUntilChanged } from 'rxjs';
2
+ import { Observable, filter, takeWhile, switchMap, of, BehaviorSubject, Subject, takeUntil, interval, startWith, tap, timer, fromEvent, take, concat, debounceTime, combineLatest, map, distinctUntilChanged } from 'rxjs';
3
3
  import * as i0 from '@angular/core';
4
4
  import { Directive, Input, InjectionToken, inject, ChangeDetectorRef, ViewContainerRef, TemplateRef, ErrorHandler, ElementRef } from '@angular/core';
5
5
  import { FormGroup } from '@angular/forms';
@@ -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) {
@@ -445,6 +494,11 @@ function filterSuccess() {
445
494
  return source.pipe(filter((value) => isQueryStateSuccess(value)));
446
495
  };
447
496
  }
497
+ function ignoreAutoRefresh() {
498
+ return function (source) {
499
+ return source.pipe(filter((value) => !isAutoRefresh(value)));
500
+ };
501
+ }
448
502
  function filterFailure() {
449
503
  return function (source) {
450
504
  return source.pipe(filter((value) => isQueryStateFailure(value)));
@@ -485,6 +539,7 @@ const isQueryStateSuccess = (state) => state?.type === "SUCCESS" /* QueryStateTy
485
539
  const isQueryStateFailure = (state) => state?.type === "FAILURE" /* QueryStateType.Failure */;
486
540
  const isQueryStateCancelled = (state) => state?.type === "CANCELLED" /* QueryStateType.Cancelled */;
487
541
  const isQueryStatePrepared = (state) => state?.type === "PREPARED" /* QueryStateType.Prepared */;
542
+ const isAutoRefresh = (state) => state?.meta.triggeredVia === 'auto';
488
543
  const mergeHeaders = (...headers) => {
489
544
  return headers.reduce((acc, headers) => {
490
545
  if (!headers) {
@@ -527,6 +582,13 @@ const getDefaultHeaders = (headers, method) => {
527
582
  }
528
583
  return headers;
529
584
  };
585
+ const castQueryCreatorTypes = (config) => {
586
+ if (config.args?.pathParams) {
587
+ console.error(config);
588
+ throw new Error('Path params cannot be overridden in castQueryCreatorTypes. Create a new query creator instead.');
589
+ }
590
+ return config.creator;
591
+ };
530
592
 
531
593
  class Query {
532
594
  get _nextId() {
@@ -628,23 +690,31 @@ class Query {
628
690
  authHeader = this._client.authProvider.header;
629
691
  }
630
692
  }
693
+ const mergedHeaders = mergeHeaders(getDefaultHeaders(this._client.config.request?.headers, this._queryConfig.method), authHeader, this._args?.headers) || undefined;
631
694
  request({
632
695
  urlWithParams: this._routeWithParams,
633
696
  method: transformMethod(this._queryConfig.method),
634
697
  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,
698
+ headers: mergedHeaders,
637
699
  reportProgress: this._queryConfig.reportProgress,
638
700
  responseType: this._queryConfig.responseType,
639
701
  withCredentials: this._queryConfig.withCredentials,
702
+ cacheAdapter: this._client.config.request?.cacheAdapter,
703
+ retryFn: this._client.config.request?.retryFn,
640
704
  })
641
705
  .pipe(takeUntil(this._onAbort$))
642
706
  .subscribe({
643
707
  next: (state) => {
644
- if (state.type === 'start') {
708
+ if (state.type === 'start' || state.type === 'delay-retry') {
709
+ const newMeta = {
710
+ ...meta,
711
+ isWaitingForRetry: state.type === 'delay-retry',
712
+ retryDelay: state.retryDelay,
713
+ retryNumber: state.retryNumber,
714
+ };
645
715
  this._state$.next({
646
716
  type: "LOADING" /* QueryStateType.Loading */,
647
- meta,
717
+ meta: newMeta,
648
718
  });
649
719
  }
650
720
  else if (state.type === 'download-progress') {
@@ -1098,6 +1168,9 @@ class InfinityQuery {
1098
1168
  this._data$.next([]);
1099
1169
  this.nextPage();
1100
1170
  }
1171
+ /**
1172
+ * @internal
1173
+ */
1101
1174
  _clearSubscriptions() {
1102
1175
  this._subscriptions.forEach((sub) => sub.unsubscribe());
1103
1176
  this._subscriptions = [];
@@ -1228,7 +1301,7 @@ class InfinityQueryDirective {
1228
1301
  this._cleanup();
1229
1302
  this._infinityQuery = v;
1230
1303
  this._infinityQueryInstance = this._setupInfinityQuery(v);
1231
- this._loadNextPage();
1304
+ this.loadNextPage();
1232
1305
  }
1233
1306
  get context() {
1234
1307
  return this._viewContext;
@@ -1301,12 +1374,25 @@ class InfinityQueryDirective {
1301
1374
  .subscribe();
1302
1375
  return instance;
1303
1376
  }
1377
+ /**
1378
+ * @deprecated Use `loadNextPage` instead
1379
+ * @breaking 3.0.0
1380
+ */
1304
1381
  _loadNextPage() {
1382
+ this.loadNextPage();
1383
+ }
1384
+ loadNextPage() {
1305
1385
  if (!this._infinityQueryInstance) {
1306
1386
  return;
1307
1387
  }
1308
1388
  this._infinityQueryInstance.nextPage();
1309
1389
  }
1390
+ reset(newConfig) {
1391
+ if (!this._infinityQueryInstance) {
1392
+ return;
1393
+ }
1394
+ this._infinityQueryInstance.reset(newConfig);
1395
+ }
1310
1396
  _renderMainView() {
1311
1397
  if (!this._isMainViewCreated) {
1312
1398
  this._isMainViewCreated = true;
@@ -1355,7 +1441,7 @@ class InfinityQueryTriggerDirective {
1355
1441
  ngOnInit() {
1356
1442
  const isInteractive = this._elementRef.nativeElement.hasAttribute('infinityQueryTrigger');
1357
1443
  if (isInteractive) {
1358
- this.click$.pipe(takeUntil(this._destroy)).subscribe(() => this._infinityQuery._loadNextPage());
1444
+ this.click$.pipe(takeUntil(this._destroy)).subscribe(() => this._infinityQuery.loadNextPage());
1359
1445
  }
1360
1446
  else {
1361
1447
  this._setupIntersectionObserver();
@@ -1370,7 +1456,7 @@ class InfinityQueryTriggerDirective {
1370
1456
  this._observer = new IntersectionObserver((entries) => {
1371
1457
  const entry = entries[0];
1372
1458
  if (entry.isIntersecting && !this._infinityQuery.context.loading) {
1373
- this._infinityQuery._loadNextPage();
1459
+ this._infinityQuery.loadNextPage();
1374
1460
  }
1375
1461
  }, {
1376
1462
  root: this.scrollContainerSelector ? document.querySelector(this.scrollContainerSelector) : null,
@@ -1398,6 +1484,8 @@ class QueryStore {
1398
1484
  this._config = _config;
1399
1485
  this._store = new Map();
1400
1486
  this._garbageCollector = null;
1487
+ this._isInLowResourceMode = false;
1488
+ this._lastBlurTimestamp = Date.now();
1401
1489
  this._initSmartQueryHandling();
1402
1490
  }
1403
1491
  add(id, query) {
@@ -1434,27 +1522,39 @@ class QueryStore {
1434
1522
  const windowBlur$ = fromEvent(window, 'blur');
1435
1523
  const windowFocus$ = fromEvent(window, 'focus');
1436
1524
  windowBlur$.subscribe(() => {
1437
- this._stopGarbageCollector();
1438
- if (this._config?.enableSmartPolling) {
1439
- this.forEach((query) => {
1440
- if (!query.isPolling || !query._enableSmartPolling) {
1441
- return;
1442
- }
1443
- query.pausePolling();
1444
- });
1445
- }
1525
+ timer(5000)
1526
+ .pipe(takeUntil(windowFocus$), take(1))
1527
+ .subscribe(() => {
1528
+ this._lastBlurTimestamp = Date.now();
1529
+ this._isInLowResourceMode = true;
1530
+ this._stopGarbageCollector();
1531
+ if (this._config?.enableSmartPolling) {
1532
+ this.forEach((query) => {
1533
+ if (!query.isPolling || !query._enableSmartPolling) {
1534
+ return;
1535
+ }
1536
+ query.pausePolling();
1537
+ });
1538
+ }
1539
+ });
1446
1540
  });
1447
1541
  windowFocus$.subscribe(() => {
1542
+ if (!this._isInLowResourceMode) {
1543
+ return;
1544
+ }
1545
+ this._isInLowResourceMode = false;
1448
1546
  if (this._config?.enableSmartPolling || this._config?.autoRefreshQueriesOnWindowFocus) {
1449
1547
  this.forEach((query) => {
1450
1548
  if (this._config?.enableSmartPolling && query._isPollingPaused) {
1451
1549
  query.resumePolling();
1452
1550
  }
1453
- if (this._config?.autoRefreshQueriesOnWindowFocus &&
1454
- query.isExpired &&
1455
- query.isInUse &&
1456
- query.autoRefreshOnConfig.windowFocus) {
1457
- query.execute({ skipCache: true, _triggeredVia: 'auto' });
1551
+ if (Date.now() - this._lastBlurTimestamp > 15000) {
1552
+ if (this._config?.autoRefreshQueriesOnWindowFocus &&
1553
+ query.isExpired &&
1554
+ query.isInUse &&
1555
+ query.autoRefreshOnConfig.windowFocus) {
1556
+ query.execute({ skipCache: true, _triggeredVia: 'auto' });
1557
+ }
1458
1558
  }
1459
1559
  });
1460
1560
  }
@@ -1485,6 +1585,7 @@ class QueryStore {
1485
1585
  if (this._garbageCollector !== null) {
1486
1586
  window.clearInterval(this._garbageCollector);
1487
1587
  this._garbageCollector = null;
1588
+ this._logGarbageCollector('Stop');
1488
1589
  }
1489
1590
  }
1490
1591
  _runGarbageCollector() {
@@ -1497,7 +1598,6 @@ class QueryStore {
1497
1598
  this._logGarbageCollector('Collection done');
1498
1599
  if (!this._store.size) {
1499
1600
  this._stopGarbageCollector();
1500
- this._logGarbageCollector('Stop');
1501
1601
  }
1502
1602
  }
1503
1603
  _logGarbageCollector(action) {
@@ -1869,5 +1969,5 @@ const def = () => null;
1869
1969
  * Generated bundle index. Do not edit.
1870
1970
  */
1871
1971
 
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 };
1972
+ export { BasicAuthProvider, BearerAuthProvider, CustomHeaderAuthProvider, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, Query, QueryClient, QueryDirective, QueryError, QueryField, QueryForm, QueryStore, buildErrorMessage, buildGqlCacheKey, buildQueryArrayString, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, createInfinityQueryConfig, createQueryCollection, decryptBearer, deepFreeze, def, detectContentTypeHeader, extractExpiresInSeconds, extractQuery, filterFailure, filterInvalidParams, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getResponseUrl, gql, hasHeader, ignoreAutoRefresh, invalidBaseRouteError, invalidBodyError, invalidRouteError, isAutoRefresh, 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
1973
  //# sourceMappingURL=ethlete-query.mjs.map