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

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, merge, EMPTY, takeWhile, 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,17 +241,48 @@ 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.
251
265
  * @internal
252
266
  */
253
267
  const resolveQueryDevtoolsFault = (target) => faultResolver?.(target) ?? null;
268
+ let tokenPayloadPatcher = null;
269
+ /**
270
+ * Installs the access-token payload patcher. Called by `provideQueryDevtools()`; nothing else may call it.
271
+ * @internal
272
+ */
273
+ const setQueryDevtoolsTokenPayloadPatcher = (fn) => {
274
+ tokenPayloadPatcher = fn;
275
+ };
276
+ /**
277
+ * A decoded access token as the devtools want it seen, which today means with an overridden lifetime
278
+ * applied. Returns the payload as decoded unless `provideQueryDevtools()` installed a patcher.
279
+ *
280
+ * Auth code must reach the override through this rather than importing the registry it lives in: a
281
+ * static import would pin that module into every bundle that builds an auth provider, and a runtime
282
+ * guard at the call site does not undo that.
283
+ * @internal
284
+ */
285
+ const patchQueryDevtoolsTokenPayload = (options) => (tokenPayloadPatcher ? tokenPayloadPatcher(options) : options.payload);
254
286
  let suppressStackRegistration = false;
255
287
  /**
256
288
  * Suppresses devtools registration for the next {@link createQueryStack} call. Used by
@@ -1609,8 +1641,14 @@ const createQueryErrorResponse = (error) => {
1609
1641
  };
1610
1642
 
1611
1643
  const SPEED_BUFFER_TIME_IN_MS = 2000;
1644
+ /**
1645
+ * The `statusText` of a response the devtools served instead of sending the request. It is what a log, a
1646
+ * network-less error and a copied report all read, so it has to say where the response came from.
1647
+ */
1648
+ const MOCKED_STATUS_TEXT = 'Served by the query devtools';
1612
1649
  const createHttpRequest = (options) => {
1613
1650
  let currentStreamSubscription = Subscription.EMPTY;
1651
+ let lastSentHeaders;
1614
1652
  const { args, clientOptions, dependencies } = options;
1615
1653
  const currentEvent = signal(null, /* @ts-ignore */
1616
1654
  ...(ngDevMode ? [{ debugName: "currentEvent" }] : /* istanbul ignore next */ []));
@@ -1680,6 +1718,36 @@ const createHttpRequest = (options) => {
1680
1718
  * same tick it was started in, and settling synchronously inside `execute()` would let an injected
1681
1719
  * failure land before the caller has finished wiring the request up.
1682
1720
  */
1721
+ /**
1722
+ * The request, or the response a devtools mock has armed for this route instead of it. A mock bypasses
1723
+ * the interceptor chain by construction - nothing is sent - so a mocked secure route never exercises
1724
+ * the token flow, which the panel says on the row.
1725
+ */
1726
+ const sendOrMock = (headers) => {
1727
+ const mock = resolveQueryDevtoolsMock({
1728
+ clientName: options.clientName ?? '',
1729
+ method: options.method,
1730
+ url: options.fullPath,
1731
+ });
1732
+ if (!mock)
1733
+ return send(headers);
1734
+ const settle$ = mock.status >= 400
1735
+ ? throwError(() => new HttpErrorResponse({
1736
+ status: mock.status,
1737
+ statusText: MOCKED_STATUS_TEXT,
1738
+ url: options.fullPath,
1739
+ error: mock.body,
1740
+ }))
1741
+ : of(new HttpResponse({
1742
+ body: mock.body,
1743
+ status: mock.status,
1744
+ statusText: MOCKED_STATUS_TEXT,
1745
+ url: options.fullPath,
1746
+ }));
1747
+ // Through `timer` even at zero latency, for the same reason a faulted attempt is: settling
1748
+ // synchronously inside `execute()` would land the response before the caller has wired the request up.
1749
+ return timer(mock.latencyMs).pipe(switchMap(() => settle$));
1750
+ };
1683
1751
  const sendWithFaults = (headers) => defer(() => {
1684
1752
  const fault = resolveQueryDevtoolsFault({
1685
1753
  clientName: options.clientName ?? '',
@@ -1688,10 +1756,10 @@ const createHttpRequest = (options) => {
1688
1756
  });
1689
1757
  lastAttemptFaulted = !!fault && fault.status !== null;
1690
1758
  if (!fault)
1691
- return send(headers);
1759
+ return sendOrMock(headers);
1692
1760
  const { status } = fault;
1693
1761
  const attempt$ = status === null
1694
- ? send(headers)
1762
+ ? sendOrMock(headers)
1695
1763
  : throwError(() => new HttpErrorResponse({
1696
1764
  status,
1697
1765
  statusText: 'Injected by the query devtools',
@@ -1702,7 +1770,8 @@ const createHttpRequest = (options) => {
1702
1770
  });
1703
1771
  const createStream = () => {
1704
1772
  const headers = resolveHeaders();
1705
- const source$ = isQueryDevtoolsFaultInjectionEnabled() ? sendWithFaults(headers) : send(headers);
1773
+ lastSentHeaders = headers;
1774
+ const source$ = isQueryDevtoolsRequestInterceptionEnabled() ? sendWithFaults(headers) : send(headers);
1706
1775
  return source$.pipe(tap((event) => updateState(event)), retry({
1707
1776
  delay: (error, retryCount) => {
1708
1777
  const retryOptions = { error, retryCount };
@@ -1874,6 +1943,7 @@ const createHttpRequest = (options) => {
1874
1943
  applyPersistedResponse,
1875
1944
  lastPersistedResponseAt: lastPersistedResponseAt.asReadonly(),
1876
1945
  resolveHeaders,
1946
+ lastSentHeaders: () => lastSentHeaders,
1877
1947
  attempts: attempts.asReadonly(),
1878
1948
  retryState: retryState.asReadonly(),
1879
1949
  lastDurationMs: lastDurationMs.asReadonly(),
@@ -3812,6 +3882,9 @@ const createSecureExecuteFactory = (options) => {
3812
3882
  let sessionRestartSubscription = Subscription.EMPTY;
3813
3883
  let hasExecuted = false;
3814
3884
  let lastExecuteArgs;
3885
+ /** The access token the last request went out with - what the 401 retry below compares against. */
3886
+ let lastRequestedWithToken = null;
3887
+ const error$ = toObservable(options.state.error, { injector: options.deps.injector });
3815
3888
  const reset = () => {
3816
3889
  authQuerySubscription.unsubscribe();
3817
3890
  authQuerySubscription = Subscription.EMPTY;
@@ -3874,15 +3947,14 @@ const createSecureExecuteFactory = (options) => {
3874
3947
  if (!accessToken) {
3875
3948
  throw tokensNotAvailableInsideAuthAndExec();
3876
3949
  }
3877
- const tokens = { accessToken, refreshToken: options.authProvider.refreshToken() ?? '' };
3878
- const headers = headerProvider();
3950
+ lastRequestedWithToken = accessToken;
3879
3951
  options.transformAuthAndExec({
3880
3952
  ...executeArgs,
3881
3953
  args: {
3882
3954
  ...(args ?? {}),
3883
- headers: headerProvider, // Pass header provider instead of static headers
3955
+ headers: headerProvider,
3884
3956
  },
3885
- }, tokens, headers, executeState);
3957
+ }, executeState);
3886
3958
  };
3887
3959
  // The auth query completing and the access token being populated happen on two
3888
3960
  // different reactive timelines: the token is set by a separate token-extraction
@@ -3919,10 +3991,20 @@ const createSecureExecuteFactory = (options) => {
3919
3991
  tokenWaitSubscription = Subscription.EMPTY;
3920
3992
  sessionRestartSubscription.unsubscribe();
3921
3993
  sessionRestartSubscription = Subscription.EMPTY;
3922
- tokenRefreshSubscription = options.authProvider.afterTokenRefresh$
3994
+ // Retrying with the token that just produced the 401 would 401 again, and that 401 asks for
3995
+ // another refresh - an endless refresh/retry loop for as long as the server keeps handing out
3996
+ // tokens it rejects. A refresh that did not change the access token is therefore not a reason to
3997
+ // retry; the subscription stays open, so a later one that does change it still is.
3998
+ //
3999
+ // The error landing is a trigger of its own: a refresh can complete before this query's 401 has
4000
+ // even come back (which is also why that 401 must not start another refresh - see
4001
+ // withRefreshQuery's 401 handling), and the refresh emission alone would find no error to act
4002
+ // on. Emissions of the error this execution armed with are not landings, only replays.
4003
+ const errorAtArm = options.state.error();
4004
+ tokenRefreshSubscription = merge(options.authProvider.afterTokenRefresh$, error$.pipe(filter((landedError) => landedError !== errorAtArm)))
3923
4005
  .pipe(filter(() => {
3924
4006
  const currentError = options.state.error();
3925
- return currentError?.code === 401;
4007
+ return currentError?.code === 401 && options.authProvider.accessToken() !== lastRequestedWithToken;
3926
4008
  }), take(1), tap(() => exec(execArgsWithDefaults)))
3927
4009
  .subscribe();
3928
4010
  // `setTokens()` seeds an auth session from outside the query registry (an SSO/OIDC callback, a
@@ -3943,19 +4025,23 @@ const createSecureExecuteFactory = (options) => {
3943
4025
  injector: options.deps.injector,
3944
4026
  }).pipe(switchMap((latestQuery) => {
3945
4027
  if (!latestQuery)
3946
- return of(null);
4028
+ return EMPTY;
3947
4029
  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
- }));
4030
+ return toObservable(query.isAlive, { injector: options.deps.injector }).pipe(filter((isAlive) => !isAlive), filter(() => !!query.error() || !!query.response()), map(() => query));
4031
+ }), tap((query) => {
4032
+ if (query.error()) {
4033
+ error(query);
4034
+ }
4035
+ else {
4036
+ authAndExecWhenTokenReady(execArgsWithDefaults);
4037
+ }
4038
+ }),
4039
+ // Stop at the first auth query that hands a session over. Every later one is a token refresh,
4040
+ // which settles before its tokens are applied - re-running here would send the token that just
4041
+ // expired; `afterTokenRefresh$` above is what re-runs a query after a refresh. A *failed* auth
4042
+ // query keeps the wait open, so a query that mounted during a failed auto-login still runs
4043
+ // once the user logs in for real.
4044
+ takeWhile((query) => !!query.error()));
3959
4045
  // A `setTokens()` call that lands while this is already waiting (e.g. the SSO redirect
3960
4046
  // resolves after this secure query mounted) never touches `latestExecutedQuery` either -
3961
4047
  // wake up on `executionState` becoming a token seed too.
@@ -3986,13 +4072,12 @@ const createSecureExecuteFn = (executeOptions) => {
3986
4072
  deps: executeOptions.deps,
3987
4073
  state: executeOptions.state,
3988
4074
  autoExecutes: shouldAutoExecuteQuery(executeOptions.creatorInternals.method) && !executeOptions.queryConfig.onlyManualExecution,
3989
- transformAuthAndExec: (executeArgs, _tokens, headers, executeState) => {
4075
+ transformAuthAndExec: (executeArgs, executeState) => {
3990
4076
  const { options: runOptions } = executeArgs ?? {};
3991
- const updatedArgs = { ...(executeArgs?.args ?? {}), headers };
3992
4077
  queryExecute({
3993
4078
  executeOptions: executeOptions,
3994
4079
  executeState,
3995
- args: updatedArgs,
4080
+ args: executeArgs?.args ?? {},
3996
4081
  options: runOptions,
3997
4082
  isSecure: true,
3998
4083
  });
@@ -4598,7 +4683,12 @@ const createBearerAuthProviderImpl = (config) => {
4598
4683
  if (!token)
4599
4684
  return null;
4600
4685
  try {
4601
- return config.bearerDecryptFn?.(token) ?? decryptBearer(token);
4686
+ const decoded = config.bearerDecryptFn?.(token) ?? decryptBearer(token);
4687
+ return patchQueryDevtoolsTokenPayload({
4688
+ payload: decoded,
4689
+ providerName: config.name,
4690
+ expiresInPropertyName: 'exp',
4691
+ });
4602
4692
  }
4603
4693
  catch (error) {
4604
4694
  if (isDevMode()) {
@@ -4668,8 +4758,10 @@ const createBearerAuthProviderImpl = (config) => {
4668
4758
  name: config.name,
4669
4759
  applyTokens,
4670
4760
  logout,
4761
+ sessionEndCause: sessionEndCause.asReadonly(),
4671
4762
  });
4672
4763
  const querySetupContext = {
4764
+ name: config.name,
4673
4765
  accessToken,
4674
4766
  refreshToken,
4675
4767
  bearerDecryptFn: config.bearerDecryptFn,
@@ -4743,6 +4835,19 @@ const createBearerAuthProviderImpl = (config) => {
4743
4835
  };
4744
4836
  const createBearerAuthProvider = (config) => defineRootProvider(() => createBearerAuthProviderImpl(config), { name: `BearerAuthProvider_${config.name}` });
4745
4837
 
4838
+ /** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
4839
+ const rescheduleDelayMs = 5000;
4840
+ /**
4841
+ * How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
4842
+ * declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
4843
+ */
4844
+ const maxRescheduleAttempts = 5;
4845
+ /**
4846
+ * How many 401-driven refreshes may run back to back before they fall back to `minRefreshInterval`.
4847
+ * Every refresh in such a streak means the token the previous one issued was rejected too, so an
4848
+ * unbounded streak is a refresh loop, not recovery. A secure request succeeding ends the streak.
4849
+ */
4850
+ const maxUnauthorizedRefreshStreak = 3;
4746
4851
  const withAuthenticationQuery = (key, config, setup) => ({
4747
4852
  _type: 'authQuery',
4748
4853
  key,
@@ -4785,30 +4890,60 @@ const withRefreshQuery = (key, config) => {
4785
4890
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
4786
4891
  const refreshIfExpired = config.refreshIfExpired ?? true;
4787
4892
  let lastRefreshTime = 0;
4893
+ let lastUnauthorizedRefreshTime = 0;
4894
+ let unauthorizedRefreshStreak = 0;
4788
4895
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4789
4896
  const refreshQuery = () => context.queries[key];
4790
4897
  const executeRefresh = (reason) => {
4791
4898
  const currentRefreshToken = context.refreshToken();
4792
4899
  if (!currentRefreshToken)
4793
- return;
4900
+ return 'noToken';
4794
4901
  if (!context.isLeader()) {
4795
4902
  // Only the leader may spend a single-use refresh token, but the event is still real - hand it
4796
4903
  // over instead of dropping it, or this tab waits out the leader's timer with a dead token.
4797
4904
  context.refreshCoordination?.request();
4798
- return;
4905
+ return 'delegated';
4799
4906
  }
4800
4907
  // Any token-issuing execution, not just another refresh: a login already in flight is about to
4801
4908
  // issue a token pair of its own, and the refresh token this would spend belongs to the session
4802
4909
  // that login is replacing.
4803
4910
  if (context.hasTokenIssuingExecutionInFlight())
4804
- return;
4911
+ return 'busy';
4805
4912
  if (reason === 'scheduled') {
4806
4913
  const now = Date.now();
4807
4914
  if (now - lastRefreshTime < minRefreshInterval)
4808
- return;
4915
+ return 'throttled';
4809
4916
  lastRefreshTime = now;
4810
4917
  }
4918
+ else {
4919
+ const now = Date.now();
4920
+ if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
4921
+ now - lastUnauthorizedRefreshTime < minRefreshInterval) {
4922
+ return 'throttled';
4923
+ }
4924
+ unauthorizedRefreshStreak++;
4925
+ lastUnauthorizedRefreshTime = now;
4926
+ }
4811
4927
  refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
4928
+ return 'executed';
4929
+ };
4930
+ /**
4931
+ * How long a scheduled refresh that did not spend the refresh token waits before trying again.
4932
+ * `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
4933
+ * the schedule anyway.
4934
+ */
4935
+ const retryScheduledIn = (attempt) => {
4936
+ switch (attempt) {
4937
+ case 'executed':
4938
+ case 'noToken':
4939
+ return null;
4940
+ case 'throttled':
4941
+ return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
4942
+ case 'busy':
4943
+ return rescheduleDelayMs;
4944
+ case 'delegated':
4945
+ return minRefreshInterval;
4946
+ }
4812
4947
  };
4813
4948
  const calculateRefreshBuffer = (tokenLifetimeMs) => {
4814
4949
  if (typeof config.refreshStrategy === 'number') {
@@ -4820,48 +4955,58 @@ const withRefreshQuery = (key, config) => {
4820
4955
  const calculatedBuffer = tokenLifetimeMs * (1 - percentage);
4821
4956
  return Math.max(minBufferMs, Math.min(maxBufferMs, calculatedBuffer));
4822
4957
  };
4823
- // Auto-refresh based on token expiration
4824
- toObservable(context.accessToken)
4825
- .pipe(switchMap((token) => {
4826
- if (!token)
4827
- return of(null);
4958
+ /** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
4959
+ const scheduledRefreshDelay = (token) => {
4828
4960
  try {
4829
- const bearerDataValue = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
4961
+ const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
4962
+ const bearerDataValue = patchQueryDevtoolsTokenPayload({
4963
+ payload: decoded,
4964
+ providerName: context.name,
4965
+ expiresInPropertyName,
4966
+ });
4830
4967
  const expiresIn = bearerDataValue?.[expiresInPropertyName];
4831
4968
  if (typeof expiresIn !== 'number') {
4832
4969
  if (isDevMode()) {
4833
4970
  console.warn(`Token does not contain valid ${expiresInPropertyName} property for auto-refresh`);
4834
4971
  }
4835
- return of(null);
4972
+ return null;
4836
4973
  }
4837
- const expiresAtMs = expiresIn * 1000;
4838
- const now = Date.now();
4839
- const tokenLifetimeMs = expiresAtMs - now;
4974
+ const tokenLifetimeMs = expiresIn * 1000 - Date.now();
4840
4975
  if (tokenLifetimeMs <= 0) {
4841
- if (refreshIfExpired) {
4842
- if (isDevMode()) {
4843
- console.warn('Token is already expired, triggering immediate refresh');
4844
- }
4845
- return of(true);
4976
+ if (!refreshIfExpired)
4977
+ return null;
4978
+ if (isDevMode()) {
4979
+ console.warn('Token is already expired, triggering immediate refresh');
4846
4980
  }
4847
- return of(null);
4848
- }
4849
- const refreshBufferMs = calculateRefreshBuffer(tokenLifetimeMs);
4850
- const timeUntilRefresh = tokenLifetimeMs - refreshBufferMs;
4851
- if (timeUntilRefresh <= 0) {
4852
- return of(true);
4981
+ return 0;
4853
4982
  }
4854
- return timer(timeUntilRefresh).pipe(tap(() => true));
4983
+ return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
4855
4984
  }
4856
4985
  catch {
4857
- return of(null);
4858
- }
4859
- }), takeUntilDestroyed())
4860
- .subscribe((shouldRefresh) => {
4861
- if (shouldRefresh) {
4862
- executeRefresh('scheduled');
4986
+ return null;
4863
4987
  }
4864
- });
4988
+ };
4989
+ /**
4990
+ * The scheduled refresh, re-armed for as long as it keeps declining to spend the refresh token.
4991
+ * Without the re-arm a single declined attempt strands the session on a token nothing renews until
4992
+ * a request fails with a 401 - by which time the refresh token may be gone too.
4993
+ */
4994
+ const scheduledRefresh = (dueInMs, attemptsLeft) => (dueInMs <= 0 ? of(0) : timer(dueInMs)).pipe(concatMap(() => {
4995
+ const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
4996
+ return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
4997
+ }));
4998
+ // The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
4999
+ // lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
5000
+ // timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
5001
+ // two tokens that happen to be due at the same moment still restart the timer.
5002
+ const nextScheduledRefresh = computed(() => {
5003
+ const token = context.accessToken();
5004
+ return { dueInMs: token ? scheduledRefreshDelay(token) : null };
5005
+ }, /* @ts-ignore */
5006
+ ...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
5007
+ toObservable(nextScheduledRefresh)
5008
+ .pipe(switchMap(({ dueInMs }) => (dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts))), takeUntilDestroyed())
5009
+ .subscribe();
4865
5010
  context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
4866
5011
  const onRefreshFailure = config.onRefreshFailure ??
4867
5012
  (({ error, logout }) => {
@@ -4880,18 +5025,22 @@ const withRefreshQuery = (key, config) => {
4880
5025
  // Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
4881
5026
  const autoRetryOn401 = config.autoRetryOn401 ?? true;
4882
5027
  if (autoRetryOn401) {
4883
- context.repository.events$
4884
- .pipe(filter((event) => {
4885
- // Only handle 401 errors for secure queries
4886
- if (event.type !== 'request-error')
4887
- return false;
4888
- if (!event.isSecure)
4889
- return false;
4890
- if (event.error?.status !== 401)
4891
- return false;
4892
- return true;
4893
- }), takeUntilDestroyed())
4894
- .subscribe(() => {
5028
+ context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
5029
+ if (event.type === 'request-success' && event.isSecure) {
5030
+ unauthorizedRefreshStreak = 0;
5031
+ return;
5032
+ }
5033
+ if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
5034
+ return;
5035
+ // A 401 from a request that went out with an older access token asks for a refresh that
5036
+ // already happened - the query re-runs itself with the current token (see
5037
+ // createSecureExecuteFactory). Refreshing again here would spend the refresh token that
5038
+ // pair came with, invalidating the tokens every other in-flight request is using; their
5039
+ // 401s would each do the same, keeping the loop alive for as long as queries are in flight.
5040
+ const sentAuthorization = event.request.subtle.lastSentHeaders()?.get('Authorization');
5041
+ const currentToken = context.accessToken();
5042
+ if (sentAuthorization && currentToken && sentAuthorization !== `Bearer ${currentToken}`)
5043
+ return;
4895
5044
  executeRefresh('unauthorized');
4896
5045
  });
4897
5046
  }
@@ -5176,6 +5325,12 @@ const resetEncryptionKey = () => {
5176
5325
  }
5177
5326
  };
5178
5327
 
5328
+ /**
5329
+ * How an incoming logout reads in the receiving tab: a deliberate logout elsewhere is this tab's
5330
+ * `otherTab`, while a session that ended on its own ended for every tab, so that cause is carried
5331
+ * through. A message without one comes from a tab running an older version.
5332
+ */
5333
+ const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
5179
5334
  /**
5180
5335
  * The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
5181
5336
  * providers reachable from the same origin do not push each other's tokens around.
@@ -5210,7 +5365,7 @@ const setupMultiTabSync = (config, context) => {
5210
5365
  if (message.type === 'logout' && syncLogout) {
5211
5366
  lastSyncedState = LOGGED_OUT;
5212
5367
  hadTokens = false;
5213
- context.logout('otherTab');
5368
+ context.logout(incomingCause(message.cause));
5214
5369
  return;
5215
5370
  }
5216
5371
  if (message.type !== 'tokens-updated' || !syncTokens)
@@ -5258,7 +5413,7 @@ const setupMultiTabSync = (config, context) => {
5258
5413
  if (lastSyncedState === LOGGED_OUT)
5259
5414
  return;
5260
5415
  lastSyncedState = LOGGED_OUT;
5261
- const message = { type: 'logout' };
5416
+ const message = { type: 'logout', cause: untracked(context.sessionEndCause) ?? undefined };
5262
5417
  channel.postMessage(message);
5263
5418
  });
5264
5419
  }
@@ -5314,6 +5469,7 @@ const withBearerAuthMultiTabSync = (config = {}) => {
5314
5469
  setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
5315
5470
  accessToken: context.accessToken,
5316
5471
  refreshToken: context.refreshToken,
5472
+ sessionEndCause: context.sessionEndCause,
5317
5473
  name: context.name,
5318
5474
  applyTokens: context.applyTokens,
5319
5475
  logout: context.logout,
@@ -5350,6 +5506,7 @@ const withPersistentAuth = (config) => {
5350
5506
  ...(config.autoLogin.excludeRoutes?.length
5351
5507
  ? [{ label: 'excluded routes', value: config.autoLogin.excludeRoutes.join(', ') }]
5352
5508
  : []),
5509
+ ...(config.autoLogin.shouldAutoLogin ? [{ label: 'auto login predicate', value: 'custom' }] : []),
5353
5510
  ],
5354
5511
  };
5355
5512
  };
@@ -5410,6 +5567,9 @@ const createPersistentAuthFeature = (config, context) => {
5410
5567
  if (shouldExclude) {
5411
5568
  return;
5412
5569
  }
5570
+ if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
5571
+ return;
5572
+ }
5413
5573
  const storedToken = getCookie(cookieName);
5414
5574
  if (storedToken) {
5415
5575
  const decryptedToken = decryptToken(storedToken);
@@ -5681,6 +5841,179 @@ const createTrackingFeature = (context, config) => {
5681
5841
  };
5682
5842
  };
5683
5843
 
5844
+ const ethlete = {};
5845
+ let app = null;
5846
+ /** What `window.ethlete` exposes - the same object {@link queryDevtoolsAbout} returns. */
5847
+ const publish = () => {
5848
+ if (typeof window === 'undefined')
5849
+ return;
5850
+ window['ethlete'] = queryDevtoolsAbout();
5851
+ };
5852
+ /**
5853
+ * Records that an `@ethlete/*` package is loaded, and at which version. Each lib passes its own
5854
+ * generated constant; a package that is never imported never reports one, which is what makes the
5855
+ * list "what is actually loaded" rather than "what is declared as a peer dependency".
5856
+ */
5857
+ const registerEthleteVersion = (name, version) => {
5858
+ ethlete[name] = version;
5859
+ publish();
5860
+ };
5861
+ const setQueryDevtoolsAppInfo = (info) => {
5862
+ app = info ?? null;
5863
+ publish();
5864
+ };
5865
+ /**
5866
+ * Everything the devtools know about what is running: the loaded `@ethlete/*` versions, the Angular
5867
+ * version, and whatever the application handed to `provideQueryDevtools({ about })`.
5868
+ *
5869
+ * Also mirrored onto `window.ethlete` - the same idea as Angular's `window.ng`, so the versions behind
5870
+ * a bug report can be read straight from the console without opening the panel.
5871
+ */
5872
+ const queryDevtoolsAbout = () => ({
5873
+ ethlete: { ...ethlete },
5874
+ angular: VERSION.full,
5875
+ app,
5876
+ });
5877
+
5878
+ const DEFAULTS = {
5879
+ viewState: 'session',
5880
+ pins: 'local',
5881
+ overrides: 'none',
5882
+ mocks: 'local',
5883
+ armedMocks: 'none',
5884
+ armedFaults: 'none',
5885
+ maxEvents: 100,
5886
+ maxDroppedCacheEntries: 20,
5887
+ responseHistory: null,
5888
+ };
5889
+ /**
5890
+ * `localStorage`, whatever the scopes below say - a scope of `none` still has to be remembered, or the
5891
+ * choice would erase itself on the first reload it applies to.
5892
+ */
5893
+ const SETTINGS_KEY = 'ethlete:query:devtools:settings:v1';
5894
+ const LIMITS = {
5895
+ maxEvents: { min: 10, max: 1000 },
5896
+ maxDroppedCacheEntries: { min: 0, max: 200 },
5897
+ responseHistory: { min: 1, max: 50 },
5898
+ };
5899
+ const settings = /* @__PURE__ */ signal(DEFAULTS, /* @ts-ignore */
5900
+ ...(ngDevMode ? [{ debugName: "settings" }] : /* istanbul ignore next */ []));
5901
+ /**
5902
+ * How the devtools are configured. Every consumer reads the live value rather than a copy, so a change
5903
+ * in the panel's Settings takes effect without a reload.
5904
+ *
5905
+ * Part of the devtools contract. **Not part of the general public contract.**
5906
+ */
5907
+ const queryDevtoolsSettings = /* @__PURE__ */ settings.asReadonly();
5908
+ /**
5909
+ * The `Storage` a scope names, or `null` for `none` and for a browser that refuses one (private mode,
5910
+ * storage disabled, server-side rendering). Callers treat "no storage" and "not persisted" alike.
5911
+ *
5912
+ * Part of the devtools contract. **Not part of the general public contract.**
5913
+ */
5914
+ const queryDevtoolsStorage = (scope) => {
5915
+ if (scope === 'none' || typeof window === 'undefined')
5916
+ return null;
5917
+ try {
5918
+ return scope === 'session' ? window.sessionStorage : window.localStorage;
5919
+ }
5920
+ catch {
5921
+ return null;
5922
+ }
5923
+ };
5924
+ /**
5925
+ * Reads one JSON-encoded devtools key from the store a scope names, or `null`.
5926
+ *
5927
+ * Part of the devtools contract. **Not part of the general public contract.**
5928
+ */
5929
+ const readQueryDevtoolsStore = (scope, key) => {
5930
+ try {
5931
+ const raw = queryDevtoolsStorage(scope)?.getItem(key);
5932
+ return raw ? JSON.parse(raw) : null;
5933
+ }
5934
+ catch {
5935
+ return null;
5936
+ }
5937
+ };
5938
+ /**
5939
+ * Writes one devtools key into the store a scope names. A scope of `none` writes nothing.
5940
+ *
5941
+ * Part of the devtools contract. **Not part of the general public contract.**
5942
+ */
5943
+ const writeQueryDevtoolsStore = (scope, key, value) => {
5944
+ try {
5945
+ queryDevtoolsStorage(scope)?.setItem(key, JSON.stringify(value));
5946
+ }
5947
+ catch {
5948
+ // ignore (private mode / disabled storage / quota)
5949
+ }
5950
+ };
5951
+ /**
5952
+ * Removes one devtools key from **both** stores, whatever the scope currently is - so a scope that
5953
+ * changed cannot leave a stale copy behind for the next load to read.
5954
+ *
5955
+ * Part of the devtools contract. **Not part of the general public contract.**
5956
+ */
5957
+ const clearQueryDevtoolsStore = (key) => {
5958
+ for (const scope of ['session', 'local']) {
5959
+ try {
5960
+ queryDevtoolsStorage(scope)?.removeItem(key);
5961
+ }
5962
+ catch {
5963
+ // ignore (private mode / disabled storage)
5964
+ }
5965
+ }
5966
+ };
5967
+ const asScope = (value, fallback) => value === 'none' || value === 'session' || value === 'local' ? value : fallback;
5968
+ const asCount = (value, limits, fallback) => {
5969
+ const count = Math.floor(Number(value));
5970
+ if (!Number.isFinite(count))
5971
+ return fallback;
5972
+ return Math.min(Math.max(count, limits.min), limits.max);
5973
+ };
5974
+ /** A count that may be absent, where absent means "leave the application's own value alone". */
5975
+ const asOptionalCount = (value, limits) => {
5976
+ if (value === null || value === undefined)
5977
+ return null;
5978
+ const count = Math.floor(Number(value));
5979
+ return Number.isFinite(count) ? Math.min(Math.max(count, limits.min), limits.max) : null;
5980
+ };
5981
+ /** Both a hand-edited store and a number typed into the panel come through here. */
5982
+ const sanitize = (value) => ({
5983
+ viewState: asScope(value?.viewState, DEFAULTS.viewState),
5984
+ pins: asScope(value?.pins, DEFAULTS.pins),
5985
+ overrides: asScope(value?.overrides, DEFAULTS.overrides),
5986
+ mocks: asScope(value?.mocks, DEFAULTS.mocks),
5987
+ armedMocks: asScope(value?.armedMocks, DEFAULTS.armedMocks),
5988
+ armedFaults: asScope(value?.armedFaults, DEFAULTS.armedFaults),
5989
+ maxEvents: asCount(value?.maxEvents, LIMITS.maxEvents, DEFAULTS.maxEvents),
5990
+ maxDroppedCacheEntries: asCount(value?.maxDroppedCacheEntries, LIMITS.maxDroppedCacheEntries, DEFAULTS.maxDroppedCacheEntries),
5991
+ responseHistory: asOptionalCount(value?.responseHistory, LIMITS.responseHistory),
5992
+ });
5993
+ /**
5994
+ * Reads the stored settings. Called by `provideQueryDevtools()` **before** the override store is read
5995
+ * and the response-history default is set - both take their configuration from here - and by nothing
5996
+ * else.
5997
+ * @internal
5998
+ */
5999
+ const initQueryDevtoolsSettings = () => {
6000
+ settings.set(sanitize(readQueryDevtoolsStore('local', SETTINGS_KEY)));
6001
+ };
6002
+ /**
6003
+ * Changes part of the devtools configuration and stores it. Values outside the range a field allows are
6004
+ * clamped rather than rejected.
6005
+ *
6006
+ * Overrides are the one field not to set here: their scope moves an existing store, so it goes through
6007
+ * `setQueryDevtoolsOverridesScope()` instead.
6008
+ *
6009
+ * Part of the devtools contract. **Not part of the general public contract.**
6010
+ */
6011
+ const setQueryDevtoolsSettings = (patch) => {
6012
+ const next = sanitize({ ...settings(), ...patch });
6013
+ settings.set(next);
6014
+ writeQueryDevtoolsStore('local', SETTINGS_KEY, next);
6015
+ };
6016
+
5684
6017
  /** The neutral fault: nothing armed. */
5685
6018
  const EMPTY_QUERY_DEVTOOLS_FAULT = {
5686
6019
  latencyMs: 0,
@@ -5705,23 +6038,73 @@ const QUERY_DEVTOOLS_FAULT_STATUSES = [
5705
6038
  { status: 502, label: 'Bad Gateway', retryable: true },
5706
6039
  { status: 503, label: 'Service Unavailable', retryable: true },
5707
6040
  ];
6041
+ const STORAGE_KEY$2 = 'ethlete:query:devtools:faults:v1';
5708
6042
  const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
5709
6043
  ...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
6044
+ const restored$1 = /* @__PURE__ */ signal(false, /* @ts-ignore */
6045
+ ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
6046
+ const scope$2 = () => queryDevtoolsSettings().armedFaults;
5710
6047
  /**
5711
6048
  * The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
5712
6049
  * are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
5713
6050
  */
5714
6051
  const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
5715
- /** Whether anything at all is armed on a fault. */
5716
- const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
5717
6052
  /**
5718
- * Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
5719
- * value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
5720
- *
5721
- * Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
5722
- * something an application should arm on itself.
6053
+ * Whether what is armed right now came back from a previous page load rather than being armed by hand.
6054
+ * The panel's armed bar says so, because injected failures nobody remembers arming look exactly like a
6055
+ * broken API.
5723
6056
  */
5724
- const setQueryDevtoolsFault = (options) => {
6057
+ const queryDevtoolsFaultsRestored = /* @__PURE__ */ restored$1.asReadonly();
6058
+ const write$1 = () => {
6059
+ if (scope$2() === 'none') {
6060
+ clearQueryDevtoolsStore(STORAGE_KEY$2);
6061
+ return;
6062
+ }
6063
+ writeQueryDevtoolsStore(scope$2(), STORAGE_KEY$2, faults());
6064
+ };
6065
+ const asFault = (value) => {
6066
+ const fault = value;
6067
+ if (!fault || typeof fault !== 'object')
6068
+ return null;
6069
+ const next = {
6070
+ latencyMs: Number(fault.latencyMs) || 0,
6071
+ failNext: Number(fault.failNext) || 0,
6072
+ failRate: Number(fault.failRate) || 0,
6073
+ status: Number.isFinite(Number(fault.status)) ? Number(fault.status) : EMPTY_QUERY_DEVTOOLS_FAULT.status,
6074
+ };
6075
+ return isQueryDevtoolsFaultArmed(next) ? next : null;
6076
+ };
6077
+ /**
6078
+ * Reads the armed faults back, when the `armedFaults` setting asks for them. Called by
6079
+ * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
6080
+ * @internal
6081
+ */
6082
+ const initQueryDevtoolsFaults = () => {
6083
+ const stored = readQueryDevtoolsStore(scope$2(), STORAGE_KEY$2);
6084
+ const inherited = {};
6085
+ for (const [clientName, value] of Object.entries(stored ?? {})) {
6086
+ const fault = asFault(value);
6087
+ if (fault)
6088
+ inherited[clientName] = fault;
6089
+ }
6090
+ faults.set(inherited);
6091
+ restored$1.set(Object.keys(inherited).length > 0);
6092
+ };
6093
+ /**
6094
+ * Where the armed faults are kept. Switching to a scope that keeps them stores whatever is armed right
6095
+ * now; switching to `none` empties the store and leaves this page's faults exactly where they are.
6096
+ */
6097
+ const setQueryDevtoolsFaultsScope = (next) => {
6098
+ if (next === scope$2())
6099
+ return;
6100
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6101
+ clearQueryDevtoolsStore(STORAGE_KEY$2);
6102
+ setQueryDevtoolsSettings({ armedFaults: next });
6103
+ write$1();
6104
+ };
6105
+ /** Whether anything at all is armed on a fault. */
6106
+ const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
6107
+ const patchFault = (options) => {
5725
6108
  const { clientName, patch } = options;
5726
6109
  faults.update((current) => {
5727
6110
  const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
@@ -5731,18 +6114,34 @@ const setQueryDevtoolsFault = (options) => {
5731
6114
  }
5732
6115
  return { ...current, [clientName]: next };
5733
6116
  });
6117
+ write$1();
6118
+ };
6119
+ /**
6120
+ * Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
6121
+ * value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
6122
+ *
6123
+ * Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
6124
+ * something an application should arm on itself.
6125
+ */
6126
+ const setQueryDevtoolsFault = (options) => {
6127
+ patchFault(options);
6128
+ restored$1.set(false);
5734
6129
  };
5735
6130
  /**
5736
6131
  * Disarms every fault of one client, or of all clients when no name is given.
5737
6132
  *
5738
6133
  * @see setQueryDevtoolsFault
5739
6134
  */
5740
- const clearQueryDevtoolsFaults = (clientName) => faults.update((current) => {
5741
- if (clientName === undefined)
5742
- return {};
5743
- const { [clientName]: _removed, ...rest } = current;
5744
- return rest;
5745
- });
6135
+ const clearQueryDevtoolsFaults = (clientName) => {
6136
+ faults.update((current) => {
6137
+ if (clientName === undefined)
6138
+ return {};
6139
+ const { [clientName]: _removed, ...rest } = current;
6140
+ return rest;
6141
+ });
6142
+ restored$1.set(false);
6143
+ write$1();
6144
+ };
5746
6145
  /**
5747
6146
  * Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
5748
6147
  * must be called once per attempt and never speculatively.
@@ -5757,7 +6156,9 @@ const resolveQueryDevtoolsFaultForAttempt = (target) => {
5757
6156
  let shouldFail = false;
5758
6157
  if (fault.failNext > 0) {
5759
6158
  shouldFail = true;
5760
- setQueryDevtoolsFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
6159
+ // Not the public setter: spending the budget is the armed fault doing its job, not someone re-arming
6160
+ // it, so it must not clear what the panel's bar says about where the fault came from.
6161
+ patchFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
5761
6162
  }
5762
6163
  else if (fault.failRate > 0) {
5763
6164
  shouldFail = Math.random() * 100 < fault.failRate;
@@ -5794,6 +6195,392 @@ const createQueryDevtoolsFormLinks = () => {
5794
6195
  };
5795
6196
  };
5796
6197
 
6198
+ /**
6199
+ * The designed library. Which of them is **armed** is a separate store under a separate scope - see
6200
+ * {@link queryDevtoolsArmedMocks}.
6201
+ */
6202
+ const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
6203
+ const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
6204
+ const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
6205
+ ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
6206
+ const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
6207
+ ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
6208
+ const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
6209
+ ...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
6210
+ const scope$1 = () => queryDevtoolsSettings().mocks;
6211
+ const armedScope = () => queryDevtoolsSettings().armedMocks;
6212
+ /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
6213
+ const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
6214
+ /**
6215
+ * The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
6216
+ * an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
6217
+ */
6218
+ const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
6219
+ /**
6220
+ * Whether what is armed right now came back from a previous page load rather than being armed by hand.
6221
+ * The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
6222
+ */
6223
+ const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
6224
+ /**
6225
+ * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
6226
+ * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
6227
+ */
6228
+ const queryDevtoolsMockId = (parts) => `${parts.clientName}|${parts.method}|${parts.pattern}${parts.query ? `?${parts.query}` : ''}`;
6229
+ const write = () => {
6230
+ if (scope$1() === 'none') {
6231
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
6232
+ return;
6233
+ }
6234
+ writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
6235
+ };
6236
+ const writeArmed = () => {
6237
+ if (armedScope() === 'none') {
6238
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6239
+ return;
6240
+ }
6241
+ writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
6242
+ };
6243
+ /** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
6244
+ const setArmed = (next) => {
6245
+ armed.set(next);
6246
+ armedRestored.set(false);
6247
+ writeArmed();
6248
+ };
6249
+ const isMock = (value) => {
6250
+ const mock = value;
6251
+ return (!!mock &&
6252
+ typeof mock.id === 'string' &&
6253
+ typeof mock.clientName === 'string' &&
6254
+ typeof mock.method === 'string' &&
6255
+ typeof mock.pattern === 'string' &&
6256
+ typeof mock.status === 'number');
6257
+ };
6258
+ /**
6259
+ * Reads the designed library, so a mock can be armed without authoring it again. Called by
6260
+ * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
6261
+ * @internal
6262
+ */
6263
+ const initQueryDevtoolsMocks = () => {
6264
+ const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
6265
+ // `query` was added after the first version of the store, so an entry written before it still loads.
6266
+ mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
6267
+ const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
6268
+ const library = new Set(mocks().map((mock) => mock.id));
6269
+ // An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
6270
+ // mock that may never be designed again.
6271
+ const inherited = Array.isArray(kept)
6272
+ ? kept.filter((id) => typeof id === 'string' && library.has(id))
6273
+ : [];
6274
+ armed.set(new Set(inherited));
6275
+ armedRestored.set(inherited.length > 0);
6276
+ };
6277
+ /** The path of a request URL, without its origin or query string - what a pattern is matched against. */
6278
+ const queryDevtoolsRequestPath = (url) => {
6279
+ const withoutQuery = url.split('?')[0] ?? '';
6280
+ const schemeEnd = withoutQuery.indexOf('://');
6281
+ if (schemeEnd === -1)
6282
+ return withoutQuery;
6283
+ const pathStart = withoutQuery.indexOf('/', schemeEnd + 3);
6284
+ return pathStart === -1 ? '/' : withoutQuery.slice(pathStart);
6285
+ };
6286
+ const segmentsOf$1 = (path) => path.split('/').filter(Boolean);
6287
+ /** The query string of a request URL, without the `?`. */
6288
+ const queryOf = (url) => url.split('?')[1] ?? '';
6289
+ /**
6290
+ * Whether a request carries every query parameter a mock declares. Extra parameters are ignored - a mock
6291
+ * says what the request must ask for, not everything it may.
6292
+ */
6293
+ const matchesQueryDevtoolsMockQuery = (declared, url) => {
6294
+ if (!declared)
6295
+ return true;
6296
+ const asked = new URLSearchParams(queryOf(url));
6297
+ return [...new URLSearchParams(declared)].every(([key, value]) => asked.get(key) === value);
6298
+ };
6299
+ /**
6300
+ * Whether a request path is the route a pattern describes. A `:param` segment matches any single
6301
+ * non-empty segment, which is the whole point: a mock is armed for a route, not for the one URL a query
6302
+ * happened to build.
6303
+ */
6304
+ const matchesQueryDevtoolsMockPattern = (pattern, path) => {
6305
+ const patternSegments = segmentsOf$1(pattern);
6306
+ const pathSegments = segmentsOf$1(path);
6307
+ if (patternSegments.length !== pathSegments.length)
6308
+ return false;
6309
+ return patternSegments.every((segment, index) => segment.startsWith(':') || segment === pathSegments[index]);
6310
+ };
6311
+ /**
6312
+ * Adds a designed mock, or replaces the one already under its id. Arming is untouched: re-designing a
6313
+ * body while it is being served changes what the next request gets.
6314
+ */
6315
+ const saveQueryDevtoolsMock = (mock) => {
6316
+ mocks.update((current) => {
6317
+ const index = current.findIndex((entry) => entry.id === mock.id);
6318
+ if (index === -1)
6319
+ return [...current, mock];
6320
+ return current.map((entry) => (entry.id === mock.id ? mock : entry));
6321
+ });
6322
+ write();
6323
+ };
6324
+ /** Removes a designed mock, disarming it on the way out. */
6325
+ const deleteQueryDevtoolsMock = (id) => {
6326
+ mocks.update((current) => current.filter((entry) => entry.id !== id));
6327
+ armQueryDevtoolsMock(id, false);
6328
+ write();
6329
+ };
6330
+ /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
6331
+ const armQueryDevtoolsMock = (id, isArmed) => {
6332
+ if (isArmed && !mocks().some((mock) => mock.id === id))
6333
+ return;
6334
+ const next = new Set(armed());
6335
+ if (isArmed)
6336
+ next.add(id);
6337
+ else
6338
+ next.delete(id);
6339
+ setArmed(next);
6340
+ };
6341
+ /** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
6342
+ const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
6343
+ /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
6344
+ const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
6345
+ /**
6346
+ * Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
6347
+ * the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
6348
+ * exactly what it was already serving.
6349
+ */
6350
+ const setQueryDevtoolsArmedMocksScope = (next) => {
6351
+ if (next === armedScope())
6352
+ return;
6353
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6354
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6355
+ setQueryDevtoolsSettings({ armedMocks: next });
6356
+ writeArmed();
6357
+ };
6358
+ /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
6359
+ const clearQueryDevtoolsMockStore = () => {
6360
+ mocks.set([]);
6361
+ armed.set(new Set());
6362
+ armedRestored.set(false);
6363
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
6364
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
6365
+ };
6366
+ const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
6367
+ /**
6368
+ * The mock to serve one upcoming attempt, or `null` to let it reach the network. The armed mock declaring
6369
+ * the most query parameters wins, so a special case armed on top of a general one is what answers.
6370
+ *
6371
+ * Installed as the mock resolver by `provideQueryDevtools()`; nothing else may call it.
6372
+ * @internal
6373
+ */
6374
+ const resolveQueryDevtoolsMockForAttempt = (target) => {
6375
+ const ids = armed();
6376
+ if (!ids.size)
6377
+ return null;
6378
+ const path = queryDevtoolsRequestPath(target.url);
6379
+ const mock = mocks()
6380
+ .filter((candidate) => ids.has(candidate.id) &&
6381
+ candidate.clientName === target.clientName &&
6382
+ candidate.method === target.method &&
6383
+ matchesQueryDevtoolsMockPattern(candidate.pattern, path) &&
6384
+ matchesQueryDevtoolsMockQuery(candidate.query, target.url))
6385
+ // The most specific armed mock answers: one that names `page=2` beats one that takes any query, so
6386
+ // arming a general mock does not shadow the special case someone armed on top of it.
6387
+ .sort((a, b) => declaredParamCount(b) - declaredParamCount(a))[0];
6388
+ if (!mock)
6389
+ return null;
6390
+ return { status: mock.status, body: mock.body, latencyMs: mock.latencyMs };
6391
+ };
6392
+
6393
+ /**
6394
+ * Not persisted at all unless asked for, and then in `sessionStorage`: "survives a reload" and "survives
6395
+ * until I notice" are different promises, and an app that stays tampered with across days is hours of
6396
+ * debugging the wrong thing. `local` is offered in Settings for the dev who has a reason, and the
6397
+ * restored-overrides bar names the scope whatever it is.
6398
+ */
6399
+ const STORAGE_KEY = 'ethlete:query:devtools:overrides:v1';
6400
+ const restored = /* @__PURE__ */ signal([], /* @ts-ignore */
6401
+ ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
6402
+ const restoredScope = /* @__PURE__ */ signal(null, /* @ts-ignore */
6403
+ ...(ngDevMode ? [{ debugName: "restoredScope" }] : /* istanbul ignore next */ []));
6404
+ const scope = () => queryDevtoolsSettings().overrides;
6405
+ /** Where "Keep across reloads" puts them back when it is switched on again. */
6406
+ let lastPersistentScope = 'session';
6407
+ /** Every recorder that currently holds at least one op, so switching persistence on can capture them. */
6408
+ const armedRecorders = /* @__PURE__ */ new Map();
6409
+ /** What the store held when this page loaded, per id - the banner's subject, not the live registry. */
6410
+ const carriedOver = /* @__PURE__ */ new Map();
6411
+ /** Of those, the ids a registering query actually took. */
6412
+ const replayed = /* @__PURE__ */ new Set();
6413
+ let ops = {};
6414
+ const readStore = () => {
6415
+ const parsed = readQueryDevtoolsStore(scope(), STORAGE_KEY);
6416
+ const stored = parsed?.ops;
6417
+ const usable = !!stored && typeof stored === 'object' && !Array.isArray(stored);
6418
+ return { enabled: parsed?.enabled === true, ops: usable ? stored : {} };
6419
+ };
6420
+ const writeStore = () => {
6421
+ if (scope() === 'none') {
6422
+ clearQueryDevtoolsStore(STORAGE_KEY);
6423
+ return;
6424
+ }
6425
+ writeQueryDevtoolsStore(scope(), STORAGE_KEY, { enabled: true, ops });
6426
+ };
6427
+ const publishRestored = () => restored.set([...carriedOver].map(([id, count]) => ({ id, count, armed: replayed.has(id) })));
6428
+ const forgetCarriedOver = () => {
6429
+ carriedOver.clear();
6430
+ replayed.clear();
6431
+ restoredScope.set(null);
6432
+ publishRestored();
6433
+ };
6434
+ const writeOps = (id, list) => {
6435
+ if (list.length)
6436
+ ops = { ...ops, [id]: list };
6437
+ else {
6438
+ const { [id]: _dropped, ...rest } = ops;
6439
+ ops = rest;
6440
+ }
6441
+ writeStore();
6442
+ };
6443
+ /**
6444
+ * Reads the store, so entries can replay from it as they register. Called by `provideQueryDevtools()`,
6445
+ * which is what keeps an app without devtools from ever touching storage.
6446
+ * @internal
6447
+ */
6448
+ const initQueryDevtoolsOverridePersistence = () => {
6449
+ const store = readStore();
6450
+ ops = store.enabled ? store.ops : {};
6451
+ armedRecorders.clear();
6452
+ carriedOver.clear();
6453
+ replayed.clear();
6454
+ for (const [id, list] of Object.entries(ops)) {
6455
+ if (list.length)
6456
+ carriedOver.set(id, list.length);
6457
+ }
6458
+ const current = scope();
6459
+ if (current !== 'none')
6460
+ lastPersistentScope = current;
6461
+ restoredScope.set(carriedOver.size ? current : null);
6462
+ publishRestored();
6463
+ };
6464
+ /**
6465
+ * Whether armed response overrides are stored and replayed on the next page load - that is, whether
6466
+ * their scope is anything other than `none`. Off by default.
6467
+ */
6468
+ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope() !== 'none', /* @ts-ignore */
6469
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsOverridePersistence" }] : /* istanbul ignore next */ []));
6470
+ /**
6471
+ * Where armed response overrides are kept. Switching to a scope that keeps them captures whatever is
6472
+ * armed right now, so the choice reads as "keep these" rather than "keep the next ones"; switching
6473
+ * between `session` and `local` moves the store; switching to `none` empties it and leaves every armed
6474
+ * op exactly where it is for the rest of this page.
6475
+ */
6476
+ const setQueryDevtoolsOverridesScope = (next) => {
6477
+ const previous = scope();
6478
+ if (next === previous)
6479
+ return;
6480
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
6481
+ clearQueryDevtoolsStore(STORAGE_KEY);
6482
+ setQueryDevtoolsSettings({ overrides: next });
6483
+ if (next === 'none') {
6484
+ ops = {};
6485
+ forgetCarriedOver();
6486
+ return;
6487
+ }
6488
+ lastPersistentScope = next;
6489
+ if (previous === 'none') {
6490
+ ops = {};
6491
+ for (const [id, recorder] of armedRecorders) {
6492
+ const list = recorder.list().map((entry) => entry.op);
6493
+ if (list.length)
6494
+ ops[id] = list;
6495
+ }
6496
+ }
6497
+ writeStore();
6498
+ };
6499
+ /**
6500
+ * Turns persistence on or off - the panel's "Keep across reloads" toggle. On restores the last scope
6501
+ * that kept them (`session` unless Settings picked `local`).
6502
+ */
6503
+ const setQueryDevtoolsOverridePersistence = (value) => {
6504
+ setQueryDevtoolsOverridesScope(value ? lastPersistentScope : 'none');
6505
+ };
6506
+ /**
6507
+ * Empties the override store in both browser stores and disarms everything a reload brought back,
6508
+ * without changing where overrides are kept - the panel's "Reset devtools".
6509
+ */
6510
+ const clearQueryDevtoolsOverrideStore = () => {
6511
+ clearRestoredQueryDevtoolsOverrides();
6512
+ ops = {};
6513
+ clearQueryDevtoolsStore(STORAGE_KEY);
6514
+ };
6515
+ /**
6516
+ * The ops this page load inherited from the previous one, and for each id whether a live query has taken
6517
+ * it. Overrides that outlive the page that armed them have to announce themselves, because an app being
6518
+ * lied to looks exactly like an app that is broken.
6519
+ */
6520
+ const restoredQueryDevtoolsOverrides =
6521
+ /* @__PURE__ */ restored.asReadonly();
6522
+ /**
6523
+ * The scope those ops came back from, or `null` when this page inherited none. `local` is the one worth
6524
+ * naming in the banner: it means they outlived closing the tab, not just a reload.
6525
+ */
6526
+ const queryDevtoolsRestoredOverridesScope =
6527
+ /* @__PURE__ */ restoredScope.asReadonly();
6528
+ /** Disarms every op this page load inherited and empties the store - the banner's one-click way out. */
6529
+ const clearRestoredQueryDevtoolsOverrides = () => {
6530
+ for (const id of replayed) {
6531
+ armedRecorders.get(id)?.clearAll();
6532
+ armedRecorders.delete(id);
6533
+ }
6534
+ for (const id of carriedOver.keys())
6535
+ writeOps(id, []);
6536
+ forgetCarriedOver();
6537
+ };
6538
+ /**
6539
+ * Wraps a query's overrides recorder so its armed ops are stored and replayed. Replay happens here, at
6540
+ * registration, rather than in the panel - a query can run long before the panel is ever opened.
6541
+ *
6542
+ * Ids are reload-deterministic (descriptor + per-descriptor counter), so the same queries created in the
6543
+ * same order re-arm correctly; an id the store holds that nobody claims is reported by
6544
+ * {@link restoredQueryDevtoolsOverrides} rather than guessed at.
6545
+ * @internal
6546
+ */
6547
+ const withQueryDevtoolsOverridePersistence = (id, recorder) => {
6548
+ const inherited = ops[id];
6549
+ if (scope() !== 'none' && inherited?.length) {
6550
+ for (const op of inherited)
6551
+ recorder.arm(op);
6552
+ armedRecorders.set(id, recorder);
6553
+ replayed.add(id);
6554
+ publishRestored();
6555
+ }
6556
+ const sync = () => {
6557
+ const list = recorder.list().map((entry) => entry.op);
6558
+ if (list.length)
6559
+ armedRecorders.set(id, recorder);
6560
+ else
6561
+ armedRecorders.delete(id);
6562
+ if (scope() !== 'none')
6563
+ writeOps(id, list);
6564
+ };
6565
+ return {
6566
+ ...recorder,
6567
+ arm: (op) => {
6568
+ recorder.arm(op);
6569
+ sync();
6570
+ },
6571
+ clear: (opId) => {
6572
+ recorder.clear(opId);
6573
+ sync();
6574
+ },
6575
+ clearAll: () => {
6576
+ recorder.clearAll();
6577
+ if (replayed.delete(id))
6578
+ publishRestored();
6579
+ sync();
6580
+ },
6581
+ };
6582
+ };
6583
+
5797
6584
  const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
5798
6585
  /**
5799
6586
  * Whether any of `entries` is armed at `path` or anywhere below it - what the panel's menu asks before
@@ -5841,6 +6628,30 @@ const withValueAtPath = (root, path, value) => {
5841
6628
  const container = root && typeof root === 'object' ? root : {};
5842
6629
  return { ...container, [head]: withValueAtPath(container[head], rest, value) };
5843
6630
  };
6631
+ /**
6632
+ * Returns a copy of `root` with the value at `path` removed from its parent, cloning only the containers
6633
+ * along the way. An array parent splices, so the elements after it shift down instead of the slot
6634
+ * becoming a hole that serializes back as `null`.
6635
+ */
6636
+ const withoutValueAtPath = (root, path) => {
6637
+ const [head, ...rest] = path;
6638
+ if (rest.length === 0) {
6639
+ if (Array.isArray(root)) {
6640
+ const next = [...root];
6641
+ next.splice(head, 1);
6642
+ return next;
6643
+ }
6644
+ const { [head]: _dropped, ...remaining } = root;
6645
+ return remaining;
6646
+ }
6647
+ if (Array.isArray(root)) {
6648
+ const next = [...root];
6649
+ next[head] = withoutValueAtPath(root[head], rest);
6650
+ return next;
6651
+ }
6652
+ const container = root;
6653
+ return { ...container, [head]: withoutValueAtPath(container[head], rest) };
6654
+ };
5844
6655
  const IDENTITY_KEY_PATTERNS = [/^id$/i, /(^|[a-z])Id$/, /^uuid$/i, /^key$/i];
5845
6656
  const isIdentityKey = (key) => IDENTITY_KEY_PATTERNS.some((pattern) => pattern.test(key));
5846
6657
  /** Whether every sibling in `array` that has `key` holds a distinct string/number value for it. */
@@ -6133,6 +6944,19 @@ const applyOp = (root, op) => {
6133
6944
  return { root, ok: false };
6134
6945
  }
6135
6946
  return { root: withValueAtPath(root, op.path, duplicateOneArrayItem(resolution.value, op.index)), ok: true };
6947
+ case 'pasteArrayItem': {
6948
+ if (!Array.isArray(resolution.value))
6949
+ return { root, ok: false };
6950
+ const next = [...resolution.value];
6951
+ const at = op.index === undefined ? next.length : Math.min(Math.max(0, op.index), next.length);
6952
+ next.splice(at, 0, op.value);
6953
+ return { root: withValueAtPath(root, op.path, next), ok: true };
6954
+ }
6955
+ case 'deleteAt':
6956
+ // The response root has no parent to be removed from, and a key that is already gone stays gone.
6957
+ if (!op.path.length || !resolution.exists)
6958
+ return { root, ok: false };
6959
+ return { root: withoutValueAtPath(root, op.path), ok: true };
6136
6960
  case 'duplicateArray':
6137
6961
  if (!Array.isArray(resolution.value))
6138
6962
  return { root, ok: false };
@@ -6165,10 +6989,14 @@ const applyQueryDevtoolsOverrides = (entries, raw) => {
6165
6989
  return { value: root, staleIds };
6166
6990
  };
6167
6991
  /**
6168
- * Accumulates the response overrides armed on one query, for the devtools panel. Only ever created
6169
- * while {@link provideQueryDevtools} is installed - like {@link createQueryDevtoolsStats}, an app
6170
- * without devtools pays nothing for it.
6171
- * @internal
6992
+ * Accumulates the response overrides armed on one query, for the devtools panel. Instrumentation only
6993
+ * creates one while {@link provideQueryDevtools} is installed - like {@link createQueryDevtoolsStats},
6994
+ * an app without devtools pays nothing for it.
6995
+ *
6996
+ * The panel also creates one that belongs to no query at all, to record the edits made to a mock's draft
6997
+ * body: the ops are flattened with {@link applyQueryDevtoolsOverrides} and thrown away on save.
6998
+ *
6999
+ * Part of the devtools contract. **Not part of the general public contract.**
6172
7000
  */
6173
7001
  const createQueryDevtoolsOverrides = () => {
6174
7002
  const list = signal([], /* @ts-ignore */
@@ -6187,6 +7015,636 @@ const createQueryDevtoolsOverrides = () => {
6187
7015
  return { list: list.asReadonly(), arm, clear, clearAll, apply };
6188
7016
  };
6189
7017
 
7018
+ const TRANSFER_KIND = 'ethlete-query-overrides';
7019
+ const TRANSFER_VERSION = 1;
7020
+ const OP_TYPES = /* @__PURE__ */ new Set([
7021
+ 'set',
7022
+ 'stringPreset',
7023
+ 'numberPreset',
7024
+ 'booleanFlip',
7025
+ 'datePreset',
7026
+ 'duplicateArrayItem',
7027
+ 'pasteArrayItem',
7028
+ 'deleteAt',
7029
+ 'duplicateArray',
7030
+ 'paginationResize',
7031
+ 'reset',
7032
+ ]);
7033
+ const isJsonPath = (value) => Array.isArray(value) && value.every((step) => typeof step === 'string' || typeof step === 'number');
7034
+ const isOverrideOp = (value) => {
7035
+ if (!value || typeof value !== 'object' || Array.isArray(value))
7036
+ return false;
7037
+ const op = value;
7038
+ return typeof op.type === 'string' && OP_TYPES.has(op.type) && isJsonPath(op.path);
7039
+ };
7040
+ /**
7041
+ * Serializes the ops armed on one query into clipboard text. `reset` never reaches a recorder's list, so
7042
+ * what comes out is exactly what a paste can put back.
7043
+ */
7044
+ const serializeQueryDevtoolsOverrideTransfer = (entries, source) => {
7045
+ const payload = {
7046
+ kind: TRANSFER_KIND,
7047
+ version: TRANSFER_VERSION,
7048
+ ...(source ? { source } : {}),
7049
+ ops: entries.map((entry) => entry.op),
7050
+ };
7051
+ return JSON.stringify(payload, null, 2);
7052
+ };
7053
+ /**
7054
+ * Reads clipboard text back into ops. A bare array of ops is accepted alongside the full envelope, so a
7055
+ * set pasted out of a ticket and trimmed by hand still lands; an op whose `type` this build doesn't know
7056
+ * is counted in `skipped` rather than failing the whole paste, so an older panel can take a newer one's
7057
+ * set minus the parts it cannot replay.
7058
+ */
7059
+ const parseQueryDevtoolsOverrideTransfer = (text) => {
7060
+ if (!text.trim())
7061
+ return { ok: false, reason: 'The clipboard is empty' };
7062
+ let parsed;
7063
+ try {
7064
+ parsed = JSON.parse(text);
7065
+ }
7066
+ catch {
7067
+ return { ok: false, reason: 'The clipboard does not hold valid JSON' };
7068
+ }
7069
+ const envelope = !Array.isArray(parsed) && parsed && typeof parsed === 'object' ? parsed : null;
7070
+ const raw = Array.isArray(parsed) ? parsed : envelope?.['ops'];
7071
+ if (!Array.isArray(raw))
7072
+ return { ok: false, reason: 'The clipboard does not hold a copied override set' };
7073
+ const ops = raw.filter(isOverrideOp);
7074
+ const skipped = raw.length - ops.length;
7075
+ if (!ops.length) {
7076
+ return { ok: false, reason: skipped ? 'None of those ops can be replayed by this build' : 'That set is empty' };
7077
+ }
7078
+ const source = envelope?.['source'];
7079
+ return {
7080
+ ok: true,
7081
+ ops,
7082
+ skipped,
7083
+ ...(source && typeof source === 'object' && !Array.isArray(source) ? { source: source } : {}),
7084
+ };
7085
+ };
7086
+ /**
7087
+ * How many of `ops` would not resolve against `raw` - what a paste onto a differently-shaped query
7088
+ * reports, so ops sitting on nothing say so instead of looking armed. Replays them the same way the
7089
+ * pipeline will, since an earlier op can be what makes a later one's path exist.
7090
+ */
7091
+ const countUnresolvedQueryDevtoolsOverrides = (ops, raw) => applyQueryDevtoolsOverrides(ops.map((op, index) => ({ id: `${index}`, op })), raw).staleIds.length;
7092
+ /** Arms every op of a parsed set onto a query, on top of whatever it already has armed. */
7093
+ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
7094
+ for (const op of ops)
7095
+ recorder.arm(op);
7096
+ };
7097
+
7098
+ // Generated from package.json by tools/scripts/generate-version.js - do not edit.
7099
+ /** The version of `@ethlete/query` this build was cut from. */
7100
+ const QUERY_VERSION = '6.0.0-next.34';
7101
+
7102
+ /** How deep a schema is expanded before a nested branch is given up on. */
7103
+ const MAX_DEPTH = 12;
7104
+ /** How many elements are generated for an array, whatever its `minItems` says. */
7105
+ const MAX_ITEMS = 3;
7106
+ const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
7107
+ const loader = /* @__PURE__ */ signal(null, /* @ts-ignore */
7108
+ ...(ngDevMode ? [{ debugName: "loader" }] : /* istanbul ignore next */ []));
7109
+ const state = /* @__PURE__ */ signal({ status: 'unavailable' }, /* @ts-ignore */
7110
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
7111
+ const schemaDocument = /* @__PURE__ */ signal(null, /* @ts-ignore */
7112
+ ...(ngDevMode ? [{ debugName: "schemaDocument" }] : /* istanbul ignore next */ []));
7113
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
7114
+ /**
7115
+ * Where the API description stands, for the panel's seeding controls.
7116
+ *
7117
+ * Part of the devtools contract. **Not part of the general public contract.**
7118
+ */
7119
+ const queryDevtoolsSchemaState = /* @__PURE__ */ state.asReadonly();
7120
+ /**
7121
+ * Installs the application's schema loader. Called by `provideQueryDevtools()`; nothing else may call it.
7122
+ * @internal
7123
+ */
7124
+ const setQueryDevtoolsSchemaLoader = (next) => {
7125
+ loader.set(next ?? null);
7126
+ state.set({ status: next ? 'idle' : 'unavailable' });
7127
+ schemaDocument.set(null);
7128
+ };
7129
+ /** A `() => import('./openapi.json')` loader hands back the module rather than the document inside it. */
7130
+ const unwrapModule = (value) => {
7131
+ if (!isRecord(value))
7132
+ return value;
7133
+ if ('paths' in value || 'components' in value || 'definitions' in value || '$defs' in value)
7134
+ return value;
7135
+ return 'default' in value ? value['default'] : value;
7136
+ };
7137
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
7138
+ /**
7139
+ * Loads the API description, once. Calling it while it is loading or after it has loaded does nothing;
7140
+ * calling it after a failure retries.
7141
+ *
7142
+ * Part of the devtools contract. **Not part of the general public contract.**
7143
+ */
7144
+ const loadQueryDevtoolsSchema = () => {
7145
+ const load = loader();
7146
+ const status = state().status;
7147
+ if (!load || status === 'loading' || status === 'ready')
7148
+ return;
7149
+ state.set({ status: 'loading' });
7150
+ Promise.resolve()
7151
+ .then(() => load())
7152
+ .then((value) => {
7153
+ const parsed = unwrapModule(value);
7154
+ if (!isRecord(parsed))
7155
+ throw new Error('The schema loader did not return an object.');
7156
+ schemaDocument.set(parsed);
7157
+ state.set({ status: 'ready' });
7158
+ })
7159
+ .catch((error) => {
7160
+ schemaDocument.set(null);
7161
+ state.set({ status: 'error', message: messageOf(error) });
7162
+ });
7163
+ };
7164
+ const namedSchemas = /* @__PURE__ */ computed(() => {
7165
+ const doc = schemaDocument();
7166
+ if (!doc)
7167
+ return {};
7168
+ const components = doc['components'];
7169
+ return {
7170
+ ...(isRecord(doc['$defs']) ? doc['$defs'] : null),
7171
+ ...(isRecord(doc['definitions']) ? doc['definitions'] : null),
7172
+ ...(isRecord(components) && isRecord(components['schemas']) ? components['schemas'] : null),
7173
+ };
7174
+ }, /* @ts-ignore */
7175
+ ...(ngDevMode ? [{ debugName: "namedSchemas" }] : /* istanbul ignore next */ []));
7176
+ /**
7177
+ * Every schema the description names, sorted - the designer's "seed from a type" list.
7178
+ *
7179
+ * Part of the devtools contract. **Not part of the general public contract.**
7180
+ */
7181
+ const queryDevtoolsSchemaNames = /* @__PURE__ */ computed(() => Object.keys(namedSchemas()).sort(), /* @ts-ignore */
7182
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsSchemaNames" }] : /* istanbul ignore next */ []));
7183
+ const decodePointer = (step) => decodeURIComponent(step.replace(/~1/g, '/').replace(/~0/g, '~'));
7184
+ /** The last segment of a `$ref` - `#/components/schemas/MatchId` is what a field's type is called. */
7185
+ const refLabel = (ref) => decodePointer(ref.split('/').pop() ?? ref);
7186
+ /** Resolves a local JSON pointer against the loaded document. A remote `$ref` cannot be followed. */
7187
+ const resolveRef = (ref) => {
7188
+ if (!ref.startsWith('#/'))
7189
+ return undefined;
7190
+ let current = schemaDocument();
7191
+ for (const step of ref.slice(2).split('/')) {
7192
+ if (!isRecord(current))
7193
+ return undefined;
7194
+ current = current[decodePointer(step)];
7195
+ }
7196
+ return current;
7197
+ };
7198
+ const at = (path) => path || 'the root';
7199
+ /** The declared `type`, tolerating the `['string', 'null']` form JSON Schema and OpenAPI 3.1 both allow. */
7200
+ const typeOf = (schema) => {
7201
+ const type = schema['type'];
7202
+ if (typeof type === 'string')
7203
+ return type;
7204
+ if (Array.isArray(type))
7205
+ return type.find((entry) => typeof entry === 'string' && entry !== 'null');
7206
+ return undefined;
7207
+ };
7208
+ const isNullable = (schema) => {
7209
+ const type = schema['type'];
7210
+ return schema['nullable'] === true || (Array.isArray(type) && type.includes('null'));
7211
+ };
7212
+ const branchesOf = (schema) => {
7213
+ for (const keyword of ['oneOf', 'anyOf']) {
7214
+ const branches = schema[keyword];
7215
+ if (Array.isArray(branches) && branches.length)
7216
+ return { keyword, branches };
7217
+ }
7218
+ return null;
7219
+ };
7220
+ /**
7221
+ * A short label for what a field is declared as: the name of the type it `$ref`s (which is the point -
7222
+ * `MatchId` says more than `string`), a union of its branches, or its primitive type and format.
7223
+ */
7224
+ const describeSchema = (schema, depth = 0) => {
7225
+ if (!isRecord(schema) || depth > 4)
7226
+ return 'unknown';
7227
+ const ref = schema['$ref'];
7228
+ if (typeof ref === 'string')
7229
+ return refLabel(ref);
7230
+ const nullSuffix = isNullable(schema) ? ' | null' : '';
7231
+ const branches = branchesOf(schema);
7232
+ if (branches) {
7233
+ const labels = [...new Set(branches.branches.slice(0, 3).map((branch) => describeSchema(branch, depth + 1)))];
7234
+ return `${labels.join(' | ')}${branches.branches.length > 3 ? ' | …' : ''}${nullSuffix}`;
7235
+ }
7236
+ const allOf = schema['allOf'];
7237
+ if (Array.isArray(allOf) && allOf.length) {
7238
+ const named = allOf.find((entry) => isRecord(entry) && typeof entry['$ref'] === 'string');
7239
+ return `${describeSchema(named ?? allOf[0], depth + 1)}${nullSuffix}`;
7240
+ }
7241
+ const enumValues = schema['enum'];
7242
+ if (Array.isArray(enumValues) && enumValues.length) {
7243
+ const labels = enumValues.slice(0, 3).map((value) => JSON.stringify(value));
7244
+ return `${labels.join(' | ')}${enumValues.length > 3 ? ' | …' : ''}${nullSuffix}`;
7245
+ }
7246
+ const type = typeOf(schema);
7247
+ if (type === 'array')
7248
+ return `${describeSchema(schema['items'], depth + 1)}[]${nullSuffix}`;
7249
+ if (type === 'string') {
7250
+ const format = schema['format'];
7251
+ return `${typeof format === 'string' ? `string (${format})` : 'string'}${nullSuffix}`;
7252
+ }
7253
+ return `${type ?? 'unknown'}${nullSuffix}`;
7254
+ };
7255
+ const FORMAT_SAMPLES = {
7256
+ 'date-time': '2026-01-01T00:00:00.000Z',
7257
+ date: '2026-01-01',
7258
+ time: '00:00:00',
7259
+ duration: 'P1D',
7260
+ uuid: '00000000-0000-4000-8000-000000000000',
7261
+ email: 'user@example.com',
7262
+ hostname: 'example.com',
7263
+ ipv4: '127.0.0.1',
7264
+ ipv6: '::1',
7265
+ uri: 'https://example.com',
7266
+ 'uri-reference': '/example',
7267
+ url: 'https://example.com',
7268
+ password: '',
7269
+ byte: '',
7270
+ binary: '',
7271
+ };
7272
+ /**
7273
+ * A placeholder string: the declared format's shape where there is one, otherwise the field's own name -
7274
+ * which reads as obviously unreal while still saying which field you are looking at.
7275
+ */
7276
+ const stringSample = (node, schema) => {
7277
+ const format = schema['format'];
7278
+ if (typeof format === 'string' && format in FORMAT_SAMPLES)
7279
+ return FORMAT_SAMPLES[format];
7280
+ const title = schema['title'];
7281
+ if (typeof title === 'string' && title)
7282
+ return title;
7283
+ return node.key ?? 'string';
7284
+ };
7285
+ const numberSample = (schema) => {
7286
+ for (const keyword of ['minimum', 'exclusiveMinimum', 'default']) {
7287
+ const value = schema[keyword];
7288
+ if (typeof value === 'number' && Number.isFinite(value))
7289
+ return keyword === 'exclusiveMinimum' ? value + 1 : value;
7290
+ }
7291
+ return 0;
7292
+ };
7293
+ const childNode = (parent, child) => ({
7294
+ schema: child.schema,
7295
+ path: parent.path ? `${parent.path}.${child.key}` : child.key,
7296
+ depth: parent.depth + 1,
7297
+ key: child.key,
7298
+ });
7299
+ const requiredOf = (schema) => {
7300
+ const required = schema['required'];
7301
+ return new Set(Array.isArray(required) ? required.filter((entry) => typeof entry === 'string') : []);
7302
+ };
7303
+ const generateObject = (node, ctx) => {
7304
+ const schema = node.schema;
7305
+ const properties = schema['properties'];
7306
+ if (!isRecord(properties)) {
7307
+ if (isRecord(schema['additionalProperties'])) {
7308
+ ctx.notes.add(`${at(node.path)} is a free-form map - generated as an empty object.`);
7309
+ }
7310
+ return {};
7311
+ }
7312
+ const required = requiredOf(schema);
7313
+ const result = {};
7314
+ for (const [key, child] of Object.entries(properties)) {
7315
+ const target = childNode(node, { schema: child, key });
7316
+ ctx.types.set(target.path, `${describeSchema(child)}${required.has(key) ? '' : '?'}`);
7317
+ result[key] = generate(target, ctx);
7318
+ }
7319
+ return result;
7320
+ };
7321
+ const generateArray = (node, ctx) => {
7322
+ const schema = node.schema;
7323
+ const items = schema['items'];
7324
+ if (!isRecord(items))
7325
+ return [];
7326
+ const minItems = typeof schema['minItems'] === 'number' ? schema['minItems'] : 1;
7327
+ const count = Math.min(Math.max(1, minItems), MAX_ITEMS);
7328
+ const target = { schema: items, path: `${node.path ? `${node.path}.` : ''}*`, depth: node.depth + 1, key: node.key };
7329
+ ctx.types.set(target.path, describeSchema(items));
7330
+ const element = generate(target, ctx);
7331
+ return Array.from({ length: count }, () => structuredCopy(element));
7332
+ };
7333
+ /** Each generated element is its own value, so editing one in the designer does not edit the others. */
7334
+ const structuredCopy = (value) => {
7335
+ if (Array.isArray(value))
7336
+ return value.map(structuredCopy);
7337
+ if (isRecord(value))
7338
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, structuredCopy(entry)]));
7339
+ return value;
7340
+ };
7341
+ /** Merges what every `allOf` member generates, so a composed schema seeds as one object. */
7342
+ const generateAllOf = (node, ctx) => {
7343
+ const members = node.schema['allOf'];
7344
+ const generated = members.map((member) => generate({ ...node, schema: member }, ctx));
7345
+ const objects = generated.filter(isRecord);
7346
+ if (objects.length !== generated.length)
7347
+ return generated.find((value) => !isRecord(value)) ?? null;
7348
+ return Object.assign({}, ...objects);
7349
+ };
7350
+ const generate = (node, ctx) => {
7351
+ if (node.depth > MAX_DEPTH) {
7352
+ ctx.notes.add(`${at(node.path)} is nested deeper than ${MAX_DEPTH} levels - generated as null.`);
7353
+ return null;
7354
+ }
7355
+ if (!isRecord(node.schema))
7356
+ return null;
7357
+ const schema = node.schema;
7358
+ const ref = schema['$ref'];
7359
+ if (typeof ref === 'string') {
7360
+ const name = refLabel(ref);
7361
+ if (ctx.refs.includes(name)) {
7362
+ ctx.notes.add(`${name} contains itself - the recursion was cut at ${at(node.path)} with null.`);
7363
+ return null;
7364
+ }
7365
+ const resolved = resolveRef(ref);
7366
+ if (resolved === undefined) {
7367
+ ctx.notes.add(`${ref} could not be resolved - generated as null at ${at(node.path)}.`);
7368
+ return null;
7369
+ }
7370
+ ctx.refs.push(name);
7371
+ const value = generate({ ...node, schema: resolved }, ctx);
7372
+ ctx.refs.pop();
7373
+ return value;
7374
+ }
7375
+ for (const keyword of ['example', 'default', 'const']) {
7376
+ if (keyword in schema)
7377
+ return schema[keyword];
7378
+ }
7379
+ const examples = schema['examples'];
7380
+ if (Array.isArray(examples) && examples.length)
7381
+ return examples[0];
7382
+ const enumValues = schema['enum'];
7383
+ if (Array.isArray(enumValues) && enumValues.length)
7384
+ return enumValues[0];
7385
+ const branches = branchesOf(schema);
7386
+ if (branches) {
7387
+ if (branches.branches.length > 1) {
7388
+ ctx.notes.add(`${at(node.path)} is a ${branches.keyword} - the first branch was taken.`);
7389
+ }
7390
+ return generate({ ...node, schema: branches.branches[0] }, ctx);
7391
+ }
7392
+ if (Array.isArray(schema['allOf']) && schema['allOf'].length)
7393
+ return generateAllOf(node, ctx);
7394
+ switch (typeOf(schema)) {
7395
+ case 'object':
7396
+ return generateObject(node, ctx);
7397
+ case 'array':
7398
+ return generateArray(node, ctx);
7399
+ case 'string':
7400
+ return stringSample(node, schema);
7401
+ case 'integer':
7402
+ case 'number':
7403
+ return numberSample(schema);
7404
+ case 'boolean':
7405
+ return false;
7406
+ case 'null':
7407
+ return null;
7408
+ default:
7409
+ break;
7410
+ }
7411
+ if (isRecord(schema['properties']))
7412
+ return generateObject(node, ctx);
7413
+ ctx.notes.add(`${at(node.path)} declares no type - generated as null.`);
7414
+ return null;
7415
+ };
7416
+ const seedFrom = (schema, name) => {
7417
+ const ctx = { types: new Map(), notes: new Set(), refs: name ? [name] : [] };
7418
+ const body = generate({ schema, path: '', depth: 0, key: null }, ctx);
7419
+ return { body, schemaName: name, types: ctx.types, notes: [...ctx.notes] };
7420
+ };
7421
+ /**
7422
+ * Generates a body from one named schema (`MatchView`), or `null` when the description does not name it.
7423
+ *
7424
+ * Part of the devtools contract. **Not part of the general public contract.**
7425
+ */
7426
+ const seedQueryDevtoolsSchemaBody = (name) => {
7427
+ const schema = namedSchemas()[name];
7428
+ if (schema === undefined)
7429
+ return null;
7430
+ return seedFrom(schema, name);
7431
+ };
7432
+ /** Where a schema was collected from, so the same name arriving from two pointers is caught. */
7433
+ const originOf = (label, pointer) => pointer ?? `named:${label}`;
7434
+ /**
7435
+ * Copies a schema, pointing every local `$ref` at `#/components/schemas/<name>` and queueing that name -
7436
+ * so a document assembled from these resolves against its own `components`, whether the description kept
7437
+ * its schemas under `components.schemas`, `definitions` or `$defs`.
7438
+ */
7439
+ const rewriteRefs = (value, queue) => {
7440
+ if (Array.isArray(value))
7441
+ return value.map((entry) => rewriteRefs(entry, queue));
7442
+ if (!isRecord(value))
7443
+ return value;
7444
+ const result = {};
7445
+ for (const [key, entry] of Object.entries(value)) {
7446
+ if (key === '$ref' && typeof entry === 'string') {
7447
+ if (!entry.startsWith('#/')) {
7448
+ result[key] = entry;
7449
+ continue;
7450
+ }
7451
+ const label = refLabel(entry);
7452
+ queue({ label, pointer: entry });
7453
+ result[key] = `#/components/schemas/${label}`;
7454
+ continue;
7455
+ }
7456
+ result[key] = rewriteRefs(entry, queue);
7457
+ }
7458
+ return result;
7459
+ };
7460
+ /**
7461
+ * Collects the named schemas a set of routes was designed against, ready to be handed back as the
7462
+ * `components.schemas` of an exported document - so a route seeded from `MatchView` can reference
7463
+ * `MatchView` instead of an anonymous shape inferred from one body.
7464
+ *
7465
+ * Part of the devtools contract. **Not part of the general public contract.**
7466
+ */
7467
+ const collectQueryDevtoolsSchemaComponents = (names) => {
7468
+ const declared = namedSchemas();
7469
+ const schemas = {};
7470
+ const notes = new Set();
7471
+ const origins = new Map();
7472
+ const pending = names.map((label) => ({ label, pointer: null }));
7473
+ while (pending.length) {
7474
+ const request = pending.shift();
7475
+ const origin = originOf(request.label, request.pointer);
7476
+ const seen = origins.get(request.label);
7477
+ if (seen !== undefined) {
7478
+ if (seen !== origin) {
7479
+ notes.add(`Two different schemas are both called ${request.label} - only the one from ${seen} was kept.`);
7480
+ }
7481
+ continue;
7482
+ }
7483
+ const schema = request.pointer === null ? declared[request.label] : resolveRef(request.pointer);
7484
+ if (schema === undefined) {
7485
+ if (request.pointer !== null) {
7486
+ notes.add(`${request.pointer} could not be resolved, so ${request.label} is missing from the export.`);
7487
+ }
7488
+ continue;
7489
+ }
7490
+ origins.set(request.label, origin);
7491
+ schemas[request.label] = rewriteRefs(schema, (next) => pending.push(next));
7492
+ }
7493
+ for (const schema of Object.values(schemas)) {
7494
+ collectRemoteRefs(schema, notes);
7495
+ }
7496
+ return { schemas, notes: [...notes] };
7497
+ };
7498
+ /** A `$ref` to another file is left as it was written, so a document says where it stops resolving. */
7499
+ const collectRemoteRefs = (value, notes) => {
7500
+ if (Array.isArray(value)) {
7501
+ for (const entry of value)
7502
+ collectRemoteRefs(entry, notes);
7503
+ return;
7504
+ }
7505
+ if (!isRecord(value))
7506
+ return;
7507
+ for (const [key, entry] of Object.entries(value)) {
7508
+ if (key === '$ref' && typeof entry === 'string' && !entry.startsWith('#/')) {
7509
+ notes.add(`${entry} points outside the description - it was exported unchanged and will not resolve.`);
7510
+ continue;
7511
+ }
7512
+ collectRemoteRefs(entry, notes);
7513
+ }
7514
+ };
7515
+ const segmentsOf = (pattern) => pattern.split('/').filter(Boolean);
7516
+ /** `/matches/{id}` and `/matches/:id` describe the same route; a mock's pattern is written the second way. */
7517
+ const toColonPattern = (path) => path.replace(/\{([^}]+)\}/g, ':$1');
7518
+ const isParam = (segment) => segment.startsWith(':');
7519
+ /** Whether two patterns describe the same route, allowing their path params to be named differently. */
7520
+ const patternsMatch = (a, b) => {
7521
+ const left = segmentsOf(a);
7522
+ const right = segmentsOf(b);
7523
+ if (left.length !== right.length)
7524
+ return false;
7525
+ return left.every((segment, index) => {
7526
+ const other = right[index];
7527
+ return isParam(segment) || isParam(other) ? isParam(segment) && isParam(other) : segment === other;
7528
+ });
7529
+ };
7530
+ /**
7531
+ * Every route the description declares, as the designer offers them - including the ones no query has
7532
+ * ever called, which is the case a mock exists for.
7533
+ *
7534
+ * Part of the devtools contract. **Not part of the general public contract.**
7535
+ */
7536
+ const queryDevtoolsSchemaRoutes = /* @__PURE__ */ computed(() => {
7537
+ const paths = schemaDocument()?.['paths'];
7538
+ if (!isRecord(paths))
7539
+ return [];
7540
+ const routes = [];
7541
+ for (const [path, item] of Object.entries(paths)) {
7542
+ if (!isRecord(item))
7543
+ continue;
7544
+ for (const method of HTTP_METHODS) {
7545
+ const operation = item[method];
7546
+ if (!isRecord(operation))
7547
+ continue;
7548
+ const summary = operation['summary'] ?? operation['operationId'];
7549
+ routes.push({
7550
+ method: method.toUpperCase(),
7551
+ pattern: toColonPattern(path),
7552
+ summary: typeof summary === 'string' ? summary : '',
7553
+ });
7554
+ }
7555
+ }
7556
+ return routes;
7557
+ }, /* @ts-ignore */
7558
+ ...(ngDevMode ? [{ debugName: "queryDevtoolsSchemaRoutes" }] : /* istanbul ignore next */ []));
7559
+ const findOperation = (target) => {
7560
+ const paths = schemaDocument()?.['paths'];
7561
+ if (!isRecord(paths))
7562
+ return null;
7563
+ const method = target.method.toLowerCase();
7564
+ const segments = segmentsOf(target.pattern);
7565
+ // A client's base path (`/api/v2`) is part of the request but not of the document, so a pattern that
7566
+ // does not match is retried without its leading segments before giving up.
7567
+ for (let skip = 0; skip < segments.length; skip++) {
7568
+ const candidate = `/${segments.slice(skip).join('/')}`;
7569
+ for (const [path, item] of Object.entries(paths)) {
7570
+ if (!isRecord(item) || !isRecord(item[method]))
7571
+ continue;
7572
+ if (!patternsMatch(toColonPattern(path), candidate))
7573
+ continue;
7574
+ return {
7575
+ operation: item[method],
7576
+ path,
7577
+ droppedPrefix: skip ? `/${segments.slice(0, skip).join('/')}` : '',
7578
+ };
7579
+ }
7580
+ }
7581
+ return null;
7582
+ };
7583
+ /** The success response of an operation: the lowest 2xx the document declares, else its `default`. */
7584
+ const successResponse = (operation) => {
7585
+ const responses = operation['responses'];
7586
+ if (!isRecord(responses))
7587
+ return null;
7588
+ const status = Object.keys(responses)
7589
+ .filter((key) => /^2\d\d$/.test(key))
7590
+ .sort()[0];
7591
+ const key = status ?? (('default' in responses && 'default') || null);
7592
+ if (!key)
7593
+ return null;
7594
+ const response = responses[key];
7595
+ return isRecord(response) ? { response, status: key } : null;
7596
+ };
7597
+ /** The JSON media type of a response, tolerating `application/vnd.x+json` and Swagger 2's flat `schema`. */
7598
+ const jsonSchemaOf = (response) => {
7599
+ const content = response['content'];
7600
+ if (!isRecord(content))
7601
+ return isRecord(response['schema']) ? { schema: response['schema'], example: undefined } : null;
7602
+ const key = Object.keys(content).find((type) => type.includes('json')) ?? Object.keys(content)[0];
7603
+ if (!key)
7604
+ return null;
7605
+ const media = content[key];
7606
+ if (!isRecord(media))
7607
+ return null;
7608
+ return { schema: media['schema'], example: media['example'] };
7609
+ };
7610
+ /**
7611
+ * Generates a body for one route from the description's own success response - the seed that makes a
7612
+ * designed mock a real view model rather than a guess. `null` when the document does not declare the
7613
+ * route, or declares no JSON response for it.
7614
+ *
7615
+ * Part of the devtools contract. **Not part of the general public contract.**
7616
+ */
7617
+ const seedQueryDevtoolsSchemaRoute = (target) => {
7618
+ const match = findOperation(target);
7619
+ if (!match)
7620
+ return null;
7621
+ const success = successResponse(match.operation);
7622
+ if (!success)
7623
+ return null;
7624
+ const json = jsonSchemaOf(success.response);
7625
+ if (!json)
7626
+ return null;
7627
+ // A top-level `$ref` is resolved here rather than by the walk, so `seedFrom` always receives the
7628
+ // schema its `name` stands for - otherwise the walk reads the name as one it is already expanding and
7629
+ // cuts the whole body as a cycle.
7630
+ const ref = isRecord(json.schema) ? json.schema['$ref'] : null;
7631
+ const named = typeof ref === 'string' ? { schema: resolveRef(ref), name: refLabel(ref) } : null;
7632
+ if (named && named.schema === undefined)
7633
+ return null;
7634
+ const seed = json.example === undefined
7635
+ ? seedFrom(named ? named.schema : json.schema, named ? named.name : null)
7636
+ : {
7637
+ body: json.example,
7638
+ schemaName: null,
7639
+ types: new Map(),
7640
+ notes: ['The document ships an example for this route - it was used as-is.'],
7641
+ };
7642
+ const notes = [...seed.notes, `Generated from ${match.path} ${target.method.toUpperCase()} ${success.status}.`];
7643
+ if (match.droppedPrefix)
7644
+ notes.push(`The document has no ${match.droppedPrefix} prefix - it was ignored when matching.`);
7645
+ return { ...seed, notes };
7646
+ };
7647
+
6190
7648
  const EMPTY_STATS = {
6191
7649
  executions: 0,
6192
7650
  requests: 0,
@@ -6233,21 +7691,22 @@ const RUN_HISTORY = 25;
6233
7691
  * decision the app has to make for itself.
6234
7692
  */
6235
7693
  const DEFAULT_RESPONSE_HISTORY = 5;
6236
- let responseHistory = DEFAULT_RESPONSE_HISTORY;
7694
+ let providedResponseHistory = DEFAULT_RESPONSE_HISTORY;
6237
7695
  /**
6238
- * How many bodies each query retains - {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run
6239
- * that still holds one can be an end of a response diff, so the panel reads this to say why an older run
6240
- * can no longer be picked.
7696
+ * How many bodies each query retains: what the panel's Settings tab asks for, or
7697
+ * {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run that still holds one can be an end of
7698
+ * a response diff, so the panel reads this to say why an older run can no longer be picked.
6241
7699
  *
6242
7700
  * Part of the devtools contract. **Not part of the general public contract.**
6243
7701
  */
6244
- const queryDevtoolsResponseHistory = () => responseHistory;
7702
+ const queryDevtoolsResponseHistory = () => queryDevtoolsSettings().responseHistory ?? providedResponseHistory;
6245
7703
  /**
6246
- * Sets how many bodies each query retains. Called by `provideQueryDevtools()`; nothing else may call it.
7704
+ * Sets the application's own retention, which the panel's Settings tab can raise for the rest of the
7705
+ * page. Called by `provideQueryDevtools()`; nothing else may call it.
6247
7706
  * @internal
6248
7707
  */
6249
7708
  const setQueryDevtoolsResponseHistory = (count) => {
6250
- responseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
7709
+ providedResponseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
6251
7710
  };
6252
7711
  /** The index of the newest run still in flight, or -1 when none is. */
6253
7712
  const lastPendingRunIndex = (runs) => {
@@ -6263,7 +7722,9 @@ const lastPendingRunIndex = (runs) => {
6263
7722
  * two. Mutates the array it is given, which is always one this module just built.
6264
7723
  */
6265
7724
  const trimRetainedBodies = (runs) => {
6266
- const budget = responseHistory;
7725
+ // Untracked: a run is recorded from whatever context the query is running in, and reading the setting
7726
+ // there would make that context re-run every time the setting changes.
7727
+ const budget = untracked(queryDevtoolsResponseHistory);
6267
7728
  let kept = 0;
6268
7729
  for (let index = runs.length - 1; index >= 0; index--) {
6269
7730
  const run = runs[index];
@@ -6465,6 +7926,90 @@ const sumQueryDevtoolsStats = (handles) => {
6465
7926
  return total;
6466
7927
  };
6467
7928
 
7929
+ /**
7930
+ * The longest lifetime an armed override may claim, in seconds (7 days). Anything beyond it is never
7931
+ * refreshed at all, which is what arming nothing already does.
7932
+ */
7933
+ const QUERY_DEVTOOLS_TOKEN_TTL_LIMIT = 604800;
7934
+ /** The claims an overridden lifetime is measured from, in the order they are tried. */
7935
+ const ANCHOR_CLAIMS = ['iat', 'nbf'];
7936
+ const ttls = /* @__PURE__ */ signal({}, /* @ts-ignore */
7937
+ ...(ngDevMode ? [{ debugName: "ttls" }] : /* istanbul ignore next */ []));
7938
+ /**
7939
+ * The access-token lifetime armed per auth provider name, in seconds, consumed by the
7940
+ * `<et-query-devtools>` panel. A provider on its token's real lifetime is absent rather than present
7941
+ * with it.
7942
+ */
7943
+ const queryDevtoolsTokenTtls = /* @__PURE__ */ ttls.asReadonly();
7944
+ const tokenTtlAnchor = (options) => {
7945
+ const { payload, expiresInPropertyName } = options;
7946
+ if (!payload || typeof payload[expiresInPropertyName] !== 'number')
7947
+ return null;
7948
+ for (const claim of ANCHOR_CLAIMS) {
7949
+ const value = payload[claim];
7950
+ if (typeof value === 'number')
7951
+ return value;
7952
+ }
7953
+ return null;
7954
+ };
7955
+ /**
7956
+ * Whether an overridden lifetime can be applied to a decoded access token at all. It needs the claim
7957
+ * carrying the expiry - so a lifetime is never invented for a token that declares none - plus `iat` or
7958
+ * `nbf` to measure the new lifetime from.
7959
+ */
7960
+ const canOverrideQueryDevtoolsTokenTtl = (options) => tokenTtlAnchor({ payload: options.payload, expiresInPropertyName: options.expiresInPropertyName ?? 'exp' }) !== null;
7961
+ /**
7962
+ * Presents one auth provider's access token as living `seconds` from the moment it was issued, rather
7963
+ * than as long as it says it does. Everything that reads the expiry sees the shortened lifetime - the
7964
+ * proactive refresh schedule, `withBearerAuthTokenExpirationWarning`, and the panel's own countdown -
7965
+ * so arming 60 seconds makes a real refresh happen a minute after each token is issued, and keeps
7966
+ * happening for every token that refresh brings back.
7967
+ *
7968
+ * It is a lifetime, not a remaining time: `0` presents the current token as long expired, which is what
7969
+ * makes a refresh happen at once. Nothing is persisted, so a reload leaves every provider on its real
7970
+ * lifetime.
7971
+ *
7972
+ * Part of the devtools contract consumed by `<et-query-devtools>`; a token's lifetime is not something
7973
+ * an application should override on itself.
7974
+ */
7975
+ const setQueryDevtoolsTokenTtl = (options) => {
7976
+ const { providerName, seconds } = options;
7977
+ const clamped = Math.min(Math.max(Math.trunc(seconds) || 0, 0), QUERY_DEVTOOLS_TOKEN_TTL_LIMIT);
7978
+ ttls.update((current) => ({ ...current, [providerName]: clamped }));
7979
+ };
7980
+ /**
7981
+ * Puts one provider back on its token's real lifetime, or every provider when no name is given.
7982
+ *
7983
+ * @see setQueryDevtoolsTokenTtl
7984
+ */
7985
+ const clearQueryDevtoolsTokenTtl = (providerName) => {
7986
+ ttls.update((current) => {
7987
+ if (providerName === undefined)
7988
+ return {};
7989
+ const { [providerName]: _removed, ...rest } = current;
7990
+ return rest;
7991
+ });
7992
+ };
7993
+ /**
7994
+ * The decoded access token as the devtools want it seen: the claim carrying the expiry rewritten to the
7995
+ * armed lifetime, measured from `iat`. Returns `payload` untouched when nothing is armed for the
7996
+ * provider, or when the token cannot carry the override - see {@link canOverrideQueryDevtoolsTokenTtl}.
7997
+ *
7998
+ * Installed as the token payload patcher by `provideQueryDevtools()`, and called directly by the panel
7999
+ * for its own countdown.
8000
+ */
8001
+ const applyQueryDevtoolsTokenTtl = (options) => {
8002
+ const { payload, providerName, expiresInPropertyName = 'exp' } = options;
8003
+ const seconds = ttls()[providerName];
8004
+ if (seconds === undefined || !payload || typeof payload !== 'object')
8005
+ return payload;
8006
+ const record = payload;
8007
+ const anchor = tokenTtlAnchor({ payload: record, expiresInPropertyName });
8008
+ if (anchor === null)
8009
+ return payload;
8010
+ return { ...record, [expiresInPropertyName]: anchor + seconds };
8011
+ };
8012
+
6468
8013
  /**
6469
8014
  * How many destroyed queries the registry keeps. A tombstone holds the last response body it captured,
6470
8015
  * so the list is capped for the same reason the repository caps its unused entries.
@@ -6682,7 +8227,7 @@ const registerEntry = (registration) => {
6682
8227
  createdAt: Date.now(),
6683
8228
  stats: registration.stats,
6684
8229
  formLinks: registration.formLinks,
6685
- overrides: registration.overrides,
8230
+ overrides: registration.overrides && withQueryDevtoolsOverridePersistence(id, registration.overrides),
6686
8231
  };
6687
8232
  // A registration that brings its own id can repeat one a tombstone still holds; the live entry wins.
6688
8233
  entries.update((list) => [...list.filter((e) => !(e.id === id && e.destroyedAt)), fullEntry]);
@@ -6736,12 +8281,24 @@ const clearQueryDevtoolsTombstones = (ids) => {
6736
8281
  * ```
6737
8282
  */
6738
8283
  const provideQueryDevtools = (options) => {
8284
+ // First: the stored settings say where the override store lives and whether the panel has raised the
8285
+ // retention this app asked for, and both are read by the calls below.
8286
+ initQueryDevtoolsSettings();
6739
8287
  setQueryDevtoolsResponseHistory(options?.responseHistory);
8288
+ registerEthleteVersion('core', CORE_VERSION);
8289
+ registerEthleteVersion('query', QUERY_VERSION);
8290
+ setQueryDevtoolsAppInfo(options?.about);
8291
+ initQueryDevtoolsOverridePersistence();
8292
+ initQueryDevtoolsMocks();
8293
+ initQueryDevtoolsFaults();
8294
+ setQueryDevtoolsSchemaLoader(options?.schema);
6740
8295
  setQueryDevtoolsRegistrar(registerEntry);
6741
8296
  setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
6742
8297
  setQueryDevtoolsFormLinksFactory(createQueryDevtoolsFormLinks);
6743
8298
  setQueryDevtoolsOverridesFactory(createQueryDevtoolsOverrides);
6744
8299
  setQueryDevtoolsFaultResolver(resolveQueryDevtoolsFaultForAttempt);
8300
+ setQueryDevtoolsMockResolver(resolveQueryDevtoolsMockForAttempt);
8301
+ setQueryDevtoolsTokenPayloadPatcher(applyQueryDevtoolsTokenTtl);
6745
8302
  if (!isDevMode()) {
6746
8303
  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.');
6747
8304
  }
@@ -6859,14 +8416,14 @@ const createSecureGqlExecuteFn = (executeOptions) => {
6859
8416
  state: executeOptions.state,
6860
8417
  autoExecutes: shouldAutoExecuteGqlQuery(executeOptions.creatorInternals.method) &&
6861
8418
  !executeOptions.queryConfig.onlyManualExecution,
6862
- transformAuthAndExec: (executeArgs, _tokens, headers, executeState) => {
8419
+ transformAuthAndExec: (executeArgs, executeState) => {
6863
8420
  const { args, options: runOptions } = executeArgs ?? {};
6864
8421
  const query = transformGql(executeOptions.creatorInternals.query);
6865
8422
  let gqlParams = query(args?.variables);
6866
8423
  if (args?.queryParams && executeOptions.creatorInternals.transport === 'GET') {
6867
8424
  gqlParams = { ...gqlParams, ...args.queryParams };
6868
8425
  }
6869
- const computedArgs = { ...(args ?? {}), headers };
8426
+ const computedArgs = { ...(args ?? {}) };
6870
8427
  if (executeOptions.creatorInternals.transport === 'GET') {
6871
8428
  computedArgs.queryParams = gqlParams;
6872
8429
  }
@@ -12282,5 +13839,5 @@ const createWebSocketClient = (options) => {
12282
13839
  * Generated bundle index. Do not edit.
12283
13840
  */
12284
13841
 
12285
- 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 };
13842
+ export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_DEVTOOLS_TOKEN_TTL_LIMIT, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryDevtoolsTokenTtl, applyQueryFeatures, armAllQueryDevtoolsMocks, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, canOverrideQueryDevtoolsTokenTtl, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTokenTtl, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryDevtoolsSchemaComponents, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsFaults, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, patchQueryDevtoolsTokenPayload, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsArmedMocksRestored, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFaultsRestored, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryDevtoolsTokenTtls, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsStore, registerEthleteVersion, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsAppInfo, setQueryDevtoolsArmedMocksScope, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFaultsScope, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setQueryDevtoolsTokenPayloadPatcher, setQueryDevtoolsTokenTtl, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
12286
13843
  //# sourceMappingURL=ethlete-query.mjs.map