@ethlete/query 6.0.0-next.28 → 6.0.0-next.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
3
- import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, setCookie, deleteCookie, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
3
+ import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, deleteCookie, setCookie, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
4
4
  import { filter, Subject, Subscription, defer, throwError, timer, switchMap, tap, retry, catchError, take, of, merge, firstValueFrom, fromEvent, throttleTime, interval, combineLatest, map, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, takeWhile, EMPTY, withLatestFrom, debounceTime } from 'rxjs';
5
5
  import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
6
6
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
@@ -50,7 +50,7 @@ const queryDevtoolsFnDetail = (fn, key) => {
50
50
  return name ? [{ label: key, value: name }] : [];
51
51
  };
52
52
 
53
- const noop$2 = () => undefined;
53
+ const noop$3 = () => undefined;
54
54
  let registrar = null;
55
55
  /**
56
56
  * Installs the real registry. Called by `provideQueryDevtools()`; nothing else may call it.
@@ -70,7 +70,7 @@ const isQueryDevtoolsEnabled = () => registrar !== null;
70
70
  * a no-op callback) unless {@link provideQueryDevtools} has been called.
71
71
  * @internal
72
72
  */
73
- const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$2;
73
+ const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$3;
74
74
  let statsFactory = null;
75
75
  /**
76
76
  * Installs the stats recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
@@ -1023,7 +1023,7 @@ const createBaseQuery = (options) => {
1023
1023
  if (devtoolsStats) {
1024
1024
  const statsSubscription = state.events$.subscribe((event) => {
1025
1025
  if (event.type === 'error') {
1026
- devtoolsStats.recordError({ faulted: event.faulted });
1026
+ devtoolsStats.recordError({ faulted: event.faulted, error: event.error });
1027
1027
  }
1028
1028
  else if (event.type === HttpEventType.Response) {
1029
1029
  devtoolsStats.recordResponse({ headers: event.headers, body: event.body });
@@ -1085,7 +1085,7 @@ const setupQueryExecuteState = () => {
1085
1085
  };
1086
1086
  };
1087
1087
  const queryExecute = (options) => {
1088
- const { executeOptions, args, executeState, options: runQueryOptions, isSecure } = options;
1088
+ const { executeOptions, args, executeState, options: runQueryOptions, isSecure, isRefreshable } = options;
1089
1089
  const { deps, state, creator, creatorInternals, queryConfig } = executeOptions;
1090
1090
  const { key, request, executed } = deps.client.repository.request({
1091
1091
  route: creatorInternals.route,
@@ -1098,6 +1098,7 @@ const queryExecute = (options) => {
1098
1098
  previousKey: executeState.previousKey(),
1099
1099
  runQueryOptions,
1100
1100
  isSecure,
1101
+ isRefreshable,
1101
1102
  silenceUncacheableAllowCacheError: queryConfig.silenceUncacheableAllowCacheError,
1102
1103
  });
1103
1104
  executeState.previousKey.set(key);
@@ -1850,8 +1851,13 @@ const createQueryStack = (options) => {
1850
1851
  removeStrategy === 'newest' ? finalQueries.slice(0, maxQueries) : finalQueries.slice(-maxQueries);
1851
1852
  }
1852
1853
  queries.set(finalQueries);
1853
- lastQuery.set(lastAppendedQuery);
1854
- return lastAppendedQuery;
1854
+ // After the eviction, not before: `removeStrategy: 'newest'` destroys the query `appendFn`
1855
+ // just returned, and `lastQuery` must never point at a query that is no longer in the stack.
1856
+ const last = lastAppendedQuery && finalQueries.includes(lastAppendedQuery)
1857
+ ? lastAppendedQuery
1858
+ : (finalQueries[finalQueries.length - 1] ?? null);
1859
+ lastQuery.set(last);
1860
+ return last;
1855
1861
  }
1856
1862
  else {
1857
1863
  const keyFn = argsKeyFn ?? ((a) => JSON.stringify(a));
@@ -2084,7 +2090,7 @@ const createPagedQueryStack = (options) => {
2084
2090
  ...(ngDevMode ? [{ debugName: "minPagination" }] : /* istanbul ignore next */ []));
2085
2091
  const isFirstPageLoaded = computed(() => {
2086
2092
  const min = minPagination();
2087
- return min ? loadedMinPage() === min.currentPage : false;
2093
+ return min ? loadedMinPage() === 1 : false;
2088
2094
  }, /* @ts-ignore */
2089
2095
  ...(ngDevMode ? [{ debugName: "isFirstPageLoaded" }] : /* istanbul ignore next */ []));
