@ethlete/query 6.0.0-next.34 → 6.0.0-next.36

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);
@@ -4757,6 +4885,7 @@ const createBearerAuthProviderImpl = (config) => {
4757
4885
  queryClient,
4758
4886
  name: config.name,
4759
4887
  applyTokens,
4888
+ setTokens,
4760
4889
  logout,
4761
4890
  sessionEndCause: sessionEndCause.asReadonly(),
4762
4891
  });
@@ -4787,6 +4916,8 @@ const createBearerAuthProviderImpl = (config) => {
4787
4916
  setTokens,
4788
4917
  isLeader: isLeader.isLeaderFn,
4789
4918
  leaderElection: isLeader.leaderElectionContext,
4919
+ sessionAdoption: isLeader.sessionAdoption,
4920
+ activityCoordination: isLeader.activityCoordination,
4790
4921
  afterTokenRefresh$,
4791
4922
  queries: queries,
4792
4923
  executionState,
@@ -4795,9 +4926,21 @@ const createBearerAuthProviderImpl = (config) => {
4795
4926
  };
4796
4927
  const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
4797
4928
  // 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.
4929
+ // `unknown` here means nothing is trying to restore a session, so there is nothing to wait for -
4930
+ // unless the join handshake is still out, in which case another tab may be about to hand this one a
4931
+ // live session. Saying `anonymous` in that window would send the auth guard to the login page for a
4932
+ // session that exists, so the fallback waits for the handshake instead.
4799
4933
  if (sessionStatus() === 'unknown') {
4800
- sessionStatus.set('anonymous');
4934
+ const adoption = isLeader.sessionAdoption;
4935
+ if (adoption?.isPending()) {
4936
+ void adoption.settled.then(() => {
4937
+ if (sessionStatus() === 'unknown')
4938
+ sessionStatus.set('anonymous');
4939
+ });
4940
+ }
4941
+ else {
4942
+ sessionStatus.set('anonymous');
4943
+ }
4801
4944
  }
4802
4945
  const provider = {
4803
4946
  queries,
@@ -4837,6 +4980,14 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
4837
4980
 
4838
4981
  /** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
4839
4982
  const rescheduleDelayMs = 5000;
4983
+ /**
4984
+ * How long a follower waits for the leader to answer a delegated refresh before asking again. The
4985
+ * request is a `BroadcastChannel` message with no ack, so a leader that was frozen or handed the
4986
+ * leadership over mid-flight simply never acts on it.
4987
+ */
4988
+ const delegatedRefreshRetryMs = 3000;
4989
+ /** How often a delegated refresh is re-asked for before the tab leaves it to the next 401. */
4990
+ const maxDelegatedRefreshAttempts = 3;
4840
4991
  /**
4841
4992
  * How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
4842
4993
  * declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
@@ -4889,9 +5040,12 @@ const withRefreshQuery = (key, config) => {
4889
5040
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
4890
5041
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
4891
5042
  const refreshIfExpired = config.refreshIfExpired ?? true;
5043
+ const destroyRef = inject(DestroyRef);
4892
5044
  let lastRefreshTime = 0;
4893
5045
  let lastUnauthorizedRefreshTime = 0;
4894
5046
  let unauthorizedRefreshStreak = 0;
5047
+ let delegatedRefreshAttempts = 0;
5048
+ let isWatchingDelegatedRefresh = false;
4895
5049
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4896
5050
  const refreshQuery = () => context.queries[key];
4897
5051
  const executeRefresh = (reason) => {
@@ -4899,9 +5053,14 @@ const withRefreshQuery = (key, config) => {
4899
5053
  if (!currentRefreshToken)
4900
5054
  return 'noToken';
4901
5055
  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.
5056
+ // A scheduled tick on a follower is pure duplicate: multi-tab sync keeps the access token
5057
+ // identical across tabs, so the leader's own timer is due at the same instant and it is the
5058
+ // only tab allowed to act. Delegation is for the 401 path, where this tab has a request that
5059
+ // really failed and the leader has no way of knowing.
5060
+ if (reason === 'scheduled')
5061
+ return 'notLeader';
4904
5062
  context.refreshCoordination?.request();
5063
+ watchDelegatedRefresh(context.accessToken());
4905
5064
  return 'delegated';
4906
5065
  }
4907
5066
  // Any token-issuing execution, not just another refresh: a login already in flight is about to
@@ -4909,14 +5068,12 @@ const withRefreshQuery = (key, config) => {
4909
5068
  // that login is replacing.
4910
5069
  if (context.hasTokenIssuingExecutionInFlight())
4911
5070
  return 'busy';
5071
+ const now = Date.now();
4912
5072
  if (reason === 'scheduled') {
4913
- const now = Date.now();
4914
5073
  if (now - lastRefreshTime < minRefreshInterval)
4915
5074
  return 'throttled';
4916
- lastRefreshTime = now;
4917
5075
  }
4918
5076
  else {
4919
- const now = Date.now();
4920
5077
  if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
4921
5078
  now - lastUnauthorizedRefreshTime < minRefreshInterval) {
4922
5079
  return 'throttled';
@@ -4924,9 +5081,37 @@ const withRefreshQuery = (key, config) => {
4924
5081
  unauthorizedRefreshStreak++;
4925
5082
  lastUnauthorizedRefreshTime = now;
4926
5083
  }
5084
+ // Stamped whatever the reason, so the scheduled floor covers a 401-driven rotation too - a
5085
+ // scheduled tick right behind one would otherwise spend a second refresh token. The 401 path
5086
+ // deliberately does not *gate* on it: a token revoked seconds after a refresh is exactly when
5087
+ // the request has to go out.
5088
+ lastRefreshTime = now;
4927
5089
  refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4928
5090
  return 'executed';
4929
5091
  };
5092
+ /**
5093
+ * Asks the leader again if a delegated refresh went unanswered - the request is a channel message
5094
+ * with no ack, so a frozen leader, or one that handed the leadership over mid-flight, drops it
5095
+ * silently. One watch at a time, and by the time it re-asks this tab may be the leader itself;
5096
+ * `executeRefresh` re-decides that on every attempt.
5097
+ */
5098
+ function watchDelegatedRefresh(tokenAtRequest) {
5099
+ if (isWatchingDelegatedRefresh || delegatedRefreshAttempts >= maxDelegatedRefreshAttempts)
5100
+ return;
5101
+ delegatedRefreshAttempts++;
5102
+ isWatchingDelegatedRefresh = true;
5103
+ timer(delegatedRefreshRetryMs)
5104
+ .pipe(takeUntilDestroyed(destroyRef))
5105
+ .subscribe(() => {
5106
+ isWatchingDelegatedRefresh = false;
5107
+ // A new access token is the leader's answer - nothing to re-ask for.
5108
+ if (context.accessToken() !== tokenAtRequest) {
5109
+ delegatedRefreshAttempts = 0;
5110
+ return;
5111
+ }
5112
+ executeRefresh('unauthorized');
5113
+ });
5114
+ }
4930
5115
  /**
4931
5116
  * How long a scheduled refresh that did not spend the refresh token waits before trying again.
4932
5117
  * `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
@@ -4941,7 +5126,10 @@ const withRefreshQuery = (key, config) => {
4941
5126
  return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
4942
5127
  case 'busy':
4943
5128
  return rescheduleDelayMs;
5129
+ // Both wait for the leader: `delegated` asked it to act, `notLeader` left it to its own timer.
5130
+ // Coming back keeps this tab covered if the leadership moves here in the meantime.
4944
5131
  case 'delegated':
5132
+ case 'notLeader':
4945
5133
  return minRefreshInterval;
4946
5134
  }
4947
5135
  };
@@ -4995,12 +5183,28 @@ const withRefreshQuery = (key, config) => {
4995
5183
  const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
4996
5184
  return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
4997
5185
  }));
5186
+ // A backgrounded tab's `timer()` does not fire on time - the browser throttles it, and a frozen
5187
+ // page stops running it altogether while keeping the Web Lock that makes it the leader. So the
5188
+ // schedule is recomputed whenever the page becomes visible again: an overdue token resolves to a
5189
+ // delay of 0 and refreshes immediately, instead of waiting for a secure query to 401.
5190
+ const foregroundEpoch = signal(0, /* @ts-ignore */
5191
+ ...(ngDevMode ? [{ debugName: "foregroundEpoch" }] : /* istanbul ignore next */ []));
5192
+ if (typeof document !== 'undefined') {
5193
+ fromEvent(document, 'visibilitychange')
5194
+ .pipe(takeUntilDestroyed(destroyRef))
5195
+ .subscribe(() => {
5196
+ if (document.visibilityState !== 'visible')
5197
+ return;
5198
+ foregroundEpoch.update((epoch) => epoch + 1);
5199
+ });
5200
+ }
4998
5201
  // The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
4999
5202
  // lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
5000
5203
  // timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
5001
5204
  // two tokens that happen to be due at the same moment still restart the timer.
5002
5205
  const nextScheduledRefresh = computed(() => {
5003
5206
  const token = context.accessToken();
5207
+ foregroundEpoch();
5004
5208
  return { dueInMs: token ? scheduledRefreshDelay(token) : null };
5005
5209
  }, /* @ts-ignore */
5006
5210
  ...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
@@ -5028,6 +5232,7 @@ const withRefreshQuery = (key, config) => {
5028
5232
  context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
5029
5233
  if (event.type === 'request-success' && event.isSecure) {
5030
5234
  unauthorizedRefreshStreak = 0;
5235
+ delegatedRefreshAttempts = 0;
5031
5236
  return;
5032
5237
  }
5033
5238
  if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
@@ -5061,6 +5266,23 @@ const withRefreshQuery = (key, config) => {
5061
5266
  };
5062
5267
  };
5063
5268
 
5269
+ /**
5270
+ * Ends the session once the user has been inactive for {@link InactivityLogoutConfig.inactivityTimeout},
5271
+ * with `sessionEndCause` reporting `'inactivity'`.
5272
+ *
5273
+ * Idleness is a property of the session, not of a tab: with `withBearerAuthMultiTabSync` on, tabs tell
5274
+ * each other about activity, so the tab the user is typing in keeps the forgotten one from logging
5275
+ * everybody out. Without it - or with `syncLogout: false`, where a logout stays in the tab that
5276
+ * decided it - each tab times out on its own.
5277
+ *
5278
+ * @example
5279
+ * export const AUTH_PROVIDER = createBearerAuthProvider({
5280
+ * name: 'my-auth',
5281
+ * queryClientRef: MY_CLIENT,
5282
+ * queries: [loginQuery, refreshQuery],
5283
+ * features: [withBearerAuthMultiTabSync(), withInactivityLogout({ inactivityTimeout: 5 * 60 * 1000 })],
5284
+ * });
5285
+ */
5064
5286
  const withInactivityLogout = (config = {}) => {
5065
5287
  return (context) => {
5066
5288
  const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
@@ -5071,8 +5293,25 @@ const withInactivityLogout = (config = {}) => {
5071
5293
  ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
5072
5294
  const lastActivityTime = signal(Date.now(), /* @ts-ignore */
5073
5295
  ...(ngDevMode ? [{ debugName: "lastActivityTime" }] : /* istanbul ignore next */ []));
5296
+ const activityCoordination = context.activityCoordination;
5297
+ // Announcing every throttled event would wake every other tab once a second for as long as the
5298
+ // user scrolls. A quarter of the timeout is far more than the other tabs need to never expire,
5299
+ // and it is one message per several minutes at the default.
5300
+ const announceInterval = inactivityTimeout / 4;
5301
+ let lastAnnouncedAt = 0;
5302
+ const markActive = () => lastActivityTime.set(Date.now());
5303
+ const announceActivity = () => {
5304
+ if (!activityCoordination)
5305
+ return;
5306
+ const now = Date.now();
5307
+ if (now - lastAnnouncedAt < announceInterval)
5308
+ return;
5309
+ lastAnnouncedAt = now;
5310
+ activityCoordination.announce();
5311
+ };
5074
5312
  const resetTimer = () => {
5075
- lastActivityTime.set(Date.now());
5313
+ markActive();
5314
+ announceActivity();
5076
5315
  };
5077
5316
  const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
5078
5317
  const activityFromCustomCheck$ = config.customActivityCheck
@@ -5084,15 +5323,35 @@ const withInactivityLogout = (config = {}) => {
5084
5323
  resetTimer();
5085
5324
  }
5086
5325
  });
5087
- const inactivityLogout$ = activity$.pipe(switchMap(() => timer(inactivityTimeout)), filter(() => enabled() && !!context.accessToken()));
5326
+ // Activity from another tab is never announced onwards - two tabs would echo one keystroke back
5327
+ // and forth for as long as they are both open.
5328
+ const remoteActivitySubscription = activityCoordination?.activity$.subscribe(() => {
5329
+ if (enabled() && context.accessToken()) {
5330
+ markActive();
5331
+ }
5332
+ });
5333
+ const isIdle = () => Date.now() - lastActivityTime() >= inactivityTimeout;
5334
+ // Driven off `lastActivityTime` rather than off the activity stream, so the countdown this feature
5335
+ // reports and the logout it performs cannot disagree: a tab that has seen no event still has a
5336
+ // deadline, and `resetTimer()` postpones the logout it says it resets.
5337
+ //
5338
+ // `isIdle` re-checks at the moment the deadline fires, because the re-arm goes through an effect:
5339
+ // activity recorded in the same tick as the old deadline has not moved it yet. The write that was
5340
+ // missed is what schedules that re-arm, so nothing is dropped by waiting for it.
5341
+ const inactivityLogout$ = toObservable(lastActivityTime).pipe(switchMap((last) => timer(last + inactivityTimeout - Date.now())), filter(() => enabled() && !!context.accessToken() && isIdle()));
5088
5342
  const logoutSubscription = inactivityLogout$.subscribe(() => {
5089
5343
  context.logout('inactivity');
5090
5344
  });
5345
+ let hadToken = false;
5346
+ // Only the start of a session counts as activity. A token _refresh_ is the app working, not the
5347
+ // user, and resetting on one would mean an app that refreshes faster than this timeout never logs
5348
+ // an idle user out at all.
5091
5349
  effect(() => {
5092
- const token = context.accessToken();
5093
- if (token && enabled()) {
5094
- resetTimer();
5350
+ const hasToken = !!context.accessToken();
5351
+ if (hasToken && !hadToken) {
5352
+ markActive();
5095
5353
  }
5354
+ hadToken = hasToken;
5096
5355
  });
5097
5356
  const enable = () => {
5098
5357
  enabled.set(true);
@@ -5109,9 +5368,9 @@ const withInactivityLogout = (config = {}) => {
5109
5368
  const remaining = inactivityTimeout - elapsed;
5110
5369
  return remaining > 0 ? remaining : 0;
5111
5370
  };
5112
- resetTimer();
5113
5371
  destroyRef.onDestroy(() => {
5114
5372
  activitySubscription.unsubscribe();
5373
+ remoteActivitySubscription?.unsubscribe();
5115
5374
  logoutSubscription.unsubscribe();
5116
5375
  });
5117
5376
  const instance = {
@@ -5127,6 +5386,7 @@ const withInactivityLogout = (config = {}) => {
5127
5386
  devtools: () => [
5128
5387
  { label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
5129
5388
  { label: 'activity events', value: activityEvents.join(', ') },
5389
+ { label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
5130
5390
  ...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
5131
5391
  ],
5132
5392
  };
@@ -5331,6 +5591,12 @@ const resetEncryptionKey = () => {
5331
5591
  * through. A message without one comes from a tab running an older version.
5332
5592
  */
5333
5593
  const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
5594
+ /**
5595
+ * The bounded wait a joining tab gives the leader to send the live session over. Short enough to be
5596
+ * invisible in front of the network request it defers, long enough for a same-origin channel message
5597
+ * plus the answer to come back.
5598
+ */
5599
+ const sessionAdoptionTimeoutMs = 250;
5334
5600
  /**
5335
5601
  * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5336
5602
  * providers reachable from the same origin do not push each other's tokens around.
@@ -5360,8 +5626,32 @@ const setupMultiTabSync = (config, context) => {
5360
5626
  // broadcasting effects below run after it, so they would always see it cleared.
5361
5627
  let lastSyncedState = null;
5362
5628
  let hadTokens = false;
5629
+ let isAdoptionPending = false;
5630
+ let settleAdoption = () => {
5631
+ /* replaced below when the handshake actually runs */
5632
+ };
5633
+ const remoteActivity = new Subject();
5363
5634
  channel.onmessage = (event) => {
5364
5635
  const message = event.data;
5636
+ if (message.type === 'activity') {
5637
+ if (syncLogout)
5638
+ remoteActivity.next();
5639
+ return;
5640
+ }
5641
+ if (message.type === 'state-request') {
5642
+ if (!syncTokens || !context.isLeader())
5643
+ return;
5644
+ const access = context.accessToken();
5645
+ const refresh = context.refreshToken();
5646
+ if (!access || !refresh)
5647
+ return;
5648
+ channel.postMessage({
5649
+ type: 'tokens-updated',
5650
+ accessToken: encryptToken(access),
5651
+ refreshToken: encryptToken(refresh),
5652
+ });
5653
+ return;
5654
+ }
5365
5655
  if (message.type === 'logout' && syncLogout) {
5366
5656
  lastSyncedState = LOGGED_OUT;
5367
5657
  hadTokens = false;
@@ -5374,14 +5664,43 @@ const setupMultiTabSync = (config, context) => {
5374
5664
  const refresh = decryptToken(message.refreshToken);
5375
5665
  if (!access || !refresh)
5376
5666
  return;
5667
+ const isNewSession = !context.accessToken();
5377
5668
  lastSyncedState = tokenState(access, refresh);
5378
5669
  hadTokens = true;
5379
- context.applyTokens(access, refresh);
5670
+ if (isNewSession) {
5671
+ context.setTokens(access, refresh);
5672
+ }
5673
+ else {
5674
+ context.applyTokens(access, refresh);
5675
+ }
5676
+ settleAdoption();
5380
5677
  };
5678
+ let adoptionTimeout;
5381
5679
  const cleanup = () => {
5680
+ clearTimeout(adoptionTimeout);
5681
+ settleAdoption();
5682
+ remoteActivity.complete();
5382
5683
  channel.close();
5383
5684
  };
5384
5685
  destroyRef.onDestroy(cleanup);
5686
+ // Sync is push-only otherwise: a tab only broadcasts tokens that just changed, so a tab joining a
5687
+ // live session would hear nothing and start its own login. Asking is the missing half.
5688
+ const sessionAdoption = syncTokens
5689
+ ? (() => {
5690
+ isAdoptionPending = true;
5691
+ const settled = new Promise((resolve) => {
5692
+ settleAdoption = () => {
5693
+ if (!isAdoptionPending)
5694
+ return;
5695
+ isAdoptionPending = false;
5696
+ resolve();
5697
+ };
5698
+ });
5699
+ adoptionTimeout = setTimeout(settleAdoption, sessionAdoptionTimeoutMs);
5700
+ channel.postMessage({ type: 'state-request' });
5701
+ return { isPending: () => isAdoptionPending, settled };
5702
+ })()
5703
+ : undefined;
5385
5704
  if (syncTokens) {
5386
5705
  effect(() => {
5387
5706
  const access = context.accessToken();
@@ -5417,7 +5736,15 @@ const setupMultiTabSync = (config, context) => {
5417
5736
  channel.postMessage(message);
5418
5737
  });
5419
5738
  }
5420
- return { cleanup };
5739
+ // Only where a logout travels: with `syncLogout: false` each tab already ends its own session and
5740
+ // nothing else's, so its idleness is genuinely its own.
5741
+ const activityCoordination = syncLogout
5742
+ ? {
5743
+ announce: () => channel.postMessage({ type: 'activity' }),
5744
+ activity$: remoteActivity.asObservable(),
5745
+ }
5746
+ : undefined;
5747
+ return { cleanup, sessionAdoption, activityCoordination };
5421
5748
  };
5422
5749
 
5423
5750
  const SINGLE_TAB = /* @__PURE__ */ (() => {
@@ -5466,17 +5793,25 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5466
5793
  // regular feature setup runs - so this half has to happen earlier than `setup`.
5467
5794
  const earlySetup = (context) => {
5468
5795
  channelName = config.channelName ?? defaultSyncChannelName(context.name);
5469
- setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5796
+ // Filled in below once the election exists. Until then this tab answers as its own leader, which
5797
+ // is what it is: the sync is set up first because the auth queries read `isLeader` while being
5798
+ // wired, and a `state-request` cannot arrive before this function returns anyway.
5799
+ let isLeaderRef = () => true;
5800
+ const sync = setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5470
5801
  accessToken: context.accessToken,
5471
5802
  refreshToken: context.refreshToken,
5472
5803
  sessionEndCause: context.sessionEndCause,
5473
5804
  name: context.name,
5805
+ isLeader: () => isLeaderRef(),
5474
5806
  applyTokens: context.applyTokens,
5807
+ setTokens: context.setTokens,
5475
5808
  logout: context.logout,
5476
5809
  });
5477
- if (config.leaderElection === false)
5478
- return {};
5810
+ if (config.leaderElection === false) {
5811
+ return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
5812
+ }
5479
5813
  const election = setupLeaderElection({ name: context.name });
5814
+ isLeaderRef = () => election.isLeader();
5480
5815
  instance = {
5481
5816
  isLeader: election.isLeader,
5482
5817
  instanceCount: election.instanceCount,
@@ -5486,6 +5821,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5486
5821
  isLeader: () => election.isLeader(),
5487
5822
  leaderElection: instance,
5488
5823
  refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
5824
+ sessionAdoption: sync.sessionAdoption,
5825
+ activityCoordination: sync.activityCoordination,
5489
5826
  };
5490
5827
  };
5491
5828
  return Object.assign(setup, { earlySetup });
@@ -5560,6 +5897,14 @@ const createPersistentAuthFeature = (config, context) => {
5560
5897
  if (wasRejected)
5561
5898
  removeCookie();
5562
5899
  });
5900
+ const exchangeCookie = () => {
5901
+ const storedToken = getCookie(cookieName);
5902
+ if (!storedToken)
5903
+ return;
5904
+ const decryptedToken = decryptToken(storedToken);
5905
+ const args = config.autoLogin.buildArgs(decryptedToken);
5906
+ context.queries[config.autoLogin.queryKey].execute(args, { triggeredBy: 'persistent-auth' });
5907
+ };
5563
5908
  const tryLogin = () => {
5564
5909
  const currentRoute = route();
5565
5910
  const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
@@ -5570,12 +5915,24 @@ const createPersistentAuthFeature = (config, context) => {
5570
5915
  if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
5571
5916
  return;
5572
5917
  }
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' });
5918
+ // A session is already here - synced in from another tab, or restored by a previous call. Spending
5919
+ // the cookie now would rotate a refresh token every other tab is holding, for nothing.
5920
+ if (refreshToken())
5921
+ return;
5922
+ const adoption = context.sessionAdoption;
5923
+ // With multi-tab sync on, another tab may be about to hand this one its live session. Wait for
5924
+ // that answer before exchanging the cookie - bounded, so a lone tab is not held up by a reply
5925
+ // that is never coming - and only when there is a cookie to spend, so a tab with nothing to
5926
+ // restore still starts up synchronously.
5927
+ if (adoption?.isPending() && getCookie(cookieName)) {
5928
+ void adoption.settled.then(() => {
5929
+ if (refreshToken())
5930
+ return;
5931
+ exchangeCookie();
5932
+ });
5933
+ return;
5578
5934
  }
5935
+ exchangeCookie();
5579
5936
  };
5580
5937
  const setRememberMe = (enabled) => {
5581
5938
  if (typeof localStorage !== 'undefined') {
@@ -7097,7 +7454,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
7097
7454
 
7098
7455
  // Generated from package.json by tools/scripts/generate-version.js - do not edit.
7099
7456
  /** The version of `@ethlete/query` this build was cut from. */
7100
- const QUERY_VERSION = '6.0.0-next.34';
7457
+ const QUERY_VERSION = '6.0.0-next.36';
7101
7458
 
7102
7459
  /** How deep a schema is expanded before a nested branch is given up on. */
7103
7460
  const MAX_DEPTH = 12;
@@ -13839,5 +14196,5 @@ const createWebSocketClient = (options) => {
13839
14196
  * Generated bundle index. Do not edit.
13840
14197
  */
13841
14198
 
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 };
14199
+ 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
14200
  //# sourceMappingURL=ethlete-query.mjs.map