@ethlete/query 6.0.0-next.33 → 6.0.0-next.35

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.
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { inject, computed, DestroyRef, InjectionToken, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, VERSION, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
3
3
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
4
4
  import { UrlTree, Router, RedirectCommand, ActivatedRoute } from '@angular/router';
5
- import { filter, take, map, defer, from, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, EMPTY, takeWhile, merge, firstValueFrom, concatMap, fromEvent, throttleTime, interval, combineLatest, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, withLatestFrom, debounceTime } from 'rxjs';
5
+ import { filter, take, map, defer, from, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, merge, EMPTY, takeWhile, firstValueFrom, concatMap, fromEvent, throttleTime, interval, combineLatest, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, withLatestFrom, debounceTime } from 'rxjs';
6
6
  import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, deleteCookie, setCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
7
7
  import { HttpClient, HttpEventType, HttpErrorResponse, HttpResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
8
8
  import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
@@ -265,6 +265,24 @@ const resolveQueryDevtoolsMock = (target) => mockResolver?.(target) ?? null;
265
265
  * @internal
266
266
  */
267
267
  const resolveQueryDevtoolsFault = (target) => faultResolver?.(target) ?? null;
268
+ let tokenPayloadPatcher = null;
269
+ /**
270
+ * Installs the access-token payload patcher. Called by `provideQueryDevtools()`; nothing else may call it.
271
+ * @internal
272
+ */
273
+ const setQueryDevtoolsTokenPayloadPatcher = (fn) => {
274
+ tokenPayloadPatcher = fn;
275
+ };
276
+ /**
277
+ * A decoded access token as the devtools want it seen, which today means with an overridden lifetime
278
+ * applied. Returns the payload as decoded unless `provideQueryDevtools()` installed a patcher.
279
+ *
280
+ * Auth code must reach the override through this rather than importing the registry it lives in: a
281
+ * static import would pin that module into every bundle that builds an auth provider, and a runtime
282
+ * guard at the call site does not undo that.
283
+ * @internal
284
+ */
285
+ const patchQueryDevtoolsTokenPayload = (options) => (tokenPayloadPatcher ? tokenPayloadPatcher(options) : options.payload);
268
286
  let suppressStackRegistration = false;
269
287
  /**
270
288
  * Suppresses devtools registration for the next {@link createQueryStack} call. Used by
@@ -362,6 +380,8 @@ const QueryRuntimeErrorCode = {
362
380
  WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
363
381
  WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
364
382
  SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT: 104,
383
+ WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 105,
384
+ WITH_LONG_POLLING_USED_WITH_POLLING: 106,
365
385
  // Auth provider
366
386
  AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
367
387
  AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
@@ -412,6 +432,13 @@ const withAutoRefreshUsedInManualQuery = () => {
412
432
  return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY, `"withAutoRefresh()" has been used inside a query that has "onlyManualExecution" set to true.` +
413
433
  ` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
414
434
  };
435
+ const withLongPollingUsedOnUnsupportedHttpMethod = (method) => {
436
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withLongPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
437
+ };
438
+ const withLongPollingUsedWithPolling = () => {
439
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_WITH_POLLING, `"withLongPolling()" and "withPolling()" cannot be used on the same query - both would drive its re-execution.` +
440
+ ` Keep the long polling feature and drop the interval, or the other way around.`);
441
+ };
415
442
  const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
416
443
  return new RuntimeError(QueryRuntimeErrorCode.SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT, `The "silenceMissingWithArgsFeatureError" config is set to true, but a "withArgs()" feature is present.`);
417
444
  };
@@ -516,11 +543,6 @@ const isSymfonyListError = (error) => {
516
543
  'invalidValue' in error[0]);
517
544
  };
518
545
 
519
- /**
520
- * Returning this inside a withArgs feature will reset the query args to null.
521
- * This will also pause polling and auto refresh until new args are set.
522
- */
523
- const CLEAR_QUERY_ARGS = /* @__PURE__ */ Symbol('CLEAR_QUERY_ARGS');
524
546
  /** A angular effect that can be nested inside another effect */
525
547
  const nestedEffect = (fn, options) => {
526
548
  const activeConsumer = getActiveConsumer();
@@ -537,6 +559,7 @@ const QueryFeatureType = {
537
559
  WITH_ERROR_HANDLING: 'WITH_ERROR_HANDLING',
538
560
  WITH_SUCCESS_HANDLING: 'WITH_SUCCESS_HANDLING',
539
561
  WITH_POLLING: 'WITH_POLLING',
562
+ WITH_LONG_POLLING: 'WITH_LONG_POLLING',
540
563
  WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
541
564
  WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
542
565
  WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
@@ -548,8 +571,8 @@ const createQueryFeature = (config) => {
548
571
  * A query feature that allows you to set the arguments of the query.
549
572
  * The arguments are read within a computed function, so you can use reactive values.
550
573
  *
551
- * To set the arguments to `null`, return the symbol `CLEAR_QUERY_ARGS`.
552
- * If you instead return `null`, the arguments won't change.
574
+ * Return `null` to park the query: its args are reset to `null`, and polling and auto refresh
575
+ * pause until args are set again.
553
576
  *
554
577
  * Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
555
578
  */
@@ -557,26 +580,11 @@ const withArgs = (args) => {
557
580
  return createQueryFeature({
558
581
  type: QueryFeatureType.WITH_ARGS,
559
582
  fn: (context) => {
560
- // `state.args` is reactively backed by this source, so every reader (exec, polling,
561
- // auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
562
- // resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
563
- let previous = null;
564
- const resolve = () => {
565
- const value = args();
566
- if (value === CLEAR_QUERY_ARGS) {
567
- previous = null;
568
- return null;
569
- }
570
- if (value === null)
571
- return previous;
572
- previous = value;
573
- return value;
574
- };
575
583
  // Only ever non-null while the devtools are installed. Wrapping the source (rather than reading
576
584
  // `state.args` from the outside) is what puts the tracking window around the *evaluation* of the
577
585
  // args - a cached read of the computed runs nothing and would record no form at all.
578
586
  const formLinks = context.state.subtle.devtoolsFormLinks;
579
- context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(resolve) : resolve);
587
+ context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(args) : args);
580
588
  // Trigger (auto-)execution whenever the resolved args change.
581
589
  nestedEffect(() => {
582
590
  const currArgsNow = context.state.args();
@@ -672,6 +680,107 @@ const withPolling = (options) => {
672
680
  },
673
681
  });
674
682
  };
683
+ const DEFAULT_LONG_POLLING_DELAY = 250;
684
+ const DEFAULT_LONG_POLLING_ERROR_DELAY = 1_000;
685
+ const DEFAULT_LONG_POLLING_MAX_ERROR_DELAY = 30_000;
686
+ const DEFAULT_LONG_POLLING_STOP_AFTER_ERRORS = 10;
687
+ /**
688
+ * A query feature for long polling: a chain of requests where each round starts once the previous one
689
+ * settled, with args derived from what it returned. Unlike {@link withPolling} there is no interval -
690
+ * the server decides the cadence by holding each request open until it has something to say.
691
+ *
692
+ * A failed round is repeated after a growing delay, so a transient failure does not silently end the
693
+ * feed. A new value from the `withArgs` source cancels the pending round and starts over from it.
694
+ *
695
+ * Rounds do not share the cache: each one is requested with `keepUnusedFor: 0`, because a cursor is
696
+ * asked for once and never again. They are also not deduped across tabs - `withMultiTabSync` elects a
697
+ * poller per cache key, and a chain's key moves with every round.
698
+ *
699
+ * @example
700
+ * const events = getEvents(
701
+ * withArgs(() => ({ queryParams: { cursor: null } })),
702
+ * withLongPolling({
703
+ * nextArgs: (response) => (response ? { queryParams: { cursor: response.cursor } } : null),
704
+ * }),
705
+ * );
706
+ *
707
+ * @throws If the query is not eligible for auto execution (e.g. a POST request)
708
+ * @throws If the query also has a `withPolling` feature
709
+ */
710
+ const withLongPolling = (options) => {
711
+ return createQueryFeature({
712
+ type: QueryFeatureType.WITH_LONG_POLLING,
713
+ devtools: () => [
714
+ { label: 'delay', value: formatQueryDevtoolsDuration(options.delay ?? DEFAULT_LONG_POLLING_DELAY) },
715
+ ...queryDevtoolsFnDetail(options.nextArgs, 'nextArgs'),
716
+ ],
717
+ fn: (context) => {
718
+ if (!context.flags.shouldAutoExecuteMethod) {
719
+ throw withLongPollingUsedOnUnsupportedHttpMethod(context.flags.method);
720
+ }
721
+ if (context.flags.hasPollingFeature) {
722
+ throw withLongPollingUsedWithPolling();
723
+ }
724
+ const delay = options.delay ?? DEFAULT_LONG_POLLING_DELAY;
725
+ const errorDelay = options.errorDelay ?? DEFAULT_LONG_POLLING_ERROR_DELAY;
726
+ const maxErrorDelay = options.maxErrorDelay ?? DEFAULT_LONG_POLLING_MAX_ERROR_DELAY;
727
+ const stopAfterErrors = options.stopAfterErrors ?? DEFAULT_LONG_POLLING_STOP_AFTER_ERRORS;
728
+ let timeoutId = null;
729
+ let consecutiveErrors = 0;
730
+ const stop = () => {
731
+ if (timeoutId !== null)
732
+ clearTimeout(timeoutId);
733
+ timeoutId = null;
734
+ };
735
+ const schedule = (args, wait) => {
736
+ stop();
737
+ timeoutId = setTimeout(() => {
738
+ timeoutId = null;
739
+ context.execute({ args, options: { keepUnusedFor: 0, triggeredBy: 'long-polling' } });
740
+ }, wait);
741
+ };
742
+ // The chain never writes `state.args`, so what a round was sent with lives on the request itself -
743
+ // which is also the only place a manual `execute({ args })` records them.
744
+ const settledArgs = () => context.state.subtle.request()?.args ?? null;
745
+ context.state.events$.pipe(takeUntilDestroyed(context.deps.destroyRef)).subscribe((event) => {
746
+ if (event.type === HttpEventType.Response) {
747
+ consecutiveErrors = 0;
748
+ const next = options.nextArgs(context.state.response(), settledArgs());
749
+ if (next === null)
750
+ return stop();
751
+ schedule(next, delay);
752
+ }
753
+ else if (event.type === 'error') {
754
+ consecutiveErrors++;
755
+ if (consecutiveErrors >= stopAfterErrors)
756
+ return stop();
757
+ schedule(settledArgs(), Math.min(errorDelay * 2 ** (consecutiveErrors - 1), maxErrorDelay));
758
+ }
759
+ });
760
+ // A pending round belongs to the args it was derived from, so a fresh value from the `withArgs`
761
+ // source must cancel it - the source's own re-execution is what starts the new chain.
762
+ //
763
+ // Only an actual change may cancel: the effect's first run can land after the chain already
764
+ // scheduled a round (effects flush later than the response that scheduled it), and cancelling
765
+ // there would end the chain before its second round.
766
+ let hasSeenArgs = false;
767
+ let seenArgs = null;
768
+ nestedEffect(() => {
769
+ const args = context.state.args();
770
+ untracked(() => {
771
+ const hasChanged = hasSeenArgs && args !== seenArgs;
772
+ hasSeenArgs = true;
773
+ seenArgs = args;
774
+ if (!hasChanged)
775
+ return;
776
+ consecutiveErrors = 0;
777
+ stop();
778
+ });
779
+ }, { injector: context.deps.injector });
780
+ context.deps.destroyRef.onDestroy(stop);
781
+ },
782
+ });
783
+ };
675
784
  /** A query feature that can be used to log the latest http event */
676
785
  const withLogging = (options) => {
677
786
  return createQueryFeature({
@@ -986,6 +1095,7 @@ const getQueryFeatureUsage = (
986
1095
  options) => {
987
1096
  const { creator, features, queryConfig, creatorInternals } = options;
988
1097
  const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
1098
+ const hasPollingFeature = features.some((f) => f.type === QueryFeatureType.WITH_POLLING);
989
1099
  const shouldAutoExecuteMethod = isCreateGqlQueryOptions(options)
990
1100
  ? shouldAutoExecuteGqlQuery(options.creatorInternals.method)
991
1101
  : shouldAutoExecuteQuery(options.creatorInternals.method);
@@ -1000,6 +1110,7 @@ options) => {
1000
1110
  }
1001
1111
  const featureFnContext = {
1002
1112
  hasWithArgsFeature,
1113
+ hasPollingFeature,
1003
1114
  shouldAutoExecuteMethod,
1004
1115
  shouldAutoExecute,
1005
1116
  hasRouteFunction,
@@ -1529,16 +1640,16 @@ const runQueryErrorParsers = (detail, response) => {
1529
1640
  return null;
1530
1641
  };
1531
1642
  const NO_RETRY = { retry: false };
1532
- let defaultRetryFn = null;
1643
+ let defaultRetryFn$1 = null;
1533
1644
  /**
1534
1645
  * Installs the retry policy used by every request that does not bring its own `retryFn`. Installed
1535
1646
  * by the {@link withDefaultRetry} query client feature; without it nothing is retried automatically.
1536
1647
  */
1537
1648
  const setDefaultQueryRetryFn = (fn) => {
1538
- defaultRetryFn = fn;
1649
+ defaultRetryFn$1 = fn;
1539
1650
  };
1540
1651
  /** @internal */
1541
- const runDefaultQueryRetry = (options) => defaultRetryFn?.(options) ?? NO_RETRY;
1652
+ const runDefaultQueryRetry = (options) => defaultRetryFn$1?.(options) ?? NO_RETRY;
1542
1653
 
1543
1654
  const isQueryErrorResponse = (error) => {
1544
1655
  return typeof error === 'object' && error !== null && 'raw' in error && 'retryState' in error && 'isList' in error;
@@ -1630,6 +1741,7 @@ const SPEED_BUFFER_TIME_IN_MS = 2000;
1630
1741
  const MOCKED_STATUS_TEXT = 'Served by the query devtools';
1631
1742
  const createHttpRequest = (options) => {
1632
1743
  let currentStreamSubscription = Subscription.EMPTY;
1744
+ let lastSentHeaders;
1633
1745
  const { args, clientOptions, dependencies } = options;
1634
1746
  const currentEvent = signal(null, /* @ts-ignore */
1635
1747
  ...(ngDevMode ? [{ debugName: "currentEvent" }] : /* istanbul ignore next */ []));
@@ -1751,6 +1863,7 @@ const createHttpRequest = (options) => {
1751
1863
  });
1752
1864
  const createStream = () => {
1753
1865
  const headers = resolveHeaders();
1866
+ lastSentHeaders = headers;
1754
1867
  const source$ = isQueryDevtoolsRequestInterceptionEnabled() ? sendWithFaults(headers) : send(headers);
1755
1868
  return source$.pipe(tap((event) => updateState(event)), retry({
1756
1869
  delay: (error, retryCount) => {
@@ -1803,12 +1916,19 @@ const createHttpRequest = (options) => {
1803
1916
  });
1804
1917
  return true;
1805
1918
  };
1806
- const destroy = () => {
1919
+ const abort = () => {
1807
1920
  const wasActive = !currentStreamSubscription.closed;
1808
1921
  currentStreamSubscription.unsubscribe();
1809
- // A request destroyed mid-backoff never runs the attempt it was waiting for, so leaving the state
1810
- // set would keep a "retrying in …" readout counting down for a request that is gone.
1922
+ currentStreamSubscription = Subscription.EMPTY;
1923
+ // A request stopped mid-backoff never runs the attempt it was waiting for, so leaving the state
1924
+ // set would keep a "retrying in …" readout counting down for a request that is not coming.
1811
1925
  retryState.set(null);
1926
+ // Nothing is on its way any more, and `execute()` refuses to run while this is set.
1927
+ loading.set(null);
1928
+ return wasActive;
1929
+ };
1930
+ const destroy = () => {
1931
+ const wasActive = abort();
1812
1932
  event$.complete();
1813
1933
  return wasActive;
1814
1934
  };
@@ -1923,9 +2043,11 @@ const createHttpRequest = (options) => {
1923
2043
  applyPersistedResponse,
1924
2044
  lastPersistedResponseAt: lastPersistedResponseAt.asReadonly(),
1925
2045
  resolveHeaders,
2046
+ lastSentHeaders: () => lastSentHeaders,
1926
2047
  attempts: attempts.asReadonly(),
1927
2048
  retryState: retryState.asReadonly(),
1928
2049
  lastDurationMs: lastDurationMs.asReadonly(),
2050
+ abort,
1929
2051
  },
1930
2052
  };
1931
2053
  return httpRequest;
@@ -3009,48 +3131,68 @@ const symfonyQueryErrorParser = (detail) => {
3009
3131
  return null;
3010
3132
  };
3011
3133
 
3012
- const shouldRetryRequest = (options) => {
3013
- const normalizedOptions = options instanceof HttpErrorResponse ? { retryCount: 0, error: options } : options;
3014
- const { retryCount, error } = normalizedOptions;
3015
- const defaultRetryDelay = clamp(1000 + 1000 * retryCount, 1000, 5000);
3016
- const { status, error: detail, headers } = error;
3017
- // Code 0 usually means the internet connection is down. We retry in this case.
3018
- // It could also be a CORS issue but that should not be the case in production.
3019
- // We will retry in this case until the connection is back.
3020
- if (status === 0) {
3021
- return { retry: true, delay: defaultRetryDelay };
3022
- }
3023
- // Don't retry if we already retried 3 times
3024
- if (retryCount > 3) {
3025
- return { retry: false };
3026
- }
3027
- // Don't retry if it's not an http error
3028
- if (!(error instanceof HttpErrorResponse)) {
3029
- return { retry: false };
3030
- }
3031
- // Retry on 5xx errors (except 500 internal server error since those are usually not recoverable)
3032
- if (status >= 501) {
3033
- // Don't retry if a requested page is out of range
3034
- if (isSymfonyPagerfantaOutOfRangeError(detail)) {
3134
+ const DEFAULT_MAX_ATTEMPTS = 3;
3135
+ const DEFAULT_BASE_DELAY_MS = 1000;
3136
+ const DEFAULT_MAX_DELAY_MS = 30_000;
3137
+ const DEFAULT_JITTER = 0.25;
3138
+ // 500 is left out on purpose: an internal server error is a bug in the backend far more often than a
3139
+ // blip, and repeating the request that triggered it does not make it go away.
3140
+ const isRetryableByDefault = (status) => status === 0 || status === 408 || status === 425 || status === 429 || status >= 501;
3141
+ /**
3142
+ * Builds a retry policy: an exponentially backing off, jittered {@link ShouldRetryRequestFn} bounded by
3143
+ * an attempt ceiling. What {@link withDefaultRetry} installs - reach for this directly to hand a single
3144
+ * client or creator its own `retryFn`.
3145
+ *
3146
+ * @example
3147
+ * const flakyEndpointRetry = createDefaultRetryFn({ maxAttempts: 8, maxDelayMs: 10_000 });
3148
+ *
3149
+ * const getReport = myApiGet<GetReportArgs>('/report').clone({ retryFn: flakyEndpointRetry });
3150
+ */
3151
+ const createDefaultRetryFn = (options = {}) => {
3152
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
3153
+ const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
3154
+ const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
3155
+ const jitter = clamp(options.jitter ?? DEFAULT_JITTER, 0, 1);
3156
+ const retryableStatusCodes = options.retryableStatusCodes;
3157
+ const isRetryableStatus = (status) => retryableStatusCodes ? retryableStatusCodes.includes(status) : isRetryableByDefault(status);
3158
+ const backoff = (retryCount) => {
3159
+ const exponential = baseDelayMs * Math.pow(2, retryCount);
3160
+ const spread = jitter === 0 ? 1 : 1 + (Math.random() * 2 - 1) * jitter;
3161
+ return Math.round(clamp(exponential * spread, 0, maxDelayMs));
3162
+ };
3163
+ return ({ retryCount, error }) => {
3164
+ // Not an HTTP failure at all, so nothing about it says a second attempt would go any better.
3165
+ if (!(error instanceof HttpErrorResponse))
3035
3166
  return { retry: false };
3167
+ if (maxAttempts > 0 && retryCount > maxAttempts)
3168
+ return { retry: false };
3169
+ const { status, error: detail, headers } = error;
3170
+ if (!isRetryableStatus(status))
3171
+ return { retry: false };
3172
+ // A page past the end of a Pagerfanta collection answers 5xx, and asking for it again keeps
3173
+ // answering 5xx - the request is wrong, not the server.
3174
+ if (status >= 501 && isSymfonyPagerfantaOutOfRangeError(detail))
3175
+ return { retry: false };
3176
+ if (status === 429) {
3177
+ const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
3178
+ const delay = retryAfter ? parseInt(retryAfter) * 1000 : NaN;
3179
+ // The server named a time, so it is used as named rather than jittered - only capped, since a
3180
+ // `retry-after` of an hour would otherwise arm a timer nobody is still around for.
3181
+ if (!Number.isNaN(delay))
3182
+ return { retry: true, delay: Math.min(delay, maxDelayMs) };
3036
3183
  }
3037
- return { retry: true, delay: defaultRetryDelay };
3038
- }
3039
- // Retry on 408 or 425 errors
3040
- if (status === 408 || status === 425) {
3041
- return { retry: true, delay: defaultRetryDelay };
3042
- }
3043
- // Retry on 429 errors
3044
- if (status === 429) {
3045
- const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
3046
- if (retryAfter) {
3047
- const delay = parseInt(retryAfter) * 1000;
3048
- return { retry: true, delay: Number.isNaN(delay) ? defaultRetryDelay : delay };
3049
- }
3050
- return { retry: true, delay: defaultRetryDelay };
3051
- }
3052
- return { retry: false };
3184
+ return { retry: true, delay: backoff(retryCount) };
3185
+ };
3053
3186
  };
3187
+ const defaultRetryFn = /* @__PURE__ */ createDefaultRetryFn();
3188
+ /**
3189
+ * The SDK's default retry policy, installed by {@link withDefaultRetry}: three retries with an
3190
+ * exponentially backing off, jittered delay, for a connection failure, a `408`, a `425`, a `429`
3191
+ * (honouring `retry-after`) and a 5xx above `500`.
3192
+ *
3193
+ * Pass {@link createDefaultRetryFn} options to {@link withDefaultRetry} to configure it.
3194
+ */
3195
+ const shouldRetryRequest = (options) => defaultRetryFn(options instanceof HttpErrorResponse ? { retryCount: 0, error: options } : options);
3054
3196
 
3055
3197
  const noop$2 = () => undefined;
3056
3198
  /** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
@@ -3454,10 +3596,9 @@ const withSymfonyErrors = () => () => {
3454
3596
  return { type: QueryClientFeatureType.SYMFONY_ERRORS, instance: null };
3455
3597
  };
3456
3598
  /**
3457
- * Installs the SDK's default retry policy for every request that does not bring its own `retryFn`:
3458
- * retry a connection failure indefinitely, a 5xx above 500, a 408/425, and a 429 (honouring
3459
- * `retry-after`), up to three times with a backing-off delay - and never a Pagerfanta out-of-range
3460
- * page.
3599
+ * Installs the SDK's default retry policy for every request that does not bring its own `retryFn`: a
3600
+ * connection failure, a 5xx above 500, a 408/425 and a 429 (honouring `retry-after`) are retried three
3601
+ * times with an exponentially backing off, jittered delay - and a Pagerfanta out-of-range page never is.
3461
3602
  *
3462
3603
  * Without it nothing is retried automatically and `error.retryState` always reads `{ retry: false }`.
3463
3604
  * A per-client or per-creator `retryFn` keeps working either way.
@@ -3466,11 +3607,11 @@ const withSymfonyErrors = () => () => {
3466
3607
  * const MY_CLIENT = createQueryClient({
3467
3608
  * name: 'my-api',
3468
3609
  * baseUrl: 'https://api.example.com',
3469
- * features: [withDefaultRetry()],
3610
+ * features: [withDefaultRetry({ maxAttempts: 5 })],
3470
3611
  * });
3471
3612
  */
3472
- const withDefaultRetry = () => () => {
3473
- setDefaultQueryRetryFn(shouldRetryRequest);
3613
+ const withDefaultRetry = (options) => () => {
3614
+ setDefaultQueryRetryFn(options ? createDefaultRetryFn(options) : shouldRetryRequest);
3474
3615
  return { type: QueryClientFeatureType.DEFAULT_RETRY, instance: null };
3475
3616
  };
3476
3617
  /**
@@ -3488,10 +3629,10 @@ const withDefaultRetry = () => () => {
3488
3629
  * features: [withEthleteApiErrors()],
3489
3630
  * });
3490
3631
  */
3491
- const withEthleteApiErrors = () => () => {
3632
+ const withEthleteApiErrors = (options) => () => {
3492
3633
  registerQueryErrorParser(htmlQueryErrorParser);
3493
3634
  registerQueryErrorParser(symfonyQueryErrorParser);
3494
- setDefaultQueryRetryFn(shouldRetryRequest);
3635
+ setDefaultQueryRetryFn(options?.retry ? createDefaultRetryFn(options.retry) : shouldRetryRequest);
3495
3636
  return { type: QueryClientFeatureType.ETHLETE_API_ERRORS, instance: null };
3496
3637
  };
3497
3638
 
@@ -3512,10 +3653,10 @@ const createQueryRepository = (config) => {
3512
3653
  const cacheVersion = signal(0, /* @ts-ignore */
3513
3654
  ...(ngDevMode ? [{ debugName: "cacheVersion" }] : /* istanbul ignore next */ []));
3514
3655
  const bumpCacheVersion = () => cacheVersion.update((v) => v + 1);
3515
- const resolveKeepUnusedFor = (creatorOptions, shouldCache) => {
3656
+ const resolveKeepUnusedFor = (creatorOptions, shouldCache, override) => {
3516
3657
  if (config.retentionEnabled === false || !shouldCache)
3517
3658
  return 0;
3518
- return Math.max(0, creatorOptions?.keepUnusedFor ?? config.keepUnusedFor ?? DEFAULT_KEEP_UNUSED_FOR);
3659
+ return Math.max(0, override ?? creatorOptions?.keepUnusedFor ?? config.keepUnusedFor ?? DEFAULT_KEEP_UNUSED_FOR);
3519
3660
  };
3520
3661
  const request = (options) => {
3521
3662
  const { args, creatorOptions, runQueryOptions } = options;
@@ -3546,7 +3687,7 @@ const createQueryRepository = (config) => {
3546
3687
  })
3547
3688
  : false;
3548
3689
  const trackingKey = cacheKey || generateUuid();
3549
- const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache);
3690
+ const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
3550
3691
  const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
3551
3692
  const isSecure = options.isSecure ?? false;
3552
3693
  // A secure response needs an explicit `persistence: true` to reach the disk: leaving an
@@ -3651,6 +3792,10 @@ const createQueryRepository = (config) => {
3651
3792
  // Only data is worth keeping around: an entry that never produced a response has nothing to
3652
3793
  // hand back to a returning consumer, so it is aborted immediately as it always was.
3653
3794
  if (cacheEntry.keepUnusedFor > 0 && cacheEntry.request.response() !== null) {
3795
+ // Retention keeps the response, not the work: a revalidation nobody is waiting for is stopped
3796
+ // rather than left running - and a failing one left running would go on retrying into an empty
3797
+ // room. A consumer that binds again re-executes anyway.
3798
+ cacheEntry.request.subtle.abort();
3654
3799
  retain(key, cacheEntry);
3655
3800
  }
3656
3801
  else {
@@ -3863,6 +4008,7 @@ const createSecureExecuteFactory = (options) => {
3863
4008
  let lastExecuteArgs;
3864
4009
  /** The access token the last request went out with - what the 401 retry below compares against. */
3865
4010
  let lastRequestedWithToken = null;
4011
+ const error$ = toObservable(options.state.error, { injector: options.deps.injector });
3866
4012
  const reset = () => {
3867
4013
  authQuerySubscription.unsubscribe();
3868
4014
  authQuerySubscription = Subscription.EMPTY;
@@ -3973,7 +4119,13 @@ const createSecureExecuteFactory = (options) => {
3973
4119
  // another refresh - an endless refresh/retry loop for as long as the server keeps handing out
3974
4120
  // tokens it rejects. A refresh that did not change the access token is therefore not a reason to
3975
4121
  // retry; the subscription stays open, so a later one that does change it still is.
3976
- tokenRefreshSubscription = options.authProvider.afterTokenRefresh$
4122
+ //
4123
+ // The error landing is a trigger of its own: a refresh can complete before this query's 401 has
4124
+ // even come back (which is also why that 401 must not start another refresh - see
4125
+ // withRefreshQuery's 401 handling), and the refresh emission alone would find no error to act
4126
+ // on. Emissions of the error this execution armed with are not landings, only replays.
4127
+ const errorAtArm = options.state.error();
4128
+ tokenRefreshSubscription = merge(options.authProvider.afterTokenRefresh$, error$.pipe(filter((landedError) => landedError !== errorAtArm)))
3977
4129
  .pipe(filter(() => {
3978
4130
  const currentError = options.state.error();
3979
4131
  return currentError?.code === 401 && options.authProvider.accessToken() !== lastRequestedWithToken;
@@ -4629,6 +4781,8 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
4629
4781
  let isLeaderFn = alwaysLeader;
4630
4782
  let leaderElectionContext;
4631
4783
  let refreshCoordination;
4784
+ let sessionAdoption;
4785
+ let activityCoordination;
4632
4786
  for (const featureBuilder of featureBuilders ?? []) {
4633
4787
  const earlySetup = featureBuilder.earlySetup;
4634
4788
  if (!earlySetup)
@@ -4637,8 +4791,10 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
4637
4791
  isLeaderFn = result.isLeader ?? isLeaderFn;
4638
4792
  leaderElectionContext = result.leaderElection ?? leaderElectionContext;
4639
4793
  refreshCoordination = result.refreshCoordination ?? refreshCoordination;
4794
+ sessionAdoption = result.sessionAdoption ?? sessionAdoption;
4795
+ activityCoordination = result.activityCoordination ?? activityCoordination;
4640
4796
  }
4641
- return { isLeaderFn, leaderElectionContext, refreshCoordination };
4797
+ return { isLeaderFn, leaderElectionContext, refreshCoordination, sessionAdoption, activityCoordination };
4642
4798
  };
4643
4799
  const createBearerAuthProviderImpl = (config) => {
4644
4800
  const injector = inject(Injector);
@@ -4655,7 +4811,12 @@ const createBearerAuthProviderImpl = (config) => {
4655
4811
  if (!token)
4656
4812
  return null;
4657
4813
  try {
4658
- return config.bearerDecryptFn?.(token) ?? decryptBearer(token);
4814
+ const decoded = config.bearerDecryptFn?.(token) ?? decryptBearer(token);
4815
+ return patchQueryDevtoolsTokenPayload({
4816
+ payload: decoded,
4817
+ providerName: config.name,
4818
+ expiresInPropertyName: 'exp',
4819
+ });
4659
4820
  }
4660
4821
  catch (error) {
4661
4822
  if (isDevMode()) {
@@ -4728,6 +4889,7 @@ const createBearerAuthProviderImpl = (config) => {
4728
4889
  sessionEndCause: sessionEndCause.asReadonly(),
4729
4890
  });
4730
4891
  const querySetupContext = {
4892
+ name: config.name,
4731
4893
  accessToken,
4732
4894
  refreshToken,
4733
4895
  bearerDecryptFn: config.bearerDecryptFn,
@@ -4753,6 +4915,8 @@ const createBearerAuthProviderImpl = (config) => {
4753
4915
  setTokens,
4754
4916
  isLeader: isLeader.isLeaderFn,
4755
4917
  leaderElection: isLeader.leaderElectionContext,
4918
+ sessionAdoption: isLeader.sessionAdoption,
4919
+ activityCoordination: isLeader.activityCoordination,
4756
4920
  afterTokenRefresh$,
4757
4921
  queries: queries,
4758
4922
  executionState,
@@ -4761,9 +4925,21 @@ const createBearerAuthProviderImpl = (config) => {
4761
4925
  };
4762
4926
  const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
4763
4927
  // Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
4764
- // `unknown` here means nothing is trying to restore a session, so there is nothing to wait for.
4928
+ // `unknown` here means nothing is trying to restore a session, so there is nothing to wait for -
4929
+ // unless the join handshake is still out, in which case another tab may be about to hand this one a
4930
+ // live session. Saying `anonymous` in that window would send the auth guard to the login page for a
4931
+ // session that exists, so the fallback waits for the handshake instead.
4765
4932
  if (sessionStatus() === 'unknown') {
4766
- sessionStatus.set('anonymous');
4933
+ const adoption = isLeader.sessionAdoption;
4934
+ if (adoption?.isPending()) {
4935
+ void adoption.settled.then(() => {
4936
+ if (sessionStatus() === 'unknown')
4937
+ sessionStatus.set('anonymous');
4938
+ });
4939
+ }
4940
+ else {
4941
+ sessionStatus.set('anonymous');
4942
+ }
4767
4943
  }
4768
4944
  const provider = {
4769
4945
  queries,
@@ -4803,11 +4979,25 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
4803
4979
 
4804
4980
  /** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
4805
4981
  const rescheduleDelayMs = 5000;
4982
+ /**
4983
+ * How long a follower waits for the leader to answer a delegated refresh before asking again. The
4984
+ * request is a `BroadcastChannel` message with no ack, so a leader that was frozen or handed the
4985
+ * leadership over mid-flight simply never acts on it.
4986
+ */
4987
+ const delegatedRefreshRetryMs = 3000;
4988
+ /** How often a delegated refresh is re-asked for before the tab leaves it to the next 401. */
4989
+ const maxDelegatedRefreshAttempts = 3;
4806
4990
  /**
4807
4991
  * How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
4808
4992
  * declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
4809
4993
  */
4810
4994
  const maxRescheduleAttempts = 5;
4995
+ /**
4996
+ * How many 401-driven refreshes may run back to back before they fall back to `minRefreshInterval`.
4997
+ * Every refresh in such a streak means the token the previous one issued was rejected too, so an
4998
+ * unbounded streak is a refresh loop, not recovery. A secure request succeeding ends the streak.
4999
+ */
5000
+ const maxUnauthorizedRefreshStreak = 3;
4811
5001
  const withAuthenticationQuery = (key, config, setup) => ({
4812
5002
  _type: 'authQuery',
4813
5003
  key,
@@ -4849,7 +5039,12 @@ const withRefreshQuery = (key, config) => {
4849
5039
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
4850
5040
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
4851
5041
  const refreshIfExpired = config.refreshIfExpired ?? true;
5042
+ const destroyRef = inject(DestroyRef);
4852
5043
  let lastRefreshTime = 0;
5044
+ let lastUnauthorizedRefreshTime = 0;
5045
+ let unauthorizedRefreshStreak = 0;
5046
+ let delegatedRefreshAttempts = 0;
5047
+ let isWatchingDelegatedRefresh = false;
4853
5048
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4854
5049
  const refreshQuery = () => context.queries[key];
4855
5050
  const executeRefresh = (reason) => {
@@ -4857,9 +5052,14 @@ const withRefreshQuery = (key, config) => {
4857
5052
  if (!currentRefreshToken)
4858
5053
  return 'noToken';
4859
5054
  if (!context.isLeader()) {
4860
- // Only the leader may spend a single-use refresh token, but the event is still real - hand it
4861
- // over instead of dropping it, or this tab waits out the leader's timer with a dead token.
5055
+ // A scheduled tick on a follower is pure duplicate: multi-tab sync keeps the access token
5056
+ // identical across tabs, so the leader's own timer is due at the same instant and it is the
5057
+ // only tab allowed to act. Delegation is for the 401 path, where this tab has a request that
5058
+ // really failed and the leader has no way of knowing.
5059
+ if (reason === 'scheduled')
5060
+ return 'notLeader';
4862
5061
  context.refreshCoordination?.request();
5062
+ watchDelegatedRefresh(context.accessToken());
4863
5063
  return 'delegated';
4864
5064
  }
4865
5065
  // Any token-issuing execution, not just another refresh: a login already in flight is about to
@@ -4867,15 +5067,50 @@ const withRefreshQuery = (key, config) => {
4867
5067
  // that login is replacing.
4868
5068
  if (context.hasTokenIssuingExecutionInFlight())
4869
5069
  return 'busy';
5070
+ const now = Date.now();
4870
5071
  if (reason === 'scheduled') {
4871
- const now = Date.now();
4872
5072
  if (now - lastRefreshTime < minRefreshInterval)
4873
5073
  return 'throttled';
4874
- lastRefreshTime = now;
4875
5074
  }
5075
+ else {
5076
+ if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
5077
+ now - lastUnauthorizedRefreshTime < minRefreshInterval) {
5078
+ return 'throttled';
5079
+ }
5080
+ unauthorizedRefreshStreak++;
5081
+ lastUnauthorizedRefreshTime = now;
5082
+ }
5083
+ // Stamped whatever the reason, so the scheduled floor covers a 401-driven rotation too - a
5084
+ // scheduled tick right behind one would otherwise spend a second refresh token. The 401 path
5085
+ // deliberately does not *gate* on it: a token revoked seconds after a refresh is exactly when
5086
+ // the request has to go out.
5087
+ lastRefreshTime = now;
4876
5088
  refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4877
5089
  return 'executed';
4878
5090
  };
5091
+ /**
5092
+ * Asks the leader again if a delegated refresh went unanswered - the request is a channel message
5093
+ * with no ack, so a frozen leader, or one that handed the leadership over mid-flight, drops it
5094
+ * silently. One watch at a time, and by the time it re-asks this tab may be the leader itself;
5095
+ * `executeRefresh` re-decides that on every attempt.
5096
+ */
5097
+ function watchDelegatedRefresh(tokenAtRequest) {
5098
+ if (isWatchingDelegatedRefresh || delegatedRefreshAttempts >= maxDelegatedRefreshAttempts)
5099
+ return;
5100
+ delegatedRefreshAttempts++;
5101
+ isWatchingDelegatedRefresh = true;
5102
+ timer(delegatedRefreshRetryMs)
5103
+ .pipe(takeUntilDestroyed(destroyRef))
5104
+ .subscribe(() => {
5105
+ isWatchingDelegatedRefresh = false;
5106
+ // A new access token is the leader's answer - nothing to re-ask for.
5107
+ if (context.accessToken() !== tokenAtRequest) {
5108
+ delegatedRefreshAttempts = 0;
5109
+ return;
5110
+ }
5111
+ executeRefresh('unauthorized');
5112
+ });
5113
+ }
4879
5114
  /**
4880
5115
  * How long a scheduled refresh that did not spend the refresh token waits before trying again.
4881
5116
  * `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
@@ -4890,7 +5125,10 @@ const withRefreshQuery = (key, config) => {
4890
5125
  return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
4891
5126
  case 'busy':
4892
5127
  return rescheduleDelayMs;
5128
+ // Both wait for the leader: `delegated` asked it to act, `notLeader` left it to its own timer.
5129
+ // Coming back keeps this tab covered if the leadership moves here in the meantime.
4893
5130
  case 'delegated':
5131
+ case 'notLeader':
4894
5132
  return minRefreshInterval;
4895
5133
  }
4896
5134
  };
@@ -4907,7 +5145,12 @@ const withRefreshQuery = (key, config) => {
4907
5145
  /** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
4908
5146
  const scheduledRefreshDelay = (token) => {
4909
5147
  try {
4910
- const bearerDataValue = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
5148
+ const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
5149
+ const bearerDataValue = patchQueryDevtoolsTokenPayload({
5150
+ payload: decoded,
5151
+ providerName: context.name,
5152
+ expiresInPropertyName,
5153
+ });
4911
5154
  const expiresIn = bearerDataValue?.[expiresInPropertyName];
4912
5155
  if (typeof expiresIn !== 'number') {
4913
5156
  if (isDevMode()) {
@@ -4939,13 +5182,33 @@ const withRefreshQuery = (key, config) => {
4939
5182
  const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
4940
5183
  return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
4941
5184
  }));
4942
- toObservable(context.accessToken)
4943
- .pipe(switchMap((token) => {
4944
- if (!token)
4945
- return EMPTY;
4946
- const dueInMs = scheduledRefreshDelay(token);
4947
- return dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts);
4948
- }), takeUntilDestroyed())
5185
+ // A backgrounded tab's `timer()` does not fire on time - the browser throttles it, and a frozen
5186
+ // page stops running it altogether while keeping the Web Lock that makes it the leader. So the
5187
+ // schedule is recomputed whenever the page becomes visible again: an overdue token resolves to a
5188
+ // delay of 0 and refreshes immediately, instead of waiting for a secure query to 401.
5189
+ const foregroundEpoch = signal(0, /* @ts-ignore */
5190
+ ...(ngDevMode ? [{ debugName: "foregroundEpoch" }] : /* istanbul ignore next */ []));
5191
+ if (typeof document !== 'undefined') {
5192
+ fromEvent(document, 'visibilitychange')
5193
+ .pipe(takeUntilDestroyed(destroyRef))
5194
+ .subscribe(() => {
5195
+ if (document.visibilityState !== 'visible')
5196
+ return;
5197
+ foregroundEpoch.update((epoch) => epoch + 1);
5198
+ });
5199
+ }
5200
+ // The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
5201
+ // lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
5202
+ // timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
5203
+ // two tokens that happen to be due at the same moment still restart the timer.
5204
+ const nextScheduledRefresh = computed(() => {
5205
+ const token = context.accessToken();
5206
+ foregroundEpoch();
5207
+ return { dueInMs: token ? scheduledRefreshDelay(token) : null };
5208
+ }, /* @ts-ignore */
5209
+ ...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
5210
+ toObservable(nextScheduledRefresh)
5211
+ .pipe(switchMap(({ dueInMs }) => (dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts))), takeUntilDestroyed())
4949
5212
  .subscribe();
4950
5213
  context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
4951
5214
  const onRefreshFailure = config.onRefreshFailure ??
@@ -4965,18 +5228,23 @@ const withRefreshQuery = (key, config) => {
4965
5228
  // Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
4966
5229
  const autoRetryOn401 = config.autoRetryOn401 ?? true;
4967
5230
  if (autoRetryOn401) {
4968
- context.repository.events$
4969
- .pipe(filter((event) => {
4970
- // Only handle 401 errors for secure queries
4971
- if (event.type !== 'request-error')
4972
- return false;
4973
- if (!event.isSecure)
4974
- return false;
4975
- if (event.error?.status !== 401)
4976
- return false;
4977
- return true;
4978
- }), takeUntilDestroyed())
4979
- .subscribe(() => {
5231
+ context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
5232
+ if (event.type === 'request-success' && event.isSecure) {
5233
+ unauthorizedRefreshStreak = 0;
5234
+ delegatedRefreshAttempts = 0;
5235
+ return;
5236
+ }
5237
+ if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
5238
+ return;
5239
+ // A 401 from a request that went out with an older access token asks for a refresh that
5240
+ // already happened - the query re-runs itself with the current token (see
5241
+ // createSecureExecuteFactory). Refreshing again here would spend the refresh token that
5242
+ // pair came with, invalidating the tokens every other in-flight request is using; their
5243
+ // 401s would each do the same, keeping the loop alive for as long as queries are in flight.
5244
+ const sentAuthorization = event.request.subtle.lastSentHeaders()?.get('Authorization');
5245
+ const currentToken = context.accessToken();
5246
+ if (sentAuthorization && currentToken && sentAuthorization !== `Bearer ${currentToken}`)
5247
+ return;
4980
5248
  executeRefresh('unauthorized');
4981
5249
  });
4982
5250
  }
@@ -4997,6 +5265,23 @@ const withRefreshQuery = (key, config) => {
4997
5265
  };
4998
5266
  };
4999
5267
 
5268
+ /**
5269
+ * Ends the session once the user has been inactive for {@link InactivityLogoutConfig.inactivityTimeout},
5270
+ * with `sessionEndCause` reporting `'inactivity'`.
5271
+ *
5272
+ * Idleness is a property of the session, not of a tab: with `withBearerAuthMultiTabSync` on, tabs tell
5273
+ * each other about activity, so the tab the user is typing in keeps the forgotten one from logging
5274
+ * everybody out. Without it - or with `syncLogout: false`, where a logout stays in the tab that
5275
+ * decided it - each tab times out on its own.
5276
+ *
5277
+ * @example
5278
+ * export const AUTH_PROVIDER = createBearerAuthProvider({
5279
+ * name: 'my-auth',
5280
+ * queryClientRef: MY_CLIENT,
5281
+ * queries: [loginQuery, refreshQuery],
5282
+ * features: [withBearerAuthMultiTabSync(), withInactivityLogout({ inactivityTimeout: 5 * 60 * 1000 })],
5283
+ * });
5284
+ */
5000
5285
  const withInactivityLogout = (config = {}) => {
5001
5286
  return (context) => {
5002
5287
  const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
@@ -5007,8 +5292,25 @@ const withInactivityLogout = (config = {}) => {
5007
5292
  ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
5008
5293
  const lastActivityTime = signal(Date.now(), /* @ts-ignore */
5009
5294
  ...(ngDevMode ? [{ debugName: "lastActivityTime" }] : /* istanbul ignore next */ []));
5295
+ const activityCoordination = context.activityCoordination;
5296
+ // Announcing every throttled event would wake every other tab once a second for as long as the
5297
+ // user scrolls. A quarter of the timeout is far more than the other tabs need to never expire,
5298
+ // and it is one message per several minutes at the default.
5299
+ const announceInterval = inactivityTimeout / 4;
5300
+ let lastAnnouncedAt = 0;
5301
+ const markActive = () => lastActivityTime.set(Date.now());
5302
+ const announceActivity = () => {
5303
+ if (!activityCoordination)
5304
+ return;
5305
+ const now = Date.now();
5306
+ if (now - lastAnnouncedAt < announceInterval)
5307
+ return;
5308
+ lastAnnouncedAt = now;
5309
+ activityCoordination.announce();
5310
+ };
5010
5311
  const resetTimer = () => {
5011
- lastActivityTime.set(Date.now());
5312
+ markActive();
5313
+ announceActivity();
5012
5314
  };
5013
5315
  const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
5014
5316
  const activityFromCustomCheck$ = config.customActivityCheck
@@ -5020,15 +5322,35 @@ const withInactivityLogout = (config = {}) => {
5020
5322
  resetTimer();
5021
5323
  }
5022
5324
  });
5023
- const inactivityLogout$ = activity$.pipe(switchMap(() => timer(inactivityTimeout)), filter(() => enabled() && !!context.accessToken()));
5325
+ // Activity from another tab is never announced onwards - two tabs would echo one keystroke back
5326
+ // and forth for as long as they are both open.
5327
+ const remoteActivitySubscription = activityCoordination?.activity$.subscribe(() => {
5328
+ if (enabled() && context.accessToken()) {
5329
+ markActive();
5330
+ }
5331
+ });
5332
+ const isIdle = () => Date.now() - lastActivityTime() >= inactivityTimeout;
5333
+ // Driven off `lastActivityTime` rather than off the activity stream, so the countdown this feature
5334
+ // reports and the logout it performs cannot disagree: a tab that has seen no event still has a
5335
+ // deadline, and `resetTimer()` postpones the logout it says it resets.
5336
+ //
5337
+ // `isIdle` re-checks at the moment the deadline fires, because the re-arm goes through an effect:
5338
+ // activity recorded in the same tick as the old deadline has not moved it yet. The write that was
5339
+ // missed is what schedules that re-arm, so nothing is dropped by waiting for it.
5340
+ const inactivityLogout$ = toObservable(lastActivityTime).pipe(switchMap((last) => timer(last + inactivityTimeout - Date.now())), filter(() => enabled() && !!context.accessToken() && isIdle()));
5024
5341
  const logoutSubscription = inactivityLogout$.subscribe(() => {
5025
5342
  context.logout('inactivity');
5026
5343
  });
5344
+ let hadToken = false;
5345
+ // Only the start of a session counts as activity. A token _refresh_ is the app working, not the
5346
+ // user, and resetting on one would mean an app that refreshes faster than this timeout never logs
5347
+ // an idle user out at all.
5027
5348
  effect(() => {
5028
- const token = context.accessToken();
5029
- if (token && enabled()) {
5030
- resetTimer();
5349
+ const hasToken = !!context.accessToken();
5350
+ if (hasToken && !hadToken) {
5351
+ markActive();
5031
5352
  }
5353
+ hadToken = hasToken;
5032
5354
  });
5033
5355
  const enable = () => {
5034
5356
  enabled.set(true);
@@ -5045,9 +5367,9 @@ const withInactivityLogout = (config = {}) => {
5045
5367
  const remaining = inactivityTimeout - elapsed;
5046
5368
  return remaining > 0 ? remaining : 0;
5047
5369
  };
5048
- resetTimer();
5049
5370
  destroyRef.onDestroy(() => {
5050
5371
  activitySubscription.unsubscribe();
5372
+ remoteActivitySubscription?.unsubscribe();
5051
5373
  logoutSubscription.unsubscribe();
5052
5374
  });
5053
5375
  const instance = {
@@ -5063,6 +5385,7 @@ const withInactivityLogout = (config = {}) => {
5063
5385
  devtools: () => [
5064
5386
  { label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
5065
5387
  { label: 'activity events', value: activityEvents.join(', ') },
5388
+ { label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
5066
5389
  ...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
5067
5390
  ],
5068
5391
  };
@@ -5267,6 +5590,12 @@ const resetEncryptionKey = () => {
5267
5590
  * through. A message without one comes from a tab running an older version.
5268
5591
  */
5269
5592
  const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
5593
+ /**
5594
+ * The bounded wait a joining tab gives the leader to send the live session over. Short enough to be
5595
+ * invisible in front of the network request it defers, long enough for a same-origin channel message
5596
+ * plus the answer to come back.
5597
+ */
5598
+ const sessionAdoptionTimeoutMs = 250;
5270
5599
  /**
5271
5600
  * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5272
5601
  * providers reachable from the same origin do not push each other's tokens around.
@@ -5296,8 +5625,32 @@ const setupMultiTabSync = (config, context) => {
5296
5625
  // broadcasting effects below run after it, so they would always see it cleared.
5297
5626
  let lastSyncedState = null;
5298
5627
  let hadTokens = false;
5628
+ let isAdoptionPending = false;
5629
+ let settleAdoption = () => {
5630
+ /* replaced below when the handshake actually runs */
5631
+ };
5632
+ const remoteActivity = new Subject();
5299
5633
  channel.onmessage = (event) => {
5300
5634
  const message = event.data;
5635
+ if (message.type === 'activity') {
5636
+ if (syncLogout)
5637
+ remoteActivity.next();
5638
+ return;
5639
+ }
5640
+ if (message.type === 'state-request') {
5641
+ if (!syncTokens || !context.isLeader())
5642
+ return;
5643
+ const access = context.accessToken();
5644
+ const refresh = context.refreshToken();
5645
+ if (!access || !refresh)
5646
+ return;
5647
+ channel.postMessage({
5648
+ type: 'tokens-updated',
5649
+ accessToken: encryptToken(access),
5650
+ refreshToken: encryptToken(refresh),
5651
+ });
5652
+ return;
5653
+ }
5301
5654
  if (message.type === 'logout' && syncLogout) {
5302
5655
  lastSyncedState = LOGGED_OUT;
5303
5656
  hadTokens = false;
@@ -5313,11 +5666,34 @@ const setupMultiTabSync = (config, context) => {
5313
5666
  lastSyncedState = tokenState(access, refresh);
5314
5667
  hadTokens = true;
5315
5668
  context.applyTokens(access, refresh);
5669
+ settleAdoption();
5316
5670
  };
5671
+ let adoptionTimeout;
5317
5672
  const cleanup = () => {
5673
+ clearTimeout(adoptionTimeout);
5674
+ settleAdoption();
5675
+ remoteActivity.complete();
5318
5676
  channel.close();
5319
5677
  };
5320
5678
  destroyRef.onDestroy(cleanup);
5679
+ // Sync is push-only otherwise: a tab only broadcasts tokens that just changed, so a tab joining a
5680
+ // live session would hear nothing and start its own login. Asking is the missing half.
5681
+ const sessionAdoption = syncTokens
5682
+ ? (() => {
5683
+ isAdoptionPending = true;
5684
+ const settled = new Promise((resolve) => {
5685
+ settleAdoption = () => {
5686
+ if (!isAdoptionPending)
5687
+ return;
5688
+ isAdoptionPending = false;
5689
+ resolve();
5690
+ };
5691
+ });
5692
+ adoptionTimeout = setTimeout(settleAdoption, sessionAdoptionTimeoutMs);
5693
+ channel.postMessage({ type: 'state-request' });
5694
+ return { isPending: () => isAdoptionPending, settled };
5695
+ })()
5696
+ : undefined;
5321
5697
  if (syncTokens) {
5322
5698
  effect(() => {
5323
5699
  const access = context.accessToken();
@@ -5353,7 +5729,15 @@ const setupMultiTabSync = (config, context) => {
5353
5729
  channel.postMessage(message);
5354
5730
  });
5355
5731
  }
5356
- return { cleanup };
5732
+ // Only where a logout travels: with `syncLogout: false` each tab already ends its own session and
5733
+ // nothing else's, so its idleness is genuinely its own.
5734
+ const activityCoordination = syncLogout
5735
+ ? {
5736
+ announce: () => channel.postMessage({ type: 'activity' }),
5737
+ activity$: remoteActivity.asObservable(),
5738
+ }
5739
+ : undefined;
5740
+ return { cleanup, sessionAdoption, activityCoordination };
5357
5741
  };
5358
5742
 
5359
5743
  const SINGLE_TAB = /* @__PURE__ */ (() => {
@@ -5402,17 +5786,24 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5402
5786
  // regular feature setup runs - so this half has to happen earlier than `setup`.
5403
5787
  const earlySetup = (context) => {
5404
5788
  channelName = config.channelName ?? defaultSyncChannelName(context.name);
5405
- setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5789
+ // Filled in below once the election exists. Until then this tab answers as its own leader, which
5790
+ // is what it is: the sync is set up first because the auth queries read `isLeader` while being
5791
+ // wired, and a `state-request` cannot arrive before this function returns anyway.
5792
+ let isLeaderRef = () => true;
5793
+ const sync = setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5406
5794
  accessToken: context.accessToken,
5407
5795
  refreshToken: context.refreshToken,
5408
5796
  sessionEndCause: context.sessionEndCause,
5409
5797
  name: context.name,
5798
+ isLeader: () => isLeaderRef(),
5410
5799
  applyTokens: context.applyTokens,
5411
5800
  logout: context.logout,
5412
5801
  });
5413
- if (config.leaderElection === false)
5414
- return {};
5802
+ if (config.leaderElection === false) {
5803
+ return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
5804
+ }
5415
5805
  const election = setupLeaderElection({ name: context.name });
5806
+ isLeaderRef = () => election.isLeader();
5416
5807
  instance = {
5417
5808
  isLeader: election.isLeader,
5418
5809
  instanceCount: election.instanceCount,
@@ -5422,6 +5813,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5422
5813
  isLeader: () => election.isLeader(),
5423
5814
  leaderElection: instance,
5424
5815
  refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
5816
+ sessionAdoption: sync.sessionAdoption,
5817
+ activityCoordination: sync.activityCoordination,
5425
5818
  };
5426
5819
  };
5427
5820
  return Object.assign(setup, { earlySetup });
@@ -5496,6 +5889,14 @@ const createPersistentAuthFeature = (config, context) => {
5496
5889
  if (wasRejected)
5497
5890
  removeCookie();
5498
5891
  });
5892
+ const exchangeCookie = () => {
5893
+ const storedToken = getCookie(cookieName);
5894
+ if (!storedToken)
5895
+ return;
5896
+ const decryptedToken = decryptToken(storedToken);
5897
+ const args = config.autoLogin.buildArgs(decryptedToken);
5898
+ context.queries[config.autoLogin.queryKey].execute(args, { triggeredBy: 'persistent-auth' });
5899
+ };
5499
5900
  const tryLogin = () => {
5500
5901
  const currentRoute = route();
5501
5902
  const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
@@ -5506,12 +5907,24 @@ const createPersistentAuthFeature = (config, context) => {
5506
5907
  if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
5507
5908
  return;
5508
5909
  }
5509
- const storedToken = getCookie(cookieName);
5510
- if (storedToken) {
5511
- const decryptedToken = decryptToken(storedToken);
5512
- const args = config.autoLogin.buildArgs(decryptedToken);
5513
- context.queries[config.autoLogin.queryKey].execute(args, { triggeredBy: 'persistent-auth' });
5910
+ // A session is already here - synced in from another tab, or restored by a previous call. Spending
5911
+ // the cookie now would rotate a refresh token every other tab is holding, for nothing.
5912
+ if (refreshToken())
5913
+ return;
5914
+ const adoption = context.sessionAdoption;
5915
+ // With multi-tab sync on, another tab may be about to hand this one its live session. Wait for
5916
+ // that answer before exchanging the cookie - bounded, so a lone tab is not held up by a reply
5917
+ // that is never coming - and only when there is a cookie to spend, so a tab with nothing to
5918
+ // restore still starts up synchronously.
5919
+ if (adoption?.isPending() && getCookie(cookieName)) {
5920
+ void adoption.settled.then(() => {
5921
+ if (refreshToken())
5922
+ return;
5923
+ exchangeCookie();
5924
+ });
5925
+ return;
5514
5926
  }
5927
+ exchangeCookie();
5515
5928
  };
5516
5929
  const setRememberMe = (enabled) => {
5517
5930
  if (typeof localStorage !== 'undefined') {
@@ -5811,124 +6224,13 @@ const queryDevtoolsAbout = () => ({
5811
6224
  app,
5812
6225
  });
5813
6226
 
5814
- /** The neutral fault: nothing armed. */
5815
- const EMPTY_QUERY_DEVTOOLS_FAULT = {
5816
- latencyMs: 0,
5817
- failNext: 0,
5818
- failRate: 0,
5819
- status: 503,
5820
- };
5821
- /**
5822
- * Statuses worth injecting, and whether the SDK's default retry policy retries them. A picker built from
5823
- * this cannot arm a 500 and then leave you wondering why no retry fired.
5824
- * @see runDefaultQueryRetry
5825
- */
5826
- const QUERY_DEVTOOLS_FAULT_STATUSES = [
5827
- { status: 0, label: 'Network error', retryable: true },
5828
- { status: 400, label: 'Bad Request', retryable: false },
5829
- { status: 401, label: 'Unauthorized', retryable: false },
5830
- { status: 403, label: 'Forbidden', retryable: false },
5831
- { status: 404, label: 'Not Found', retryable: false },
5832
- { status: 408, label: 'Request Timeout', retryable: true },
5833
- { status: 429, label: 'Too Many Requests', retryable: true },
5834
- { status: 500, label: 'Internal Server Error', retryable: false },
5835
- { status: 502, label: 'Bad Gateway', retryable: true },
5836
- { status: 503, label: 'Service Unavailable', retryable: true },
5837
- ];
5838
- const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
5839
- ...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
5840
- /**
5841
- * The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
5842
- * are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
5843
- */
5844
- const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
5845
- /** Whether anything at all is armed on a fault. */
5846
- const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
5847
- /**
5848
- * Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
5849
- * value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
5850
- *
5851
- * Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
5852
- * something an application should arm on itself.
5853
- */
5854
- const setQueryDevtoolsFault = (options) => {
5855
- const { clientName, patch } = options;
5856
- faults.update((current) => {
5857
- const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
5858
- if (!isQueryDevtoolsFaultArmed(next)) {
5859
- const { [clientName]: _removed, ...rest } = current;
5860
- return rest;
5861
- }
5862
- return { ...current, [clientName]: next };
5863
- });
5864
- };
5865
- /**
5866
- * Disarms every fault of one client, or of all clients when no name is given.
5867
- *
5868
- * @see setQueryDevtoolsFault
5869
- */
5870
- const clearQueryDevtoolsFaults = (clientName) => faults.update((current) => {
5871
- if (clientName === undefined)
5872
- return {};
5873
- const { [clientName]: _removed, ...rest } = current;
5874
- return rest;
5875
- });
5876
- /**
5877
- * Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
5878
- * must be called once per attempt and never speculatively.
5879
- *
5880
- * Installed as the fault resolver by `provideQueryDevtools()`; nothing else may call it.
5881
- * @internal
5882
- */
5883
- const resolveQueryDevtoolsFaultForAttempt = (target) => {
5884
- const fault = faults()[target.clientName];
5885
- if (!fault || !isQueryDevtoolsFaultArmed(fault))
5886
- return null;
5887
- let shouldFail = false;
5888
- if (fault.failNext > 0) {
5889
- shouldFail = true;
5890
- setQueryDevtoolsFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
5891
- }
5892
- else if (fault.failRate > 0) {
5893
- shouldFail = Math.random() * 100 < fault.failRate;
5894
- }
5895
- return {
5896
- latencyMs: fault.latencyMs,
5897
- status: shouldFail ? fault.status : null,
5898
- };
5899
- };
5900
-
5901
- /**
5902
- * Records which query forms feed one query's args, so the devtools can answer "which query does this
5903
- * form drive?" from the reads themselves rather than from a naming convention.
5904
- * @internal
5905
- */
5906
- const createQueryDevtoolsFormLinks = () => {
5907
- const ids = signal([], /* @ts-ignore */
5908
- ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
5909
- return {
5910
- ids: ids.asReadonly(),
5911
- track: (build) => {
5912
- const collected = collectQueryFormLinks(build);
5913
- // Args are built inside a reactive computation, which may not write to a signal - `untracked`
5914
- // lifts that restriction, and skipping the write unless something is new keeps the panel from
5915
- // being notified on every evaluation.
5916
- untracked(() => {
5917
- const known = ids();
5918
- const added = collected.ids.filter((id) => !known.includes(id));
5919
- if (added.length)
5920
- ids.set([...known, ...added]);
5921
- });
5922
- return collected.value;
5923
- },
5924
- };
5925
- };
5926
-
5927
6227
  const DEFAULTS = {
5928
6228
  viewState: 'session',
5929
6229
  pins: 'local',
5930
6230
  overrides: 'none',
5931
6231
  mocks: 'local',
6232
+ armedMocks: 'none',
6233
+ armedFaults: 'none',
5932
6234
  maxEvents: 100,
5933
6235
  maxDroppedCacheEntries: 20,
5934
6236
  responseHistory: null,
@@ -6031,6 +6333,8 @@ const sanitize = (value) => ({
6031
6333
  pins: asScope(value?.pins, DEFAULTS.pins),
6032
6334
  overrides: asScope(value?.overrides, DEFAULTS.overrides),
6033
6335
  mocks: asScope(value?.mocks, DEFAULTS.mocks),
6336
+ armedMocks: asScope(value?.armedMocks, DEFAULTS.armedMocks),
6337
+ armedFaults: asScope(value?.armedFaults, DEFAULTS.armedFaults),
6034
6338
  maxEvents: asCount(value?.maxEvents, LIMITS.maxEvents, DEFAULTS.maxEvents),
6035
6339
  maxDroppedCacheEntries: asCount(value?.maxDroppedCacheEntries, LIMITS.maxDroppedCacheEntries, DEFAULTS.maxDroppedCacheEntries),
6036
6340
  responseHistory: asOptionalCount(value?.responseHistory, LIMITS.responseHistory),
@@ -6059,23 +6363,213 @@ const setQueryDevtoolsSettings = (patch) => {
6059
6363
  writeQueryDevtoolsStore('local', SETTINGS_KEY, next);
6060
6364
  };
6061
6365
 
6366
+ /** The neutral fault: nothing armed. */
6367
+ const EMPTY_QUERY_DEVTOOLS_FAULT = {
6368
+ latencyMs: 0,
6369
+ failNext: 0,
6370
+ failRate: 0,
6371
+ status: 503,
6372
+ };
6373
+ /**
6374
+ * Statuses worth injecting, and whether the SDK's default retry policy retries them. A picker built from
6375
+ * this cannot arm a 500 and then leave you wondering why no retry fired.
6376
+ * @see runDefaultQueryRetry
6377
+ */
6378
+ const QUERY_DEVTOOLS_FAULT_STATUSES = [
6379
+ { status: 0, label: 'Network error', retryable: true },
6380
+ { status: 400, label: 'Bad Request', retryable: false },
6381
+ { status: 401, label: 'Unauthorized', retryable: false },
6382
+ { status: 403, label: 'Forbidden', retryable: false },
6383
+ { status: 404, label: 'Not Found', retryable: false },
6384
+ { status: 408, label: 'Request Timeout', retryable: true },
6385
+ { status: 429, label: 'Too Many Requests', retryable: true },
6386
+ { status: 500, label: 'Internal Server Error', retryable: false },
6387
+ { status: 502, label: 'Bad Gateway', retryable: true },
6388
+ { status: 503, label: 'Service Unavailable', retryable: true },
6389
+ ];
6390
+ const STORAGE_KEY$2 = 'ethlete:query:devtools:faults:v1';
6391
+ const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
6392
+ ...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
6393
+ const restored$1 = /* @__PURE__ */ signal(false, /* @ts-ignore */
6394
+ ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
6395
+ const scope$2 = () => queryDevtoolsSettings().armedFaults;
6396
+ /**
6397
+ * The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
6398
+ * are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
6399
+ */
6400
+ const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
6401
+ /**
6402
+ * Whether what is armed right now came back from a previous page load rather than being armed by hand.
6403
+ * The panel's armed bar says so, because injected failures nobody remembers arming look exactly like a
6404
+ * broken API.
6405
+ */
6406
+ const queryDevtoolsFaultsRestored = /* @__PURE__ */ restored$1.asReadonly();
6407
+ const write$1 = () => {
6408
+ if (scope$2() === 'none') {
6409
+ clearQueryDevtoolsStore(STORAGE_KEY$2);
6410
+ return;
6411
+ }
6412
+ writeQueryDevtoolsStore(scope$2(), STORAGE_KEY$2, faults());
6413
+ };
6414
+ const asFault = (value) => {
6415
+ const fault = value;
6416
+ if (!fault || typeof fault !== 'object')
6417
+ return null;
6418
+ const next = {
6419
+ latencyMs: Number(fault.latencyMs) || 0,
6420
+ failNext: Number(fault.failNext) || 0,
6421
+ failRate: Number(fault.failRate) || 0,
6422
+ status: Number.isFinite(Number(fault.status)) ? Number(fault.status) : EMPTY_QUERY_DEVTOOLS_FAULT.status,
6423
+ };
6424
+ return isQueryDevtoolsFaultArmed(next) ? next : null;
6425
+ };
6426
+ /**
6427
+ * Reads the armed faults back, when the `armedFaults` setting asks for them. Called by
6428
+ * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
6429
+ * @internal
6430
+ */
6431
+ const initQueryDevtoolsFaults = () => {
6432
+ const stored = readQueryDevtoolsStore(scope$2(), STORAGE_KEY$2);
6433
+ const inherited = {};
6434
+ for (const [clientName, value] of Object.entries(stored ?? {})) {
6435
+ const fault = asFault(value);
6436
+ if (fault)
6437
+ inherited[clientName] = fault;
6438
+ }
6439
+ faults.set(inherited);
6440
+ restored$1.set(Object.keys(inherited).length > 0);
6441
+ };
6442
+ /**
6443
+ * Where the armed faults are kept. Switching to a scope that keeps them stores whatever is armed right
6444
+ * now; switching to `none` empties the store and leaves this page's faults exactly where they are.
6445
+ */
6446
+ const setQueryDevtoolsFaultsScope = (next) => {
6447
+ if (next === scope$2())
6448
+ return;
6449
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6450
+ clearQueryDevtoolsStore(STORAGE_KEY$2);
6451
+ setQueryDevtoolsSettings({ armedFaults: next });
6452
+ write$1();
6453
+ };
6454
+ /** Whether anything at all is armed on a fault. */
6455
+ const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
6456
+ const patchFault = (options) => {
6457
+ const { clientName, patch } = options;
6458
+ faults.update((current) => {
6459
+ const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
6460
+ if (!isQueryDevtoolsFaultArmed(next)) {
6461
+ const { [clientName]: _removed, ...rest } = current;
6462
+ return rest;
6463
+ }
6464
+ return { ...current, [clientName]: next };
6465
+ });
6466
+ write$1();
6467
+ };
6468
+ /**
6469
+ * Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
6470
+ * value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
6471
+ *
6472
+ * Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
6473
+ * something an application should arm on itself.
6474
+ */
6475
+ const setQueryDevtoolsFault = (options) => {
6476
+ patchFault(options);
6477
+ restored$1.set(false);
6478
+ };
6479
+ /**
6480
+ * Disarms every fault of one client, or of all clients when no name is given.
6481
+ *
6482
+ * @see setQueryDevtoolsFault
6483
+ */
6484
+ const clearQueryDevtoolsFaults = (clientName) => {
6485
+ faults.update((current) => {
6486
+ if (clientName === undefined)
6487
+ return {};
6488
+ const { [clientName]: _removed, ...rest } = current;
6489
+ return rest;
6490
+ });
6491
+ restored$1.set(false);
6492
+ write$1();
6493
+ };
6494
+ /**
6495
+ * Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
6496
+ * must be called once per attempt and never speculatively.
6497
+ *
6498
+ * Installed as the fault resolver by `provideQueryDevtools()`; nothing else may call it.
6499
+ * @internal
6500
+ */
6501
+ const resolveQueryDevtoolsFaultForAttempt = (target) => {
6502
+ const fault = faults()[target.clientName];
6503
+ if (!fault || !isQueryDevtoolsFaultArmed(fault))
6504
+ return null;
6505
+ let shouldFail = false;
6506
+ if (fault.failNext > 0) {
6507
+ shouldFail = true;
6508
+ // Not the public setter: spending the budget is the armed fault doing its job, not someone re-arming
6509
+ // it, so it must not clear what the panel's bar says about where the fault came from.
6510
+ patchFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
6511
+ }
6512
+ else if (fault.failRate > 0) {
6513
+ shouldFail = Math.random() * 100 < fault.failRate;
6514
+ }
6515
+ return {
6516
+ latencyMs: fault.latencyMs,
6517
+ status: shouldFail ? fault.status : null,
6518
+ };
6519
+ };
6520
+
6521
+ /**
6522
+ * Records which query forms feed one query's args, so the devtools can answer "which query does this
6523
+ * form drive?" from the reads themselves rather than from a naming convention.
6524
+ * @internal
6525
+ */
6526
+ const createQueryDevtoolsFormLinks = () => {
6527
+ const ids = signal([], /* @ts-ignore */
6528
+ ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
6529
+ return {
6530
+ ids: ids.asReadonly(),
6531
+ track: (build) => {
6532
+ const collected = collectQueryFormLinks(build);
6533
+ // Args are built inside a reactive computation, which may not write to a signal - `untracked`
6534
+ // lifts that restriction, and skipping the write unless something is new keeps the panel from
6535
+ // being notified on every evaluation.
6536
+ untracked(() => {
6537
+ const known = ids();
6538
+ const added = collected.ids.filter((id) => !known.includes(id));
6539
+ if (added.length)
6540
+ ids.set([...known, ...added]);
6541
+ });
6542
+ return collected.value;
6543
+ },
6544
+ };
6545
+ };
6546
+
6062
6547
  /**
6063
- * The designed library. Whether a mock is **armed** is deliberately not part of it - see
6548
+ * The designed library. Which of them is **armed** is a separate store under a separate scope - see
6064
6549
  * {@link queryDevtoolsArmedMocks}.
6065
6550
  */
6066
6551
  const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
6552
+ const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
6067
6553
  const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
6068
6554
  ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
6069
6555
  const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
6070
6556
  ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
6557
+ const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
6558
+ ...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
6071
6559
  const scope$1 = () => queryDevtoolsSettings().mocks;
6560
+ const armedScope = () => queryDevtoolsSettings().armedMocks;
6072
6561
  /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
6073
6562
  const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
6074
6563
  /**
6075
- * The ids currently being served. **Never persisted, at any scope**: an app that silently serves designed
6076
- * data tomorrow morning is worse than losing which mocks were armed, so arming is per page load.
6564
+ * The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
6565
+ * an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
6077
6566
  */
6078
6567
  const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
6568
+ /**
6569
+ * Whether what is armed right now came back from a previous page load rather than being armed by hand.
6570
+ * The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
6571
+ */
6572
+ const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
6079
6573
  /**
6080
6574
  * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
6081
6575
  * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
@@ -6088,6 +6582,19 @@ const write = () => {
6088
6582
  }
6089
6583
  writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
6090
6584
  };
6585
+ const writeArmed = () => {
6586
+ if (armedScope() === 'none') {
6587
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6588
+ return;
6589
+ }
6590
+ writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
6591
+ };
6592
+ /** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
6593
+ const setArmed = (next) => {
6594
+ armed.set(next);
6595
+ armedRestored.set(false);
6596
+ writeArmed();
6597
+ };
6091
6598
  const isMock = (value) => {
6092
6599
  const mock = value;
6093
6600
  return (!!mock &&
@@ -6106,7 +6613,15 @@ const initQueryDevtoolsMocks = () => {
6106
6613
  const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
6107
6614
  // `query` was added after the first version of the store, so an entry written before it still loads.
6108
6615
  mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
6109
- armed.set(new Set());
6616
+ const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
6617
+ const library = new Set(mocks().map((mock) => mock.id));
6618
+ // An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
6619
+ // mock that may never be designed again.
6620
+ const inherited = Array.isArray(kept)
6621
+ ? kept.filter((id) => typeof id === 'string' && library.has(id))
6622
+ : [];
6623
+ armed.set(new Set(inherited));
6624
+ armedRestored.set(inherited.length > 0);
6110
6625
  };
6111
6626
  /** The path of a request URL, without its origin or query string - what a pattern is matched against. */
6112
6627
  const queryDevtoolsRequestPath = (url) => {
@@ -6163,24 +6678,39 @@ const deleteQueryDevtoolsMock = (id) => {
6163
6678
  };
6164
6679
  /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
6165
6680
  const armQueryDevtoolsMock = (id, isArmed) => {
6166
- armed.update((current) => {
6167
- if (isArmed && !mocks().some((mock) => mock.id === id))
6168
- return current;
6169
- const next = new Set(current);
6170
- if (isArmed)
6171
- next.add(id);
6172
- else
6173
- next.delete(id);
6174
- return next;
6175
- });
6176
- };
6681
+ if (isArmed && !mocks().some((mock) => mock.id === id))
6682
+ return;
6683
+ const next = new Set(armed());
6684
+ if (isArmed)
6685
+ next.add(id);
6686
+ else
6687
+ next.delete(id);
6688
+ setArmed(next);
6689
+ };
6690
+ /** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
6691
+ const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
6177
6692
  /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
6178
- const clearQueryDevtoolsArmedMocks = () => armed.set(new Set());
6693
+ const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
6694
+ /**
6695
+ * Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
6696
+ * the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
6697
+ * exactly what it was already serving.
6698
+ */
6699
+ const setQueryDevtoolsArmedMocksScope = (next) => {
6700
+ if (next === armedScope())
6701
+ return;
6702
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6703
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6704
+ setQueryDevtoolsSettings({ armedMocks: next });
6705
+ writeArmed();
6706
+ };
6179
6707
  /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
6180
6708
  const clearQueryDevtoolsMockStore = () => {
6181
6709
  mocks.set([]);
6182
6710
  armed.set(new Set());
6711
+ armedRestored.set(false);
6183
6712
  clearQueryDevtoolsStore(STORAGE_KEY$1);
6713
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6184
6714
  };
6185
6715
  const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
6186
6716
  /**
@@ -6916,7 +7446,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
6916
7446
 
6917
7447
  // Generated from package.json by tools/scripts/generate-version.js - do not edit.
6918
7448
  /** The version of `@ethlete/query` this build was cut from. */
6919
- const QUERY_VERSION = '6.0.0-next.33';
7449
+ const QUERY_VERSION = '6.0.0-next.35';
6920
7450
 
6921
7451
  /** How deep a schema is expanded before a nested branch is given up on. */
6922
7452
  const MAX_DEPTH = 12;
@@ -7248,6 +7778,89 @@ const seedQueryDevtoolsSchemaBody = (name) => {
7248
7778
  return null;
7249
7779
  return seedFrom(schema, name);
7250
7780
  };
7781
+ /** Where a schema was collected from, so the same name arriving from two pointers is caught. */
7782
+ const originOf = (label, pointer) => pointer ?? `named:${label}`;
7783
+ /**
7784
+ * Copies a schema, pointing every local `$ref` at `#/components/schemas/<name>` and queueing that name -
7785
+ * so a document assembled from these resolves against its own `components`, whether the description kept
7786
+ * its schemas under `components.schemas`, `definitions` or `$defs`.
7787
+ */
7788
+ const rewriteRefs = (value, queue) => {
7789
+ if (Array.isArray(value))
7790
+ return value.map((entry) => rewriteRefs(entry, queue));
7791
+ if (!isRecord(value))
7792
+ return value;
7793
+ const result = {};
7794
+ for (const [key, entry] of Object.entries(value)) {
7795
+ if (key === '$ref' && typeof entry === 'string') {
7796
+ if (!entry.startsWith('#/')) {
7797
+ result[key] = entry;
7798
+ continue;
7799
+ }
7800
+ const label = refLabel(entry);
7801
+ queue({ label, pointer: entry });
7802
+ result[key] = `#/components/schemas/${label}`;
7803
+ continue;
7804
+ }
7805
+ result[key] = rewriteRefs(entry, queue);
7806
+ }
7807
+ return result;
7808
+ };
7809
+ /**
7810
+ * Collects the named schemas a set of routes was designed against, ready to be handed back as the
7811
+ * `components.schemas` of an exported document - so a route seeded from `MatchView` can reference
7812
+ * `MatchView` instead of an anonymous shape inferred from one body.
7813
+ *
7814
+ * Part of the devtools contract. **Not part of the general public contract.**
7815
+ */
7816
+ const collectQueryDevtoolsSchemaComponents = (names) => {
7817
+ const declared = namedSchemas();
7818
+ const schemas = {};
7819
+ const notes = new Set();
7820
+ const origins = new Map();
7821
+ const pending = names.map((label) => ({ label, pointer: null }));
7822
+ while (pending.length) {
7823
+ const request = pending.shift();
7824
+ const origin = originOf(request.label, request.pointer);
7825
+ const seen = origins.get(request.label);
7826
+ if (seen !== undefined) {
7827
+ if (seen !== origin) {
7828
+ notes.add(`Two different schemas are both called ${request.label} - only the one from ${seen} was kept.`);
7829
+ }
7830
+ continue;
7831
+ }
7832
+ const schema = request.pointer === null ? declared[request.label] : resolveRef(request.pointer);
7833
+ if (schema === undefined) {
7834
+ if (request.pointer !== null) {
7835
+ notes.add(`${request.pointer} could not be resolved, so ${request.label} is missing from the export.`);
7836
+ }
7837
+ continue;
7838
+ }
7839
+ origins.set(request.label, origin);
7840
+ schemas[request.label] = rewriteRefs(schema, (next) => pending.push(next));
7841
+ }
7842
+ for (const schema of Object.values(schemas)) {
7843
+ collectRemoteRefs(schema, notes);
7844
+ }
7845
+ return { schemas, notes: [...notes] };
7846
+ };
7847
+ /** A `$ref` to another file is left as it was written, so a document says where it stops resolving. */
7848
+ const collectRemoteRefs = (value, notes) => {
7849
+ if (Array.isArray(value)) {
7850
+ for (const entry of value)
7851
+ collectRemoteRefs(entry, notes);
7852
+ return;
7853
+ }
7854
+ if (!isRecord(value))
7855
+ return;
7856
+ for (const [key, entry] of Object.entries(value)) {
7857
+ if (key === '$ref' && typeof entry === 'string' && !entry.startsWith('#/')) {
7858
+ notes.add(`${entry} points outside the description - it was exported unchanged and will not resolve.`);
7859
+ continue;
7860
+ }
7861
+ collectRemoteRefs(entry, notes);
7862
+ }
7863
+ };
7251
7864
  const segmentsOf = (pattern) => pattern.split('/').filter(Boolean);
7252
7865
  /** `/matches/{id}` and `/matches/:id` describe the same route; a mock's pattern is written the second way. */
7253
7866
  const toColonPattern = (path) => path.replace(/\{([^}]+)\}/g, ':$1');
@@ -7662,6 +8275,90 @@ const sumQueryDevtoolsStats = (handles) => {
7662
8275
  return total;
7663
8276
  };
7664
8277
 
8278
+ /**
8279
+ * The longest lifetime an armed override may claim, in seconds (7 days). Anything beyond it is never
8280
+ * refreshed at all, which is what arming nothing already does.
8281
+ */
8282
+ const QUERY_DEVTOOLS_TOKEN_TTL_LIMIT = 604800;
8283
+ /** The claims an overridden lifetime is measured from, in the order they are tried. */
8284
+ const ANCHOR_CLAIMS = ['iat', 'nbf'];
8285
+ const ttls = /* @__PURE__ */ signal({}, /* @ts-ignore */
8286
+ ...(ngDevMode ? [{ debugName: "ttls" }] : /* istanbul ignore next */ []));
8287
+ /**
8288
+ * The access-token lifetime armed per auth provider name, in seconds, consumed by the
8289
+ * `<et-query-devtools>` panel. A provider on its token's real lifetime is absent rather than present
8290
+ * with it.
8291
+ */
8292
+ const queryDevtoolsTokenTtls = /* @__PURE__ */ ttls.asReadonly();
8293
+ const tokenTtlAnchor = (options) => {
8294
+ const { payload, expiresInPropertyName } = options;
8295
+ if (!payload || typeof payload[expiresInPropertyName] !== 'number')
8296
+ return null;
8297
+ for (const claim of ANCHOR_CLAIMS) {
8298
+ const value = payload[claim];
8299
+ if (typeof value === 'number')
8300
+ return value;
8301
+ }
8302
+ return null;
8303
+ };
8304
+ /**
8305
+ * Whether an overridden lifetime can be applied to a decoded access token at all. It needs the claim
8306
+ * carrying the expiry - so a lifetime is never invented for a token that declares none - plus `iat` or
8307
+ * `nbf` to measure the new lifetime from.
8308
+ */
8309
+ const canOverrideQueryDevtoolsTokenTtl = (options) => tokenTtlAnchor({ payload: options.payload, expiresInPropertyName: options.expiresInPropertyName ?? 'exp' }) !== null;
8310
+ /**
8311
+ * Presents one auth provider's access token as living `seconds` from the moment it was issued, rather
8312
+ * than as long as it says it does. Everything that reads the expiry sees the shortened lifetime - the
8313
+ * proactive refresh schedule, `withBearerAuthTokenExpirationWarning`, and the panel's own countdown -
8314
+ * so arming 60 seconds makes a real refresh happen a minute after each token is issued, and keeps
8315
+ * happening for every token that refresh brings back.
8316
+ *
8317
+ * It is a lifetime, not a remaining time: `0` presents the current token as long expired, which is what
8318
+ * makes a refresh happen at once. Nothing is persisted, so a reload leaves every provider on its real
8319
+ * lifetime.
8320
+ *
8321
+ * Part of the devtools contract consumed by `<et-query-devtools>`; a token's lifetime is not something
8322
+ * an application should override on itself.
8323
+ */
8324
+ const setQueryDevtoolsTokenTtl = (options) => {
8325
+ const { providerName, seconds } = options;
8326
+ const clamped = Math.min(Math.max(Math.trunc(seconds) || 0, 0), QUERY_DEVTOOLS_TOKEN_TTL_LIMIT);
8327
+ ttls.update((current) => ({ ...current, [providerName]: clamped }));
8328
+ };
8329
+ /**
8330
+ * Puts one provider back on its token's real lifetime, or every provider when no name is given.
8331
+ *
8332
+ * @see setQueryDevtoolsTokenTtl
8333
+ */
8334
+ const clearQueryDevtoolsTokenTtl = (providerName) => {
8335
+ ttls.update((current) => {
8336
+ if (providerName === undefined)
8337
+ return {};
8338
+ const { [providerName]: _removed, ...rest } = current;
8339
+ return rest;
8340
+ });
8341
+ };
8342
+ /**
8343
+ * The decoded access token as the devtools want it seen: the claim carrying the expiry rewritten to the
8344
+ * armed lifetime, measured from `iat`. Returns `payload` untouched when nothing is armed for the
8345
+ * provider, or when the token cannot carry the override - see {@link canOverrideQueryDevtoolsTokenTtl}.
8346
+ *
8347
+ * Installed as the token payload patcher by `provideQueryDevtools()`, and called directly by the panel
8348
+ * for its own countdown.
8349
+ */
8350
+ const applyQueryDevtoolsTokenTtl = (options) => {
8351
+ const { payload, providerName, expiresInPropertyName = 'exp' } = options;
8352
+ const seconds = ttls()[providerName];
8353
+ if (seconds === undefined || !payload || typeof payload !== 'object')
8354
+ return payload;
8355
+ const record = payload;
8356
+ const anchor = tokenTtlAnchor({ payload: record, expiresInPropertyName });
8357
+ if (anchor === null)
8358
+ return payload;
8359
+ return { ...record, [expiresInPropertyName]: anchor + seconds };
8360
+ };
8361
+
7665
8362
  /**
7666
8363
  * How many destroyed queries the registry keeps. A tombstone holds the last response body it captured,
7667
8364
  * so the list is capped for the same reason the repository caps its unused entries.
@@ -7942,6 +8639,7 @@ const provideQueryDevtools = (options) => {
7942
8639
  setQueryDevtoolsAppInfo(options?.about);
7943
8640
  initQueryDevtoolsOverridePersistence();
7944
8641
  initQueryDevtoolsMocks();
8642
+ initQueryDevtoolsFaults();
7945
8643
  setQueryDevtoolsSchemaLoader(options?.schema);
7946
8644
  setQueryDevtoolsRegistrar(registerEntry);
7947
8645
  setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
@@ -7949,6 +8647,7 @@ const provideQueryDevtools = (options) => {
7949
8647
  setQueryDevtoolsOverridesFactory(createQueryDevtoolsOverrides);
7950
8648
  setQueryDevtoolsFaultResolver(resolveQueryDevtoolsFaultForAttempt);
7951
8649
  setQueryDevtoolsMockResolver(resolveQueryDevtoolsMockForAttempt);
8650
+ setQueryDevtoolsTokenPayloadPatcher(applyQueryDevtoolsTokenTtl);
7952
8651
  if (!isDevMode()) {
7953
8652
  console.warn('You are using the Query Devtools in production mode. This increases the size of your bundle and should only be used for development purposes.');
7954
8653
  }
@@ -13489,5 +14188,5 @@ const createWebSocketClient = (options) => {
13489
14188
  * Generated bundle index. Do not edit.
13490
14189
  */
13491
14190
 
13492
- export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsStore, registerEthleteVersion, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsAppInfo, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
14191
+ export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_DEVTOOLS_TOKEN_TTL_LIMIT, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryDevtoolsTokenTtl, applyQueryFeatures, armAllQueryDevtoolsMocks, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, canOverrideQueryDevtoolsTokenTtl, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTokenTtl, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryDevtoolsSchemaComponents, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDefaultRetryFn, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsFaults, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, patchQueryDevtoolsTokenPayload, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsArmedMocksRestored, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFaultsRestored, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryDevtoolsTokenTtls, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsStore, registerEthleteVersion, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsAppInfo, setQueryDevtoolsArmedMocksScope, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFaultsScope, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setQueryDevtoolsTokenPayloadPatcher, setQueryDevtoolsTokenTtl, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withLongPolling, withLongPollingUsedOnUnsupportedHttpMethod, withLongPollingUsedWithPolling, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
13493
14192
  //# sourceMappingURL=ethlete-query.mjs.map