2090
2096
  const isLastPageLoaded = computed(() => {
@@ -2164,14 +2170,16 @@ const createPagedQueryStack = (options) => {
2164
2170
  };
2165
2171
  const canFetchPreviousPage = computed(() => {
2166
2172
  const currentMinPagination = minPagination();
2167
- if (!currentMinPagination)
2173
+ if (!currentMinPagination || stack.anyLoading())
2168
2174
  return false;
2169
2175
  return loadedMinPage() > 1;
2170
2176
  }, /* @ts-ignore */
2171
2177
  ...(ngDevMode ? [{ debugName: "canFetchPreviousPage" }] : /* istanbul ignore next */ []));
2172
2178
  const canFetchNextPage = computed(() => {
2173
2179
  const currentMaxPagination = maxPagination();
2174
- return currentMaxPagination ? loadedMaxPage() < currentMaxPagination.totalPages : false;
2180
+ if (!currentMaxPagination || stack.anyLoading())
2181
+ return false;
2182
+ return loadedMaxPage() < currentMaxPagination.totalPages;
2175
2183
  }, /* @ts-ignore */
2176
2184
  ...(ngDevMode ? [{ debugName: "canFetchNextPage" }] : /* istanbul ignore next */ []));
2177
2185
  const items = computed(() => {
@@ -2891,9 +2899,9 @@ const shouldRetryRequest = (options) => {
2891
2899
  return { retry: false };
2892
2900
  };
2893
2901
 
2894
- const noop$1 = () => undefined;
2902
+ const noop$2 = () => undefined;
2895
2903
  /** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
2896
- const alwaysHolder = { isHolder: /* @__PURE__ */ signal(true).asReadonly(), release: noop$1 };
2904
+ const alwaysHolder = { isHolder: /* @__PURE__ */ signal(true).asReadonly(), release: noop$2 };
2897
2905
  const noKeyStates = /* @__PURE__ */ signal({}).asReadonly();
2898
2906
  const createQueryKeyLockManager = (namespace) => {
2899
2907
  const locks = typeof navigator === 'undefined' ? undefined : navigator.locks;
@@ -3081,10 +3089,10 @@ const unwrapQuerySyncMessage = (data) => {
3081
3089
  return null;
3082
3090
  };
3083
3091
 
3084
- const noop = () => undefined;
3092
+ const noop$1 = () => undefined;
3085
3093
  const createQuerySyncTransport = (channelName) => {
3086
3094
  if (typeof BroadcastChannel === 'undefined') {
3087
- return { post: noop, listen: () => noop, destroy: noop, isSupported: false };
3095
+ return { post: noop$1, listen: () => noop$1, destroy: noop$1, isSupported: false };
3088
3096
  }
3089
3097
  const channel = new BroadcastChannel(channelName);
3090
3098
  const listeners = new Set();
@@ -3361,6 +3369,7 @@ const createQueryRepository = (config) => {
3361
3369
  const shouldCache = creatorOptions?.subtle?.useQueryRepositoryCache === false
3362
3370
  ? false
3363
3371
  : shouldCacheQuery(options.method) || creatorOptions?.subtle?.useQueryRepositoryCache === true;
3372
+ const isRefreshable = shouldCache && (options.isRefreshable ?? shouldCacheQuery(options.method));
3364
3373
  if (!shouldCache && options.key)
3365
3374
  throw uncacheableRequestHasCacheKeyParam(options.key);
3366
3375
  // `allowCache` is never read on the uncacheable path below - there is no cache entry to reuse - so this throw
@@ -3407,6 +3416,7 @@ const createQueryRepository = (config) => {
3407
3416
  request: cacheEntry.request,
3408
3417
  isSecure,
3409
3418
  isCached: true,
3419
+ isRefreshable,
3410
3420
  isMultiTabSyncEnabled,
3411
3421
  isPersistEnabled,
3412
3422
  keepUnusedFor,
@@ -3435,6 +3445,7 @@ const createQueryRepository = (config) => {
3435
3445
  request,
3436
3446
  isSecure,
3437
3447
  isCached: shouldCache,
3448
+ isRefreshable,
3438
3449
  isMultiTabSyncEnabled,
3439
3450
  isPersistEnabled,
3440
3451
  keepUnusedFor,
@@ -3446,11 +3457,13 @@ const createQueryRepository = (config) => {
3446
3457
  return { key: trackingKey, request, executed };
3447
3458
  };
3448
3459
  /** Tears an entry down for good, whether it currently has consumers or not. */
3449
- const destroyEntry = (key, cacheEntry) => {
3460
+ const destroyEntry = (key, cacheEntry, cause) => {
3461
+ const { method, url } = cacheEntry.request;
3450
3462
  clearTimeout(cacheEntry.evictTimer);
3451
3463
  cacheEntry.request.destroy();
3452
3464
  cacheEntry.eventSubscription?.unsubscribe();
3453
3465
  cache.delete(key);
3466
+ eventsSubject.next({ type: 'entry-destroyed', key, isSecure: cacheEntry.isSecure, method, url, cause });
3454
3467
  };
3455
3468
  const cancelEviction = (cacheEntry) => {
3456
3469
  if (cacheEntry.evictTimer === undefined)
@@ -3466,12 +3479,12 @@ const createQueryRepository = (config) => {
3466
3479
  return;
3467
3480
  unused.sort(([, a], [, b]) => (a.unusedSince ?? 0) - (b.unusedSince ?? 0));
3468
3481
  for (const [key, entry] of unused.slice(0, unused.length - MAX_UNUSED_ENTRIES)) {
3469
- destroyEntry(key, entry);
3482
+ destroyEntry(key, entry, 'unused-cap');
3470
3483
  }
3471
3484
  };
3472
3485
  const retain = (key, cacheEntry) => {
3473
3486
  cacheEntry.unusedSince = Date.now();
3474
- cacheEntry.evictTimer = setTimeout(() => evict(key), cacheEntry.keepUnusedFor);
3487
+ cacheEntry.evictTimer = setTimeout(() => evict(key, 'expired'), cacheEntry.keepUnusedFor);
3475
3488
  enforceUnusedEntryLimit();
3476
3489
  };
3477
3490
  const unbind = (key, consumerDestroyRef) => {
@@ -3488,7 +3501,7 @@ const createQueryRepository = (config) => {
3488
3501
  retain(key, cacheEntry);
3489
3502
  }
3490
3503
  else {
3491
- destroyEntry(key, cacheEntry);
3504
+ destroyEntry(key, cacheEntry, 'unbind');
3492
3505
  }
3493
3506
  }
3494
3507
  bumpCacheVersion();
@@ -3501,7 +3514,7 @@ const createQueryRepository = (config) => {
3501
3514
  // Force the teardown instead of routing through `unbind`: a logged out session must not leave a
3502
3515
  // retained response body behind waiting out its `keepUnusedFor` window.
3503
3516
  cacheEntry.consumers.clear();
3504
- destroyEntry(key, cacheEntry);
3517
+ destroyEntry(key, cacheEntry, 'logout');
3505
3518
  }
3506
3519
  bumpCacheVersion();
3507
3520
  eventsSubject.next({ type: 'unbind-all-secure' });
@@ -3511,10 +3524,11 @@ const createQueryRepository = (config) => {
3511
3524
  for (const cacheEntry of cache.values()) {
3512
3525
  if (cacheEntry.consumers.size === 0)
3513
3526
  continue;
3514
- // Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed -
3515
- // "read" meaning cacheable, which also covers a GQL query transported via POST that opted into
3516
- // the cache explicitly.
3517
- if (!cacheEntry.isCached)
3527
+ // Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed:
3528
+ // a cacheable method, or a GQL query transported via POST. Opting into the cache alone does not
3529
+ // qualify - the auth queries do that for a stable cache key, and replaying a login POST would
3530
+ // end the session.
3531
+ if (!cacheEntry.isRefreshable)
3518
3532
  continue;
3519
3533
  if (filter && !filter(cacheEntry.request))
3520
3534
  continue;
@@ -3548,7 +3562,7 @@ const createQueryRepository = (config) => {
3548
3562
  return true;
3549
3563
  };
3550
3564
  const bind = (options) => {
3551
- const { key, consumerDestroyRef, request, isSecure, isCached, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
3565
+ const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
3552
3566
  const destroyListener = consumerDestroyRef.onDestroy(() => unbind(key, consumerDestroyRef));
3553
3567
  const cacheEntry = cache.get(key);
3554
3568
  if (cacheEntry && isCached) {
@@ -3585,6 +3599,7 @@ const createQueryRepository = (config) => {
3585
3599
  eventSubscription,
3586
3600
  keepUnusedFor,
3587
3601
  isCached,
3602
+ isRefreshable,
3588
3603
  isMultiTabSyncEnabled,
3589
3604
  isPersistEnabled,
3590
3605
  });
@@ -3598,11 +3613,11 @@ const createQueryRepository = (config) => {
3598
3613
  isUnused: entry.consumers.size === 0,
3599
3614
  request: entry.request,
3600
3615
  }));
3601
- const evict = (key) => {
3616
+ const evict = (key, cause = 'manual') => {
3602
3617
  const entry = cache.get(key);
3603
3618
  if (!entry)
3604
3619
  return;
3605
- destroyEntry(key, entry);
3620
+ destroyEntry(key, entry, cause);
3606
3621
  bumpCacheVersion();
3607
3622
  };
3608
3623
  const repository = {
@@ -4295,61 +4310,103 @@ const deriveExecutionStateType = (builder, triggeredBy) => {
4295
4310
  return 'revocation';
4296
4311
  return builder.key;
4297
4312
  };
4298
- const setupBearerQueryRegistry = (builders, injector, latestExecutedQuery, latestNonInternalQuery, applyTokens, executionState) => {
4313
+ const setupBearerQueryRegistry = (builders, context) => {
4314
+ const { injector, latestExecutedQuery, latestNonInternalQuery, applyTokens, executionState, sessionStatus } = context;
4299
4315
  const queries = {};
4300
4316
  const querySnapshots = new Map();
4317
+ // Supersession is provider-wide, not per key. Within one key the reused query already handles it,
4318
+ // but a token refresh landing while a login is in flight would otherwise apply its tokens over the
4319
+ // login's and report its own outcome as the session's - two writers, two different executions.
4320
+ let latestExecutionId = 0;
4321
+ const currentExecutions = [];
4301
4322
  for (const builder of builders) {
4302
4323
  const querySnapshot = signal(null, /* @ts-ignore */
4303
4324
  ...(ngDevMode ? [{ debugName: "querySnapshot" }] : /* istanbul ignore next */ []));
4304
4325
  querySnapshots.set(builder.key, querySnapshot);
4305
4326
  const extractTokens = builder.config.extractTokens ?? defaultExtractTokens;
4327
+ // A 2xx whose body carries no usable tokens is a failed execution, not a successful one - the
4328
+ // state effect below reads this so it cannot report `success` for an attempt that left the tab
4329
+ // unauthenticated.
4330
+ const extractionError = signal(null, /* @ts-ignore */
4331
+ ...(ngDevMode ? [{ debugName: "extractionError" }] : /* istanbul ignore next */ []));
4332
+ const currentExecution = signal(null, /* @ts-ignore */
4333
+ ...(ngDevMode ? [{ debugName: "currentExecution" }] : /* istanbul ignore next */ []));
4334
+ currentExecutions.push(currentExecution);
4335
+ const isSuperseded = (execution) => execution.id !== null && execution.id !== latestExecutionId;
4336
+ // Runs before the state effect below, which reads the extraction error this one writes. Angular
4337
+ // runs effects in creation order, so the two must stay in this order.
4306
4338
  effect(() => {
4307
- const snapshot = querySnapshot();
4308
- if (!snapshot)
4339
+ const execution = currentExecution();
4340
+ if (!execution)
4309
4341
  return;
4342
+ const { snapshot } = execution;
4310
4343
  const response = snapshot.response();
4311
4344
  const loading = snapshot.loading();
4312
4345
  const error = snapshot.error();
4313
- if (response && !loading && !error) {
4346
+ if (response && !loading && !error && !isSuperseded(execution)) {
4314
4347
  try {
4315
4348
  const tokens = extractTokens(response);
4349
+ extractionError.set(null);
4316
4350
  applyTokens(tokens.accessToken, tokens.refreshToken);
4317
4351
  }
4318
4352
  catch (extractError) {
4353
+ extractionError.set(createQueryErrorResponse(extractError));
4319
4354
  if (isDevMode()) {
4320
4355
  console.error(`Failed to extract tokens from ${builder.key} response:`, extractError);
4321
4356
  }
4322
4357
  }
4323
4358
  }
4324
4359
  });
4360
+ effect(() => {
4361
+ const execution = currentExecution();
4362
+ if (!execution)
4363
+ return;
4364
+ const { type, snapshot } = execution;
4365
+ const response = snapshot.response();
4366
+ const loading = snapshot.loading();
4367
+ const error = snapshot.error();
4368
+ if (loading || isSuperseded(execution))
4369
+ return;
4370
+ if (error) {
4371
+ executionState.set({ type, state: 'error', error });
4372
+ }
4373
+ else if (response) {
4374
+ const failedExtraction = extractionError();
4375
+ if (failedExtraction) {
4376
+ executionState.set({ type, state: 'error', error: failedExtraction });
4377
+ }
4378
+ else {
4379
+ executionState.set({ type, state: 'success', response });
4380
+ }
4381
+ }
4382
+ // Keyed on `isAlive` rather than on the branches above: a cancelled restore produces neither a
4383
+ // response nor an error, and would otherwise leave `sessionStatus` at `restoring` forever.
4384
+ if (type === 'autoLogin' && !snapshot.isAlive() && !untracked(context.isAuthenticated)) {
4385
+ sessionStatus.set('anonymous');
4386
+ }
4387
+ });
4388
+ // One query per builder, reused by every execution. A query owns a child injector, a devtools
4389
+ // entry and a repository binding holding the request and response bodies - creating one per login
4390
+ // attempt or token refresh grows all of that for as long as the tab lives.
4391
+ let query = null;
4325
4392
  const execute = (args, options) => {
4326
- const query = builder.config.queryCreator({
4327
- onlyManualExecution: true,
4328
- injector,
4329
- });
4393
+ query ??= builder.config.queryCreator({ onlyManualExecution: true, injector });
4394
+ extractionError.set(null);
4330
4395
  query.execute({ args, options });
4331
4396
  const snapshot = query.createSnapshot();
4332
4397
  const stateType = deriveExecutionStateType(builder, options?.triggeredBy);
4333
- if (stateType !== 'revocation') {
4398
+ const isRevocation = stateType === 'revocation';
4399
+ if (!isRevocation) {
4334
4400
  latestExecutedQuery.set({ key: builder.key, snapshot });
4335
4401
  if (!snapshot.triggeredBy()) {
4336
4402
  latestNonInternalQuery.set({ key: builder.key, snapshot });
4337
4403
  }
4338
4404
  }
4339
4405
  executionState.set({ type: stateType, state: 'loading' });
4340
- effect(() => {
4341
- const response = snapshot.response();
4342
- const loading = snapshot.loading();
4343
- const error = snapshot.error();
4344
- if (loading)
4345
- return;
4346
- if (error) {
4347
- executionState.set({ type: stateType, state: 'error', error });
4348
- }
4349
- else if (response) {
4350
- executionState.set({ type: stateType, state: 'success', response });
4351
- }
4352
- }, { injector });
4406
+ if (stateType === 'autoLogin') {
4407
+ sessionStatus.set('restoring');
4408
+ }
4409
+ currentExecution.set({ id: isRevocation ? null : ++latestExecutionId, type: stateType, snapshot });
4353
4410
  querySnapshot.set(snapshot);
4354
4411
  return snapshot;
4355
4412
  };
@@ -4358,7 +4415,12 @@ const setupBearerQueryRegistry = (builders, injector, latestExecutedQuery, lates
4358
4415
  snapshot: querySnapshot.asReadonly(),
4359
4416
  };
4360
4417
  }
4361
- return { queries, querySnapshots };
4418
+ const hasTokenIssuingExecutionInFlight = computed(() => currentExecutions.some((execution) => {
4419
+ const current = execution();
4420
+ return !!current && current.id !== null && current.snapshot.isAlive();
4421
+ }), /* @ts-ignore */
4422
+ ...(ngDevMode ? [{ debugName: "hasTokenIssuingExecutionInFlight" }] : /* istanbul ignore next */ []));
4423
+ return { queries, querySnapshots, hasTokenIssuingExecutionInFlight };
4362
4424
  };
4363
4425
  const setupFeatures = (featureBuilders, context) => {
4364
4426
  const features = {};
@@ -4403,6 +4465,7 @@ const alwaysLeader = () => true;
4403
4465
  const runEarlyFeatureSetup = (featureBuilders, context) => {
4404
4466
  let isLeaderFn = alwaysLeader;
4405
4467
  let leaderElectionContext;
4468
+ let refreshCoordination;
4406
4469
  for (const featureBuilder of featureBuilders ?? []) {
4407
4470
  const earlySetup = featureBuilder.earlySetup;
4408
4471
  if (!earlySetup)
@@ -4410,8 +4473,9 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
4410
4473
  const result = earlySetup(context);
4411
4474
  isLeaderFn = result.isLeader ?? isLeaderFn;
4412
4475
  leaderElectionContext = result.leaderElection ?? leaderElectionContext;
4476
+ refreshCoordination = result.refreshCoordination ?? refreshCoordination;
4413
4477
  }
4414
- return { isLeaderFn, leaderElectionContext };
4478
+ return { isLeaderFn, leaderElectionContext, refreshCoordination };
4415
4479
  };
4416
4480
  const createBearerAuthProviderImpl = (config) => {
4417
4481
  const injector = inject(Injector);
@@ -4446,9 +4510,15 @@ const createBearerAuthProviderImpl = (config) => {
4446
4510
  ...(ngDevMode ? [{ debugName: "latestNonInternalQuery" }] : /* istanbul ignore next */ []));
4447
4511
  const executionState = signal(null, /* @ts-ignore */
4448
4512
  ...(ngDevMode ? [{ debugName: "executionState" }] : /* istanbul ignore next */ []));
4513
+ const sessionStatus = signal('unknown', /* @ts-ignore */
4514
+ ...(ngDevMode ? [{ debugName: "sessionStatus" }] : /* istanbul ignore next */ []));
4515
+ const sessionEndCause = signal(null, /* @ts-ignore */
4516
+ ...(ngDevMode ? [{ debugName: "sessionEndCause" }] : /* istanbul ignore next */ []));
4449
4517
  const applyTokens = (access, refresh) => {
4450
4518
  accessToken.set(access);
4451
4519
  refreshToken.set(refresh);
4520
+ sessionStatus.set('authenticated');
4521
+ sessionEndCause.set(null);
4452
4522
  afterTokenRefresh$.next();
4453
4523
  };
4454
4524
  // Public entry point only - a query-driven login already reports its own, more specific
@@ -4458,20 +4528,37 @@ const createBearerAuthProviderImpl = (config) => {
4458
4528
  applyTokens(access, refresh);
4459
4529
  executionState.set({ type: 'tokenSeed', state: 'success' });
4460
4530
  };
4461
- const { queries } = setupBearerQueryRegistry(config.queries, injector, latestExecutedQuery, latestNonInternalQuery, applyTokens, executionState);
4462
- const logout = () => {
4531
+ const { queries, hasTokenIssuingExecutionInFlight } = setupBearerQueryRegistry(config.queries, {
4532
+ injector,
4533
+ latestExecutedQuery,
4534
+ latestNonInternalQuery,
4535
+ applyTokens,
4536
+ executionState,
4537
+ sessionStatus,
4538
+ isAuthenticated,
4539
+ });
4540
+ const logout = (cause = 'user') => {
4463
4541
  accessToken.set(null);
4464
4542
  refreshToken.set(null);
4465
4543
  queryClient.repository.unbindAllSecure();
4466
4544
  latestExecutedQuery.set(null);
4467
4545
  latestNonInternalQuery.set(null);
4468
4546
  executionState.set({ type: 'logout', state: 'success' });
4547
+ sessionStatus.set('anonymous');
4548
+ sessionEndCause.set(cause);
4469
4549
  // Unsaved edits can no longer be saved once the session is gone. Guarding them past this point
4470
4550
  // only strands a "discard your changes?" dialog over the login page the app redirects to, and
4471
4551
  // leaves the tab locked against closing. See `injectUnsavedChangesCoordinator`.
4472
4552
  unsavedChanges.abandonAll('logout');
4473
4553
  };
4474
- const isLeader = runEarlyFeatureSetup(config.features, { accessToken, refreshToken, queryClient });
4554
+ const isLeader = runEarlyFeatureSetup(config.features, {
4555
+ accessToken,
4556
+ refreshToken,
4557
+ queryClient,
4558
+ name: config.name,
4559
+ applyTokens,
4560
+ logout,
4561
+ });
4475
4562
  const querySetupContext = {
4476
4563
  accessToken,
4477
4564
  refreshToken,
@@ -4479,8 +4566,11 @@ const createBearerAuthProviderImpl = (config) => {
4479
4566
  queryClient,
4480
4567
  repository: queryClient.repository,
4481
4568
  isLeader: isLeader.isLeaderFn,
4569
+ refreshCoordination: isLeader.refreshCoordination,
4570
+ logout,
4482
4571
  afterTokenRefresh$,
4483
4572
  queries: queries,
4573
+ hasTokenIssuingExecutionInFlight,
4484
4574
  };
4485
4575
  for (const builder of config.queries) {
4486
4576
  builder.setup?.(querySetupContext);
@@ -4498,8 +4588,15 @@ const createBearerAuthProviderImpl = (config) => {
4498
4588
  afterTokenRefresh$,
4499
4589
  queries: queries,
4500
4590
  executionState,
4591
+ sessionStatus: sessionStatus.asReadonly(),
4592
+ sessionEndCause: sessionEndCause.asReadonly(),
4501
4593
  };
4502
4594
  const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
4595
+ // Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
4596
+ // `unknown` here means nothing is trying to restore a session, so there is nothing to wait for.
4597
+ if (sessionStatus() === 'unknown') {
4598
+ sessionStatus.set('anonymous');
4599
+ }
4503
4600
  const provider = {
4504
4601
  queries,
4505
4602
  features: features,
@@ -4510,6 +4607,8 @@ const createBearerAuthProviderImpl = (config) => {
4510
4607
  latestExecutedQuery: latestExecutedQuery.asReadonly(),
4511
4608
  latestNonInternalQuery: latestNonInternalQuery.asReadonly(),
4512
4609
  executionState: executionState.asReadonly(),
4610
+ sessionStatus: sessionStatus.asReadonly(),
4611
+ sessionEndCause: sessionEndCause.asReadonly(),
4513
4612
  setTokens,
4514
4613
  logout,
4515
4614
  afterTokenRefresh$,
@@ -4539,7 +4638,9 @@ const withAuthenticationQuery = (key, config, setup) => ({
4539
4638
  key,
4540
4639
  config: {
4541
4640
  ...config,
4542
- queryCreator: config.queryCreator.clone({ subtle: { useQueryRepositoryCache: true } }),
4641
+ // A spent credential entry must not sit out a retention window: the request body holds the
4642
+ // username and password, the response the tokens it was exchanged for.
4643
+ queryCreator: config.queryCreator.clone({ keepUnusedFor: 0, subtle: { useQueryRepositoryCache: true } }),
4543
4644
  },
4544
4645
  setup,
4545
4646
  });
@@ -4574,20 +4675,30 @@ const withRefreshQuery = (key, config) => {
4574
4675
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
4575
4676
  const refreshIfExpired = config.refreshIfExpired ?? true;
4576
4677
  let lastRefreshTime = 0;
4577
- const executeRefresh = (triggeredInternally = true) => {
4678
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4679
+ const refreshQuery = () => context.queries[key];
4680
+ const executeRefresh = (reason) => {
4578
4681
  const currentRefreshToken = context.refreshToken();
4579
- if (!currentRefreshToken || !context.isLeader())
4682
+ if (!currentRefreshToken)
4580
4683
  return;
4581
- const now = Date.now();
4582
- const timeSinceLastRefresh = now - lastRefreshTime;
4583
- // Prevent too-frequent refreshes
4584
- if (timeSinceLastRefresh < minRefreshInterval)
4684
+ if (!context.isLeader()) {
4685
+ // Only the leader may spend a single-use refresh token, but the event is still real - hand it
4686
+ // over instead of dropping it, or this tab waits out the leader's timer with a dead token.
4687
+ context.refreshCoordination?.request();
4585
4688
  return;
4586
- lastRefreshTime = now;
4587
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4588
- context.queries[key].execute(buildArgs(currentRefreshToken), {
4589
- triggeredBy: triggeredInternally ? 'token-refresh' : undefined,
4590
- });
4689
+ }
4690
+ // Any token-issuing execution, not just another refresh: a login already in flight is about to
4691
+ // issue a token pair of its own, and the refresh token this would spend belongs to the session
4692
+ // that login is replacing.
4693
+ if (context.hasTokenIssuingExecutionInFlight())
4694
+ return;
4695
+ if (reason === 'scheduled') {
4696
+ const now = Date.now();
4697
+ if (now - lastRefreshTime < minRefreshInterval)
4698
+ return;
4699
+ lastRefreshTime = now;
4700
+ }
4701
+ refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4591
4702
  };
4592
4703
  const calculateRefreshBuffer = (tokenLifetimeMs) => {
4593
4704
  if (typeof config.refreshStrategy === 'number') {
@@ -4638,9 +4749,24 @@ const withRefreshQuery = (key, config) => {
4638
4749
  }), takeUntilDestroyed())
4639
4750
  .subscribe((shouldRefresh) => {
4640
4751
  if (shouldRefresh) {
4641
- executeRefresh(true);
4752
+ executeRefresh('scheduled');
4642
4753
  }
4643
4754
  });
4755
+ context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
4756
+ const onRefreshFailure = config.onRefreshFailure ??
4757
+ (({ error, logout }) => {
4758
+ if (!retryableStatusCodes.includes(error.code))
4759
+ logout();
4760
+ });
4761
+ effect(() => {
4762
+ const snapshot = refreshQuery().snapshot();
4763
+ if (!snapshot || snapshot.loading())
4764
+ return;
4765
+ const error = snapshot.error();
4766
+ if (!error)
4767
+ return;
4768
+ onRefreshFailure({ error, logout: () => context.logout('expired') });
4769
+ });
4644
4770
  // Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
4645
4771
  const autoRetryOn401 = config.autoRetryOn401 ?? true;
4646
4772
  if (autoRetryOn401) {
@@ -4656,7 +4782,7 @@ const withRefreshQuery = (key, config) => {
4656
4782
  return true;
4657
4783
  }), takeUntilDestroyed())
4658
4784
  .subscribe(() => {
4659
- executeRefresh(true);
4785
+ executeRefresh('unauthorized');
4660
4786
  });
4661
4787
  }
4662
4788
  };
@@ -4665,7 +4791,11 @@ const withRefreshQuery = (key, config) => {
4665
4791
  key,
4666
4792
  config: {
4667
4793
  ...config,
4668
- queryCreator: config.queryCreator.clone({ retryFn: refreshRetryFn, subtle: { useQueryRepositoryCache: true } }),
4794
+ queryCreator: config.queryCreator.clone({
4795
+ retryFn: refreshRetryFn,
4796
+ keepUnusedFor: 0,
4797
+ subtle: { useQueryRepositoryCache: true },
4798
+ }),
4669
4799
  },
4670
4800
  setup,
4671
4801
  buildArgs,
@@ -4697,7 +4827,7 @@ const withInactivityLogout = (config = {}) => {
4697
4827
  });
4698
4828
  const inactivityLogout$ = activity$.pipe(switchMap(() => timer(inactivityTimeout)), filter(() => enabled() && !!context.accessToken()));
4699
4829
  const logoutSubscription = inactivityLogout$.subscribe(() => {
4700
- context.logout();
4830
+ context.logout('inactivity');
4701
4831
  });
4702
4832
  effect(() => {
4703
4833
  const token = context.accessToken();
@@ -4747,18 +4877,20 @@ const withInactivityLogout = (config = {}) => {
4747
4877
  /**
4748
4878
  * Namespace and key of the one lock the whole election is: whoever holds it is the leader, and every
4749
4879
  * other tab of the app is queued behind it. The lock name is what the instance count is derived from,
4750
- * so the two can never disagree about who is taking part.
4880
+ * so the two can never disagree about who is taking part. Both carry the provider's name, so two
4881
+ * providers reachable from one origin elect a leader each instead of one between them.
4751
4882
  */
4752
4883
  const LEADER_LOCK_NAMESPACE = 'ethlete-auth';
4753
- const LEADER_LOCK_KEY = 'leader';
4754
- const LEADER_LOCK_NAME = 'ethlete-auth:leader';
4884
+ const leaderLockKey = (name) => `leader:${name}`;
4885
+ const leaderLockName = (name) => `${LEADER_LOCK_NAMESPACE}:${leaderLockKey(name)}`;
4755
4886
  /**
4756
4887
  * Web Locks has no "someone joined" event, so tabs announce themselves on this channel. Two messages
4757
- * per tab lifetime, in place of the heartbeat this used to run once a second.
4888
+ * per tab lifetime, in place of the heartbeat this used to run once a second. It also carries a
4889
+ * follower's request for a refresh, which is the other thing a tab can only say to the leader.
4758
4890
  */
4759
- const PRESENCE_CHANNEL_NAME = 'ethlete-auth-leader';
4760
- const countLeaderRequests = (snapshot) => {
4761
- const isLeaderLock = (info) => info.name === LEADER_LOCK_NAME;
4891
+ const presenceChannelName = (name) => `ethlete-auth-leader:${name}`;
4892
+ const countLeaderRequests = (snapshot, lockName) => {
4893
+ const isLeaderLock = (info) => info.name === lockName;
4762
4894
  // Held plus pending is exactly the tab count: every tab requests this one lock, one gets it and the
4763
4895
  // rest queue. The platform drops a crashed tab from both lists on its own.
4764
4896
  return (snapshot.held?.filter(isLeaderLock).length ?? 0) + (snapshot.pending?.filter(isLeaderLock).length ?? 0);
@@ -4775,20 +4907,29 @@ const countLeaderRequests = (snapshot) => {
4775
4907
  * `localStorage` implementation fell back to, and the only safe default: a session that refreshes its
4776
4908
  * token in every tab is wasteful, one that refreshes it in none logs the user out.
4777
4909
  */
4778
- const setupLeaderElection = () => {
4910
+ const setupLeaderElection = (options) => {
4779
4911
  const destroyRef = inject(DestroyRef);
4912
+ const lockName = leaderLockName(options.name);
4780
4913
  const lockManager = createQueryKeyLockManager(LEADER_LOCK_NAMESPACE);
4781
- const hold = lockManager.hold(LEADER_LOCK_KEY);
4914
+ const hold = lockManager.hold(leaderLockKey(options.name));
4782
4915
  const instanceCount = signal(1, /* @ts-ignore */
4783
4916
  ...(ngDevMode ? [{ debugName: "instanceCount" }] : /* istanbul ignore next */ []));
4784
- const noopCleanup = () => {
4917
+ const refreshRequests = new Subject();
4918
+ const noop = () => {
4785
4919
  /* nothing was set up */
4786
4920
  };
4787
- // `hold.isHolder` is already `true` in this case, so the tab reads as the leader it effectively is.
4921
+ // `hold.isHolder` is already `true` in this case, so the tab reads as the leader it effectively is,
4922
+ // and asks nobody else for a refresh.
4788
4923
  if (!lockManager.isSupported) {
4789
- return { isLeader: hold.isHolder, instanceCount: instanceCount.asReadonly(), cleanup: noopCleanup };
4924
+ return {
4925
+ isLeader: hold.isHolder,
4926
+ instanceCount: instanceCount.asReadonly(),
4927
+ requestRefresh: noop,
4928
+ refreshRequests$: refreshRequests.asObservable(),
4929
+ cleanup: noop,
4930
+ };
4790
4931
  }
4791
- const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(PRESENCE_CHANNEL_NAME);
4932
+ const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(presenceChannelName(options.name));
4792
4933
  let isDestroyed = false;
4793
4934
  const recount = async () => {
4794
4935
  const snapshot = await navigator.locks.query();
@@ -4796,14 +4937,21 @@ const setupLeaderElection = () => {
4796
4937
  return;
4797
4938
  // Never below one: the snapshot can be taken before this tab's own request is registered, and a
4798
4939
  // count of zero would say the app is not running in the tab reading it.
4799
- instanceCount.set(Math.max(countLeaderRequests(snapshot), 1));
4940
+ instanceCount.set(Math.max(countLeaderRequests(snapshot, lockName), 1));
4800
4941
  };
4801
4942
  const announce = () => channel?.postMessage({ type: 'presence' });
4943
+ const requestRefresh = () => channel?.postMessage({ type: 'refresh-requested' });
4802
4944
  if (channel) {
4803
4945
  channel.onmessage = (event) => {
4804
- if (event.data?.type !== 'presence')
4946
+ const message = event.data;
4947
+ if (message?.type === 'presence') {
4948
+ void recount();
4805
4949
  return;
4806
- void recount();
4950
+ }
4951
+ // Every tab hears the request; only the one that may spend the refresh token acts on it.
4952
+ if (message?.type === 'refresh-requested' && hold.isHolder()) {
4953
+ refreshRequests.next();
4954
+ }
4807
4955
  };
4808
4956
  }
4809
4957
  effect(() => {
@@ -4824,9 +4972,16 @@ const setupLeaderElection = () => {
4824
4972
  hold.release();
4825
4973
  announce();
4826
4974
  channel?.close();
4975
+ refreshRequests.complete();
4827
4976
  };
4828
4977
  destroyRef.onDestroy(cleanup);
4829
- return { isLeader: hold.isHolder, instanceCount: instanceCount.asReadonly(), cleanup };
4978
+ return {
4979
+ isLeader: hold.isHolder,
4980
+ instanceCount: instanceCount.asReadonly(),
4981
+ requestRefresh,
4982
+ refreshRequests$: refreshRequests.asObservable(),
4983
+ cleanup,
4984
+ };
4830
4985
  };
4831
4986
 
4832
4987
  const ENCRYPTION_KEY_STORAGE = '__eth_ek';
@@ -4909,13 +5064,19 @@ const resetEncryptionKey = () => {
4909
5064
  }
4910
5065
  };
4911
5066
 
4912
- const setupMultiTabSync = (config, accessToken, refreshToken, queryClient) => {
5067
+ /**
5068
+ * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5069
+ * providers reachable from the same origin do not push each other's tokens around.
5070
+ */
5071
+ const defaultSyncChannelName = (name) => `ethlete-auth-sync:${name}`;
5072
+ /** How a logout is recorded in {@link lastSyncedState}, where a token pair records its two tokens. */
5073
+ const LOGGED_OUT = 'logout';
5074
+ const tokenState = (access, refresh) => `${access}\0${refresh}`;
5075
+ const setupMultiTabSync = (config, context) => {
4913
5076
  const destroyRef = inject(DestroyRef);
4914
- const channelName = config.channelName ?? 'ethlete-auth-sync';
5077
+ const channelName = config.channelName ?? defaultSyncChannelName(context.name);
4915
5078
  const syncTokens = config.syncTokens ?? true;
4916
5079
  const syncLogout = config.syncLogout ?? true;
4917
- let channel = null;
4918
- let isProcessingExternalUpdate = false;
4919
5080
  if (typeof BroadcastChannel === 'undefined') {
4920
5081
  if (isDevMode()) {
4921
5082
  console.warn('BroadcastChannel is not supported in this environment. Multi-tab sync will be disabled.');
@@ -4926,60 +5087,67 @@ const setupMultiTabSync = (config, accessToken, refreshToken, queryClient) => {
4926
5087
  },
4927
5088
  };
4928
5089
  }
4929
- channel = new BroadcastChannel(channelName);
5090
+ const channel = new BroadcastChannel(channelName);
5091
+ // The session state this tab last sent or received, which is what stops an incoming message from being
5092
+ // echoed straight back out. A flag cleared once the message handler returns cannot do that job: the
5093
+ // broadcasting effects below run after it, so they would always see it cleared.
5094
+ let lastSyncedState = null;
5095
+ let hadTokens = false;
4930
5096
  channel.onmessage = (event) => {
4931
5097
  const message = event.data;
4932
- // Prevent infinite loops
4933
- if (isProcessingExternalUpdate)
5098
+ if (message.type === 'logout' && syncLogout) {
5099
+ lastSyncedState = LOGGED_OUT;
5100
+ hadTokens = false;
5101
+ context.logout('otherTab');
4934
5102
  return;
4935
- isProcessingExternalUpdate = true;
4936
- try {
4937
- if (message.type === 'logout' && syncLogout) {
4938
- accessToken.set(null);
4939
- refreshToken.set(null);
4940
- queryClient.repository.unbindAllSecure();
4941
- }
4942
- else if (message.type === 'tokens-updated' && syncTokens) {
4943
- accessToken.set(decryptToken(message.accessToken));
4944
- refreshToken.set(decryptToken(message.refreshToken));
4945
- }
4946
- }
4947
- finally {
4948
- isProcessingExternalUpdate = false;
4949
5103
  }
5104
+ if (message.type !== 'tokens-updated' || !syncTokens)
5105
+ return;
5106
+ const access = decryptToken(message.accessToken);
5107
+ const refresh = decryptToken(message.refreshToken);
5108
+ if (!access || !refresh)
5109
+ return;
5110
+ lastSyncedState = tokenState(access, refresh);
5111
+ hadTokens = true;
5112
+ context.applyTokens(access, refresh);
4950
5113
  };
4951
5114
  const cleanup = () => {
4952
- channel?.close();
5115
+ channel.close();
4953
5116
  };
4954
5117
  destroyRef.onDestroy(cleanup);
4955
5118
  if (syncTokens) {
4956
5119
  effect(() => {
4957
- const access = accessToken();
4958
- const refresh = refreshToken();
4959
- if (access && refresh && !isProcessingExternalUpdate && channel) {
4960
- const message = {
4961
- type: 'tokens-updated',
4962
- accessToken: encryptToken(access),
4963
- refreshToken: encryptToken(refresh),
4964
- };
4965
- channel.postMessage(message);
4966
- }
5120
+ const access = context.accessToken();
5121
+ const refresh = context.refreshToken();
5122
+ if (!access || !refresh)
5123
+ return;
5124
+ const state = tokenState(access, refresh);
5125
+ if (state === lastSyncedState)
5126
+ return;
5127
+ lastSyncedState = state;
5128
+ const message = {
5129
+ type: 'tokens-updated',
5130
+ accessToken: encryptToken(access),
5131
+ refreshToken: encryptToken(refresh),
5132
+ };
5133
+ channel.postMessage(message);
4967
5134
  });
4968
5135
  }
4969
5136
  if (syncLogout) {
4970
- let hadTokens = false;
4971
5137
  effect(() => {
4972
- const access = accessToken();
5138
+ const access = context.accessToken();
4973
5139
  if (access) {
4974
5140
  hadTokens = true;
5141
+ return;
4975
5142
  }
4976
- else if (hadTokens && !isProcessingExternalUpdate && channel) {
4977
- const message = {
4978
- type: 'logout',
4979
- };
4980
- channel.postMessage(message);
4981
- hadTokens = false;
4982
- }
5143
+ if (!hadTokens)
5144
+ return;
5145
+ hadTokens = false;
5146
+ if (lastSyncedState === LOGGED_OUT)
5147
+ return;
5148
+ lastSyncedState = LOGGED_OUT;
5149
+ const message = { type: 'logout' };
5150
+ channel.postMessage(message);
4983
5151
  });
4984
5152
  }
4985
5153
  return { cleanup };
@@ -5013,11 +5181,12 @@ const SINGLE_TAB = /* @__PURE__ */ (() => {
5013
5181
  */
5014
5182
  const withBearerAuthMultiTabSync = (config = {}) => {
5015
5183
  let instance = SINGLE_TAB;
5184
+ let channelName = config.channelName ?? '';
5016
5185
  const setup = (_context) => ({
5017
5186
  type: BearerAuthFeatureType.MULTI_TAB_SYNC,
5018
5187
  instance,
5019
5188
  devtools: () => [
5020
- { label: 'channel', value: config.channelName ?? 'ethlete-auth-sync' },
5189
+ { label: 'channel', value: channelName },
5021
5190
  { label: 'tokens', value: config.syncTokens === false ? 'tab local' : 'synced' },
5022
5191
  { label: 'logout', value: config.syncLogout === false ? 'tab local' : 'synced' },
5023
5192
  {
@@ -5029,12 +5198,23 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5029
5198
  // The elected leader gates the auth queries' automatic refresh, which is wired up before the
5030
5199
  // regular feature setup runs - so this half has to happen earlier than `setup`.
5031
5200
  const earlySetup = (context) => {
5032
- setupMultiTabSync({ channelName: config.channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, context.accessToken, context.refreshToken, context.queryClient);
5201
+ channelName = config.channelName ?? defaultSyncChannelName(context.name);
5202
+ setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5203
+ accessToken: context.accessToken,
5204
+ refreshToken: context.refreshToken,
5205
+ name: context.name,
5206
+ applyTokens: context.applyTokens,
5207
+ logout: context.logout,
5208
+ });
5033
5209
  if (config.leaderElection === false)
5034
5210
  return {};
5035
- const election = setupLeaderElection();
5211
+ const election = setupLeaderElection({ name: context.name });
5036
5212
  instance = { isLeader: election.isLeader, instanceCount: election.instanceCount };
5037
- return { isLeader: () => election.isLeader(), leaderElection: instance };
5213
+ return {
5214
+ isLeader: () => election.isLeader(),
5215
+ leaderElection: instance,
5216
+ refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
5217
+ };
5038
5218
  };
5039
5219
  return Object.assign(setup, { earlySetup });
5040
5220
  };
@@ -5059,7 +5239,7 @@ const withPersistentAuth = (config) => {
5059
5239
  };
5060
5240
  };
5061
5241
  const createPersistentAuthFeature = (config, context) => {
5062
- const { refreshToken } = context;
5242
+ const { refreshToken, executionState } = context;
5063
5243
  const cookieName = config.cookie?.name ?? 'etAuth';
5064
5244
  const rememberMeStorageKey = `${cookieName}-rememberMe`;
5065
5245
  const route = injectRoute();
@@ -5076,25 +5256,36 @@ const createPersistentAuthFeature = (config, context) => {
5076
5256
  };
5077
5257
  const rememberMeSignal = signal(initializeRememberMe(), /* @ts-ignore */
5078
5258
  ...(ngDevMode ? [{ debugName: "rememberMeSignal" }] : /* istanbul ignore next */ []));
5259
+ const cookieDomain = () => config.cookie?.domain ?? getDomain() ?? 'localhost';
5260
+ const cookiePath = () => config.cookie?.path ?? '/';
5261
+ const removeCookie = () => deleteCookie(cookieName, cookiePath(), cookieDomain());
5079
5262
  effect(() => {
5080
5263
  const token = refreshToken();
5081
5264
  const rememberMe = rememberMeSignal();
5082
- const domain = config.cookie?.domain ?? getDomain() ?? 'localhost';
5083
- const path = config.cookie?.path ?? '/';
5084
- const sameSite = config.cookie?.sameSite ?? 'lax';
5085
- if (token) {
5086
- const encryptedToken = encryptToken(token);
5087
- if (rememberMe) {
5088
- const expiresInDays = config.cookie?.expiresInDays ?? 30;
5089
- setCookie(cookieName, encryptedToken, expiresInDays, domain, path, sameSite);
5090
- }
5091
- else {
5092
- setCookie(cookieName, encryptedToken, null, domain, path, sameSite);
5093
- }
5094
- }
5095
- else {
5096
- deleteCookie(cookieName, path, domain);
5265
+ // A missing token is not a reason to drop the cookie. It is missing on every startup - `tryLogin`
5266
+ // reads the cookie synchronously and the auto-login only resolves a tick later - so deleting here
5267
+ // would throw away a 30-day refresh token before it was ever used. Deletion is driven by the events
5268
+ // that actually end a session instead, below.
5269
+ if (!token)
5270
+ return;
5271
+ const expiresInDays = rememberMe ? (config.cookie?.expiresInDays ?? 30) : null;
5272
+ setCookie(cookieName, encryptToken(token), expiresInDays, cookieDomain(), cookiePath(), config.cookie?.sameSite ?? 'lax');
5273
+ });
5274
+ effect(() => {
5275
+ const state = executionState();
5276
+ if (!state)
5277
+ return;
5278
+ if (state.type === 'logout') {
5279
+ removeCookie();
5280
+ return;
5097
5281
  }
5282
+ // A token the server itself rejected is worth nothing on the next visit; anything else (offline, a
5283
+ // 500, an aborted startup) leaves it in place so a reload can try again.
5284
+ const wasRejected = state.state === 'error' &&
5285
+ (state.type === 'tokenRefresh' || state.type === 'autoLogin') &&
5286
+ (state.error.code === 401 || state.error.code === 403);
5287
+ if (wasRejected)
5288
+ removeCookie();
5098
5289
  });
5099
5290
  const tryLogin = () => {
5100
5291
  const currentRoute = route();
@@ -5340,7 +5531,7 @@ const createTrackingFeature = (context, config) => {
5340
5531
  const refreshToken = context.refreshToken();
5341
5532
  const hasTokens = !!(accessToken || refreshToken);
5342
5533
  if (hadTokens && !hasTokens) {
5343
- emit('logout', undefined);
5534
+ emit('logout', { cause: untracked(context.sessionEndCause) });
5344
5535
  }
5345
5536
  hadTokens = hasTokens;
5346
5537
  });
@@ -5487,7 +5678,12 @@ const createQueryDevtoolsFormLinks = () => {
5487
5678
  };
5488
5679
  };
5489
5680
 
5490
- const pathsEqual = (a, b) => a.length === b.length && a.every((step, index) => step === b[index]);
5681
+ const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
5682
+ /**
5683
+ * Whether any of `entries` is armed at `path` or anywhere below it - what the panel's menu asks before
5684
+ * offering "Reset", so the destructive action is hidden where it would be a silent no-op.
5685
+ */
5686
+ const hasQueryDevtoolsOverridesAtPath = (entries, path) => entries.some((entry) => isPathAtOrBelow(entry.op.path, path));
5491
5687
  /**
5492
5688
  * Walks `path` into `root`, stopping as soon as a step's container doesn't exist. An empty path
5493
5689
  * resolves to `root` itself, which is how an op targets the response's top-level value.
@@ -5695,15 +5891,84 @@ const resizePagination = (value, shape, mode, amount) => {
5695
5891
  const STRING_PRESETS = {
5696
5892
  short: 'Ab',
5697
5893
  long: /* @__PURE__ */ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt. '.repeat(4),
5894
+ longWord: 'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
5698
5895
  unicode: 'مرحبا بالعالم 👋 日本語テスト 🎉 Ñoño',
5699
5896
  };
5700
- const stringPresetValue = (preset, custom) => preset === 'custom' ? (custom ?? '') : STRING_PRESETS[preset];
5897
+ const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS[preset]);
5701
5898
  const NUMBER_PRESETS = {
5702
5899
  zero: 0,
5703
5900
  negative: -1,
5704
5901
  huge: Number.MAX_SAFE_INTEGER,
5705
5902
  };
5706
- const numberPresetValue = (preset, custom) => preset === 'custom' ? (custom ?? 0) : NUMBER_PRESETS[preset];
5903
+ const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS[preset]);
5904
+ const pickRandom = (values) => values[Math.floor(Math.random() * values.length)];
5905
+ const randomIntBetween = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
5906
+ const randomHex = (length) => {
5907
+ let out = '';
5908
+ while (out.length < length)
5909
+ out += Math.random().toString(16).slice(2);
5910
+ return out.slice(0, length);
5911
+ };
5912
+ const SHORT_WORDS = ['Ok', 'Ada', 'Fig', 'Kiwi', 'Nova', 'Echo', 'Amber', 'Delta', 'Mango', 'Quartz', 'Zephyr'];
5913
+ const LOREM_SENTENCE = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ';
5914
+ const LONG_COMPOUNDS = [
5915
+ 'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
5916
+ 'Donaudampfschifffahrtsgesellschaftskapitänskajütenschlüssel',
5917
+ 'Kraftfahrzeughaftpflichtversicherungsbescheinigung',
5918
+ ];
5919
+ const UNICODE_SAMPLES = [
5920
+ 'مرحبا بالعالم 👋 اختبار النص',
5921
+ '日本語テスト 🎉 カタカナとひらがな',
5922
+ '한국어 테스트 💡 조합형 한글',
5923
+ 'עברית מימין לשמאל 🚀 בדיקה',
5924
+ 'Ñoño über façade — Ω≈ç√∫ 🧪',
5925
+ ];
5926
+ /**
5927
+ * Generates a fresh sample value for a string preset, so filling twenty fields yields twenty different
5928
+ * values instead of twenty identical ones. Call it at arm time and store the result in the op's
5929
+ * `custom` field - generating inside apply would reshuffle the response on every refetch. `longWord`
5930
+ * is a single unbreakable token (a compound word, a URL, a hex blob) - it tests overflow, where
5931
+ * `long` tests wrapping.
5932
+ */
5933
+ const generateQueryDevtoolsStringPreset = (preset) => {
5934
+ switch (preset) {
5935
+ case 'short':
5936
+ return pickRandom(SHORT_WORDS);
5937
+ case 'long': {
5938
+ const length = randomIntBetween(80, 480);
5939
+ return LOREM_SENTENCE.repeat(Math.ceil(length / LOREM_SENTENCE.length))
5940
+ .slice(0, length)
5941
+ .trimEnd();
5942
+ }
5943
+ case 'longWord':
5944
+ return pickRandom([
5945
+ () => pickRandom(LONG_COMPOUNDS),
5946
+ () => `https://api.example.com/v2/resources/${randomHex(24)}/attachments?signature=${randomHex(randomIntBetween(24, 96))}`,
5947
+ () => randomHex(randomIntBetween(64, 160)),
5948
+ ])();
5949
+ case 'unicode':
5950
+ return pickRandom(UNICODE_SAMPLES);
5951
+ }
5952
+ };
5953
+ /**
5954
+ * Generates a fresh sample value for a number preset - same arm-time contract as
5955
+ * {@link generateQueryDevtoolsStringPreset}. `negative` and `huge` vary in magnitude so a batch fill
5956
+ * produces varied digit counts; `zero` is always `0`.
5957
+ */
5958
+ const generateQueryDevtoolsNumberPreset = (preset) => {
5959
+ switch (preset) {
5960
+ case 'zero':
5961
+ return 0;
5962
+ case 'negative': {
5963
+ const magnitude = 10 ** randomIntBetween(0, 8);
5964
+ return -randomIntBetween(magnitude, magnitude * 10 - 1);
5965
+ }
5966
+ case 'huge': {
5967
+ const magnitude = 10 ** randomIntBetween(9, 15);
5968
+ return Math.min(randomIntBetween(magnitude, magnitude * 10 - 1), Number.MAX_SAFE_INTEGER);
5969
+ }
5970
+ }
5971
+ };
5707
5972
  const DAY_MS = 24 * 60 * 60 * 1000;
5708
5973
  /** `invalid` is deliberately unparseable - "the API sent a date my code can't parse" is a real bug class. */
5709
5974
  const datePresetValue = (preset) => {
@@ -5795,7 +6060,7 @@ const createQueryDevtoolsOverrides = () => {
5795
6060
  let counter = 0;
5796
6061
  const arm = (op) => {
5797
6062
  if (op.type === 'reset') {
5798
- list.update((current) => current.filter((entry) => !pathsEqual(entry.op.path, op.path)));
6063
+ list.update((current) => current.filter((entry) => !isPathAtOrBelow(entry.op.path, op.path)));
5799
6064
  return;
5800
6065
  }
5801
6066
  list.update((current) => [...current, { id: `override-${++counter}`, op }]);
@@ -5847,10 +6112,27 @@ const estimateBodySize = (body) => {
5847
6112
  /** How many runs a query keeps. Enough to see a stampede or a chain without growing unbounded. */
5848
6113
  const RUN_HISTORY = 25;
5849
6114
  /**
5850
- * How many of those runs keep their response body. Bodies dominate what the buffer retains, and a diff
5851
- * only ever looks a couple of runs back.
6115
+ * How many of those runs keep a body - a response, or the body an error came with. Bodies dominate what
6116
+ * the buffer retains, which is why this is far below {@link RUN_HISTORY} and why raising it is a memory
6117
+ * decision the app has to make for itself.
5852
6118
  */
5853
- const RESPONSE_HISTORY = 5;
6119
+ const DEFAULT_RESPONSE_HISTORY = 5;
6120
+ let responseHistory = DEFAULT_RESPONSE_HISTORY;
6121
+ /**
6122
+ * How many bodies each query retains - {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run
6123
+ * that still holds one can be an end of a response diff, so the panel reads this to say why an older run
6124
+ * can no longer be picked.
6125
+ *
6126
+ * Part of the devtools contract. **Not part of the general public contract.**
6127
+ */
6128
+ const queryDevtoolsResponseHistory = () => responseHistory;
6129
+ /**
6130
+ * Sets how many bodies each query retains. Called by `provideQueryDevtools()`; nothing else may call it.
6131
+ * @internal
6132
+ */
6133
+ const setQueryDevtoolsResponseHistory = (count) => {
6134
+ responseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
6135
+ };
5854
6136
  /** The index of the newest run still in flight, or -1 when none is. */
5855
6137
  const lastPendingRunIndex = (runs) => {
5856
6138
  for (let index = runs.length - 1; index >= 0; index--) {
@@ -5860,17 +6142,25 @@ const lastPendingRunIndex = (runs) => {
5860
6142
  return -1;
5861
6143
  };
5862
6144
  /**
5863
- * Drops the response body of every run past the newest {@link RESPONSE_HISTORY} that hold one. Mutates
5864
- * the array it is given, which is always one this module just built.
6145
+ * Drops the body of every run past the newest {@link queryDevtoolsResponseHistory} that hold one - a
6146
+ * response body and an error body count against the same budget, since a run only ever has one of the
6147
+ * two. Mutates the array it is given, which is always one this module just built.
5865
6148
  */
5866
- const trimRetainedResponses = (runs) => {
6149
+ const trimRetainedBodies = (runs) => {
6150
+ const budget = responseHistory;
5867
6151
  let kept = 0;
5868
6152
  for (let index = runs.length - 1; index >= 0; index--) {
5869
6153
  const run = runs[index];
5870
- if (!run?.hasResponse)
6154
+ if (!run || (!run.hasResponse && !run.error?.hasBody))
6155
+ continue;
6156
+ if (++kept <= budget)
5871
6157
  continue;
5872
- if (++kept > RESPONSE_HISTORY)
5873
- runs[index] = { ...run, response: null, hasResponse: false };
6158
+ runs[index] = {
6159
+ ...run,
6160
+ response: null,
6161
+ hasResponse: false,
6162
+ error: run.error?.hasBody ? { ...run.error, body: null, hasBody: false } : run.error,
6163
+ };
5874
6164
  }
5875
6165
  return runs;
5876
6166
  };
@@ -5907,7 +6197,7 @@ const createQueryDevtoolsStats = () => {
5907
6197
  let runCounter = 0;
5908
6198
  const appendRun = (run, previous) => {
5909
6199
  const appended = { ...run, index: ++runCounter };
5910
- runs.set(trimRetainedResponses([...previous, appended].slice(-RUN_HISTORY)));
6200
+ runs.set(trimRetainedBodies([...previous, appended].slice(-RUN_HISTORY)));
5911
6201
  };
5912
6202
  /**
5913
6203
  * Ends the newest run still in flight. A response with no run of the query's own waiting for it came
@@ -5920,7 +6210,7 @@ const createQueryDevtoolsStats = () => {
5920
6210
  appendRun({ ...end, startedAt: end.endedAt, didRequest: false, url: lastUrl, sentBytes: 0, attempts: 0 }, current);
5921
6211
  return;
5922
6212
  }
5923
- runs.set(trimRetainedResponses(current.map((run, index) => (index === pending ? { ...run, ...end } : run))));
6213
+ runs.set(trimRetainedBodies(current.map((run, index) => (index === pending ? { ...run, ...end } : run))));
5924
6214
  };
5925
6215
  const recordExecution = ({ didRequest, body, url }) => {
5926
6216
  const now = Date.now();
@@ -5950,6 +6240,7 @@ const createQueryDevtoolsStats = () => {
5950
6240
  receivedBytes: 0,
5951
6241
  response: null,
5952
6242
  hasResponse: false,
6243
+ error: null,
5953
6244
  }, runs().map((run) => (run.status === 'pending' ? { ...run, status: 'aborted', endedAt: now } : run)));
5954
6245
  };
5955
6246
  const recordResponse = (payload) => {
@@ -5972,15 +6263,31 @@ const createQueryDevtoolsStats = () => {
5972
6263
  receivedBytes: received.bytes,
5973
6264
  response: payload.body,
5974
6265
  hasResponse: true,
6266
+ error: null,
5975
6267
  });
5976
6268
  };
5977
6269
  const recordError = (options) => {
6270
+ const error = options?.error;
5978
6271
  stats.update((current) => ({
5979
6272
  ...current,
5980
6273
  errors: current.errors + 1,
5981
6274
  lastResponseWasFaulted: options?.faulted ?? false,
5982
6275
  }));
5983
- endRun({ endedAt: Date.now(), status: 'error', receivedBytes: 0, response: null, hasResponse: false });
6276
+ endRun({
6277
+ endedAt: Date.now(),
6278
+ status: 'error',
6279
+ receivedBytes: 0,
6280
+ response: null,
6281
+ hasResponse: false,
6282
+ error: error
6283
+ ? {
6284
+ code: error.code,
6285
+ messages: queryErrorMessages(error),
6286
+ body: error.raw.error,
6287
+ hasBody: error.raw.error !== null && error.raw.error !== undefined,
6288
+ }
6289
+ : null,
6290
+ });
5984
6291
  };
5985
6292
  const recordRetry = ({ attempt }) => {
5986
6293
  const current = runs();
@@ -6042,6 +6349,106 @@ const sumQueryDevtoolsStats = (handles) => {
6042
6349
  return total;
6043
6350
  };
6044
6351
 
6352
+ /**
6353
+ * How many destroyed queries the registry keeps. A tombstone holds the last response body it captured,
6354
+ * so the list is capped for the same reason the repository caps its unused entries.
6355
+ */
6356
+ const MAX_QUERY_DEVTOOLS_TOMBSTONES = 50;
6357
+ const frozen = (value) => () => value;
6358
+ const noop = () => undefined;
6359
+ /**
6360
+ * Reads one signal off a handle that is being destroyed. A query whose injector is already gone can
6361
+ * throw here, and losing one field is better than losing the whole tombstone.
6362
+ */
6363
+ const read = (get, fallback) => {
6364
+ try {
6365
+ return get();
6366
+ }
6367
+ catch {
6368
+ return fallback;
6369
+ }
6370
+ };
6371
+ /**
6372
+ * A destroyed query's last state, shaped like the query itself: every signal the panel reads is
6373
+ * replaced by a function returning the value it held when the query went away, and every action is a
6374
+ * no-op. Rendering a tombstone therefore costs the panel no second code path - the drawer reads
6375
+ * `entry.handle` exactly as it does for a live query, and gates its actions on
6376
+ * {@link QueryDevtoolsEntry.destroyedAt} instead.
6377
+ *
6378
+ * The captured request is a plain copy, not the request itself: the real one is destroyed the moment
6379
+ * its cache entry is, and holding it would keep its subscriptions and its cache entry alive.
6380
+ */
6381
+ const snapshotQueryDevtoolsHandle = (handle) => {
6382
+ const query = handle;
6383
+ if (!query || typeof query !== 'object')
6384
+ return handle;
6385
+ const live = read(() => query.subtle?.request() ?? null, null);
6386
+ const request = live
6387
+ ? {
6388
+ method: live.method,
6389
+ url: live.url,
6390
+ args: live.args,
6391
+ execute: () => false,
6392
+ destroy: () => false,
6393
+ isStale: frozen(false),
6394
+ loading: frozen(null),
6395
+ error: read(() => frozen(live.error()), frozen(null)),
6396
+ response: read(() => frozen(live.response()), frozen(null)),
6397
+ currentEvent: read(() => frozen(live.currentEvent()), frozen(null)),
6398
+ expiresAt: read(() => frozen(live.expiresAt()), frozen(null)),
6399
+ subtle: {
6400
+ retryState: frozen(null),
6401
+ attempts: read(() => frozen(live.subtle.attempts()), frozen(1)),
6402
+ lastDurationMs: read(() => frozen(live.subtle.lastDurationMs()), frozen(null)),
6403
+ resolveHeaders: read(() => frozen(live.subtle.resolveHeaders()), frozen(undefined)),
6404
+ lastPersistedResponseAt: read(() => frozen(live.subtle.lastPersistedResponseAt()), frozen(null)),
6405
+ lastExternalResponseAt: read(() => frozen(live.subtle.lastExternalResponseAt()), frozen(null)),
6406
+ },
6407
+ }
6408
+ : null;
6409
+ return {
6410
+ id: read(() => frozen(query.id()), frozen(null)),
6411
+ args: read(() => frozen(query.args()), frozen(null)),
6412
+ response: read(() => frozen(query.response()), frozen(null)),
6413
+ error: read(() => frozen(query.error()), frozen(null)),
6414
+ latestHttpEvent: read(() => frozen(query.latestHttpEvent()), frozen(null)),
6415
+ lastTimeExecutedAt: read(() => frozen(query.lastTimeExecutedAt()), frozen(null)),
6416
+ triggeredBy: read(() => frozen(query.triggeredBy()), frozen(null)),
6417
+ executionState: read(() => frozen(query.executionState()), frozen(null)),
6418
+ // A tombstone never loads: a forced loading state captured mid-flight would spin forever.
6419
+ loading: frozen(null),
6420
+ execute: () => false,
6421
+ reset: noop,
6422
+ subtle: {
6423
+ request: frozen(request),
6424
+ destroy: noop,
6425
+ setResponse: noop,
6426
+ setLoading: noop,
6427
+ setError: noop,
6428
+ },
6429
+ };
6430
+ };
6431
+ /**
6432
+ * The entry a destroyed query leaves behind: the same id, kind and metadata, a frozen
6433
+ * {@link snapshotQueryDevtoolsHandle} in place of the live query, and no `element` - a DOM node whose
6434
+ * component is gone must not be kept alive by the panel.
6435
+ *
6436
+ * `stats` is carried over as-is. The recorder closes over its own signals rather than over the query,
6437
+ * so the run history that makes a failed request readable survives without retaining anything.
6438
+ */
6439
+ const tombstoneOf = (entry, destroyedAt) => {
6440
+ const { element: _element, ...meta } = entry.meta;
6441
+ return {
6442
+ id: entry.id,
6443
+ kind: entry.kind,
6444
+ meta,
6445
+ handle: snapshotQueryDevtoolsHandle(entry.handle),
6446
+ createdAt: entry.createdAt,
6447
+ destroyedAt,
6448
+ stats: entry.stats,
6449
+ };
6450
+ };
6451
+
6045
6452
  const entries = /* @__PURE__ */ signal([], /* @ts-ignore */
6046
6453
  ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
6047
6454
  /**
@@ -6161,9 +6568,39 @@ const registerEntry = (registration) => {
6161
6568
  formLinks: registration.formLinks,
6162
6569
  overrides: registration.overrides,
6163
6570
  };
6164
- entries.update((list) => [...list, fullEntry]);
6165
- return () => entries.update((list) => list.filter((e) => e.id !== fullEntry.id));
6571
+ // A registration that brings its own id can repeat one a tombstone still holds; the live entry wins.
6572
+ entries.update((list) => [...list.filter((e) => !(e.id === id && e.destroyedAt)), fullEntry]);
6573
+ return () => entries.update((list) => {
6574
+ const index = list.findIndex((e) => e.id === fullEntry.id);
6575
+ if (index === -1)
6576
+ return list;
6577
+ const next = list.slice();
6578
+ // Only queries leave a tombstone. A stack, sequence, form or auth provider is a container whose
6579
+ // interesting state is the queries it owns - each of which tombstones on its own.
6580
+ if (fullEntry.kind !== 'query') {
6581
+ next.splice(index, 1);
6582
+ return next;
6583
+ }
6584
+ next[index] = tombstoneOf(fullEntry, Date.now());
6585
+ return capTombstones(next);
6586
+ });
6166
6587
  };
6588
+ /** Drops the oldest tombstones once there are more than {@link MAX_QUERY_DEVTOOLS_TOMBSTONES} of them. */
6589
+ const capTombstones = (list) => {
6590
+ const excess = list.filter((e) => e.destroyedAt).length - MAX_QUERY_DEVTOOLS_TOMBSTONES;
6591
+ if (excess <= 0)
6592
+ return list;
6593
+ const doomed = new Set(list
6594
+ .filter((e) => e.destroyedAt)
6595
+ .sort((a, b) => (a.destroyedAt ?? 0) - (b.destroyedAt ?? 0))
6596
+ .slice(0, excess));
6597
+ return list.filter((e) => !doomed.has(e));
6598
+ };
6599
+ /**
6600
+ * Forgets every destroyed entry the registry is holding - the panel's "clear gone" action. Live
6601
+ * entries are untouched.
6602
+ */
6603
+ const clearQueryDevtoolsTombstones = () => entries.update((list) => list.filter((e) => !e.destroyedAt));
6167
6604
  /**
6168
6605
  * Enables the `@ethlete/query` devtools. Add this to your application providers (e.g. in
6169
6606
  * `bootstrapApplication`) to make query clients, queries, stacks, sequences and auth providers
@@ -6179,7 +6616,8 @@ const registerEntry = (registration) => {
6179
6616
  * });
6180
6617
  * ```
6181
6618
  */
6182
- const provideQueryDevtools = () => {
6619
+ const provideQueryDevtools = (options) => {
6620
+ setQueryDevtoolsResponseHistory(options?.responseHistory);
6183
6621
  setQueryDevtoolsRegistrar(registerEntry);
6184
6622
  setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
6185
6623
  setQueryDevtoolsFormLinksFactory(createQueryDevtoolsFormLinks);
@@ -6262,6 +6700,7 @@ const createGqlExecuteFn = (executeOptions) => {
6262
6700
  executeState,
6263
6701
  args: computedArgs,
6264
6702
  options,
6703
+ isRefreshable: executeOptions.creatorInternals.method === 'QUERY',
6265
6704
  });
6266
6705
  };
6267
6706
  exec['reset'] = reset;
@@ -11074,6 +11513,7 @@ const buildDefaults = (fields) => {
11074
11513
  }
11075
11514
  return out;
11076
11515
  };
11516
+ const MAX_RESET_PASSES = 10;
11077
11517
  const changedKeysBetween = (previous, current) => Object.keys(current).filter((key) => !equal(previous?.[key], current[key]));
11078
11518
  /** Best-effort URL string → value coercion, mirroring the auto-transform of the legacy QueryForm. */
11079
11519
  const autoCoerce = (raw, defaultValue) => {
@@ -11236,6 +11676,26 @@ const defineQueryForm = (config) => {
11236
11676
  }
11237
11677
  return next;
11238
11678
  };
11679
+ /**
11680
+ * Resets are transitive: a field this pass reset counts as changed in the next one, so
11681
+ * `country → league → team` clears `team` as well when only `country` moved. Passes repeat until
11682
+ * nothing moves, which has to happen before `committed` is written - one query execution for the
11683
+ * whole chain, not one per hop. The cap only guards a cyclic `isResetBy` graph; a field already at
11684
+ * its default stops triggering, so a well-formed graph settles in as many passes as it is deep.
11685
+ */
11686
+ const resolveResets = (prev, live) => {
11687
+ let next = live;
11688
+ for (let pass = 0; pass < MAX_RESET_PASSES; pass++) {
11689
+ const applied = applyResets(next, changedKeysBetween(prev, next));
11690
+ if (equal(applied, next))
11691
+ return applied;
11692
+ next = applied;
11693
+ }
11694
+ if (isDevMode()) {
11695
+ console.warn(`defineQueryForm: isResetBy did not settle within ${MAX_RESET_PASSES} passes. Check for a cycle.`);
11696
+ }
11697
+ return next;
11698
+ };
11239
11699
  const flush = () => {
11240
11700
  clearTimer();
11241
11701
  const live = model();
@@ -11244,8 +11704,7 @@ const defineQueryForm = (config) => {
11244
11704
  skipNextResets = false;
11245
11705
  return;
11246
11706
  }
11247
- const changedKeys = changedKeysBetween(prev, live);
11248
- const next = skipNextResets ? { ...live } : applyResets(live, changedKeys);
11707
+ const next = skipNextResets ? { ...live } : resolveResets(prev, live);
11249
11708
  skipNextResets = false;
11250
11709
  previous.set(clone(prev));
11251
11710
  committed.set(clone(next));
@@ -11589,12 +12048,15 @@ const createWebSocketClient = (options) => {
11589
12048
  const join = (name) => {
11590
12049
  emit({ event: 'join-room', data: name, room: name });
11591
12050
  const existingRoom = rooms.get(name);
11592
- if (existingRoom)
12051
+ if (existingRoom) {
12052
+ existingRoom.joinCount++;
11593
12053
  return existingRoom;
12054
+ }
11594
12055
  const message = signal(null, /* @ts-ignore */
11595
12056
  ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
11596
12057
  const newRoom = {
11597
12058
  latestMessage: message,
12059
+ joinCount: 1,
11598
12060
  };
11599
12061
  rooms.set(name, newRoom);
11600
12062
  syncDevtoolsRooms();
@@ -11628,10 +12090,15 @@ const createWebSocketClient = (options) => {
11628
12090
  return roomData.asReadonly();
11629
12091
  };
11630
12092
  const leaveRoom = (room) => {
11631
- if (!rooms.has(room)) {
12093
+ const joinedRoom = rooms.get(room);
12094
+ if (!joinedRoom) {
11632
12095
  if (isDevMode())
11633
12096
  throw roomNotJoined(room);
12097
+ return;
11634
12098
  }
12099
+ joinedRoom.joinCount--;
12100
+ if (joinedRoom.joinCount > 0)
12101
+ return;
11635
12102
  emit({ event: 'leave-room', data: room, room });
11636
12103
  rooms.delete(room);
11637
12104
  syncDevtoolsRooms();
@@ -11696,5 +12163,5 @@ const createWebSocketClient = (options) => {
11696
12163
  * Generated bundle index. Do not edit.
11697
12164
  */
11698
12165
 
11699
- 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_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsFaults, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, 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, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, 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, isQueryDevtoolsFaultInjectionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsOverridesFactory, setQueryDevtoolsRegistrar, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, 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, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
12166
+ export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsFaults, clearQueryDevtoolsTombstones, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, 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, 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, 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, isQueryDevtoolsFaultInjectionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryDevtoolsResponseHistory, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsOverridesFactory, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
11700
12167
  //# sourceMappingURL=ethlete-query.mjs.map