@ethlete/query 6.0.0-next.34 → 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.
@@ -380,6 +380,8 @@ const QueryRuntimeErrorCode = {
380
380
  WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
381
381
  WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
382
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,
383
385
  // Auth provider
384
386
  AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
385
387
  AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
@@ -430,6 +432,13 @@ const withAutoRefreshUsedInManualQuery = () => {
430
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.` +
431
433
  ` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
432
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
+ };
433
442
  const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
434
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.`);
435
444
  };
@@ -534,11 +543,6 @@ const isSymfonyListError = (error) => {
534
543
  'invalidValue' in error[0]);
535
544
  };
536
545
 
537
- /**
538
- * Returning this inside a withArgs feature will reset the query args to null.
539
- * This will also pause polling and auto refresh until new args are set.
540
- */
541
- const CLEAR_QUERY_ARGS = /* @__PURE__ */ Symbol('CLEAR_QUERY_ARGS');
542
546
  /** A angular effect that can be nested inside another effect */
543
547
  const nestedEffect = (fn, options) => {
544
548
  const activeConsumer = getActiveConsumer();
@@ -555,6 +559,7 @@ const QueryFeatureType = {
555
559
  WITH_ERROR_HANDLING: 'WITH_ERROR_HANDLING',
556
560
  WITH_SUCCESS_HANDLING: 'WITH_SUCCESS_HANDLING',
557
561
  WITH_POLLING: 'WITH_POLLING',
562
+ WITH_LONG_POLLING: 'WITH_LONG_POLLING',
558
563
  WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
559
564
  WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
560
565
  WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
@@ -566,8 +571,8 @@ const createQueryFeature = (config) => {
566
571
  * A query feature that allows you to set the arguments of the query.
567
572
  * The arguments are read within a computed function, so you can use reactive values.
568
573
  *
569
- * To set the arguments to `null`, return the symbol `CLEAR_QUERY_ARGS`.
570
- * 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.
571
576
  *
572
577
  * Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
573
578
  */
@@ -575,26 +580,11 @@ const withArgs = (args) => {
575
580
  return createQueryFeature({
576
581
  type: QueryFeatureType.WITH_ARGS,
577
582
  fn: (context) => {
578
- // `state.args` is reactively backed by this source, so every reader (exec, polling,
579
- // auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
580
- // resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
581
- let previous = null;
582
- const resolve = () => {
583
- const value = args();
584
- if (value === CLEAR_QUERY_ARGS) {
585
- previous = null;
586
- return null;
587
- }
588
- if (value === null)
589
- return previous;
590
- previous = value;
591
- return value;
592
- };
593
583
  // Only ever non-null while the devtools are installed. Wrapping the source (rather than reading
594
584
  // `state.args` from the outside) is what puts the tracking window around the *evaluation* of the
595
585
  // args - a cached read of the computed runs nothing and would record no form at all.
596
586
  const formLinks = context.state.subtle.devtoolsFormLinks;
597
- context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(resolve) : resolve);
587
+ context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(args) : args);
598
588
  // Trigger (auto-)execution whenever the resolved args change.
599
589
  nestedEffect(() => {
600
590
  const currArgsNow = context.state.args();
@@ -690,6 +680,107 @@ const withPolling = (options) => {
690
680
  },
691
681
  });
692
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
+ };
693
784
  /** A query feature that can be used to log the latest http event */
694
785
  const withLogging = (options) => {
695
786
  return createQueryFeature({
@@ -1004,6 +1095,7 @@ const getQueryFeatureUsage = (
1004
1095
  options) => {
1005
1096
  const { creator, features, queryConfig, creatorInternals } = options;
1006
1097
  const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
1098
+ const hasPollingFeature = features.some((f) => f.type === QueryFeatureType.WITH_POLLING);
1007
1099
  const shouldAutoExecuteMethod = isCreateGqlQueryOptions(options)
1008
1100
  ? shouldAutoExecuteGqlQuery(options.creatorInternals.method)
1009
1101
  : shouldAutoExecuteQuery(options.creatorInternals.method);
@@ -1018,6 +1110,7 @@ options) => {
1018
1110
  }
1019
1111
  const featureFnContext = {
1020
1112
  hasWithArgsFeature,
1113
+ hasPollingFeature,
1021
1114
  shouldAutoExecuteMethod,
1022
1115
  shouldAutoExecute,
1023
1116
  hasRouteFunction,
@@ -1547,16 +1640,16 @@ const runQueryErrorParsers = (detail, response) => {
1547
1640
  return null;
1548
1641
  };
1549
1642
  const NO_RETRY = { retry: false };
1550
- let defaultRetryFn = null;
1643
+ let defaultRetryFn$1 = null;
1551
1644
  /**
1552
1645
  * Installs the retry policy used by every request that does not bring its own `retryFn`. Installed
1553
1646
  * by the {@link withDefaultRetry} query client feature; without it nothing is retried automatically.
1554
1647
  */
1555
1648
  const setDefaultQueryRetryFn = (fn) => {
1556
- defaultRetryFn = fn;
1649
+ defaultRetryFn$1 = fn;
1557
1650
  };
1558
1651
  /** @internal */
1559
- const runDefaultQueryRetry = (options) => defaultRetryFn?.(options) ?? NO_RETRY;
1652
+ const runDefaultQueryRetry = (options) => defaultRetryFn$1?.(options) ?? NO_RETRY;
1560
1653
 
1561
1654
  const isQueryErrorResponse = (error) => {
1562
1655
  return typeof error === 'object' && error !== null && 'raw' in error && 'retryState' in error && 'isList' in error;
@@ -1823,12 +1916,19 @@ const createHttpRequest = (options) => {
1823
1916
  });
1824
1917
  return true;
1825
1918
  };
1826
- const destroy = () => {
1919
+ const abort = () => {
1827
1920
  const wasActive = !currentStreamSubscription.closed;
1828
1921
  currentStreamSubscription.unsubscribe();
1829
- // A request destroyed mid-backoff never runs the attempt it was waiting for, so leaving the state
1830
- // 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.
1831
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();
1832
1932
  event$.complete();
1833
1933
  return wasActive;
1834
1934
  };
@@ -1947,6 +2047,7 @@ const createHttpRequest = (options) => {
1947
2047
  attempts: attempts.asReadonly(),
1948
2048
  retryState: retryState.asReadonly(),
1949
2049
  lastDurationMs: lastDurationMs.asReadonly(),
2050
+ abort,
1950
2051
  },
1951
2052
  };
1952
2053
  return httpRequest;
@@ -3030,48 +3131,68 @@ const symfonyQueryErrorParser = (detail) => {
3030
3131
  return null;
3031
3132
  };
3032
3133
 
3033
- const shouldRetryRequest = (options) => {
3034
- const normalizedOptions = options instanceof HttpErrorResponse ? { retryCount: 0, error: options } : options;
3035
- const { retryCount, error } = normalizedOptions;
3036
- const defaultRetryDelay = clamp(1000 + 1000 * retryCount, 1000, 5000);
3037
- const { status, error: detail, headers } = error;
3038
- // Code 0 usually means the internet connection is down. We retry in this case.
3039
- // It could also be a CORS issue but that should not be the case in production.
3040
- // We will retry in this case until the connection is back.
3041
- if (status === 0) {
3042
- return { retry: true, delay: defaultRetryDelay };
3043
- }
3044
- // Don't retry if we already retried 3 times
3045
- if (retryCount > 3) {
3046
- return { retry: false };
3047
- }
3048
- // Don't retry if it's not an http error
3049
- if (!(error instanceof HttpErrorResponse)) {
3050
- return { retry: false };
3051
- }
3052
- // Retry on 5xx errors (except 500 internal server error since those are usually not recoverable)
3053
- if (status >= 501) {
3054
- // Don't retry if a requested page is out of range
3055
- 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))
3056
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) };
3057
3183
  }
3058
- return { retry: true, delay: defaultRetryDelay };
3059
- }
3060
- // Retry on 408 or 425 errors
3061
- if (status === 408 || status === 425) {
3062
- return { retry: true, delay: defaultRetryDelay };
3063
- }
3064
- // Retry on 429 errors
3065
- if (status === 429) {
3066
- const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
3067
- if (retryAfter) {
3068
- const delay = parseInt(retryAfter) * 1000;
3069
- return { retry: true, delay: Number.isNaN(delay) ? defaultRetryDelay : delay };
3070
- }
3071
- return { retry: true, delay: defaultRetryDelay };
3072
- }
3073
- return { retry: false };
3184
+ return { retry: true, delay: backoff(retryCount) };
3185
+ };
3074
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);
3075
3196
 
3076
3197
  const noop$2 = () => undefined;
3077
3198
  /** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
@@ -3475,10 +3596,9 @@ const withSymfonyErrors = () => () => {
3475
3596
  return { type: QueryClientFeatureType.SYMFONY_ERRORS, instance: null };
3476
3597
  };
3477
3598
  /**
3478
- * Installs the SDK's default retry policy for every request that does not bring its own `retryFn`:
3479
- * retry a connection failure indefinitely, a 5xx above 500, a 408/425, and a 429 (honouring
3480
- * `retry-after`), up to three times with a backing-off delay - and never a Pagerfanta out-of-range
3481
- * 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.
3482
3602
  *
3483
3603
  * Without it nothing is retried automatically and `error.retryState` always reads `{ retry: false }`.
3484
3604
  * A per-client or per-creator `retryFn` keeps working either way.
@@ -3487,11 +3607,11 @@ const withSymfonyErrors = () => () => {
3487
3607
  * const MY_CLIENT = createQueryClient({
3488
3608
  * name: 'my-api',
3489
3609
  * baseUrl: 'https://api.example.com',
3490
- * features: [withDefaultRetry()],
3610
+ * features: [withDefaultRetry({ maxAttempts: 5 })],
3491
3611
  * });
3492
3612
  */
3493
- const withDefaultRetry = () => () => {
3494
- setDefaultQueryRetryFn(shouldRetryRequest);
3613
+ const withDefaultRetry = (options) => () => {
3614
+ setDefaultQueryRetryFn(options ? createDefaultRetryFn(options) : shouldRetryRequest);
3495
3615
  return { type: QueryClientFeatureType.DEFAULT_RETRY, instance: null };
3496
3616
  };
3497
3617
  /**
@@ -3509,10 +3629,10 @@ const withDefaultRetry = () => () => {
3509
3629
  * features: [withEthleteApiErrors()],
3510
3630
  * });
3511
3631
  */
3512
- const withEthleteApiErrors = () => () => {
3632
+ const withEthleteApiErrors = (options) => () => {
3513
3633
  registerQueryErrorParser(htmlQueryErrorParser);
3514
3634
  registerQueryErrorParser(symfonyQueryErrorParser);
3515
- setDefaultQueryRetryFn(shouldRetryRequest);
3635
+ setDefaultQueryRetryFn(options?.retry ? createDefaultRetryFn(options.retry) : shouldRetryRequest);
3516
3636
  return { type: QueryClientFeatureType.ETHLETE_API_ERRORS, instance: null };
3517
3637
  };
3518
3638
 
@@ -3533,10 +3653,10 @@ const createQueryRepository = (config) => {
3533
3653
  const cacheVersion = signal(0, /* @ts-ignore */
3534
3654
  ...(ngDevMode ? [{ debugName: "cacheVersion" }] : /* istanbul ignore next */ []));
3535
3655
  const bumpCacheVersion = () => cacheVersion.update((v) => v + 1);
3536
- const resolveKeepUnusedFor = (creatorOptions, shouldCache) => {
3656
+ const resolveKeepUnusedFor = (creatorOptions, shouldCache, override) => {
3537
3657
  if (config.retentionEnabled === false || !shouldCache)
3538
3658
  return 0;
3539
- 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);
3540
3660
  };
3541
3661
  const request = (options) => {
3542
3662
  const { args, creatorOptions, runQueryOptions } = options;
@@ -3567,7 +3687,7 @@ const createQueryRepository = (config) => {
3567
3687
  })
3568
3688
  : false;
3569
3689
  const trackingKey = cacheKey || generateUuid();
3570
- const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache);
3690
+ const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
3571
3691
  const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
3572
3692
  const isSecure = options.isSecure ?? false;
3573
3693
  // A secure response needs an explicit `persistence: true` to reach the disk: leaving an
@@ -3672,6 +3792,10 @@ const createQueryRepository = (config) => {
3672
3792
  // Only data is worth keeping around: an entry that never produced a response has nothing to
3673
3793
  // hand back to a returning consumer, so it is aborted immediately as it always was.
3674
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();
3675
3799
  retain(key, cacheEntry);
3676
3800
  }
3677
3801
  else {
@@ -4657,6 +4781,8 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
4657
4781
  let isLeaderFn = alwaysLeader;
4658
4782
  let leaderElectionContext;
4659
4783
  let refreshCoordination;
4784
+ let sessionAdoption;
4785
+ let activityCoordination;
4660
4786
  for (const featureBuilder of featureBuilders ?? []) {
4661
4787
  const earlySetup = featureBuilder.earlySetup;
4662
4788
  if (!earlySetup)
@@ -4665,8 +4791,10 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
4665
4791
  isLeaderFn = result.isLeader ?? isLeaderFn;
4666
4792
  leaderElectionContext = result.leaderElection ?? leaderElectionContext;
4667
4793
  refreshCoordination = result.refreshCoordination ?? refreshCoordination;
4794
+ sessionAdoption = result.sessionAdoption ?? sessionAdoption;
4795
+ activityCoordination = result.activityCoordination ?? activityCoordination;
4668
4796
  }
4669
- return { isLeaderFn, leaderElectionContext, refreshCoordination };
4797
+ return { isLeaderFn, leaderElectionContext, refreshCoordination, sessionAdoption, activityCoordination };
4670
4798
  };
4671
4799
  const createBearerAuthProviderImpl = (config) => {
4672
4800
  const injector = inject(Injector);
@@ -4787,6 +4915,8 @@ const createBearerAuthProviderImpl = (config) => {
4787
4915
  setTokens,
4788
4916
  isLeader: isLeader.isLeaderFn,
4789
4917
  leaderElection: isLeader.leaderElectionContext,
4918
+ sessionAdoption: isLeader.sessionAdoption,
4919
+ activityCoordination: isLeader.activityCoordination,
4790
4920
  afterTokenRefresh$,
4791
4921
  queries: queries,
4792
4922
  executionState,
@@ -4795,9 +4925,21 @@ const createBearerAuthProviderImpl = (config) => {
4795
4925
  };
4796
4926
  const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
4797
4927
  // Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
4798
- // `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.
4799
4932
  if (sessionStatus() === 'unknown') {
4800
- 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
+ }
4801
4943
  }
