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

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,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, computed, DestroyRef, InjectionToken, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
2
+ import { inject, computed, DestroyRef, InjectionToken, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, VERSION, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
3
3
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
4
4
  import { UrlTree, Router, RedirectCommand, ActivatedRoute } from '@angular/router';
5
- import { filter, take, map, defer, from, Subject, Subscription, throwError, timer, switchMap, tap, retry, catchError, of, merge, firstValueFrom, fromEvent, throttleTime, interval, combineLatest, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, takeWhile, EMPTY, withLatestFrom, debounceTime } from 'rxjs';
6
- 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';
7
- import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
5
+ import { filter, take, map, defer, from, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, EMPTY, takeWhile, merge, firstValueFrom, concatMap, fromEvent, throttleTime, interval, combineLatest, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, withLatestFrom, debounceTime } from 'rxjs';
6
+ import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, deleteCookie, setCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
7
+ import { HttpClient, HttpEventType, HttpErrorResponse, HttpResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
8
8
  import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
9
9
  import { isPlatformBrowser, NgClass, AsyncPipe, JsonPipe } from '@angular/common';
10
10
  import { validateAsync, form } from '@angular/forms/signals';
@@ -232,6 +232,7 @@ const noteQueryFormRead = (id) => {
232
232
  formLinkSink?.add(id);
233
233
  };
234
234
  let faultResolver = null;
235
+ let mockResolver = null;
235
236
  /**
236
237
  * Installs the fault resolver. Called by `provideQueryDevtools()`; nothing else may call it.
237
238
  * @internal
@@ -240,11 +241,24 @@ const setQueryDevtoolsFaultResolver = (fn) => {
240
241
  faultResolver = fn;
241
242
  };
242
243
  /**
243
- * Whether fault injection can do anything at all. Checked before building the wrapper that resolves a
244
- * fault per attempt, so a request in an app without devtools keeps the pipeline it always had.
244
+ * Installs the mock resolver. Called by `provideQueryDevtools()`; nothing else may call it.
245
245
  * @internal
246
246
  */
247
- const isQueryDevtoolsFaultInjectionEnabled = () => faultResolver !== null;
247
+ const setQueryDevtoolsMockResolver = (fn) => {
248
+ mockResolver = fn;
249
+ };
250
+ /**
251
+ * Whether the devtools can do anything to a request at all - inject a fault, or serve a mock instead of
252
+ * sending it. Checked before building the wrapper that resolves both per attempt, so a request in an app
253
+ * without devtools keeps the pipeline it always had.
254
+ * @internal
255
+ */
256
+ const isQueryDevtoolsRequestInterceptionEnabled = () => faultResolver !== null || mockResolver !== null;
257
+ /**
258
+ * The mock to serve one upcoming attempt instead of sending it, or `null` to let it through.
259
+ * @internal
260
+ */
261
+ const resolveQueryDevtoolsMock = (target) => mockResolver?.(target) ?? null;
248
262
  /**
249
263
  * Resolves the fault to apply to one upcoming attempt, or `null` when nothing is armed. Consumes a
250
264
  * `failNext` budget, so call it exactly once per attempt.
@@ -1609,6 +1623,11 @@ const createQueryErrorResponse = (error) => {
1609
1623
  };
1610
1624
 
1611
1625
  const SPEED_BUFFER_TIME_IN_MS = 2000;
1626
+ /**
1627
+ * The `statusText` of a response the devtools served instead of sending the request. It is what a log, a
1628
+ * network-less error and a copied report all read, so it has to say where the response came from.
1629
+ */
1630
+ const MOCKED_STATUS_TEXT = 'Served by the query devtools';
1612
1631
  const createHttpRequest = (options) => {
1613
1632
  let currentStreamSubscription = Subscription.EMPTY;
1614
1633
  const { args, clientOptions, dependencies } = options;
@@ -1680,6 +1699,36 @@ const createHttpRequest = (options) => {
1680
1699
  * same tick it was started in, and settling synchronously inside `execute()` would let an injected
1681
1700
  * failure land before the caller has finished wiring the request up.
1682
1701
  */
1702
+ /**
1703
+ * The request, or the response a devtools mock has armed for this route instead of it. A mock bypasses
1704
+ * the interceptor chain by construction - nothing is sent - so a mocked secure route never exercises
1705
+ * the token flow, which the panel says on the row.
1706
+ */
1707
+ const sendOrMock = (headers) => {
1708
+ const mock = resolveQueryDevtoolsMock({
1709
+ clientName: options.clientName ?? '',
1710
+ method: options.method,
1711
+ url: options.fullPath,
1712
+ });
1713
+ if (!mock)
1714
+ return send(headers);
1715
+ const settle$ = mock.status >= 400
1716
+ ? throwError(() => new HttpErrorResponse({
1717
+ status: mock.status,
1718
+ statusText: MOCKED_STATUS_TEXT,
1719
+ url: options.fullPath,
1720
+ error: mock.body,
1721
+ }))
1722
+ : of(new HttpResponse({
1723
+ body: mock.body,
1724
+ status: mock.status,
1725
+ statusText: MOCKED_STATUS_TEXT,
1726
+ url: options.fullPath,
1727
+ }));
1728
+ // Through `timer` even at zero latency, for the same reason a faulted attempt is: settling
1729
+ // synchronously inside `execute()` would land the response before the caller has wired the request up.
1730
+ return timer(mock.latencyMs).pipe(switchMap(() => settle$));
1731
+ };
1683
1732
  const sendWithFaults = (headers) => defer(() => {
1684
1733
  const fault = resolveQueryDevtoolsFault({
1685
1734
  clientName: options.clientName ?? '',
@@ -1688,10 +1737,10 @@ const createHttpRequest = (options) => {
1688
1737
  });
1689
1738
  lastAttemptFaulted = !!fault && fault.status !== null;
1690
1739
  if (!fault)
1691
- return send(headers);
1740
+ return sendOrMock(headers);
1692
1741
  const { status } = fault;
1693
1742
  const attempt$ = status === null
1694
- ? send(headers)
1743
+ ? sendOrMock(headers)
1695
1744
  : throwError(() => new HttpErrorResponse({
1696
1745
  status,
1697
1746
  statusText: 'Injected by the query devtools',
@@ -1702,7 +1751,7 @@ const createHttpRequest = (options) => {
1702
1751
  });
1703
1752
  const createStream = () => {
1704
1753
  const headers = resolveHeaders();
1705
- const source$ = isQueryDevtoolsFaultInjectionEnabled() ? sendWithFaults(headers) : send(headers);
1754
+ const source$ = isQueryDevtoolsRequestInterceptionEnabled() ? sendWithFaults(headers) : send(headers);
1706
1755
  return source$.pipe(tap((event) => updateState(event)), retry({
1707
1756
  delay: (error, retryCount) => {
1708
1757
  const retryOptions = { error, retryCount };
@@ -3812,6 +3861,8 @@ const createSecureExecuteFactory = (options) => {
3812
3861
  let sessionRestartSubscription = Subscription.EMPTY;
3813
3862
  let hasExecuted = false;
3814
3863
  let lastExecuteArgs;
3864
+ /** The access token the last request went out with - what the 401 retry below compares against. */
3865
+ let lastRequestedWithToken = null;
3815
3866
  const reset = () => {
3816
3867
  authQuerySubscription.unsubscribe();
3817
3868
  authQuerySubscription = Subscription.EMPTY;
@@ -3874,15 +3925,14 @@ const createSecureExecuteFactory = (options) => {
3874
3925
  if (!accessToken) {
3875
3926
  throw tokensNotAvailableInsideAuthAndExec();
3876
3927
  }
3877
- const tokens = { accessToken, refreshToken: options.authProvider.refreshToken() ?? '' };
3878
- const headers = headerProvider();
3928
+ lastRequestedWithToken = accessToken;
3879
3929
  options.transformAuthAndExec({
3880
3930
  ...executeArgs,
3881
3931
  args: {
3882
3932
  ...(args ?? {}),
3883
- headers: headerProvider, // Pass header provider instead of static headers
3933
+ headers: headerProvider,
3884
3934
  },
3885
- }, tokens, headers, executeState);
3935
+ }, executeState);
3886
3936
  };
3887
3937
  // The auth query completing and the access token being populated happen on two
3888
3938
  // different reactive timelines: the token is set by a separate token-extraction
@@ -3919,10 +3969,14 @@ const createSecureExecuteFactory = (options) => {
3919
3969
  tokenWaitSubscription = Subscription.EMPTY;
3920
3970
  sessionRestartSubscription.unsubscribe();
3921
3971
  sessionRestartSubscription = Subscription.EMPTY;
3972
+ // Retrying with the token that just produced the 401 would 401 again, and that 401 asks for
3973
+ // another refresh - an endless refresh/retry loop for as long as the server keeps handing out
3974
+ // tokens it rejects. A refresh that did not change the access token is therefore not a reason to
3975
+ // retry; the subscription stays open, so a later one that does change it still is.
3922
3976
  tokenRefreshSubscription = options.authProvider.afterTokenRefresh$
3923
3977
  .pipe(filter(() => {
3924
3978
  const currentError = options.state.error();
3925
- return currentError?.code === 401;
3979
+ return currentError?.code === 401 && options.authProvider.accessToken() !== lastRequestedWithToken;
3926
3980
  }), take(1), tap(() => exec(execArgsWithDefaults)))
3927
3981
  .subscribe();
3928
3982
  // `setTokens()` seeds an auth session from outside the query registry (an SSO/OIDC callback, a
@@ -3943,19 +3997,23 @@ const createSecureExecuteFactory = (options) => {
3943
3997
  injector: options.deps.injector,
3944
3998
  }).pipe(switchMap((latestQuery) => {
3945
3999
  if (!latestQuery)
3946
- return of(null);
4000
+ return EMPTY;
3947
4001
  const query = latestQuery.snapshot;
3948
- return toObservable(query.isAlive, { injector: options.deps.injector }).pipe(tap((isAlive) => {
3949
- if (isAlive)
3950
- return;
3951
- if (query.error()) {
3952
- error(query);
3953
- }
3954
- else if (query.response()) {
3955
- authAndExecWhenTokenReady(execArgsWithDefaults);
3956
- }
3957
- }));
3958
- }));
4002
+ return toObservable(query.isAlive, { injector: options.deps.injector }).pipe(filter((isAlive) => !isAlive), filter(() => !!query.error() || !!query.response()), map(() => query));
4003
+ }), tap((query) => {
4004
+ if (query.error()) {
4005
+ error(query);
4006
+ }
4007
+ else {
4008
+ authAndExecWhenTokenReady(execArgsWithDefaults);
4009
+ }
4010
+ }),
4011
+ // Stop at the first auth query that hands a session over. Every later one is a token refresh,
4012
+ // which settles before its tokens are applied - re-running here would send the token that just
4013
+ // expired; `afterTokenRefresh$` above is what re-runs a query after a refresh. A *failed* auth
4014
+ // query keeps the wait open, so a query that mounted during a failed auto-login still runs
4015
+ // once the user logs in for real.
4016
+ takeWhile((query) => !!query.error()));
3959
4017
  // A `setTokens()` call that lands while this is already waiting (e.g. the SSO redirect
3960
4018
  // resolves after this secure query mounted) never touches `latestExecutedQuery` either -
3961
4019
  // wake up on `executionState` becoming a token seed too.
@@ -3986,13 +4044,12 @@ const createSecureExecuteFn = (executeOptions) => {
3986
4044
  deps: executeOptions.deps,
3987
4045
  state: executeOptions.state,
3988
4046
  autoExecutes: shouldAutoExecuteQuery(executeOptions.creatorInternals.method) && !executeOptions.queryConfig.onlyManualExecution,
3989
- transformAuthAndExec: (executeArgs, _tokens, headers, executeState) => {
4047
+ transformAuthAndExec: (executeArgs, executeState) => {
3990
4048
  const { options: runOptions } = executeArgs ?? {};
3991
- const updatedArgs = { ...(executeArgs?.args ?? {}), headers };
3992
4049
  queryExecute({
3993
4050
  executeOptions: executeOptions,
3994
4051
  executeState,
3995
- args: updatedArgs,
4052
+ args: executeArgs?.args ?? {},
3996
4053
  options: runOptions,
3997
4054
  isSecure: true,
3998
4055
  });
@@ -4625,7 +4682,11 @@ const createBearerAuthProviderImpl = (config) => {
4625
4682
  refreshToken.set(refresh);
4626
4683
  sessionStatus.set('authenticated');
4627
4684
  sessionEndCause.set(null);
4628
- afterTokenRefresh$.next();
4685
+ // Emit on the next microtask so that synchronous reactive work triggered by applying
4686
+ // the tokens (signals/effects) has a chance to settle before subscribers react.
4687
+ // This avoids rare races where a subscriber reacting to the emission would run
4688
+ // before other effects have fully applied the new token state.
4689
+ queueMicrotask(() => afterTokenRefresh$.next());
4629
4690
  };
4630
4691
  // Public entry point only - a query-driven login already reports its own, more specific
4631
4692
  // executionState (e.g. `{ type: 'login', state: 'success' }`) via setupBearerQueryRegistry's
@@ -4664,6 +4725,7 @@ const createBearerAuthProviderImpl = (config) => {
4664
4725
  name: config.name,
4665
4726
  applyTokens,
4666
4727
  logout,
4728
+ sessionEndCause: sessionEndCause.asReadonly(),
4667
4729
  });
4668
4730
  const querySetupContext = {
4669
4731
  accessToken,
@@ -4739,6 +4801,13 @@ const createBearerAuthProviderImpl = (config) => {
4739
4801
  };
4740
4802
  const createBearerAuthProvider = (config) => defineRootProvider(() => createBearerAuthProviderImpl(config), { name: `BearerAuthProvider_${config.name}` });
4741
4803
 
4804
+ /** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
4805
+ const rescheduleDelayMs = 5000;
4806
+ /**
4807
+ * How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
4808
+ * declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
4809
+ */
4810
+ const maxRescheduleAttempts = 5;
4742
4811
  const withAuthenticationQuery = (key, config, setup) => ({
4743
4812
  _type: 'authQuery',
4744
4813
  key,
@@ -4786,25 +4855,44 @@ const withRefreshQuery = (key, config) => {
4786
4855
  const executeRefresh = (reason) => {
4787
4856
  const currentRefreshToken = context.refreshToken();
4788
4857
  if (!currentRefreshToken)
4789
- return;
4858
+ return 'noToken';
4790
4859
  if (!context.isLeader()) {
4791
4860
  // Only the leader may spend a single-use refresh token, but the event is still real - hand it
4792
4861
  // over instead of dropping it, or this tab waits out the leader's timer with a dead token.
4793
4862
  context.refreshCoordination?.request();
4794
- return;
4863
+ return 'delegated';
4795
4864
  }
4796
4865
  // Any token-issuing execution, not just another refresh: a login already in flight is about to
4797
4866
  // issue a token pair of its own, and the refresh token this would spend belongs to the session
4798
4867
  // that login is replacing.
4799
4868
  if (context.hasTokenIssuingExecutionInFlight())
4800
- return;
4869
+ return 'busy';
4801
4870
  if (reason === 'scheduled') {
4802
4871
  const now = Date.now();
4803
4872
  if (now - lastRefreshTime < minRefreshInterval)
4804
- return;
4873
+ return 'throttled';
4805
4874
  lastRefreshTime = now;
4806
4875
  }
4807
4876
  refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4877
+ return 'executed';
4878
+ };
4879
+ /**
4880
+ * How long a scheduled refresh that did not spend the refresh token waits before trying again.
4881
+ * `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
4882
+ * the schedule anyway.
4883
+ */
4884
+ const retryScheduledIn = (attempt) => {
4885
+ switch (attempt) {
4886
+ case 'executed':
4887
+ case 'noToken':
4888
+ return null;
4889
+ case 'throttled':
4890
+ return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
4891
+ case 'busy':
4892
+ return rescheduleDelayMs;
4893
+ case 'delegated':
4894
+ return minRefreshInterval;
4895
+ }
4808
4896
  };
4809
4897
  const calculateRefreshBuffer = (tokenLifetimeMs) => {
4810
4898
  if (typeof config.refreshStrategy === 'number') {
@@ -4816,11 +4904,8 @@ const withRefreshQuery = (key, config) => {
4816
4904
  const calculatedBuffer = tokenLifetimeMs * (1 - percentage);
4817
4905
  return Math.max(minBufferMs, Math.min(maxBufferMs, calculatedBuffer));
4818
4906
  };
4819
- // Auto-refresh based on token expiration
4820
- toObservable(context.accessToken)
4821
- .pipe(switchMap((token) => {
4822
- if (!token)
4823
- return of(null);
4907
+ /** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
4908
+ const scheduledRefreshDelay = (token) => {
4824
4909
  try {
4825
4910
  const bearerDataValue = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
4826
4911
  const expiresIn = bearerDataValue?.[expiresInPropertyName];
@@ -4828,36 +4913,40 @@ const withRefreshQuery = (key, config) => {
4828
4913
  if (isDevMode()) {
4829
4914
  console.warn(`Token does not contain valid ${expiresInPropertyName} property for auto-refresh`);
4830
4915
  }
4831
- return of(null);
4916
+ return null;
4832
4917
  }
4833
- const expiresAtMs = expiresIn * 1000;
4834
- const now = Date.now();
4835
- const tokenLifetimeMs = expiresAtMs - now;
4918
+ const tokenLifetimeMs = expiresIn * 1000 - Date.now();
4836
4919
  if (tokenLifetimeMs <= 0) {
4837
- if (refreshIfExpired) {
4838
- if (isDevMode()) {
4839
- console.warn('Token is already expired, triggering immediate refresh');
4840
- }
4841
- return of(true);
4920
+ if (!refreshIfExpired)
4921
+ return null;
4922
+ if (isDevMode()) {
4923
+ console.warn('Token is already expired, triggering immediate refresh');
4842
4924
  }
4843
- return of(null);
4925
+ return 0;
4844
4926
  }
4845
- const refreshBufferMs = calculateRefreshBuffer(tokenLifetimeMs);
4846
- const timeUntilRefresh = tokenLifetimeMs - refreshBufferMs;
4847
- if (timeUntilRefresh <= 0) {
4848
- return of(true);
4849
- }
4850
- return timer(timeUntilRefresh).pipe(tap(() => true));
4927
+ return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
4851
4928
  }
4852
4929
  catch {
4853
- return of(null);
4930
+ return null;
4854
4931
  }
4932
+ };
4933
+ /**
4934
+ * The scheduled refresh, re-armed for as long as it keeps declining to spend the refresh token.
4935
+ * Without the re-arm a single declined attempt strands the session on a token nothing renews until
4936
+ * a request fails with a 401 - by which time the refresh token may be gone too.
4937
+ */
4938
+ const scheduledRefresh = (dueInMs, attemptsLeft) => (dueInMs <= 0 ? of(0) : timer(dueInMs)).pipe(concatMap(() => {
4939
+ const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
4940
+ return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
4941
+ }));
4942
+ toObservable(context.accessToken)
4943
+ .pipe(switchMap((token) => {
4944
+ if (!token)
4945
+ return EMPTY;
4946
+ const dueInMs = scheduledRefreshDelay(token);
4947
+ return dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts);
4855
4948
  }), takeUntilDestroyed())
4856
- .subscribe((shouldRefresh) => {
4857
- if (shouldRefresh) {
4858
- executeRefresh('scheduled');
4859
- }
4860
- });
4949
+ .subscribe();
4861
4950
  context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
4862
4951
  const onRefreshFailure = config.onRefreshFailure ??
4863
4952
  (({ error, logout }) => {
@@ -5172,6 +5261,12 @@ const resetEncryptionKey = () => {
5172
5261
  }
5173
5262
  };
5174
5263
 
5264
+ /**
5265
+ * How an incoming logout reads in the receiving tab: a deliberate logout elsewhere is this tab's
5266
+ * `otherTab`, while a session that ended on its own ended for every tab, so that cause is carried
5267
+ * through. A message without one comes from a tab running an older version.
5268
+ */
5269
+ const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
5175
5270
  /**
5176
5271
  * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5177
5272
  * providers reachable from the same origin do not push each other's tokens around.
@@ -5206,7 +5301,7 @@ const setupMultiTabSync = (config, context) => {
5206
5301
  if (message.type === 'logout' && syncLogout) {
5207
5302
  lastSyncedState = LOGGED_OUT;
5208
5303
  hadTokens = false;
5209
- context.logout('otherTab');
5304
+ context.logout(incomingCause(message.cause));
5210
5305
  return;
5211
5306
  }
5212
5307
  if (message.type !== 'tokens-updated' || !syncTokens)
@@ -5254,7 +5349,7 @@ const setupMultiTabSync = (config, context) => {
5254
5349
  if (lastSyncedState === LOGGED_OUT)
5255
5350
  return;
5256
5351
  lastSyncedState = LOGGED_OUT;
5257
- const message = { type: 'logout' };
5352
+ const message = { type: 'logout', cause: untracked(context.sessionEndCause) ?? undefined };
5258
5353
  channel.postMessage(message);
5259
5354
  });
5260
5355
  }
@@ -5310,6 +5405,7 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5310
5405
  setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5311
5406
  accessToken: context.accessToken,
5312
5407
  refreshToken: context.refreshToken,
5408
+ sessionEndCause: context.sessionEndCause,
5313
5409
  name: context.name,
5314
5410
  applyTokens: context.applyTokens,
5315
5411
  logout: context.logout,
@@ -5346,6 +5442,7 @@ const withPersistentAuth = (config) => {
5346
5442
  ...(config.autoLogin.excludeRoutes?.length
5347
5443
  ? [{ label: 'excluded routes', value: config.autoLogin.excludeRoutes.join(', ') }]
5348
5444
  : []),
5445
+ ...(config.autoLogin.shouldAutoLogin ? [{ label: 'auto login predicate', value: 'custom' }] : []),
5349
5446
  ],
5350
5447
  };
5351
5448
  };
@@ -5406,6 +5503,9 @@ const createPersistentAuthFeature = (config, context) => {
5406
5503
  if (shouldExclude) {
5407
5504
  return;
5408
5505
  }
5506
+ if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
5507
+ return;
5508
+ }
5409
5509
  const storedToken = getCookie(cookieName);
5410
5510
  if (storedToken) {
5411
5511
  const decryptedToken = decryptToken(storedToken);
@@ -5677,6 +5777,40 @@ const createTrackingFeature = (context, config) => {
5677
5777
  };
5678
5778
  };
5679
5779
 
5780
+ const ethlete = {};
5781
+ let app = null;
5782
+ /** What `window.ethlete` exposes - the same object {@link queryDevtoolsAbout} returns. */
5783
+ const publish = () => {
5784
+ if (typeof window === 'undefined')
5785
+ return;
5786
+ window['ethlete'] = queryDevtoolsAbout();
5787
+ };
5788
+ /**
5789
+ * Records that an `@ethlete/*` package is loaded, and at which version. Each lib passes its own
5790
+ * generated constant; a package that is never imported never reports one, which is what makes the
5791
+ * list "what is actually loaded" rather than "what is declared as a peer dependency".
5792
+ */
5793
+ const registerEthleteVersion = (name, version) => {
5794
+ ethlete[name] = version;
5795
+ publish();
5796
+ };
5797
+ const setQueryDevtoolsAppInfo = (info) => {
5798
+ app = info ?? null;
5799
+ publish();
5800
+ };
5801
+ /**
5802
+ * Everything the devtools know about what is running: the loaded `@ethlete/*` versions, the Angular
5803
+ * version, and whatever the application handed to `provideQueryDevtools({ about })`.
5804
+ *
5805
+ * Also mirrored onto `window.ethlete` - the same idea as Angular's `window.ng`, so the versions behind
5806
+ * a bug report can be read straight from the console without opening the panel.
5807
+ */
5808
+ const queryDevtoolsAbout = () => ({
5809
+ ethlete: { ...ethlete },
5810
+ angular: VERSION.full,
5811
+ app,
5812
+ });
5813
+
5680
5814
  /** The neutral fault: nothing armed. */
5681
5815
  const EMPTY_QUERY_DEVTOOLS_FAULT = {
5682
5816
  latencyMs: 0,
@@ -5790,6 +5924,482 @@ const createQueryDevtoolsFormLinks = () => {
5790
5924
  };
5791
5925
  };
5792
5926
 
5927
+ const DEFAULTS = {
5928
+ viewState: 'session',
5929
+ pins: 'local',
5930
+ overrides: 'none',
5931
+ mocks: 'local',
5932
+ maxEvents: 100,
5933
+ maxDroppedCacheEntries: 20,
5934
+ responseHistory: null,
5935
+ };
5936
+ /**
5937
+ * `localStorage`, whatever the scopes below say - a scope of `none` still has to be remembered, or the
5938
+ * choice would erase itself on the first reload it applies to.
5939
+ */
5940
+ const SETTINGS_KEY = 'ethlete:query:devtools:settings:v1';
5941
+ const LIMITS = {
5942
+ maxEvents: { min: 10, max: 1000 },
5943
+ maxDroppedCacheEntries: { min: 0, max: 200 },
5944
+ responseHistory: { min: 1, max: 50 },
5945
+ };
5946
+ const settings = /* @__PURE__ */ signal(DEFAULTS, /* @ts-ignore */
5947
+ ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
5948
+ /**
5949
+ * How the devtools are configured. Every consumer reads the live value rather than a copy, so a change
5950
+ * in the panel's Settings takes effect without a reload.
5951
+ *
5952
+ * Part of the devtools contract. **Not part of the general public contract.**
5953
+ */
5954
+ const queryDevtoolsSettings = /* @__PURE__ */ settings.asReadonly();
5955
+ /**
5956
+ * The `Storage` a scope names, or `null` for `none` and for a browser that refuses one (private mode,
5957
+ * storage disabled, server-side rendering). Callers treat "no storage" and "not persisted" alike.
5958
+ *
5959
+ * Part of the devtools contract. **Not part of the general public contract.**
5960
+ */
5961
+ const queryDevtoolsStorage = (scope) => {
5962
+ if (scope === 'none' || typeof window === 'undefined')
5963
+ return null;
5964
+ try {
5965
+ return scope === 'session' ? window.sessionStorage : window.localStorage;
5966
+ }
5967
+ catch {
5968
+ return null;
5969
+ }
5970
+ };
5971
+ /**
5972
+ * Reads one JSON-encoded devtools key from the store a scope names, or `null`.
5973
+ *
5974
+ * Part of the devtools contract. **Not part of the general public contract.**
5975
+ */
5976
+ const readQueryDevtoolsStore = (scope, key) => {
5977
+ try {
5978
+ const raw = queryDevtoolsStorage(scope)?.getItem(key);
5979
+ return raw ? JSON.parse(raw) : null;
5980
+ }
5981
+ catch {
5982
+ return null;
5983
+ }
5984
+ };
5985
+ /**
5986
+ * Writes one devtools key into the store a scope names. A scope of `none` writes nothing.
5987
+ *
5988
+ * Part of the devtools contract. **Not part of the general public contract.**
5989
+ */
5990
+ const writeQueryDevtoolsStore = (scope, key, value) => {
5991
+ try {
5992
+ queryDevtoolsStorage(scope)?.setItem(key, JSON.stringify(value));
5993
+ }
5994
+ catch {
5995
+ // ignore (private mode / disabled storage / quota)
5996
+ }
5997
+ };
5998
+ /**
5999
+ * Removes one devtools key from **both** stores, whatever the scope currently is - so a scope that
6000
+ * changed cannot leave a stale copy behind for the next load to read.
6001
+ *
6002
+ * Part of the devtools contract. **Not part of the general public contract.**
6003
+ */
6004
+ const clearQueryDevtoolsStore = (key) => {
6005
+ for (const scope of ['session', 'local']) {
6006
+ try {
6007
+ queryDevtoolsStorage(scope)?.removeItem(key);
6008
+ }
6009
+ catch {
6010
+ // ignore (private mode / disabled storage)
6011
+ }
6012
+ }
6013
+ };
6014
+ const asScope = (value, fallback) => value === 'none' || value === 'session' || value === 'local' ? value : fallback;
6015
+ const asCount = (value, limits, fallback) => {
6016
+ const count = Math.floor(Number(value));
6017
+ if (!Number.isFinite(count))
6018
+ return fallback;
6019
+ return Math.min(Math.max(count, limits.min), limits.max);
6020
+ };
6021
+ /** A count that may be absent, where absent means "leave the application's own value alone". */
6022
+ const asOptionalCount = (value, limits) => {
6023
+ if (value === null || value === undefined)
6024
+ return null;
6025
+ const count = Math.floor(Number(value));
6026
+ return Number.isFinite(count) ? Math.min(Math.max(count, limits.min), limits.max) : null;
6027
+ };
6028
+ /** Both a hand-edited store and a number typed into the panel come through here. */
6029
+ const sanitize = (value) => ({
6030
+ viewState: asScope(value?.viewState, DEFAULTS.viewState),
6031
+ pins: asScope(value?.pins, DEFAULTS.pins),
6032
+ overrides: asScope(value?.overrides, DEFAULTS.overrides),
6033
+ mocks: asScope(value?.mocks, DEFAULTS.mocks),
6034
+ maxEvents: asCount(value?.maxEvents, LIMITS.maxEvents, DEFAULTS.maxEvents),
6035
+ maxDroppedCacheEntries: asCount(value?.maxDroppedCacheEntries, LIMITS.maxDroppedCacheEntries, DEFAULTS.maxDroppedCacheEntries),
6036
+ responseHistory: asOptionalCount(value?.responseHistory, LIMITS.responseHistory),
6037
+ });
6038
+ /**
6039
+ * Reads the stored settings. Called by `provideQueryDevtools()` **before** the override store is read
6040
+ * and the response-history default is set - both take their configuration from here - and by nothing
6041
+ * else.
6042
+ * @internal
6043
+ */
6044
+ const initQueryDevtoolsSettings = () => {
6045
+ settings.set(sanitize(readQueryDevtoolsStore('local', SETTINGS_KEY)));
6046
+ };
6047
+ /**
6048
+ * Changes part of the devtools configuration and stores it. Values outside the range a field allows are
6049
+ * clamped rather than rejected.
6050
+ *
6051
+ * Overrides are the one field not to set here: their scope moves an existing store, so it goes through
6052
+ * `setQueryDevtoolsOverridesScope()` instead.
6053
+ *
6054
+ * Part of the devtools contract. **Not part of the general public contract.**
6055
+ */
6056
+ const setQueryDevtoolsSettings = (patch) => {
6057
+ const next = sanitize({ ...settings(), ...patch });
6058
+ settings.set(next);
6059
+ writeQueryDevtoolsStore('local', SETTINGS_KEY, next);
6060
+ };
6061
+
6062
+ /**
6063
+ * The designed library. Whether a mock is **armed** is deliberately not part of it - see
6064
+ * {@link queryDevtoolsArmedMocks}.
6065
+ */
6066
+ const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
6067
+ const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
6068
+ ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
6069
+ const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
6070
+ ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
6071
+ const scope$1 = () => queryDevtoolsSettings().mocks;
6072
+ /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
6073
+ const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
6074
+ /**
6075
+ * The ids currently being served. **Never persisted, at any scope**: an app that silently serves designed
6076
+ * data tomorrow morning is worse than losing which mocks were armed, so arming is per page load.
6077
+ */
6078
+ const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
6079
+ /**
6080
+ * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
6081
+ * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
6082
+ */
6083
+ const queryDevtoolsMockId = (parts) => `${parts.clientName}|${parts.method}|${parts.pattern}${parts.query ? `?${parts.query}` : ''}`;
6084
+ const write = () => {
6085
+ if (scope$1() === 'none') {
6086
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
6087
+ return;
6088
+ }
6089
+ writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
6090
+ };
6091
+ const isMock = (value) => {
6092
+ const mock = value;
6093
+ return (!!mock &&
6094
+ typeof mock.id === 'string' &&
6095
+ typeof mock.clientName === 'string' &&
6096
+ typeof mock.method === 'string' &&
6097
+ typeof mock.pattern === 'string' &&
6098
+ typeof mock.status === 'number');
6099
+ };
6100
+ /**
6101
+ * Reads the designed library, so a mock can be armed without authoring it again. Called by
6102
+ * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
6103
+ * @internal
6104
+ */
6105
+ const initQueryDevtoolsMocks = () => {
6106
+ const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
6107
+ // `query` was added after the first version of the store, so an entry written before it still loads.
6108
+ mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
6109
+ armed.set(new Set());
6110
+ };
6111
+ /** The path of a request URL, without its origin or query string - what a pattern is matched against. */
6112
+ const queryDevtoolsRequestPath = (url) => {
6113
+ const withoutQuery = url.split('?')[0] ?? '';
6114
+ const schemeEnd = withoutQuery.indexOf('://');
6115
+ if (schemeEnd === -1)
6116
+ return withoutQuery;
6117
+ const pathStart = withoutQuery.indexOf('/', schemeEnd + 3);
6118
+ return pathStart === -1 ? '/' : withoutQuery.slice(pathStart);
6119
+ };
6120
+ const segmentsOf$1 = (path) => path.split('/').filter(Boolean);
6121
+ /** The query string of a request URL, without the `?`. */
6122
+ const queryOf = (url) => url.split('?')[1] ?? '';
6123
+ /**
6124
+ * Whether a request carries every query parameter a mock declares. Extra parameters are ignored - a mock
6125
+ * says what the request must ask for, not everything it may.
6126
+ */
6127
+ const matchesQueryDevtoolsMockQuery = (declared, url) => {
6128
+ if (!declared)
6129
+ return true;
6130
+ const asked = new URLSearchParams(queryOf(url));
6131
+ return [...new URLSearchParams(declared)].every(([key, value]) => asked.get(key) === value);
6132
+ };
6133
+ /**
6134
+ * Whether a request path is the route a pattern describes. A `:param` segment matches any single
6135
+ * non-empty segment, which is the whole point: a mock is armed for a route, not for the one URL a query
6136
+ * happened to build.
6137
+ */
6138
+ const matchesQueryDevtoolsMockPattern = (pattern, path) => {
6139
+ const patternSegments = segmentsOf$1(pattern);
6140
+ const pathSegments = segmentsOf$1(path);
6141
+ if (patternSegments.length !== pathSegments.length)
6142
+ return false;
6143
+ return patternSegments.every((segment, index) => segment.startsWith(':') || segment === pathSegments[index]);
6144
+ };
6145
+ /**
6146
+ * Adds a designed mock, or replaces the one already under its id. Arming is untouched: re-designing a
6147
+ * body while it is being served changes what the next request gets.
6148
+ */
6149
+ const saveQueryDevtoolsMock = (mock) => {
6150
+ mocks.update((current) => {
6151
+ const index = current.findIndex((entry) => entry.id === mock.id);
6152
+ if (index === -1)
6153
+ return [...current, mock];
6154
+ return current.map((entry) => (entry.id === mock.id ? mock : entry));
6155
+ });
6156
+ write();
6157
+ };
6158
+ /** Removes a designed mock, disarming it on the way out. */
6159
+ const deleteQueryDevtoolsMock = (id) => {
6160
+ mocks.update((current) => current.filter((entry) => entry.id !== id));
6161
+ armQueryDevtoolsMock(id, false);
6162
+ write();
6163
+ };
6164
+ /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
6165
+ const armQueryDevtoolsMock = (id, isArmed) => {
6166
+ armed.update((current) => {
6167
+ if (isArmed && !mocks().some((mock) => mock.id === id))
6168
+ return current;
6169
+ const next = new Set(current);
6170
+ if (isArmed)
6171
+ next.add(id);
6172
+ else
6173
+ next.delete(id);
6174
+ return next;
6175
+ });
6176
+ };
6177
+ /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
6178
+ const clearQueryDevtoolsArmedMocks = () => armed.set(new Set());
6179
+ /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
6180
+ const clearQueryDevtoolsMockStore = () => {
6181
+ mocks.set([]);
6182
+ armed.set(new Set());
6183
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
6184
+ };
6185
+ const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
6186
+ /**
6187
+ * The mock to serve one upcoming attempt, or `null` to let it reach the network. The armed mock declaring
6188
+ * the most query parameters wins, so a special case armed on top of a general one is what answers.
6189
+ *
6190
+ * Installed as the mock resolver by `provideQueryDevtools()`; nothing else may call it.
6191
+ * @internal
6192
+ */
6193
+ const resolveQueryDevtoolsMockForAttempt = (target) => {
6194
+ const ids = armed();
6195
+ if (!ids.size)
6196
+ return null;
6197
+ const path = queryDevtoolsRequestPath(target.url);
6198
+ const mock = mocks()
6199
+ .filter((candidate) => ids.has(candidate.id) &&
6200
+ candidate.clientName === target.clientName &&
6201
+ candidate.method === target.method &&
6202
+ matchesQueryDevtoolsMockPattern(candidate.pattern, path) &&
6203
+ matchesQueryDevtoolsMockQuery(candidate.query, target.url))
6204
+ // The most specific armed mock answers: one that names `page=2` beats one that takes any query, so
6205
+ // arming a general mock does not shadow the special case someone armed on top of it.
6206
+ .sort((a, b) => declaredParamCount(b) - declaredParamCount(a))[0];
6207
+ if (!mock)
6208
+ return null;
6209
+ return { status: mock.status, body: mock.body, latencyMs: mock.latencyMs };
6210
+ };
6211
+
6212
+ /**
6213
+ * Not persisted at all unless asked for, and then in `sessionStorage`: "survives a reload" and "survives
6214
+ * until I notice" are different promises, and an app that stays tampered with across days is hours of
6215
+ * debugging the wrong thing. `local` is offered in Settings for the dev who has a reason, and the
6216
+ * restored-overrides bar names the scope whatever it is.
6217
+ */
6218
+ const STORAGE_KEY = 'ethlete:query:devtools:overrides:v1';
6219
+ const restored = /* @__PURE__ */ signal([], /* @ts-ignore */
6220
+ ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
6221
+ const restoredScope = /* @__PURE__ */ signal(null, /* @ts-ignore */
6222
+ ...(ngDevMode ? [{ debugName: "restoredScope" }] : /* istanbul ignore next */ []));
6223
+ const scope = () => queryDevtoolsSettings().overrides;
6224
+ /** Where "Keep across reloads" puts them back when it is switched on again. */
6225
+ let lastPersistentScope = 'session';
6226
+ /** Every recorder that currently holds at least one op, so switching persistence on can capture them. */
6227
+ const armedRecorders = /* @__PURE__ */ new Map();
6228
+ /** What the store held when this page loaded, per id - the banner's subject, not the live registry. */
6229
+ const carriedOver = /* @__PURE__ */ new Map();
6230
+ /** Of those, the ids a registering query actually took. */
6231
+ const replayed = /* @__PURE__ */ new Set();
6232
+ let ops = {};
6233
+ const readStore = () => {
6234
+ const parsed = readQueryDevtoolsStore(scope(), STORAGE_KEY);
6235
+ const stored = parsed?.ops;
6236
+ const usable = !!stored && typeof stored === 'object' && !Array.isArray(stored);
6237
+ return { enabled: parsed?.enabled === true, ops: usable ? stored : {} };
6238
+ };
6239
+ const writeStore = () => {
6240
+ if (scope() === 'none') {
6241
+ clearQueryDevtoolsStore(STORAGE_KEY);
6242
+ return;
6243
+ }
6244
+ writeQueryDevtoolsStore(scope(), STORAGE_KEY, { enabled: true, ops });
6245
+ };
6246
+ const publishRestored = () => restored.set([...carriedOver].map(([id, count]) => ({ id, count, armed: replayed.has(id) })));
6247
+ const forgetCarriedOver = () => {
6248
+ carriedOver.clear();
6249
+ replayed.clear();
6250
+ restoredScope.set(null);
6251
+ publishRestored();
6252
+ };
6253
+ const writeOps = (id, list) => {
6254
+ if (list.length)
6255
+ ops = { ...ops, [id]: list };
6256
+ else {
6257
+ const { [id]: _dropped, ...rest } = ops;
6258
+ ops = rest;
6259
+ }
6260
+ writeStore();
6261
+ };
6262
+ /**
6263
+ * Reads the store, so entries can replay from it as they register. Called by `provideQueryDevtools()`,
6264
+ * which is what keeps an app without devtools from ever touching storage.
6265
+ * @internal
6266
+ */
6267
+ const initQueryDevtoolsOverridePersistence = () => {
6268
+ const store = readStore();
6269
+ ops = store.enabled ? store.ops : {};
6270
+ armedRecorders.clear();
6271
+ carriedOver.clear();
6272
+ replayed.clear();
6273
+ for (const [id, list] of Object.entries(ops)) {
6274
+ if (list.length)
6275
+ carriedOver.set(id, list.length);
6276
+ }
6277
+ const current = scope();
6278
+ if (current !== 'none')
6279
+ lastPersistentScope = current;
6280
+ restoredScope.set(carriedOver.size ? current : null);
6281
+ publishRestored();
6282
+ };
6283
+ /**
6284
+ * Whether armed response overrides are stored and replayed on the next page load - that is, whether
6285
+ * their scope is anything other than `none`. Off by default.
6286
+ */
6287
+ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope() !== 'none', /* @ts-ignore */
6288
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsOverridePersistence" }] : /* istanbul ignore next */ []));
6289
+ /**
6290
+ * Where armed response overrides are kept. Switching to a scope that keeps them captures whatever is
6291
+ * armed right now, so the choice reads as "keep these" rather than "keep the next ones"; switching
6292
+ * between `session` and `local` moves the store; switching to `none` empties it and leaves every armed
6293
+ * op exactly where it is for the rest of this page.
6294
+ */
6295
+ const setQueryDevtoolsOverridesScope = (next) => {
6296
+ const previous = scope();
6297
+ if (next === previous)
6298
+ return;
6299
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6300
+ clearQueryDevtoolsStore(STORAGE_KEY);
6301
+ setQueryDevtoolsSettings({ overrides: next });
6302
+ if (next === 'none') {
6303
+ ops = {};
6304
+ forgetCarriedOver();
6305
+ return;
6306
+ }
6307
+ lastPersistentScope = next;
6308
+ if (previous === 'none') {
6309
+ ops = {};
6310
+ for (const [id, recorder] of armedRecorders) {
6311
+ const list = recorder.list().map((entry) => entry.op);
6312
+ if (list.length)
6313
+ ops[id] = list;
6314
+ }
6315
+ }
6316
+ writeStore();
6317
+ };
6318
+ /**
6319
+ * Turns persistence on or off - the panel's "Keep across reloads" toggle. On restores the last scope
6320
+ * that kept them (`session` unless Settings picked `local`).
6321
+ */
6322
+ const setQueryDevtoolsOverridePersistence = (value) => {
6323
+ setQueryDevtoolsOverridesScope(value ? lastPersistentScope : 'none');
6324
+ };
6325
+ /**
6326
+ * Empties the override store in both browser stores and disarms everything a reload brought back,
6327
+ * without changing where overrides are kept - the panel's "Reset devtools".
6328
+ */
6329
+ const clearQueryDevtoolsOverrideStore = () => {
6330
+ clearRestoredQueryDevtoolsOverrides();
6331
+ ops = {};
6332
+ clearQueryDevtoolsStore(STORAGE_KEY);
6333
+ };
6334
+ /**
6335
+ * The ops this page load inherited from the previous one, and for each id whether a live query has taken
6336
+ * it. Overrides that outlive the page that armed them have to announce themselves, because an app being
6337
+ * lied to looks exactly like an app that is broken.
6338
+ */
6339
+ const restoredQueryDevtoolsOverrides =
6340
+ /* @__PURE__ */ restored.asReadonly();
6341
+ /**
6342
+ * The scope those ops came back from, or `null` when this page inherited none. `local` is the one worth
6343
+ * naming in the banner: it means they outlived closing the tab, not just a reload.
6344
+ */
6345
+ const queryDevtoolsRestoredOverridesScope =
6346
+ /* @__PURE__ */ restoredScope.asReadonly();
6347
+ /** Disarms every op this page load inherited and empties the store - the banner's one-click way out. */
6348
+ const clearRestoredQueryDevtoolsOverrides = () => {
6349
+ for (const id of replayed) {
6350
+ armedRecorders.get(id)?.clearAll();
6351
+ armedRecorders.delete(id);
6352
+ }
6353
+ for (const id of carriedOver.keys())
6354
+ writeOps(id, []);
6355
+ forgetCarriedOver();
6356
+ };
6357
+ /**
6358
+ * Wraps a query's overrides recorder so its armed ops are stored and replayed. Replay happens here, at
6359
+ * registration, rather than in the panel - a query can run long before the panel is ever opened.
6360
+ *
6361
+ * Ids are reload-deterministic (descriptor + per-descriptor counter), so the same queries created in the
6362
+ * same order re-arm correctly; an id the store holds that nobody claims is reported by
6363
+ * {@link restoredQueryDevtoolsOverrides} rather than guessed at.
6364
+ * @internal
6365
+ */
6366
+ const withQueryDevtoolsOverridePersistence = (id, recorder) => {
6367
+ const inherited = ops[id];
6368
+ if (scope() !== 'none' && inherited?.length) {
6369
+ for (const op of inherited)
6370
+ recorder.arm(op);
6371
+ armedRecorders.set(id, recorder);
6372
+ replayed.add(id);
6373
+ publishRestored();
6374
+ }
6375
+ const sync = () => {
6376
+ const list = recorder.list().map((entry) => entry.op);
6377
+ if (list.length)
6378
+ armedRecorders.set(id, recorder);
6379
+ else
6380
+ armedRecorders.delete(id);
6381
+ if (scope() !== 'none')
6382
+ writeOps(id, list);
6383
+ };
6384
+ return {
6385
+ ...recorder,
6386
+ arm: (op) => {
6387
+ recorder.arm(op);
6388
+ sync();
6389
+ },
6390
+ clear: (opId) => {
6391
+ recorder.clear(opId);
6392
+ sync();
6393
+ },
6394
+ clearAll: () => {
6395
+ recorder.clearAll();
6396
+ if (replayed.delete(id))
6397
+ publishRestored();
6398
+ sync();
6399
+ },
6400
+ };
6401
+ };
6402
+
5793
6403
  const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
5794
6404
  /**
5795
6405
  * Whether any of `entries` is armed at `path` or anywhere below it - what the panel's menu asks before
@@ -5837,6 +6447,30 @@ const withValueAtPath = (root, path, value) => {
5837
6447
  const container = root && typeof root === 'object' ? root : {};
5838
6448
  return { ...container, [head]: withValueAtPath(container[head], rest, value) };
5839
6449
  };
6450
+ /**
6451
+ * Returns a copy of `root` with the value at `path` removed from its parent, cloning only the containers
6452
+ * along the way. An array parent splices, so the elements after it shift down instead of the slot
6453
+ * becoming a hole that serializes back as `null`.
6454
+ */
6455
+ const withoutValueAtPath = (root, path) => {
6456
+ const [head, ...rest] = path;
6457
+ if (rest.length === 0) {
6458
+ if (Array.isArray(root)) {
6459
+ const next = [...root];
6460
+ next.splice(head, 1);
6461
+ return next;
6462
+ }
6463
+ const { [head]: _dropped, ...remaining } = root;
6464
+ return remaining;
6465
+ }
6466
+ if (Array.isArray(root)) {
6467
+ const next = [...root];
6468
+ next[head] = withoutValueAtPath(root[head], rest);
6469
+ return next;
6470
+ }
6471
+ const container = root;
6472
+ return { ...container, [head]: withoutValueAtPath(container[head], rest) };
6473
+ };
5840
6474
  const IDENTITY_KEY_PATTERNS = [/^id$/i, /(^|[a-z])Id$/, /^uuid$/i, /^key$/i];
5841
6475
  const isIdentityKey = (key) => IDENTITY_KEY_PATTERNS.some((pattern) => pattern.test(key));
5842
6476
  /** Whether every sibling in `array` that has `key` holds a distinct string/number value for it. */
@@ -6129,6 +6763,19 @@ const applyOp = (root, op) => {
6129
6763
  return { root, ok: false };
6130
6764
  }
6131
6765
  return { root: withValueAtPath(root, op.path, duplicateOneArrayItem(resolution.value, op.index)), ok: true };
6766
+ case 'pasteArrayItem': {
6767
+ if (!Array.isArray(resolution.value))
6768
+ return { root, ok: false };
6769
+ const next = [...resolution.value];
6770
+ const at = op.index === undefined ? next.length : Math.min(Math.max(0, op.index), next.length);
6771
+ next.splice(at, 0, op.value);
6772
+ return { root: withValueAtPath(root, op.path, next), ok: true };
6773
+ }
6774
+ case 'deleteAt':
6775
+ // The response root has no parent to be removed from, and a key that is already gone stays gone.
6776
+ if (!op.path.length || !resolution.exists)
6777
+ return { root, ok: false };
6778
+ return { root: withoutValueAtPath(root, op.path), ok: true };
6132
6779
  case 'duplicateArray':
6133
6780
  if (!Array.isArray(resolution.value))
6134
6781
  return { root, ok: false };
@@ -6161,10 +6808,14 @@ const applyQueryDevtoolsOverrides = (entries, raw) => {
6161
6808
  return { value: root, staleIds };
6162
6809
  };
6163
6810
  /**
6164
- * Accumulates the response overrides armed on one query, for the devtools panel. Only ever created
6165
- * while {@link provideQueryDevtools} is installed - like {@link createQueryDevtoolsStats}, an app
6166
- * without devtools pays nothing for it.
6167
- * @internal
6811
+ * Accumulates the response overrides armed on one query, for the devtools panel. Instrumentation only
6812
+ * creates one while {@link provideQueryDevtools} is installed - like {@link createQueryDevtoolsStats},
6813
+ * an app without devtools pays nothing for it.
6814
+ *
6815
+ * The panel also creates one that belongs to no query at all, to record the edits made to a mock's draft
6816
+ * body: the ops are flattened with {@link applyQueryDevtoolsOverrides} and thrown away on save.
6817
+ *
6818
+ * Part of the devtools contract. **Not part of the general public contract.**
6168
6819
  */
6169
6820
  const createQueryDevtoolsOverrides = () => {
6170
6821
  const list = signal([], /* @ts-ignore */
@@ -6183,6 +6834,553 @@ const createQueryDevtoolsOverrides = () => {
6183
6834
  return { list: list.asReadonly(), arm, clear, clearAll, apply };
6184
6835
  };
6185
6836
 
6837
+ const TRANSFER_KIND = 'ethlete-query-overrides';
6838
+ const TRANSFER_VERSION = 1;
6839
+ const OP_TYPES = /* @__PURE__ */ new Set([
6840
+ 'set',
6841
+ 'stringPreset',
6842
+ 'numberPreset',
6843
+ 'booleanFlip',
6844
+ 'datePreset',
6845
+ 'duplicateArrayItem',
6846
+ 'pasteArrayItem',
6847
+ 'deleteAt',
6848
+ 'duplicateArray',
6849
+ 'paginationResize',
6850
+ 'reset',
6851
+ ]);
6852
+ const isJsonPath = (value) => Array.isArray(value) && value.every((step) => typeof step === 'string' || typeof step === 'number');
6853
+ const isOverrideOp = (value) => {
6854
+ if (!value || typeof value !== 'object' || Array.isArray(value))
6855
+ return false;
6856
+ const op = value;
6857
+ return typeof op.type === 'string' && OP_TYPES.has(op.type) && isJsonPath(op.path);
6858
+ };
6859
+ /**
6860
+ * Serializes the ops armed on one query into clipboard text. `reset` never reaches a recorder's list, so
6861
+ * what comes out is exactly what a paste can put back.
6862
+ */
6863
+ const serializeQueryDevtoolsOverrideTransfer = (entries, source) => {
6864
+ const payload = {
6865
+ kind: TRANSFER_KIND,
6866
+ version: TRANSFER_VERSION,
6867
+ ...(source ? { source } : {}),
6868
+ ops: entries.map((entry) => entry.op),
6869
+ };
6870
+ return JSON.stringify(payload, null, 2);
6871
+ };
6872
+ /**
6873
+ * Reads clipboard text back into ops. A bare array of ops is accepted alongside the full envelope, so a
6874
+ * set pasted out of a ticket and trimmed by hand still lands; an op whose `type` this build doesn't know
6875
+ * is counted in `skipped` rather than failing the whole paste, so an older panel can take a newer one's
6876
+ * set minus the parts it cannot replay.
6877
+ */
6878
+ const parseQueryDevtoolsOverrideTransfer = (text) => {
6879
+ if (!text.trim())
6880
+ return { ok: false, reason: 'The clipboard is empty' };
6881
+ let parsed;
6882
+ try {
6883
+ parsed = JSON.parse(text);
6884
+ }
6885
+ catch {
6886
+ return { ok: false, reason: 'The clipboard does not hold valid JSON' };
6887
+ }
6888
+ const envelope = !Array.isArray(parsed) && parsed && typeof parsed === 'object' ? parsed : null;
6889
+ const raw = Array.isArray(parsed) ? parsed : envelope?.['ops'];
6890
+ if (!Array.isArray(raw))
6891
+ return { ok: false, reason: 'The clipboard does not hold a copied override set' };
6892
+ const ops = raw.filter(isOverrideOp);
6893
+ const skipped = raw.length - ops.length;
6894
+ if (!ops.length) {
6895
+ return { ok: false, reason: skipped ? 'None of those ops can be replayed by this build' : 'That set is empty' };
6896
+ }
6897
+ const source = envelope?.['source'];
6898
+ return {
6899
+ ok: true,
6900
+ ops,
6901
+ skipped,
6902
+ ...(source && typeof source === 'object' && !Array.isArray(source) ? { source: source } : {}),
6903
+ };
6904
+ };
6905
+ /**
6906
+ * How many of `ops` would not resolve against `raw` - what a paste onto a differently-shaped query
6907
+ * reports, so ops sitting on nothing say so instead of looking armed. Replays them the same way the
6908
+ * pipeline will, since an earlier op can be what makes a later one's path exist.
6909
+ */
6910
+ const countUnresolvedQueryDevtoolsOverrides = (ops, raw) => applyQueryDevtoolsOverrides(ops.map((op, index) => ({ id: `${index}`, op })), raw).staleIds.length;
6911
+ /** Arms every op of a parsed set onto a query, on top of whatever it already has armed. */
6912
+ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
6913
+ for (const op of ops)
6914
+ recorder.arm(op);
6915
+ };
6916
+
6917
+ // Generated from package.json by tools/scripts/generate-version.js - do not edit.
6918
+ /** The version of `@ethlete/query` this build was cut from. */
6919
+ const QUERY_VERSION = '6.0.0-next.33';
6920
+
6921
+ /** How deep a schema is expanded before a nested branch is given up on. */
6922
+ const MAX_DEPTH = 12;
6923
+ /** How many elements are generated for an array, whatever its `minItems` says. */
6924
+ const MAX_ITEMS = 3;
6925
+ const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
6926
+ const loader = /* @__PURE__ */ signal(null, /* @ts-ignore */
6927
+ ...(ngDevMode ? [{ debugName: "loader" }] : /* istanbul ignore next */ []));
6928
+ const state = /* @__PURE__ */ signal({ status: 'unavailable' }, /* @ts-ignore */
6929
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
6930
+ const schemaDocument = /* @__PURE__ */ signal(null, /* @ts-ignore */
6931
+ ...(ngDevMode ? [{ debugName: "schemaDocument" }] : /* istanbul ignore next */ []));
6932
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
6933
+ /**
6934
+ * Where the API description stands, for the panel's seeding controls.
6935
+ *
6936
+ * Part of the devtools contract. **Not part of the general public contract.**
6937
+ */
6938
+ const queryDevtoolsSchemaState = /* @__PURE__ */ state.asReadonly();
6939
+ /**
6940
+ * Installs the application's schema loader. Called by `provideQueryDevtools()`; nothing else may call it.
6941
+ * @internal
6942
+ */
6943
+ const setQueryDevtoolsSchemaLoader = (next) => {
6944
+ loader.set(next ?? null);
6945
+ state.set({ status: next ? 'idle' : 'unavailable' });
6946
+ schemaDocument.set(null);
6947
+ };
6948
+ /** A `() => import('./openapi.json')` loader hands back the module rather than the document inside it. */
6949
+ const unwrapModule = (value) => {
6950
+ if (!isRecord(value))
6951
+ return value;
6952
+ if ('paths' in value || 'components' in value || 'definitions' in value || '$defs' in value)
6953
+ return value;
6954
+ return 'default' in value ? value['default'] : value;
6955
+ };
6956
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
6957
+ /**
6958
+ * Loads the API description, once. Calling it while it is loading or after it has loaded does nothing;
6959
+ * calling it after a failure retries.
6960
+ *
6961
+ * Part of the devtools contract. **Not part of the general public contract.**
6962
+ */
6963
+ const loadQueryDevtoolsSchema = () => {
6964
+ const load = loader();
6965
+ const status = state().status;
6966
+ if (!load || status === 'loading' || status === 'ready')
6967
+ return;
6968
+ state.set({ status: 'loading' });
6969
+ Promise.resolve()
6970
+ .then(() => load())
6971
+ .then((value) => {
6972
+ const parsed = unwrapModule(value);
6973
+ if (!isRecord(parsed))
6974
+ throw new Error('The schema loader did not return an object.');
6975
+ schemaDocument.set(parsed);
6976
+ state.set({ status: 'ready' });
6977
+ })
6978
+ .catch((error) => {
6979
+ schemaDocument.set(null);
6980
+ state.set({ status: 'error', message: messageOf(error) });
6981
+ });
6982
+ };
6983
+ const namedSchemas = /* @__PURE__ */ computed(() => {
6984
+ const doc = schemaDocument();
6985
+ if (!doc)
6986
+ return {};
6987
+ const components = doc['components'];
6988
+ return {
6989
+ ...(isRecord(doc['$defs']) ? doc['$defs'] : null),
6990
+ ...(isRecord(doc['definitions']) ? doc['definitions'] : null),
6991
+ ...(isRecord(components) && isRecord(components['schemas']) ? components['schemas'] : null),
6992
+ };
6993
+ }, /* @ts-ignore */
6994
+ ...(ngDevMode ? [{ debugName: "namedSchemas" }] : /* istanbul ignore next */ []));
6995
+ /**
6996
+ * Every schema the description names, sorted - the designer's "seed from a type" list.
6997
+ *
6998
+ * Part of the devtools contract. **Not part of the general public contract.**
6999
+ */
7000
+ const queryDevtoolsSchemaNames = /* @__PURE__ */ computed(() => Object.keys(namedSchemas()).sort(), /* @ts-ignore */
7001
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsSchemaNames" }] : /* istanbul ignore next */ []));
7002
+ const decodePointer = (step) => decodeURIComponent(step.replace(/~1/g, '/').replace(/~0/g, '~'));
7003
+ /** The last segment of a `$ref` - `#/components/schemas/MatchId` is what a field's type is called. */
7004
+ const refLabel = (ref) => decodePointer(ref.split('/').pop() ?? ref);
7005
+ /** Resolves a local JSON pointer against the loaded document. A remote `$ref` cannot be followed. */
7006
+ const resolveRef = (ref) => {
7007
+ if (!ref.startsWith('#/'))
7008
+ return undefined;
7009
+ let current = schemaDocument();
7010
+ for (const step of ref.slice(2).split('/')) {
7011
+ if (!isRecord(current))
7012
+ return undefined;
7013
+ current = current[decodePointer(step)];
7014
+ }
7015
+ return current;
7016
+ };
7017
+ const at = (path) => path || 'the root';
7018
+ /** The declared `type`, tolerating the `['string', 'null']` form JSON Schema and OpenAPI 3.1 both allow. */
7019
+ const typeOf = (schema) => {
7020
+ const type = schema['type'];
7021
+ if (typeof type === 'string')
7022
+ return type;
7023
+ if (Array.isArray(type))
7024
+ return type.find((entry) => typeof entry === 'string' && entry !== 'null');
7025
+ return undefined;
7026
+ };
7027
+ const isNullable = (schema) => {
7028
+ const type = schema['type'];
7029
+ return schema['nullable'] === true || (Array.isArray(type) && type.includes('null'));
7030
+ };
7031
+ const branchesOf = (schema) => {
7032
+ for (const keyword of ['oneOf', 'anyOf']) {
7033
+ const branches = schema[keyword];
7034
+ if (Array.isArray(branches) && branches.length)
7035
+ return { keyword, branches };
7036
+ }
7037
+ return null;
7038
+ };
7039
+ /**
7040
+ * A short label for what a field is declared as: the name of the type it `$ref`s (which is the point -
7041
+ * `MatchId` says more than `string`), a union of its branches, or its primitive type and format.
7042
+ */
7043
+ const describeSchema = (schema, depth = 0) => {
7044
+ if (!isRecord(schema) || depth > 4)
7045
+ return 'unknown';
7046
+ const ref = schema['$ref'];
7047
+ if (typeof ref === 'string')
7048
+ return refLabel(ref);
7049
+ const nullSuffix = isNullable(schema) ? ' | null' : '';
7050
+ const branches = branchesOf(schema);
7051
+ if (branches) {
7052
+ const labels = [...new Set(branches.branches.slice(0, 3).map((branch) => describeSchema(branch, depth + 1)))];
7053
+ return `${labels.join(' | ')}${branches.branches.length > 3 ? ' | …' : ''}${nullSuffix}`;
7054
+ }
7055
+ const allOf = schema['allOf'];
7056
+ if (Array.isArray(allOf) && allOf.length) {
7057
+ const named = allOf.find((entry) => isRecord(entry) && typeof entry['$ref'] === 'string');
7058
+ return `${describeSchema(named ?? allOf[0], depth + 1)}${nullSuffix}`;
7059
+ }
7060
+ const enumValues = schema['enum'];
7061
+ if (Array.isArray(enumValues) && enumValues.length) {
7062
+ const labels = enumValues.slice(0, 3).map((value) => JSON.stringify(value));
7063
+ return `${labels.join(' | ')}${enumValues.length > 3 ? ' | …' : ''}${nullSuffix}`;
7064
+ }
7065
+ const type = typeOf(schema);
7066
+ if (type === 'array')
7067
+ return `${describeSchema(schema['items'], depth + 1)}[]${nullSuffix}`;
7068
+ if (type === 'string') {
7069
+ const format = schema['format'];
7070
+ return `${typeof format === 'string' ? `string (${format})` : 'string'}${nullSuffix}`;
7071
+ }
7072
+ return `${type ?? 'unknown'}${nullSuffix}`;
7073
+ };
7074
+ const FORMAT_SAMPLES = {
7075
+ 'date-time': '2026-01-01T00:00:00.000Z',
7076
+ date: '2026-01-01',
7077
+ time: '00:00:00',
7078
+ duration: 'P1D',
7079
+ uuid: '00000000-0000-4000-8000-000000000000',
7080
+ email: 'user@example.com',
7081
+ hostname: 'example.com',
7082
+ ipv4: '127.0.0.1',
7083
+ ipv6: '::1',
7084
+ uri: 'https://example.com',
7085
+ 'uri-reference': '/example',
7086
+ url: 'https://example.com',
7087
+ password: '',
7088
+ byte: '',
7089
+ binary: '',
7090
+ };
7091
+ /**
7092
+ * A placeholder string: the declared format's shape where there is one, otherwise the field's own name -
7093
+ * which reads as obviously unreal while still saying which field you are looking at.
7094
+ */
7095
+ const stringSample = (node, schema) => {
7096
+ const format = schema['format'];
7097
+ if (typeof format === 'string' && format in FORMAT_SAMPLES)
7098
+ return FORMAT_SAMPLES[format];
7099
+ const title = schema['title'];
7100
+ if (typeof title === 'string' && title)
7101
+ return title;
7102
+ return node.key ?? 'string';
7103
+ };
7104
+ const numberSample = (schema) => {
7105
+ for (const keyword of ['minimum', 'exclusiveMinimum', 'default']) {
7106
+ const value = schema[keyword];
7107
+ if (typeof value === 'number' && Number.isFinite(value))
7108
+ return keyword === 'exclusiveMinimum' ? value + 1 : value;
7109
+ }
7110
+ return 0;
7111
+ };
7112
+ const childNode = (parent, child) => ({
7113
+ schema: child.schema,
7114
+ path: parent.path ? `${parent.path}.${child.key}` : child.key,
7115
+ depth: parent.depth + 1,
7116
+ key: child.key,
7117
+ });
7118
+ const requiredOf = (schema) => {
7119
+ const required = schema['required'];
7120
+ return new Set(Array.isArray(required) ? required.filter((entry) => typeof entry === 'string') : []);
7121
+ };
7122
+ const generateObject = (node, ctx) => {
7123
+ const schema = node.schema;
7124
+ const properties = schema['properties'];
7125
+ if (!isRecord(properties)) {
7126
+ if (isRecord(schema['additionalProperties'])) {
7127
+ ctx.notes.add(`${at(node.path)} is a free-form map - generated as an empty object.`);
7128
+ }
7129
+ return {};
7130
+ }
7131
+ const required = requiredOf(schema);
7132
+ const result = {};
7133
+ for (const [key, child] of Object.entries(properties)) {
7134
+ const target = childNode(node, { schema: child, key });
7135
+ ctx.types.set(target.path, `${describeSchema(child)}${required.has(key) ? '' : '?'}`);
7136
+ result[key] = generate(target, ctx);
7137
+ }
7138
+ return result;
7139
+ };
7140
+ const generateArray = (node, ctx) => {
7141
+ const schema = node.schema;
7142
+ const items = schema['items'];
7143
+ if (!isRecord(items))
7144
+ return [];
7145
+ const minItems = typeof schema['minItems'] === 'number' ? schema['minItems'] : 1;
7146
+ const count = Math.min(Math.max(1, minItems), MAX_ITEMS);
7147
+ const target = { schema: items, path: `${node.path ? `${node.path}.` : ''}*`, depth: node.depth + 1, key: node.key };
7148
+ ctx.types.set(target.path, describeSchema(items));
7149
+ const element = generate(target, ctx);
7150
+ return Array.from({ length: count }, () => structuredCopy(element));
7151
+ };
7152
+ /** Each generated element is its own value, so editing one in the designer does not edit the others. */
7153
+ const structuredCopy = (value) => {
7154
+ if (Array.isArray(value))
7155
+ return value.map(structuredCopy);
7156
+ if (isRecord(value))
7157
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, structuredCopy(entry)]));
7158
+ return value;
7159
+ };
7160
+ /** Merges what every `allOf` member generates, so a composed schema seeds as one object. */
7161
+ const generateAllOf = (node, ctx) => {
7162
+ const members = node.schema['allOf'];
7163
+ const generated = members.map((member) => generate({ ...node, schema: member }, ctx));
7164
+ const objects = generated.filter(isRecord);
7165
+ if (objects.length !== generated.length)
7166
+ return generated.find((value) => !isRecord(value)) ?? null;
7167
+ return Object.assign({}, ...objects);
7168
+ };
7169
+ const generate = (node, ctx) => {
7170
+ if (node.depth > MAX_DEPTH) {
7171
+ ctx.notes.add(`${at(node.path)} is nested deeper than ${MAX_DEPTH} levels - generated as null.`);
7172
+ return null;
7173
+ }
7174
+ if (!isRecord(node.schema))
7175
+ return null;
7176
+ const schema = node.schema;
7177
+ const ref = schema['$ref'];
7178
+ if (typeof ref === 'string') {
7179
+ const name = refLabel(ref);
7180
+ if (ctx.refs.includes(name)) {
7181
+ ctx.notes.add(`${name} contains itself - the recursion was cut at ${at(node.path)} with null.`);
7182
+ return null;
7183
+ }
7184
+ const resolved = resolveRef(ref);
7185
+ if (resolved === undefined) {
7186
+ ctx.notes.add(`${ref} could not be resolved - generated as null at ${at(node.path)}.`);
7187
+ return null;
7188
+ }
7189
+ ctx.refs.push(name);
7190
+ const value = generate({ ...node, schema: resolved }, ctx);
7191
+ ctx.refs.pop();
7192
+ return value;
7193
+ }
7194
+ for (const keyword of ['example', 'default', 'const']) {
7195
+ if (keyword in schema)
7196
+ return schema[keyword];
7197
+ }
7198
+ const examples = schema['examples'];
7199
+ if (Array.isArray(examples) && examples.length)
7200
+ return examples[0];
7201
+ const enumValues = schema['enum'];
7202
+ if (Array.isArray(enumValues) && enumValues.length)
7203
+ return enumValues[0];
7204
+ const branches = branchesOf(schema);
7205
+ if (branches) {
7206
+ if (branches.branches.length > 1) {
7207
+ ctx.notes.add(`${at(node.path)} is a ${branches.keyword} - the first branch was taken.`);
7208
+ }
7209
+ return generate({ ...node, schema: branches.branches[0] }, ctx);
7210
+ }
7211
+ if (Array.isArray(schema['allOf']) && schema['allOf'].length)
7212
+ return generateAllOf(node, ctx);
7213
+ switch (typeOf(schema)) {
7214
+ case 'object':
7215
+ return generateObject(node, ctx);
7216
+ case 'array':
7217
+ return generateArray(node, ctx);
7218
+ case 'string':
7219
+ return stringSample(node, schema);
7220
+ case 'integer':
7221
+ case 'number':
7222
+ return numberSample(schema);
7223
+ case 'boolean':
7224
+ return false;
7225
+ case 'null':
7226
+ return null;
7227
+ default:
7228
+ break;
7229
+ }
7230
+ if (isRecord(schema['properties']))
7231
+ return generateObject(node, ctx);
7232
+ ctx.notes.add(`${at(node.path)} declares no type - generated as null.`);
7233
+ return null;
7234
+ };
7235
+ const seedFrom = (schema, name) => {
7236
+ const ctx = { types: new Map(), notes: new Set(), refs: name ? [name] : [] };
7237
+ const body = generate({ schema, path: '', depth: 0, key: null }, ctx);
7238
+ return { body, schemaName: name, types: ctx.types, notes: [...ctx.notes] };
7239
+ };
7240
+ /**
7241
+ * Generates a body from one named schema (`MatchView`), or `null` when the description does not name it.
7242
+ *
7243
+ * Part of the devtools contract. **Not part of the general public contract.**
7244
+ */
7245
+ const seedQueryDevtoolsSchemaBody = (name) => {
7246
+ const schema = namedSchemas()[name];
7247
+ if (schema === undefined)
7248
+ return null;
7249
+ return seedFrom(schema, name);
7250
+ };
7251
+ const segmentsOf = (pattern) => pattern.split('/').filter(Boolean);
7252
+ /** `/matches/{id}` and `/matches/:id` describe the same route; a mock's pattern is written the second way. */
7253
+ const toColonPattern = (path) => path.replace(/\{([^}]+)\}/g, ':$1');
7254
+ const isParam = (segment) => segment.startsWith(':');
7255
+ /** Whether two patterns describe the same route, allowing their path params to be named differently. */
7256
+ const patternsMatch = (a, b) => {
7257
+ const left = segmentsOf(a);
7258
+ const right = segmentsOf(b);
7259
+ if (left.length !== right.length)
7260
+ return false;
7261
+ return left.every((segment, index) => {
7262
+ const other = right[index];
7263
+ return isParam(segment) || isParam(other) ? isParam(segment) && isParam(other) : segment === other;
7264
+ });
7265
+ };
7266
+ /**
7267
+ * Every route the description declares, as the designer offers them - including the ones no query has
7268
+ * ever called, which is the case a mock exists for.
7269
+ *
7270
+ * Part of the devtools contract. **Not part of the general public contract.**
7271
+ */
7272
+ const queryDevtoolsSchemaRoutes = /* @__PURE__ */ computed(() => {
7273
+ const paths = schemaDocument()?.['paths'];
7274
+ if (!isRecord(paths))
7275
+ return [];
7276
+ const routes = [];
7277
+ for (const [path, item] of Object.entries(paths)) {
7278
+ if (!isRecord(item))
7279
+ continue;
7280
+ for (const method of HTTP_METHODS) {
7281
+ const operation = item[method];
7282
+ if (!isRecord(operation))
7283
+ continue;
7284
+ const summary = operation['summary'] ?? operation['operationId'];
7285
+ routes.push({
7286
+ method: method.toUpperCase(),
7287
+ pattern: toColonPattern(path),
7288
+ summary: typeof summary === 'string' ? summary : '',
7289
+ });
7290
+ }
7291
+ }
7292
+ return routes;
7293
+ }, /* @ts-ignore */
7294
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsSchemaRoutes" }] : /* istanbul ignore next */ []));
7295
+ const findOperation = (target) => {
7296
+ const paths = schemaDocument()?.['paths'];
7297
+ if (!isRecord(paths))
7298
+ return null;
7299
+ const method = target.method.toLowerCase();
7300
+ const segments = segmentsOf(target.pattern);
7301
+ // A client's base path (`/api/v2`) is part of the request but not of the document, so a pattern that
7302
+ // does not match is retried without its leading segments before giving up.
7303
+ for (let skip = 0; skip < segments.length; skip++) {
7304
+ const candidate = `/${segments.slice(skip).join('/')}`;
7305
+ for (const [path, item] of Object.entries(paths)) {
7306
+ if (!isRecord(item) || !isRecord(item[method]))
7307
+ continue;
7308
+ if (!patternsMatch(toColonPattern(path), candidate))
7309
+ continue;
7310
+ return {
7311
+ operation: item[method],
7312
+ path,
7313
+ droppedPrefix: skip ? `/${segments.slice(0, skip).join('/')}` : '',
7314
+ };
7315
+ }
7316
+ }
7317
+ return null;
7318
+ };
7319
+ /** The success response of an operation: the lowest 2xx the document declares, else its `default`. */
7320
+ const successResponse = (operation) => {
7321
+ const responses = operation['responses'];
7322
+ if (!isRecord(responses))
7323
+ return null;
7324
+ const status = Object.keys(responses)
7325
+ .filter((key) => /^2\d\d$/.test(key))
7326
+ .sort()[0];
7327
+ const key = status ?? (('default' in responses && 'default') || null);
7328
+ if (!key)
7329
+ return null;
7330
+ const response = responses[key];
7331
+ return isRecord(response) ? { response, status: key } : null;
7332
+ };
7333
+ /** The JSON media type of a response, tolerating `application/vnd.x+json` and Swagger 2's flat `schema`. */
7334
+ const jsonSchemaOf = (response) => {
7335
+ const content = response['content'];
7336
+ if (!isRecord(content))
7337
+ return isRecord(response['schema']) ? { schema: response['schema'], example: undefined } : null;
7338
+ const key = Object.keys(content).find((type) => type.includes('json')) ?? Object.keys(content)[0];
7339
+ if (!key)
7340
+ return null;
7341
+ const media = content[key];
7342
+ if (!isRecord(media))
7343
+ return null;
7344
+ return { schema: media['schema'], example: media['example'] };
7345
+ };
7346
+ /**
7347
+ * Generates a body for one route from the description's own success response - the seed that makes a
7348
+ * designed mock a real view model rather than a guess. `null` when the document does not declare the
7349
+ * route, or declares no JSON response for it.
7350
+ *
7351
+ * Part of the devtools contract. **Not part of the general public contract.**
7352
+ */
7353
+ const seedQueryDevtoolsSchemaRoute = (target) => {
7354
+ const match = findOperation(target);
7355
+ if (!match)
7356
+ return null;
7357
+ const success = successResponse(match.operation);
7358
+ if (!success)
7359
+ return null;
7360
+ const json = jsonSchemaOf(success.response);
7361
+ if (!json)
7362
+ return null;
7363
+ // A top-level `$ref` is resolved here rather than by the walk, so `seedFrom` always receives the
7364
+ // schema its `name` stands for - otherwise the walk reads the name as one it is already expanding and
7365
+ // cuts the whole body as a cycle.
7366
+ const ref = isRecord(json.schema) ? json.schema['$ref'] : null;
7367
+ const named = typeof ref === 'string' ? { schema: resolveRef(ref), name: refLabel(ref) } : null;
7368
+ if (named && named.schema === undefined)
7369
+ return null;
7370
+ const seed = json.example === undefined
7371
+ ? seedFrom(named ? named.schema : json.schema, named ? named.name : null)
7372
+ : {
7373
+ body: json.example,
7374
+ schemaName: null,
7375
+ types: new Map(),
7376
+ notes: ['The document ships an example for this route - it was used as-is.'],
7377
+ };
7378
+ const notes = [...seed.notes, `Generated from ${match.path} ${target.method.toUpperCase()} ${success.status}.`];
7379
+ if (match.droppedPrefix)
7380
+ notes.push(`The document has no ${match.droppedPrefix} prefix - it was ignored when matching.`);
7381
+ return { ...seed, notes };
7382
+ };
7383
+
6186
7384
  const EMPTY_STATS = {
6187
7385
  executions: 0,
6188
7386
  requests: 0,
@@ -6229,21 +7427,22 @@ const RUN_HISTORY = 25;
6229
7427
  * decision the app has to make for itself.
6230
7428
  */
6231
7429
  const DEFAULT_RESPONSE_HISTORY = 5;
6232
- let responseHistory = DEFAULT_RESPONSE_HISTORY;
7430
+ let providedResponseHistory = DEFAULT_RESPONSE_HISTORY;
6233
7431
  /**
6234
- * How many bodies each query retains - {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run
6235
- * that still holds one can be an end of a response diff, so the panel reads this to say why an older run
6236
- * can no longer be picked.
7432
+ * How many bodies each query retains: what the panel's Settings tab asks for, or
7433
+ * {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run that still holds one can be an end of
7434
+ * a response diff, so the panel reads this to say why an older run can no longer be picked.
6237
7435
  *
6238
7436
  * Part of the devtools contract. **Not part of the general public contract.**
6239
7437
  */
6240
- const queryDevtoolsResponseHistory = () => responseHistory;
7438
+ const queryDevtoolsResponseHistory = () => queryDevtoolsSettings().responseHistory ?? providedResponseHistory;
6241
7439
  /**
6242
- * Sets how many bodies each query retains. Called by `provideQueryDevtools()`; nothing else may call it.
7440
+ * Sets the application's own retention, which the panel's Settings tab can raise for the rest of the
7441
+ * page. Called by `provideQueryDevtools()`; nothing else may call it.
6243
7442
  * @internal
6244
7443
  */
6245
7444
  const setQueryDevtoolsResponseHistory = (count) => {
6246
- responseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
7445
+ providedResponseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
6247
7446
  };
6248
7447
  /** The index of the newest run still in flight, or -1 when none is. */
6249
7448
  const lastPendingRunIndex = (runs) => {
@@ -6259,7 +7458,9 @@ const lastPendingRunIndex = (runs) => {
6259
7458
  * two. Mutates the array it is given, which is always one this module just built.
6260
7459
  */
6261
7460
  const trimRetainedBodies = (runs) => {
6262
- const budget = responseHistory;
7461
+ // Untracked: a run is recorded from whatever context the query is running in, and reading the setting
7462
+ // there would make that context re-run every time the setting changes.
7463
+ const budget = untracked(queryDevtoolsResponseHistory);
6263
7464
  let kept = 0;
6264
7465
  for (let index = runs.length - 1; index >= 0; index--) {
6265
7466
  const run = runs[index];
@@ -6678,7 +7879,7 @@ const registerEntry = (registration) => {
6678
7879
  createdAt: Date.now(),
6679
7880
  stats: registration.stats,
6680
7881
  formLinks: registration.formLinks,
6681
- overrides: registration.overrides,
7882
+ overrides: registration.overrides && withQueryDevtoolsOverridePersistence(id, registration.overrides),
6682
7883
  };
6683
7884
  // A registration that brings its own id can repeat one a tombstone still holds; the live entry wins.
6684
7885
  entries.update((list) => [...list.filter((e) => !(e.id === id && e.destroyedAt)), fullEntry]);
@@ -6732,12 +7933,22 @@ const clearQueryDevtoolsTombstones = (ids) => {
6732
7933
  * ```
6733
7934
  */
6734
7935
  const provideQueryDevtools = (options) => {
7936
+ // First: the stored settings say where the override store lives and whether the panel has raised the
7937
+ // retention this app asked for, and both are read by the calls below.
7938
+ initQueryDevtoolsSettings();
6735
7939
  setQueryDevtoolsResponseHistory(options?.responseHistory);
7940
+ registerEthleteVersion('core', CORE_VERSION);
7941
+ registerEthleteVersion('query', QUERY_VERSION);
7942
+ setQueryDevtoolsAppInfo(options?.about);
7943
+ initQueryDevtoolsOverridePersistence();
7944
+ initQueryDevtoolsMocks();
7945
+ setQueryDevtoolsSchemaLoader(options?.schema);
6736
7946
  setQueryDevtoolsRegistrar(registerEntry);
6737
7947
  setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
6738
7948
  setQueryDevtoolsFormLinksFactory(createQueryDevtoolsFormLinks);
6739
7949
  setQueryDevtoolsOverridesFactory(createQueryDevtoolsOverrides);
6740
7950
  setQueryDevtoolsFaultResolver(resolveQueryDevtoolsFaultForAttempt);
7951
+ setQueryDevtoolsMockResolver(resolveQueryDevtoolsMockForAttempt);
6741
7952
  if (!isDevMode()) {
6742
7953
  console.warn('You are using the Query Devtools in production mode. This increases the size of your bundle and should only be used for development purposes.');
6743
7954
  }
@@ -6855,14 +8066,14 @@ const createSecureGqlExecuteFn = (executeOptions) => {
6855
8066
  state: executeOptions.state,
6856
8067
  autoExecutes: shouldAutoExecuteGqlQuery(executeOptions.creatorInternals.method) &&
6857
8068
  !executeOptions.queryConfig.onlyManualExecution,
6858
- transformAuthAndExec: (executeArgs, _tokens, headers, executeState) => {
8069
+ transformAuthAndExec: (executeArgs, executeState) => {
6859
8070
  const { args, options: runOptions } = executeArgs ?? {};
6860
8071
  const query = transformGql(executeOptions.creatorInternals.query);
6861
8072
  let gqlParams = query(args?.variables);
6862
8073
  if (args?.queryParams && executeOptions.creatorInternals.transport === 'GET') {
6863
8074
  gqlParams = { ...gqlParams, ...args.queryParams };
6864
8075
  }
6865
- const computedArgs = { ...(args ?? {}), headers };
8076
+ const computedArgs = { ...(args ?? {}) };
6866
8077
  if (executeOptions.creatorInternals.transport === 'GET') {
6867
8078
  computedArgs.queryParams = gqlParams;
6868
8079
  }
@@ -12278,5 +13489,5 @@ const createWebSocketClient = (options) => {
12278
13489
  * Generated bundle index. Do not edit.
12279
13490
  */
12280
13491
 
12281
- 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, 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, 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 };
13492
+ export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsStore, registerEthleteVersion, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsAppInfo, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
12282
13493
  //# sourceMappingURL=ethlete-query.mjs.map