4802
4944
  const provider = {
4803
4945
  queries,
@@ -4837,6 +4979,14 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
4837
4979
 
4838
4980
  /** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
4839
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;
4840
4990
  /**
4841
4991
  * How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
4842
4992
  * declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
@@ -4889,9 +5039,12 @@ const withRefreshQuery = (key, config) => {
4889
5039
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
4890
5040
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
4891
5041
  const refreshIfExpired = config.refreshIfExpired ?? true;
5042
+ const destroyRef = inject(DestroyRef);
4892
5043
  let lastRefreshTime = 0;
4893
5044
  let lastUnauthorizedRefreshTime = 0;
4894
5045
  let unauthorizedRefreshStreak = 0;
5046
+ let delegatedRefreshAttempts = 0;
5047
+ let isWatchingDelegatedRefresh = false;
4895
5048
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4896
5049
  const refreshQuery = () => context.queries[key];
4897
5050
  const executeRefresh = (reason) => {
@@ -4899,9 +5052,14 @@ const withRefreshQuery = (key, config) => {
4899
5052
  if (!currentRefreshToken)
4900
5053
  return 'noToken';
4901
5054
  if (!context.isLeader()) {
4902
- // Only the leader may spend a single-use refresh token, but the event is still real - hand it
4903
- // 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';
4904
5061
  context.refreshCoordination?.request();
5062
+ watchDelegatedRefresh(context.accessToken());
4905
5063
  return 'delegated';
4906
5064
  }
4907
5065
  // Any token-issuing execution, not just another refresh: a login already in flight is about to
@@ -4909,14 +5067,12 @@ const withRefreshQuery = (key, config) => {
4909
5067
  // that login is replacing.
4910
5068
  if (context.hasTokenIssuingExecutionInFlight())
4911
5069
  return 'busy';
5070
+ const now = Date.now();
4912
5071
  if (reason === 'scheduled') {
4913
- const now = Date.now();
4914
5072
  if (now - lastRefreshTime < minRefreshInterval)
4915
5073
  return 'throttled';
4916
- lastRefreshTime = now;
4917
5074
  }
4918
5075
  else {
4919
- const now = Date.now();
4920
5076
  if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
4921
5077
  now - lastUnauthorizedRefreshTime < minRefreshInterval) {
4922
5078
  return 'throttled';
@@ -4924,9 +5080,37 @@ const withRefreshQuery = (key, config) => {
4924
5080
  unauthorizedRefreshStreak++;
4925
5081
  lastUnauthorizedRefreshTime = now;
4926
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;
4927
5088
  refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4928
5089
  return 'executed';
4929
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
+ }
4930
5114
  /**
4931
5115
  * How long a scheduled refresh that did not spend the refresh token waits before trying again.
4932
5116
  * `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
@@ -4941,7 +5125,10 @@ const withRefreshQuery = (key, config) => {
4941
5125
  return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
4942
5126
  case 'busy':
4943
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.
4944
5130
  case 'delegated':
5131
+ case 'notLeader':
4945
5132
  return minRefreshInterval;
4946
5133
  }
4947
5134
  };
@@ -4995,12 +5182,28 @@ const withRefreshQuery = (key, config) => {
4995
5182
  const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
4996
5183
  return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
4997
5184
  }));
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
+ }
4998
5200
  // The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
4999
5201
  // lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
5000
5202
  // timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
5001
5203
  // two tokens that happen to be due at the same moment still restart the timer.
5002
5204
  const nextScheduledRefresh = computed(() => {
5003
5205
  const token = context.accessToken();
5206
+ foregroundEpoch();
5004
5207
  return { dueInMs: token ? scheduledRefreshDelay(token) : null };
5005
5208
  }, /* @ts-ignore */
5006
5209
  ...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
@@ -5028,6 +5231,7 @@ const withRefreshQuery = (key, config) => {
5028
5231
  context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
5029
5232
  if (event.type === 'request-success' && event.isSecure) {
5030
5233
  unauthorizedRefreshStreak = 0;
5234
+ delegatedRefreshAttempts = 0;
5031
5235
  return;
5032
5236
  }
5033
5237
  if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
@@ -5061,6 +5265,23 @@ const withRefreshQuery = (key, config) => {
5061
5265
  };
5062
5266
  };
5063
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
+ */
5064
5285
  const withInactivityLogout = (config = {}) => {
5065
5286
  return (context) => {
5066
5287
  const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
@@ -5071,8 +5292,25 @@ const withInactivityLogout = (config = {}) => {
5071
5292
  ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
5072
5293
  const lastActivityTime = signal(Date.now(), /* @ts-ignore */
5073
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
+ };
5074
5311
  const resetTimer = () => {
5075
- lastActivityTime.set(Date.now());
5312
+ markActive();
5313
+ announceActivity();
5076
5314
  };
5077
5315
  const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
5078
5316
  const activityFromCustomCheck$ = config.customActivityCheck
@@ -5084,15 +5322,35 @@ const withInactivityLogout = (config = {}) => {
5084
5322
  resetTimer();
5085
5323
  }
5086
5324
  });
5087
- 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()));
5088
5341
  const logoutSubscription = inactivityLogout$.subscribe(() => {
5089
5342
  context.logout('inactivity');
5090
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.
5091
5348
  effect(() => {
5092
- const token = context.accessToken();
5093
- if (token && enabled()) {
5094
- resetTimer();
5349
+ const hasToken = !!context.accessToken();
5350
+ if (hasToken && !hadToken) {
5351
+ markActive();
5095
5352
  }
5353
+ hadToken = hasToken;
5096
5354
  });
5097
5355
  const enable = () => {
5098
5356
  enabled.set(true);
@@ -5109,9 +5367,9 @@ const withInactivityLogout = (config = {}) => {
5109
5367
  const remaining = inactivityTimeout - elapsed;
5110
5368
  return remaining > 0 ? remaining : 0;
5111
5369
  };
5112
- resetTimer();
5113
5370
  destroyRef.onDestroy(() => {
5114
5371
  activitySubscription.unsubscribe();
5372
+ remoteActivitySubscription?.unsubscribe();
5115
5373
  logoutSubscription.unsubscribe();
5116
5374
  });
5117
5375
  const instance = {
@@ -5127,6 +5385,7 @@ const withInactivityLogout = (config = {}) => {
5127
5385
  devtools: () => [
5128
5386
  { label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
5129
5387
  { label: 'activity events', value: activityEvents.join(', ') },
5388
+ { label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
5130
5389
  ...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
5131
5390
  ],
5132
5391
  };
@@ -5331,6 +5590,12 @@ const resetEncryptionKey = () => {
5331
5590
  * through. A message without one comes from a tab running an older version.
5332
5591
  */
5333
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;
5334
5599
  /**
5335
5600
  * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5336
5601
  * providers reachable from the same origin do not push each other's tokens around.
@@ -5360,8 +5625,32 @@ const setupMultiTabSync = (config, context) => {
5360
5625
  // broadcasting effects below run after it, so they would always see it cleared.
5361
5626
  let lastSyncedState = null;
5362
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();
5363
5633
  channel.onmessage = (event) => {
5364
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
+ }
5365
5654
  if (message.type === 'logout' && syncLogout) {
5366
5655
  lastSyncedState = LOGGED_OUT;
5367
5656
  hadTokens = false;
@@ -5377,11 +5666,34 @@ const setupMultiTabSync = (config, context) => {
5377
5666
  lastSyncedState = tokenState(access, refresh);
5378
5667
  hadTokens = true;
5379
5668
  context.applyTokens(access, refresh);
5669
+ settleAdoption();
5380
5670
  };
5671
+ let adoptionTimeout;
5381
5672
  const cleanup = () => {
5673
+ clearTimeout(adoptionTimeout);
5674
+ settleAdoption();
5675
+ remoteActivity.complete();
5382
5676
  channel.close();
5383
5677
  };
5384
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;
5385
5697
  if (syncTokens) {
5386
5698
  effect(() => {
5387
5699
  const access = context.accessToken();
@@ -5417,7 +5729,15 @@ const setupMultiTabSync = (config, context) => {
5417
5729
  channel.postMessage(message);
5418
5730
  });
5419
5731
  }
5420
- 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 };
5421
5741
  };
5422
5742
 
5423
5743
  const SINGLE_TAB = /* @__PURE__ */ (() => {
@@ -5466,17 +5786,24 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5466
5786
  // regular feature setup runs - so this half has to happen earlier than `setup`.
5467
5787
  const earlySetup = (context) => {
5468
5788
  channelName = config.channelName ?? defaultSyncChannelName(context.name);
5469
- 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 }, {
5470
5794
  accessToken: context.accessToken,
5471
5795
  refreshToken: context.refreshToken,
5472
5796
  sessionEndCause: context.sessionEndCause,
5473
5797
  name: context.name,
5798
+ isLeader: () => isLeaderRef(),
5474
5799
  applyTokens: context.applyTokens,
5475
5800
  logout: context.logout,
5476
5801
  });
5477
- if (config.leaderElection === false)
5478
- return {};
5802
+ if (config.leaderElection === false) {
5803
+ return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
5804
+ }
5479
5805
  const election = setupLeaderElection({ name: context.name });
5806
+ isLeaderRef = () => election.isLeader();
5480
5807
  instance = {
5481
5808
  isLeader: election.isLeader,
5482
5809
  instanceCount: election.instanceCount,
@@ -5486,6 +5813,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5486
5813
  isLeader: () => election.isLeader(),
5487
5814
  leaderElection: instance,
5488
5815
  refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
5816
+ sessionAdoption: sync.sessionAdoption,
5817
+ activityCoordination: sync.activityCoordination,
5489
5818
  };
5490
5819
  };
5491
5820
  return Object.assign(setup, { earlySetup });
@@ -5560,6 +5889,14 @@ const createPersistentAuthFeature = (config, context) => {
5560
5889
  if (wasRejected)
5561
5890
  removeCookie();
5562
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
+ };
5563
5900
  const tryLogin = () => {
5564
5901
  const currentRoute = route();
5565
5902
  const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
@@ -5570,12 +5907,24 @@ const createPersistentAuthFeature = (config, context) => {
5570
5907
  if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
5571
5908
  return;
5572
5909
  }
5573
- const storedToken = getCookie(cookieName);
5574
- if (storedToken) {
5575
- const decryptedToken = decryptToken(storedToken);
5576
- const args = config.autoLogin.buildArgs(decryptedToken);
5577
- 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;
5578
5926
  }
5927
+ exchangeCookie();
5579
5928
  };
5580
5929
  const setRememberMe = (enabled) => {
5581
5930
  if (typeof localStorage !== 'undefined') {
@@ -7097,7 +7446,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
7097
7446
 
7098
7447
  // Generated from package.json by tools/scripts/generate-version.js - do not edit.
7099
7448
  /** The version of `@ethlete/query` this build was cut from. */
7100
- const QUERY_VERSION = '6.0.0-next.34';
7449
+ const QUERY_VERSION = '6.0.0-next.35';
7101
7450
 
7102
7451
  /** How deep a schema is expanded before a nested branch is given up on. */
7103
7452
  const MAX_DEPTH = 12;
@@ -13839,5 +14188,5 @@ const createWebSocketClient = (options) => {
13839
14188
  * Generated bundle index. Do not edit.
13840
14189
  */
13841
14190
 
13842
- 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_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, 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, 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 };
13843
14192
  //# sourceMappingURL=ethlete-query.mjs.map