@ethlete/query 6.0.0-next.23 → 6.0.0-next.24

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,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, makeEnvironmentProviders, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
2
+ import { DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
3
3
  import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, setCookie, deleteCookie, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
4
- import { filter, Subject, Subscription, tap, retry, throwError, timer, catchError, take, switchMap, of, firstValueFrom, merge, fromEvent, throttleTime, interval, combineLatest, map, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, takeWhile, EMPTY, withLatestFrom, debounceTime } from 'rxjs';
5
- import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
4
+ import { filter, Subject, Subscription, defer, throwError, timer, switchMap, tap, retry, catchError, take, of, firstValueFrom, merge, fromEvent, throttleTime, interval, combineLatest, map, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, takeWhile, EMPTY, withLatestFrom, debounceTime } from 'rxjs';
6
5
  import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
6
+ import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
7
7
  import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
8
8
  import { isPlatformBrowser, NgClass, AsyncPipe, JsonPipe } from '@angular/common';
9
9
  import { validateAsync, form } from '@angular/forms/signals';
@@ -12,6 +12,45 @@ import { FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
12
12
  import { Router, ActivatedRoute } from '@angular/router';
13
13
  import { io } from 'socket.io-client';
14
14
 
15
+ /**
16
+ * Turns the features applied at creation into their devtools representation.
17
+ * @internal
18
+ */
19
+ const describeQueryDevtoolsFeatures = (features) => features?.map((feature) => ({ type: feature.type, details: feature.devtools?.() ?? [] })) ?? [];
20
+ /**
21
+ * Renders a millisecond duration the way feature details spell one out - `500ms`, `1.5s`, `15m`.
22
+ * @internal
23
+ */
24
+ const formatQueryDevtoolsDuration = (ms) => {
25
+ if (!Number.isFinite(ms))
26
+ return String(ms);
27
+ if (ms < 1000)
28
+ return `${ms}ms`;
29
+ const round = (value, unit) => `${Number(value.toFixed(1))}${unit}`;
30
+ if (ms < 60_000)
31
+ return round(ms / 1000, 's');
32
+ if (ms < 3_600_000)
33
+ return round(ms / 60_000, 'm');
34
+ return round(ms / 3_600_000, 'h');
35
+ };
36
+ /**
37
+ * The name of a function passed as `options.<key>`, or `null` when it is an inline lambda. An inline
38
+ * `{ handler: (e) => … }` inherits the name `handler` from the key it is assigned to, which tells the
39
+ * reader nothing - a reference to a declared function does.
40
+ */
41
+ const queryDevtoolsFnName = (fn, key) => {
42
+ const name = typeof fn === 'function' ? fn.name.replace(/^bound /, '') : '';
43
+ return !name || name === key ? null : name;
44
+ };
45
+ /**
46
+ * A detail for the function passed as `options.<key>`, omitted when it is an inline lambda.
47
+ * @internal
48
+ */
49
+ const queryDevtoolsFnDetail = (fn, key) => {
50
+ const name = queryDevtoolsFnName(fn, key);
51
+ return name ? [{ label: key, value: name }] : [];
52
+ };
53
+
15
54
  const noop$2 = () => undefined;
16
55
  let registrar = null;
17
56
  /**
@@ -33,6 +72,85 @@ const isQueryDevtoolsEnabled = () => registrar !== null;
33
72
  * @internal
34
73
  */
35
74
  const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$2;
75
+ let statsFactory = null;
76
+ /**
77
+ * Installs the stats recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
78
+ * @internal
79
+ */
80
+ const setQueryDevtoolsStatsFactory = (fn) => {
81
+ statsFactory = fn;
82
+ };
83
+ /**
84
+ * A stats recorder for one entry, or `null` unless {@link provideQueryDevtools} has been called.
85
+ *
86
+ * Instrumentation sites must go through this instead of importing `createQueryDevtoolsStats`: a static
87
+ * import pins the whole stats module (~1.4 kB) into every bundle that touches a query, and a runtime
88
+ * `isQueryDevtoolsEnabled()` guard at the call site does not undo that. Reached only from the provider,
89
+ * it is dead code in an app without devtools.
90
+ * @internal
91
+ */
92
+ const createQueryDevtoolsStatsRecorder = () => statsFactory?.() ?? null;
93
+ let formLinksFactory = null;
94
+ /**
95
+ * Installs the form-links recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
96
+ * @internal
97
+ */
98
+ const setQueryDevtoolsFormLinksFactory = (fn) => {
99
+ formLinksFactory = fn;
100
+ };
101
+ /**
102
+ * A form-links recorder for one query, or `null` unless {@link provideQueryDevtools} has been called.
103
+ * Reached only from the provider for the same reason {@link createQueryDevtoolsStatsRecorder} is.
104
+ * @internal
105
+ */
106
+ const createQueryDevtoolsFormLinksRecorder = () => formLinksFactory?.() ?? null;
107
+ /** The ids of the query forms read by the args build currently running, or `null` outside of one. */
108
+ let formLinkSink = null;
109
+ /**
110
+ * Runs `build` and reports which query forms it read. Nested builds are supported - the outer one
111
+ * resumes collecting once the inner one is done.
112
+ * @internal
113
+ */
114
+ const collectQueryFormLinks = (build) => {
115
+ const outer = formLinkSink;
116
+ const sink = new Set();
117
+ formLinkSink = sink;
118
+ try {
119
+ return { value: build(), ids: [...sink] };
120
+ }
121
+ finally {
122
+ formLinkSink = outer;
123
+ }
124
+ };
125
+ /**
126
+ * Records that a query form's committed value was read. The form calls this from its `value` signal, so
127
+ * a read that happens while a query builds its args is what links the two - no naming convention, and
128
+ * nothing to keep in sync when the args change.
129
+ * @internal
130
+ */
131
+ const noteQueryFormRead = (id) => {
132
+ formLinkSink?.add(id);
133
+ };
134
+ let faultResolver = null;
135
+ /**
136
+ * Installs the fault resolver. Called by `provideQueryDevtools()`; nothing else may call it.
137
+ * @internal
138
+ */
139
+ const setQueryDevtoolsFaultResolver = (fn) => {
140
+ faultResolver = fn;
141
+ };
142
+ /**
143
+ * Whether fault injection can do anything at all. Checked before building the wrapper that resolves a
144
+ * fault per attempt, so a request in an app without devtools keeps the pipeline it always had.
145
+ * @internal
146
+ */
147
+ const isQueryDevtoolsFaultInjectionEnabled = () => faultResolver !== null;
148
+ /**
149
+ * Resolves the fault to apply to one upcoming attempt, or `null` when nothing is armed. Consumes a
150
+ * `failNext` budget, so call it exactly once per attempt.
151
+ * @internal
152
+ */
153
+ const resolveQueryDevtoolsFault = (target) => faultResolver?.(target) ?? null;
36
154
  let suppressStackRegistration = false;
37
155
  /**
38
156
  * Suppresses devtools registration for the next {@link createQueryStack} call. Used by
@@ -230,12 +348,15 @@ const invalidStateInsideSecureExecuteFactory = () => {
230
348
  const circularQueryDependency = () => {
231
349
  return new RuntimeError(QueryRuntimeErrorCode.CIRCULAR_QUERY_DEPENDENCY, `Query was executed more than 5 times in less than 100ms. This is usually a sign of a circular dependency.`);
232
350
  };
233
- const legacyPrepareWithoutInjectionContext = (creatorName) => {
351
+ const legacyPrepareWithoutInjectionContext = (creatorName, method = 'prepare') => {
234
352
  const label = creatorName ? `"${creatorName}"` : 'A legacy query creator';
235
- return new RuntimeError(QueryRuntimeErrorCode.LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT, `${label}.prepare() was called outside of an injection context. Unlike the v2 client, the legacy interop layer needs an injector to build the underlying query.\n\n` +
353
+ const example = method === 'prepare'
354
+ ? ` search = (term: string) => myQuery.prepare({ queryParams: { term }, injector: this.injector });`
355
+ : ` container = myQuery.${method}(null, { injector: this.injector });`;
356
+ return new RuntimeError(QueryRuntimeErrorCode.LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT, `${label}.${method}() was called outside of an injection context. Unlike the v2 client, the legacy interop layer needs an injector to build the underlying query.\n\n` +
236
357
  `Capture one where a context does exist (field initializer, constructor) and pass it along:\n\n` +
237
358
  ` private injector = inject(Injector);\n` +
238
- ` search = (term: string) => myQuery.prepare({ queryParams: { term }, injector: this.injector });\n\n` +
359
+ `${example}\n\n` +
239
360
  `Prefer an injector that outlives the call site. A component injector passed from a callback that can still fire after the component is gone leaves prepare() with a destroyed injector, which returns an inert query instead of executing.`);
240
361
  };
241
362
  const querySequenceAlreadyRunning = () => {
@@ -322,7 +443,7 @@ const withArgs = (args) => {
322
443
  // auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
323
444
  // resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
324
445
  let previous = null;
325
- context.state.subtle.setArgsSource(() => {
446
+ const resolve = () => {
326
447
  const value = args();
327
448
  if (value === CLEAR_QUERY_ARGS) {
328
449
  previous = null;
@@ -332,7 +453,12 @@ const withArgs = (args) => {
332
453
  return previous;
333
454
  previous = value;
334
455
  return value;
335
- });
456
+ };
457
+ // Only ever non-null while the devtools are installed. Wrapping the source (rather than reading
458
+ // `state.args` from the outside) is what puts the tracking window around the *evaluation* of the
459
+ // args - a cached read of the computed runs nothing and would record no form at all.
460
+ const formLinks = context.state.subtle.devtoolsFormLinks;
461
+ context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(resolve) : resolve);
336
462
  // Trigger (auto-)execution whenever the resolved args change.
337
463
  nestedEffect(() => {
338
464
  const currArgsNow = context.state.args();
@@ -362,6 +488,10 @@ const withArgs = (args) => {
362
488
  const withPolling = (options) => {
363
489
  return createQueryFeature({
364
490
  type: QueryFeatureType.WITH_POLLING,
491
+ devtools: () => [
492
+ { label: 'interval', value: formatQueryDevtoolsDuration(options.interval) },
493
+ { label: 'execute initially', value: options.executeInitially ? 'yes' : 'no' },
494
+ ],
365
495
  fn: (context) => {
366
496
  if (!context.flags.shouldAutoExecuteMethod) {
367
497
  throw withPollingUsedOnUnsupportedHttpMethod(context.flags.method);
@@ -428,6 +558,7 @@ const withPolling = (options) => {
428
558
  const withLogging = (options) => {
429
559
  return createQueryFeature({
430
560
  type: QueryFeatureType.WITH_LOGGING,
561
+ devtools: () => queryDevtoolsFnDetail(options.logFn, 'logFn'),
431
562
  fn: (context) => {
432
563
  context.state.events$.pipe(takeUntilDestroyed(context.deps.destroyRef)).subscribe((event) => {
433
564
  options.logFn(event);
@@ -439,6 +570,7 @@ const withLogging = (options) => {
439
570
  const withErrorHandling = (options) => {
440
571
  return createQueryFeature({
441
572
  type: QueryFeatureType.WITH_ERROR_HANDLING,
573
+ devtools: () => queryDevtoolsFnDetail(options.handler, 'handler'),
442
574
  fn: (context) => {
443
575
  context.state.events$
444
576
  .pipe(filter((event) => event.type === 'error'), takeUntilDestroyed(context.deps.destroyRef))
@@ -452,6 +584,7 @@ const withErrorHandling = (options) => {
452
584
  const withSuccessHandling = (options) => {
453
585
  return createQueryFeature({
454
586
  type: QueryFeatureType.WITH_SUCCESS_HANDLING,
587
+ devtools: () => queryDevtoolsFnDetail(options.handler, 'handler'),
455
588
  fn: (context) => {
456
589
  context.state.events$
457
590
  .pipe(filter((event) => event.type === HttpEventType.Response), takeUntilDestroyed(context.deps.destroyRef))
@@ -469,6 +602,10 @@ const withSuccessHandling = (options) => {
469
602
  const withAutoRefresh = (options) => {
470
603
  return createQueryFeature({
471
604
  type: QueryFeatureType.WITH_AUTO_REFRESH,
605
+ devtools: () => [
606
+ { label: 'signals', value: String(options.onSignalChanges.length) },
607
+ ...(options.ignoreOnlyManualExecution ? [{ label: 'ignores manual only', value: 'yes' }] : []),
608
+ ],
472
609
  fn: (context) => {
473
610
  if (!context.flags.shouldAutoExecuteMethod) {
474
611
  throw withAutoRefreshUsedOnUnsupportedHttpMethod(context.flags.method);
@@ -495,6 +632,7 @@ const withAutoRefresh = (options) => {
495
632
  const withResponseUpdate = (options) => {
496
633
  return createQueryFeature({
497
634
  type: QueryFeatureType.WITH_RESPONSE_UPDATE,
635
+ devtools: () => queryDevtoolsFnDetail(options.updater, 'updater'),
498
636
  fn: (context) => {
499
637
  nestedEffect(() => {
500
638
  const currentResponse = untracked(() => context.state.response());
@@ -545,6 +683,11 @@ const withPageResetOnError = (options) => {
545
683
  const matches = options.when ?? isPageOutOfRangeError;
546
684
  return createQueryFeature({
547
685
  type: QueryFeatureType.WITH_PAGE_RESET_ON_ERROR,
686
+ devtools: () => [
687
+ { label: 'resets', value: options.reset ? 'custom callback' : 'page signal' },
688
+ ...(options.reset ? [] : [{ label: 'reset to', value: String(options.resetTo ?? 1) }]),
689
+ { label: 'when', value: options.when ? 'custom' : 'page out of range' },
690
+ ],
548
691
  fn: (context) => {
549
692
  context.state.events$
550
693
  .pipe(filter((event) => event.type === 'error'), takeUntilDestroyed(context.deps.destroyRef))
@@ -624,7 +767,10 @@ const setupQueryState = (options) => {
624
767
  error: currentError,
625
768
  };
626
769
  }
627
- else if (currentResponse) {
770
+ else if (currentResponse !== null || latestHttpEvent()?.type === HttpEventType.Response) {
771
+ // A response event having arrived is what makes the execution a success, not a truthy body: a 204 (or any
772
+ // other empty payload) sets `response` to `null`, which would otherwise be indistinguishable from a query
773
+ // that never ran - leaving `filterSuccess`, `onSuccess` and the success handlers waiting forever.
628
774
  return {
629
775
  type: 'success',
630
776
  response: currentResponse,
@@ -651,6 +797,8 @@ const setupQueryState = (options) => {
651
797
  rawResponse,
652
798
  bindRequestEvents,
653
799
  setArgsSource,
800
+ devtoolsStats: options.devtoolsStats ?? null,
801
+ devtoolsFormLinks: options.devtoolsFormLinks ?? null,
654
802
  },
655
803
  };
656
804
  return state;
@@ -824,8 +972,12 @@ const createBaseQuery = (options) => {
824
972
  queryConfig: options.queryConfig,
825
973
  });
826
974
  return runInInjectionContext(deps.injector, () => {
975
+ const devtoolsStats = createQueryDevtoolsStatsRecorder();
976
+ const devtoolsFormLinks = createQueryDevtoolsFormLinksRecorder();
827
977
  const state = setupQueryState({
828
978
  transformResponse: options.creator?.transformResponse,
979
+ devtoolsStats,
980
+ devtoolsFormLinks,
829
981
  });
830
982
  const flags = getQueryFeatureUsage(options);
831
983
  const execute = options.executeFactory({
@@ -844,18 +996,40 @@ const createBaseQuery = (options) => {
844
996
  applyQueryFeatures(options.features, featureFnContext);
845
997
  maybeExecute({ execute, flags });
846
998
  const query = createQueryObject({ state, execute, deps });
847
- if (isQueryDevtoolsEnabled()) {
999
+ // Only ever non-null while the devtools are installed, so this whole block - the entry and the
1000
+ // per-response payload measuring behind it - is dead code in an app without them.
1001
+ if (devtoolsStats) {
1002
+ const statsSubscription = state.events$.subscribe((event) => {
1003
+ if (event.type === 'error') {
1004
+ devtoolsStats.recordError();
1005
+ }
1006
+ else if (event.type === HttpEventType.Response) {
1007
+ devtoolsStats.recordResponse({ headers: event.headers, body: event.body });
1008
+ }
1009
+ });
1010
+ deps.destroyRef.onDestroy(() => statsSubscription.unsubscribe());
1011
+ // A retry happens inside the request, which is shared by every query hitting the same cache key -
1012
+ // so the recorder reads it off the request instead of being handed down into one. `recordRetry` is
1013
+ // idempotent per attempt, which is what makes reading a signal a safe way to count them.
1014
+ effect(() => {
1015
+ const retry = state.subtle.request()?.subtle.retryState();
1016
+ if (retry)
1017
+ devtoolsStats.recordRetry({ attempt: retry.attempt });
1018
+ });
848
1019
  const unregister = registerQueryDevtoolsEntry({
849
1020
  kind: 'query',
850
1021
  handle: query,
851
1022
  clientRef: client,
1023
+ stats: devtoolsStats,
1024
+ formLinks: devtoolsFormLinks ?? undefined,
852
1025
  route: options.creatorInternals.route ??
853
1026
  options.creator?.route ??
854
1027
  null,
1028
+ authProviderRef: options.creatorInternals.authProvider,
855
1029
  meta: {
856
1030
  clientBaseUrl: deps.client.baseUrl,
857
1031
  method: flags.method,
858
- featureTypes: options.features.map((f) => f.type),
1032
+ features: describeQueryDevtoolsFeatures(options.features),
859
1033
  queryConfig: options.queryConfig,
860
1034
  creator: options.creator,
861
1035
  repository: deps.client.repository,
@@ -890,7 +1064,7 @@ const setupQueryExecuteState = () => {
890
1064
  const queryExecute = (options) => {
891
1065
  const { executeOptions, args, executeState, options: runQueryOptions, isSecure } = options;
892
1066
  const { deps, state, creator, creatorInternals, queryConfig } = executeOptions;
893
- const { key, request } = deps.client.repository.request({
1067
+ const { key, request, executed } = deps.client.repository.request({
894
1068
  route: creatorInternals.route,
895
1069
  method: creatorInternals.method,
896
1070
  args,
@@ -908,6 +1082,7 @@ const queryExecute = (options) => {
908
1082
  state.lastTriggeredBy.set(runQueryOptions?.triggeredBy ?? null);
909
1083
  state.subtle.request.set(request);
910
1084
  state.subtle.bindRequestEvents(request);
1085
+ state.subtle.devtoolsStats?.recordExecution({ didRequest: executed, body: args?.body, url: request.url });
911
1086
  };
912
1087
  const circularQueryDependencyChecker = () => {
913
1088
  let lastTriggerTs = 0;
@@ -1009,6 +1184,10 @@ const createBaseQueryCreator = (config) => {
1009
1184
  options: { ...config.options, ...additionalOptions },
1010
1185
  });
1011
1186
  };
1187
+ queryCreator.subtle = {
1188
+ creatorInternals: config.internals,
1189
+ creatorOptions: config.options,
1190
+ };
1012
1191
  return queryCreator;
1013
1192
  };
1014
1193
 
@@ -1341,6 +1520,10 @@ const createHttpRequest = (options) => {
1341
1520
  ...(ngDevMode ? [{ debugName: "lastExecuteTime" }] : /* istanbul ignore next */ []));
1342
1521
  const expiresIn = signal(null, /* @ts-ignore */
1343
1522
  ...(ngDevMode ? [{ debugName: "expiresIn" }] : /* istanbul ignore next */ []));
1523
+ const attempts = signal(1, /* @ts-ignore */
1524
+ ...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
1525
+ const retryState = signal(null, /* @ts-ignore */
1526
+ ...(ngDevMode ? [{ debugName: "retryState" }] : /* istanbul ignore next */ []));
1344
1527
  // NOTE: This must be a plain function, not a `computed`. The freshness check compares against
1345
1528
  // `Date.now()`, which is not reactive, so a memoized computed would only ever recompute when
1346
1529
  // `expiresIn` changes - once it evaluated to `false` (fresh) it would stay `false` forever, even
@@ -1362,26 +1545,68 @@ const createHttpRequest = (options) => {
1362
1545
  // client-level one instead of being appended next to it.
1363
1546
  return argHeaders.keys().reduce((merged, key) => merged.set(key, argHeaders.getAll(key) ?? []), clientHeaders);
1364
1547
  };
1548
+ const send = (headers) => dependencies.httpClient.request(options.method, options.fullPath, {
1549
+ observe: 'events',
1550
+ body: args?.body,
1551
+ reportProgress: clientOptions?.reportProgress,
1552
+ withCredentials: clientOptions?.withCredentials,
1553
+ transferCache: clientOptions?.transferCache,
1554
+ responseType: clientOptions?.responseType || 'json',
1555
+ headers,
1556
+ });
1557
+ /**
1558
+ * Wraps the request in the fault the devtools have armed for this client, if any.
1559
+ *
1560
+ * Inside a `defer` so a fault is resolved per subscription rather than per execution - which is what
1561
+ * makes `retry` re-roll it, so an armed "fail the next two attempts" lets the third one through like a
1562
+ * flaky server would. Injected latency delays the attempt rather than its response, so the loading
1563
+ * state genuinely lasts that long and a retry waits it out too.
1564
+ *
1565
+ * A faulted attempt always goes through `timer`, even at zero latency: no real request can fail in the
1566
+ * same tick it was started in, and settling synchronously inside `execute()` would let an injected
1567
+ * failure land before the caller has finished wiring the request up.
1568
+ */
1569
+ const sendWithFaults = (headers) => defer(() => {
1570
+ const fault = resolveQueryDevtoolsFault({
1571
+ clientName: options.clientName ?? '',
1572
+ method: options.method,
1573
+ url: options.fullPath,
1574
+ });
1575
+ if (!fault)
1576
+ return send(headers);
1577
+ const { status } = fault;
1578
+ const attempt$ = status === null
1579
+ ? send(headers)
1580
+ : throwError(() => new HttpErrorResponse({
1581
+ status,
1582
+ statusText: 'Injected by the query devtools',
1583
+ url: options.fullPath,
1584
+ error: { message: `Injected ${status} (query devtools)` },
1585
+ }));
1586
+ return timer(fault.latencyMs).pipe(switchMap(() => attempt$));
1587
+ });
1365
1588
  const createStream = () => {
1366
1589
  const headers = resolveHeaders();
1367
- return dependencies.httpClient
1368
- .request(options.method, options.fullPath, {
1369
- observe: 'events',
1370
- body: args?.body,
1371
- reportProgress: clientOptions?.reportProgress,
1372
- withCredentials: clientOptions?.withCredentials,
1373
- transferCache: clientOptions?.transferCache,
1374
- responseType: clientOptions?.responseType || 'json',
1375
- headers,
1376
- })
1377
- .pipe(tap((event) => updateState(event)), retry({
1590
+ const source$ = isQueryDevtoolsFaultInjectionEnabled() ? sendWithFaults(headers) : send(headers);
1591
+ return source$.pipe(tap((event) => updateState(event)), retry({
1378
1592
  delay: (error, retryCount) => {
1379
1593
  const retryOptions = { error, retryCount };
1380
1594
  const retryResult = options.retryFn?.(retryOptions) || runDefaultQueryRetry(retryOptions);
1381
1595
  if (!retryResult.retry) {
1382
1596
  return throwError(() => error);
1383
1597
  }
1384
- return timer(retryResult.delay);
1598
+ // rxjs counts retries from 1, so the attempt this delay leads up to is one further along.
1599
+ const attempt = retryCount + 1;
1600
+ retryState.set({
1601
+ attempt,
1602
+ delayMs: retryResult.delay,
1603
+ startsAt: Date.now() + retryResult.delay,
1604
+ status: error instanceof HttpErrorResponse ? error.status : 0,
1605
+ });
1606
+ return timer(retryResult.delay).pipe(tap(() => {
1607
+ retryState.set(null);
1608
+ attempts.set(attempt);
1609
+ }));
1385
1610
  },
1386
1611
  }), catchError((e) => {
1387
1612
  updateErrorState(e);
@@ -1402,6 +1627,8 @@ const createHttpRequest = (options) => {
1402
1627
  });
1403
1628
  error.set(null);
1404
1629
  expiresIn.set(null);
1630
+ attempts.set(1);
1631
+ retryState.set(null);
1405
1632
  const stream = createStream();
1406
1633
  currentStreamSubscription = stream.subscribe({
1407
1634
  error: () => {
@@ -1414,6 +1641,9 @@ const createHttpRequest = (options) => {
1414
1641
  const destroy = () => {
1415
1642
  const wasActive = !currentStreamSubscription.closed;
1416
1643
  currentStreamSubscription.unsubscribe();
1644
+ // A request destroyed mid-backoff never runs the attempt it was waiting for, so leaving the state
1645
+ // set would keep a "retrying in …" readout counting down for a request that is gone.
1646
+ retryState.set(null);
1417
1647
  event$.complete();
1418
1648
  return wasActive;
1419
1649
  };
@@ -1504,6 +1734,7 @@ const createHttpRequest = (options) => {
1504
1734
  const httpRequest = {
1505
1735
  method: options.method,
1506
1736
  url: options.fullPath,
1737
+ args: args ?? null,
1507
1738
  execute,
1508
1739
  destroy,
1509
1740
  loading: loading.asReadonly(),
@@ -1518,6 +1749,9 @@ const createHttpRequest = (options) => {
1518
1749
  lastExternalResponseAt: lastExternalResponseAt.asReadonly(),
1519
1750
  applyPersistedResponse,
1520
1751
  lastPersistedResponseAt: lastPersistedResponseAt.asReadonly(),
1752
+ resolveHeaders,
1753
+ attempts: attempts.asReadonly(),
1754
+ retryState: retryState.asReadonly(),
1521
1755
  },
1522
1756
  };
1523
1757
  return httpRequest;
@@ -1685,7 +1919,7 @@ const createQueryStack = (options) => {
1685
1919
  kind: 'query-stack',
1686
1920
  handle: stack,
1687
1921
  meta: {
1688
- featureTypes: features.map((f) => f.type),
1922
+ features: describeQueryDevtoolsFeatures(features),
1689
1923
  },
1690
1924
  });
1691
1925
  inject(DestroyRef).onDestroy(unregister);
@@ -1974,7 +2208,7 @@ const createPagedQueryStack = (options) => {
1974
2208
  kind: 'paged-query-stack',
1975
2209
  handle: pagedQuery,
1976
2210
  meta: {
1977
- featureTypes: features?.map((f) => f.type),
2211
+ features: describeQueryDevtoolsFeatures(features),
1978
2212
  },
1979
2213
  });
1980
2214
  inject(DestroyRef).onDestroy(unregister);
@@ -2732,13 +2966,21 @@ const createQuerySyncEngine = (options) => {
2732
2966
  // An invalidation is something the other tab's app code asked for explicitly, so unlike the
2733
2967
  // mutation heuristic it is not something `refreshOnMutation: false` opts out of.
2734
2968
  if (message.type === 'invalidate') {
2735
- repository.refreshInUse(createQueryInvalidationFilter({ url: message.url }));
2969
+ repository.refreshInUse(createQueryInvalidationFilter({ url: message.url }), {
2970
+ type: 'invalidation',
2971
+ url: message.url,
2972
+ otherTab: true,
2973
+ });
2736
2974
  return;
2737
2975
  }
2738
2976
  if (!refreshOnMutation)
2739
2977
  return;
2740
2978
  const mutation = { method: message.method, url: message.url };
2741
- repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined);
2979
+ repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined, {
2980
+ type: 'mutation',
2981
+ url: message.url,
2982
+ otherTab: true,
2983
+ });
2742
2984
  };
2743
2985
  const postInvalidation = (url) => transport.post({ type: 'invalidate', url });
2744
2986
  const eventSubscription = repository.events$.subscribe(broadcast);
@@ -2853,6 +3095,13 @@ const QueryClientFeatureType = {
2853
3095
  DEFAULT_RETRY: 'DEFAULT_RETRY',
2854
3096
  ETHLETE_API_ERRORS: 'ETHLETE_API_ERRORS',
2855
3097
  };
3098
+ const describeRefreshOnMutation = (config) => {
3099
+ if (config === false)
3100
+ return 'no';
3101
+ if (config === true || config === undefined)
3102
+ return 'every query in use';
3103
+ return 'filtered';
3104
+ };
2856
3105
  /**
2857
3106
  * Coordinates a query client with its own instances in the user's **other tabs**, over a
2858
3107
  * `BroadcastChannel` and the Web Locks API. Three things happen once it is on:
@@ -2893,7 +3142,16 @@ const withMultiTabSync = (config) => ({ clientName, repository, destroyRef, isBr
2893
3142
  lockManager: createQueryKeyLockManager(`et-query-poll:${channelName}`),
2894
3143
  });
2895
3144
  destroyRef.onDestroy(sync.destroy);
2896
- return { type: QueryClientFeatureType.MULTI_TAB_SYNC, instance: sync };
3145
+ return {
3146
+ type: QueryClientFeatureType.MULTI_TAB_SYNC,
3147
+ instance: sync,
3148
+ devtools: () => [
3149
+ { label: 'channel', value: channelName },
3150
+ { label: 'share responses', value: syncConfig.syncResponses === false ? 'no' : 'yes' },
3151
+ { label: 'dedupe polling', value: syncConfig.dedupePolling === false ? 'no' : 'yes' },
3152
+ { label: 'refresh on mutation', value: describeRefreshOnMutation(syncConfig.refreshOnMutation) },
3153
+ ],
3154
+ };
2897
3155
  };
2898
3156
  /**
2899
3157
  * Keeps a query client's successful reads on disk (IndexedDB), so a reload renders the last known
@@ -2947,7 +3205,19 @@ const withQueryPersistence = (config) => ({ clientName, repository, destroyRef,
2947
3205
  window.removeEventListener('pagehide', flush);
2948
3206
  engine.destroy();
2949
3207
  });
2950
- return { type: QueryClientFeatureType.PERSISTENCE, instance: engine };
3208
+ return {
3209
+ type: QueryClientFeatureType.PERSISTENCE,
3210
+ instance: engine,
3211
+ devtools: () => [
3212
+ { label: 'store', value: persistenceConfig.storageName ?? `et-query-persistence-${clientName}` },
3213
+ { label: 'version', value: String(persistenceConfig.version ?? 1) },
3214
+ { label: 'max age', value: formatQueryDevtoolsDuration(persistenceConfig.maxAge ?? 86_400_000) },
3215
+ { label: 'max entries', value: String(persistenceConfig.maxEntries ?? 50) },
3216
+ { label: 'write delay', value: formatQueryDevtoolsDuration(persistenceConfig.writeDelay ?? 1000) },
3217
+ ...(persistenceConfig.adapter ? [{ label: 'adapter', value: 'custom' }] : []),
3218
+ ...queryDevtoolsFnDetail(persistenceConfig.filter, 'filter'),
3219
+ ],
3220
+ };
2951
3221
  };
2952
3222
  /**
2953
3223
  * Teaches the error pipeline to read an **HTML error page** - the 502 a proxy answers with, a
@@ -3104,10 +3374,10 @@ const createQueryRepository = (config) => {
3104
3374
  isPersistEnabled,
3105
3375
  keepUnusedFor,
3106
3376
  });
3107
- if (!runQueryOptions?.allowCache || cacheEntry.request.isStale()) {
3108
- cacheEntry.request.execute({ allowCache: runQueryOptions?.allowCache });
3109
- }
3110
- return { key: cacheKey, request: cacheEntry.request };
3377
+ const executed = !runQueryOptions?.allowCache || cacheEntry.request.isStale()
3378
+ ? cacheEntry.request.execute({ allowCache: runQueryOptions?.allowCache })
3379
+ : false;
3380
+ return { key: cacheKey, request: cacheEntry.request, executed };
3111
3381
  }
3112
3382
  }
3113
3383
  const request = createHttpRequest({
@@ -3116,11 +3386,12 @@ const createQueryRepository = (config) => {
3116
3386
  method: options.method,
3117
3387
  dependencies: config.dependencies,
3118
3388
  clientOptions: creatorOptions,
3389
+ clientName: config.name,
3119
3390
  clientHeaders: config.headers,
3120
3391
  cacheAdapter: config.cacheAdapter,
3121
3392
  retryFn: options.retryFn ?? config.retryFn,
3122
3393
  });
3123
- request.execute();
3394
+ const executed = request.execute();
3124
3395
  bind({
3125
3396
  key: trackingKey,
3126
3397
  consumerDestroyRef: options.consumerDestroyRef,
@@ -3135,7 +3406,7 @@ const createQueryRepository = (config) => {
3135
3406
  // on it - hydration from the persisted store - is only ever allowed to fill a gap this request has
3136
3407
  // not filled itself.
3137
3408
  eventsSubject.next({ type: 'entry-created', key: trackingKey, isCached: shouldCache, isPersistEnabled });
3138
- return { key: trackingKey, request };
3409
+ return { key: trackingKey, request, executed };
3139
3410
  };
3140
3411
  /** Tears an entry down for good, whether it currently has consumers or not. */
3141
3412
  const destroyEntry = (key, cacheEntry) => {
@@ -3198,7 +3469,8 @@ const createQueryRepository = (config) => {
3198
3469
  bumpCacheVersion();
3199
3470
  eventsSubject.next({ type: 'unbind-all-secure' });
3200
3471
  };
3201
- const refreshInUse = (filter) => {
3472
+ const refreshInUse = (filter, cause) => {
3473
+ const refreshed = [];
3202
3474
  for (const cacheEntry of cache.values()) {
3203
3475
  if (cacheEntry.consumers.size === 0)
3204
3476
  continue;
@@ -3209,8 +3481,14 @@ const createQueryRepository = (config) => {
3209
3481
  continue;
3210
3482
  if (filter && !filter(cacheEntry.request))
3211
3483
  continue;
3484
+ refreshed.push(cacheEntry.request);
3212
3485
  cacheEntry.request.execute({ force: true });
3213
3486
  }
3487
+ eventsSubject.next({
3488
+ type: 'queries-refreshed',
3489
+ cause: cause ?? { type: 'refresh', url: null, otherTab: false },
3490
+ requests: refreshed,
3491
+ });
3214
3492
  };
3215
3493
  const applyExternalResponse = (options) => {
3216
3494
  const cacheEntry = cache.get(options.key);
@@ -3322,6 +3600,7 @@ const createQueryClient = (options) => defineRootProvider(() => {
3322
3600
  const destroyRef = inject(DestroyRef);
3323
3601
  let sync = null;
3324
3602
  let persistenceEngine = null;
3603
+ const appliedFeatures = [];
3325
3604
  if (options.features?.length) {
3326
3605
  const seen = new Set();
3327
3606
  for (const featureFn of options.features) {
@@ -3329,6 +3608,7 @@ const createQueryClient = (options) => defineRootProvider(() => {
3329
3608
  if (seen.has(feature.type))
3330
3609
  throw queryClientFeatureUsedMultipleTimes(feature.type);
3331
3610
  seen.add(feature.type);
3611
+ appliedFeatures.push(feature);
3332
3612
  if (feature.type === QueryClientFeatureType.MULTI_TAB_SYNC) {
3333
3613
  sync = feature.instance;
3334
3614
  }
@@ -3343,7 +3623,11 @@ const createQueryClient = (options) => defineRootProvider(() => {
3343
3623
  refreshQueriesInUse: () => repository.refreshInUse(),
3344
3624
  invalidateQueries: (invalidation) => {
3345
3625
  const url = invalidation?.url ? resolveInvalidationUrl(options.baseUrl, invalidation.url) : null;
3346
- repository.refreshInUse(createQueryInvalidationFilter({ url, filter: invalidation?.filter }));
3626
+ repository.refreshInUse(createQueryInvalidationFilter({ url, filter: invalidation?.filter }), {
3627
+ type: 'invalidation',
3628
+ url,
3629
+ otherTab: false,
3630
+ });
3347
3631
  // The resolved URL is what travels: the other tabs are the same client, so they would
3348
3632
  // resolve it identically, and a message that needs no interpretation cannot drift.
3349
3633
  if (invalidation?.otherTabs ?? true)
@@ -3351,7 +3635,11 @@ const createQueryClient = (options) => defineRootProvider(() => {
3351
3635
  },
3352
3636
  clearPersistedQueries: () => persistenceEngine?.clear() ?? Promise.resolve(),
3353
3637
  whenPersistenceReady: persistenceEngine?.whenReady ?? Promise.resolve(),
3354
- subtle: { sync, persistence: persistenceEngine },
3638
+ subtle: {
3639
+ sync,
3640
+ persistence: persistenceEngine,
3641
+ devtoolsFeatures: isQueryDevtoolsEnabled() ? describeQueryDevtoolsFeatures(appliedFeatures) : [],
3642
+ },
3355
3643
  };
3356
3644
  return client;
3357
3645
  }, {
@@ -3998,8 +4286,10 @@ const setupBearerQueryRegistry = (builders, injector, latestExecutedQuery, lates
3998
4286
  };
3999
4287
  const setupFeatures = (featureBuilders, context) => {
4000
4288
  const features = {};
4289
+ /** The features as applied, in order - what the devtools describe. */
4290
+ const applied = [];
4001
4291
  if (!featureBuilders?.length) {
4002
- return features;
4292
+ return { features, applied };
4003
4293
  }
4004
4294
  const featureTypes = new Set();
4005
4295
  for (const featureSetup of featureBuilders) {
@@ -4014,9 +4304,25 @@ const setupFeatures = (featureBuilders, context) => {
4014
4304
  .map((word, index) => (index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))
4015
4305
  .join('');
4016
4306
  features[featureName] = feature.instance;
4307
+ applied.push({ type: feature.type, devtools: feature.devtools });
4017
4308
  }
4018
- return features;
4309
+ return { features, applied };
4019
4310
  };
4311
+ /**
4312
+ * Describes the provider's queries for the devtools from what their creators were built with, so a
4313
+ * refresh request can be exported before the refresh query has ever run.
4314
+ */
4315
+ const describeAuthQueries = (builders) => builders.map((builder) => {
4316
+ const internals = builder.config.queryCreator.subtle.creatorInternals;
4317
+ const isRefresh = builder._type === 'tokenRefreshQuery';
4318
+ return {
4319
+ key: builder.key,
4320
+ kind: (isRefresh ? 'token-refresh' : 'auth'),
4321
+ method: internals.method ?? 'POST',
4322
+ route: internals.route,
4323
+ buildArgs: isRefresh ? builder.buildArgs : undefined,
4324
+ };
4325
+ });
4020
4326
  const alwaysLeader = () => true;
4021
4327
  const runEarlyFeatureSetup = (featureBuilders, context) => {
4022
4328
  let isLeaderFn = alwaysLeader;
@@ -4110,7 +4416,7 @@ const createBearerAuthProviderImpl = (config) => {
4110
4416
  queries: queries,
4111
4417
  executionState,
4112
4418
  };
4113
- const features = setupFeatures(config.features, featureSetupContext);
4419
+ const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
4114
4420
  const provider = {
4115
4421
  queries,
4116
4422
  features: features,
@@ -4130,11 +4436,13 @@ const createBearerAuthProviderImpl = (config) => {
4130
4436
  kind: 'auth-provider',
4131
4437
  handle: provider,
4132
4438
  clientRef: config.queryClientRef,
4439
+ authQueries: describeAuthQueries(config.queries),
4133
4440
  meta: {
4134
4441
  name: config.name,
4135
4442
  clientBaseUrl: queryClient.baseUrl,
4136
4443
  repository: queryClient.repository,
4137
4444
  client: queryClient,
4445
+ features: describeQueryDevtoolsFeatures(appliedFeatures),
4138
4446
  },
4139
4447
  });
4140
4448
  destroyRef.onDestroy(unregister);
@@ -4177,6 +4485,7 @@ const withRefreshQuery = (key, config) => {
4177
4485
  const delay = Math.min(exponentialDelay, maxRetryDelayMs);
4178
4486
  return { retry: true, delay };
4179
4487
  };
4488
+ const buildArgs = (refreshToken) => ({ body: { token: refreshToken } });
4180
4489
  const setup = (context) => {
4181
4490
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
4182
4491
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
@@ -4192,9 +4501,8 @@ const withRefreshQuery = (key, config) => {
4192
4501
  if (timeSinceLastRefresh < minRefreshInterval)
4193
4502
  return;
4194
4503
  lastRefreshTime = now;
4195
- const refreshArgs = { body: { token: currentRefreshToken } };
4196
4504
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4197
- context.queries[key].execute(refreshArgs, {
4505
+ context.queries[key].execute(buildArgs(currentRefreshToken), {
4198
4506
  triggeredBy: triggeredInternally ? 'token-refresh' : undefined,
4199
4507
  });
4200
4508
  };
@@ -4277,6 +4585,7 @@ const withRefreshQuery = (key, config) => {
4277
4585
  queryCreator: config.queryCreator.clone({ retryFn: refreshRetryFn, subtle: { useQueryRepositoryCache: true } }),
4278
4586
  },
4279
4587
  setup,
4588
+ buildArgs,
4280
4589
  };
4281
4590
  };
4282
4591
 
@@ -4343,6 +4652,11 @@ const withInactivityLogout = (config = {}) => {
4343
4652
  return {
4344
4653
  type: BearerAuthFeatureType.INACTIVITY_LOGOUT,
4345
4654
  instance,
4655
+ devtools: () => [
4656
+ { label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
4657
+ { label: 'activity events', value: activityEvents.join(', ') },
4658
+ ...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
4659
+ ],
4346
4660
  };
4347
4661
  };
4348
4662
  };
@@ -4619,6 +4933,15 @@ const withBearerAuthMultiTabSync = (config = {}) => {
4619
4933
  const setup = (_context) => ({
4620
4934
  type: BearerAuthFeatureType.MULTI_TAB_SYNC,
4621
4935
  instance,
4936
+ devtools: () => [
4937
+ { label: 'channel', value: config.channelName ?? 'ethlete-auth-sync' },
4938
+ { label: 'tokens', value: config.syncTokens === false ? 'tab local' : 'synced' },
4939
+ { label: 'logout', value: config.syncLogout === false ? 'tab local' : 'synced' },
4940
+ {
4941
+ label: 'leader election',
4942
+ value: config.leaderElection === false ? 'every tab refreshes' : 'one tab refreshes',
4943
+ },
4944
+ ],
4622
4945
  });
4623
4946
  // The elected leader gates the auth queries' automatic refresh, which is wired up before the
4624
4947
  // regular feature setup runs - so this half has to happen earlier than `setup`.
@@ -4639,6 +4962,16 @@ const withPersistentAuth = (config) => {
4639
4962
  return {
4640
4963
  type: BearerAuthFeatureType.PERSISTENT_AUTH,
4641
4964
  instance,
4965
+ devtools: () => [
4966
+ { label: 'cookie', value: config.cookie?.name ?? 'etAuth' },
4967
+ { label: 'remembered for', value: `${config.cookie?.expiresInDays ?? 30}d` },
4968
+ { label: 'same site', value: config.cookie?.sameSite ?? 'lax' },
4969
+ { label: 'remember me default', value: config.defaultRememberMe ? 'yes' : 'no' },
4970
+ { label: 'auto login', value: config.autoLogin.queryKey },
4971
+ ...(config.autoLogin.excludeRoutes?.length
4972
+ ? [{ label: 'excluded routes', value: config.autoLogin.excludeRoutes.join(', ') }]
4973
+ : []),
4974
+ ],
4642
4975
  };
4643
4976
  };
4644
4977
  };
@@ -4745,6 +5078,10 @@ const withTokenExpirationWarning = (config = {}) => {
4745
5078
  return {
4746
5079
  type: BearerAuthFeatureType.TOKEN_EXPIRATION_WARNING,
4747
5080
  instance,
5081
+ devtools: () => [
5082
+ { label: 'warn before', value: formatQueryDevtoolsDuration(warningThreshold) },
5083
+ { label: 'check every', value: formatQueryDevtoolsDuration(checkInterval) },
5084
+ ],
4748
5085
  };
4749
5086
  };
4750
5087
  };
@@ -4799,6 +5136,10 @@ const withTokenRevocation = (config) => {
4799
5136
  return {
4800
5137
  type: BearerAuthFeatureType.TOKEN_REVOCATION,
4801
5138
  instance,
5139
+ devtools: () => [
5140
+ { label: 'query', value: config.queryKey },
5141
+ { label: 'on logout', value: revokeOnLogout ? 'yes' : 'no' },
5142
+ ],
4802
5143
  };
4803
5144
  };
4804
5145
  };
@@ -4806,9 +5147,14 @@ const withTokenRevocation = (config) => {
4806
5147
  const withTracking = (config) => {
4807
5148
  return (context) => {
4808
5149
  const instance = createTrackingFeature(context, config);
5150
+ const handledEvents = Object.keys(config?.on ?? {});
4809
5151
  return {
4810
5152
  type: BearerAuthFeatureType.TRACKING,
4811
5153
  instance,
5154
+ devtools: () => [
5155
+ { label: 'internal events', value: config?.trackInternalEvents === false ? 'ignored' : 'tracked' },
5156
+ ...(handledEvents.length ? [{ label: 'handlers', value: handledEvents.join(', ') }] : []),
5157
+ ],
4812
5158
  };
4813
5159
  };
4814
5160
  };
@@ -4945,6 +5291,347 @@ const createTrackingFeature = (context, config) => {
4945
5291
  };
4946
5292
  };
4947
5293
 
5294
+ /** The neutral fault: nothing armed. */
5295
+ const EMPTY_QUERY_DEVTOOLS_FAULT = {
5296
+ latencyMs: 0,
5297
+ failNext: 0,
5298
+ failRate: 0,
5299
+ status: 503,
5300
+ };
5301
+ /**
5302
+ * Statuses worth injecting, and whether the SDK's default retry policy retries them. A picker built from
5303
+ * this cannot arm a 500 and then leave you wondering why no retry fired.
5304
+ * @see runDefaultQueryRetry
5305
+ */
5306
+ const QUERY_DEVTOOLS_FAULT_STATUSES = [
5307
+ { status: 0, label: 'Network error', retryable: true },
5308
+ { status: 400, label: 'Bad Request', retryable: false },
5309
+ { status: 401, label: 'Unauthorized', retryable: false },
5310
+ { status: 403, label: 'Forbidden', retryable: false },
5311
+ { status: 404, label: 'Not Found', retryable: false },
5312
+ { status: 408, label: 'Request Timeout', retryable: true },
5313
+ { status: 429, label: 'Too Many Requests', retryable: true },
5314
+ { status: 500, label: 'Internal Server Error', retryable: false },
5315
+ { status: 502, label: 'Bad Gateway', retryable: true },
5316
+ { status: 503, label: 'Service Unavailable', retryable: true },
5317
+ ];
5318
+ const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
5319
+ ...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
5320
+ /**
5321
+ * The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
5322
+ * are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
5323
+ */
5324
+ const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
5325
+ /** Whether anything at all is armed on a fault. */
5326
+ const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
5327
+ /**
5328
+ * Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
5329
+ * value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
5330
+ *
5331
+ * Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
5332
+ * something an application should arm on itself.
5333
+ */
5334
+ const setQueryDevtoolsFault = (options) => {
5335
+ const { clientName, patch } = options;
5336
+ faults.update((current) => {
5337
+ const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
5338
+ if (!isQueryDevtoolsFaultArmed(next)) {
5339
+ const { [clientName]: _removed, ...rest } = current;
5340
+ return rest;
5341
+ }
5342
+ return { ...current, [clientName]: next };
5343
+ });
5344
+ };
5345
+ /**
5346
+ * Disarms every fault of one client, or of all clients when no name is given.
5347
+ *
5348
+ * @see setQueryDevtoolsFault
5349
+ */
5350
+ const clearQueryDevtoolsFaults = (clientName) => faults.update((current) => {
5351
+ if (clientName === undefined)
5352
+ return {};
5353
+ const { [clientName]: _removed, ...rest } = current;
5354
+ return rest;
5355
+ });
5356
+ /**
5357
+ * Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
5358
+ * must be called once per attempt and never speculatively.
5359
+ *
5360
+ * Installed as the fault resolver by `provideQueryDevtools()`; nothing else may call it.
5361
+ * @internal
5362
+ */
5363
+ const resolveQueryDevtoolsFaultForAttempt = (target) => {
5364
+ const fault = faults()[target.clientName];
5365
+ if (!fault || !isQueryDevtoolsFaultArmed(fault))
5366
+ return null;
5367
+ let shouldFail = false;
5368
+ if (fault.failNext > 0) {
5369
+ shouldFail = true;
5370
+ setQueryDevtoolsFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
5371
+ }
5372
+ else if (fault.failRate > 0) {
5373
+ shouldFail = Math.random() * 100 < fault.failRate;
5374
+ }
5375
+ return {
5376
+ latencyMs: fault.latencyMs,
5377
+ status: shouldFail ? fault.status : null,
5378
+ };
5379
+ };
5380
+
5381
+ /**
5382
+ * Records which query forms feed one query's args, so the devtools can answer "which query does this
5383
+ * form drive?" from the reads themselves rather than from a naming convention.
5384
+ * @internal
5385
+ */
5386
+ const createQueryDevtoolsFormLinks = () => {
5387
+ const ids = signal([], /* @ts-ignore */
5388
+ ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
5389
+ return {
5390
+ ids: ids.asReadonly(),
5391
+ track: (build) => {
5392
+ const collected = collectQueryFormLinks(build);
5393
+ // Args are built inside a reactive computation, which may not write to a signal - `untracked`
5394
+ // lifts that restriction, and skipping the write unless something is new keeps the panel from
5395
+ // being notified on every evaluation.
5396
+ untracked(() => {
5397
+ const known = ids();
5398
+ const added = collected.ids.filter((id) => !known.includes(id));
5399
+ if (added.length)
5400
+ ids.set([...known, ...added]);
5401
+ });
5402
+ return collected.value;
5403
+ },
5404
+ };
5405
+ };
5406
+
5407
+ const EMPTY_STATS = {
5408
+ executions: 0,
5409
+ requests: 0,
5410
+ responses: 0,
5411
+ errors: 0,
5412
+ retries: 0,
5413
+ receivedBytes: 0,
5414
+ sentBytes: 0,
5415
+ hasEstimatedBytes: false,
5416
+ firstExecutedAt: null,
5417
+ lastResponseAt: null,
5418
+ lastDurationMs: null,
5419
+ totalDurationMs: 0,
5420
+ };
5421
+ const textEncoder = typeof TextEncoder === 'function' ? /* @__PURE__ */ new TextEncoder() : null;
5422
+ const byteLengthOf = (text) => textEncoder?.encode(text).length ?? text.length;
5423
+ const estimateBodySize = (body) => {
5424
+ if (body === null || body === undefined)
5425
+ return 0;
5426
+ if (typeof body === 'string')
5427
+ return byteLengthOf(body);
5428
+ if (typeof Blob !== 'undefined' && body instanceof Blob)
5429
+ return body.size;
5430
+ if (body instanceof ArrayBuffer)
5431
+ return body.byteLength;
5432
+ if (ArrayBuffer.isView(body))
5433
+ return body.byteLength;
5434
+ if (typeof FormData !== 'undefined' && body instanceof FormData)
5435
+ return 0;
5436
+ try {
5437
+ return byteLengthOf(JSON.stringify(body) ?? '');
5438
+ }
5439
+ catch {
5440
+ // A body holding a circular reference or a BigInt has no serialized size to report.
5441
+ return 0;
5442
+ }
5443
+ };
5444
+ /** How many runs a query keeps. Enough to see a stampede or a chain without growing unbounded. */
5445
+ const RUN_HISTORY = 25;
5446
+ /**
5447
+ * How many of those runs keep their response body. Bodies dominate what the buffer retains, and a diff
5448
+ * only ever looks a couple of runs back.
5449
+ */
5450
+ const RESPONSE_HISTORY = 5;
5451
+ /** The index of the newest run still in flight, or -1 when none is. */
5452
+ const lastPendingRunIndex = (runs) => {
5453
+ for (let index = runs.length - 1; index >= 0; index--) {
5454
+ if (runs[index]?.status === 'pending')
5455
+ return index;
5456
+ }
5457
+ return -1;
5458
+ };
5459
+ /**
5460
+ * Drops the response body of every run past the newest {@link RESPONSE_HISTORY} that hold one. Mutates
5461
+ * the array it is given, which is always one this module just built.
5462
+ */
5463
+ const trimRetainedResponses = (runs) => {
5464
+ let kept = 0;
5465
+ for (let index = runs.length - 1; index >= 0; index--) {
5466
+ const run = runs[index];
5467
+ if (!run?.hasResponse)
5468
+ continue;
5469
+ if (++kept > RESPONSE_HISTORY)
5470
+ runs[index] = { ...run, response: null, hasResponse: false };
5471
+ }
5472
+ return runs;
5473
+ };
5474
+ /**
5475
+ * The size of a payload in bytes, and whether that size is the one that went over the wire. Only a
5476
+ * `content-length` header is exact; everything else is measured by serializing the body.
5477
+ * @internal
5478
+ */
5479
+ const measureQueryDevtoolsPayload = (payload) => {
5480
+ const contentLength = Number(payload.headers?.get('content-length'));
5481
+ if (Number.isFinite(contentLength) && contentLength > 0)
5482
+ return { bytes: contentLength, isExact: true };
5483
+ return { bytes: estimateBodySize(payload.body), isExact: false };
5484
+ };
5485
+ /**
5486
+ * Accumulates what a query does, for the devtools panel. Only ever created while
5487
+ * {@link provideQueryDevtools} is installed - measuring a body's size serializes it, which no app
5488
+ * without devtools should pay for.
5489
+ * @internal
5490
+ */
5491
+ const createQueryDevtoolsStats = () => {
5492
+ const stats = signal(EMPTY_STATS, /* @ts-ignore */
5493
+ ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
5494
+ const runs = signal([], /* @ts-ignore */
5495
+ ...(ngDevMode ? [{ debugName: "runs" }] : /* istanbul ignore next */ []));
5496
+ // The baseline the next response's duration is measured against. Deliberately not part of the stats:
5497
+ // a duration is only ever reported once the response it belongs to has arrived.
5498
+ let lastExecutionAt = null;
5499
+ // The URL of the last execution, whether or not it requested - so a response that arrives without a run
5500
+ // of the query's own is still attributed to the URL the query was pointed at.
5501
+ let lastUrl = null;
5502
+ let runCounter = 0;
5503
+ const appendRun = (run, previous) => {
5504
+ const appended = { ...run, index: ++runCounter };
5505
+ runs.set(trimRetainedResponses([...previous, appended].slice(-RUN_HISTORY)));
5506
+ };
5507
+ /**
5508
+ * Ends the newest run still in flight. A response with no run of the query's own waiting for it came
5509
+ * from elsewhere (see {@link QueryDevtoolsRun.didRequest}), so it is recorded as an instant instead.
5510
+ */
5511
+ const endRun = (end) => {
5512
+ const current = runs();
5513
+ const pending = lastPendingRunIndex(current);
5514
+ if (pending === -1) {
5515
+ appendRun({ ...end, startedAt: end.endedAt, didRequest: false, url: lastUrl, sentBytes: 0, attempts: 0 }, current);
5516
+ return;
5517
+ }
5518
+ runs.set(trimRetainedResponses(current.map((run, index) => (index === pending ? { ...run, ...end } : run))));
5519
+ };
5520
+ const recordExecution = ({ didRequest, body, url }) => {
5521
+ const now = Date.now();
5522
+ const sent = didRequest ? measureQueryDevtoolsPayload({ body }) : null;
5523
+ lastExecutionAt = now;
5524
+ lastUrl = url ?? null;
5525
+ stats.update((current) => ({
5526
+ ...current,
5527
+ executions: current.executions + 1,
5528
+ requests: current.requests + (didRequest ? 1 : 0),
5529
+ sentBytes: current.sentBytes + (sent?.bytes ?? 0),
5530
+ hasEstimatedBytes: current.hasEstimatedBytes || (!!sent?.bytes && !sent.isExact),
5531
+ firstExecutedAt: current.firstExecutedAt ?? now,
5532
+ }));
5533
+ // An execution answered from the cache, or one that joined a request already in flight, starts no
5534
+ // run of its own - the response it eventually gets (if any) is what records one.
5535
+ if (!didRequest)
5536
+ return;
5537
+ appendRun({
5538
+ startedAt: now,
5539
+ endedAt: null,
5540
+ status: 'pending',
5541
+ didRequest: true,
5542
+ url: lastUrl,
5543
+ attempts: 1,
5544
+ sentBytes: sent?.bytes ?? 0,
5545
+ receivedBytes: 0,
5546
+ response: null,
5547
+ hasResponse: false,
5548
+ }, runs().map((run) => (run.status === 'pending' ? { ...run, status: 'aborted', endedAt: now } : run)));
5549
+ };
5550
+ const recordResponse = (payload) => {
5551
+ const now = Date.now();
5552
+ const received = measureQueryDevtoolsPayload(payload);
5553
+ const duration = lastExecutionAt === null ? null : now - lastExecutionAt;
5554
+ stats.update((current) => ({
5555
+ ...current,
5556
+ responses: current.responses + 1,
5557
+ receivedBytes: current.receivedBytes + received.bytes,
5558
+ hasEstimatedBytes: current.hasEstimatedBytes || (received.bytes > 0 && !received.isExact),
5559
+ lastResponseAt: now,
5560
+ lastDurationMs: duration,
5561
+ totalDurationMs: current.totalDurationMs + (duration ?? 0),
5562
+ }));
5563
+ endRun({
5564
+ endedAt: now,
5565
+ status: 'success',
5566
+ receivedBytes: received.bytes,
5567
+ response: payload.body,
5568
+ hasResponse: true,
5569
+ });
5570
+ };
5571
+ const recordError = () => {
5572
+ stats.update((current) => ({ ...current, errors: current.errors + 1 }));
5573
+ endRun({ endedAt: Date.now(), status: 'error', receivedBytes: 0, response: null, hasResponse: false });
5574
+ };
5575
+ const recordRetry = ({ attempt }) => {
5576
+ const current = runs();
5577
+ const pending = lastPendingRunIndex(current);
5578
+ const run = current[pending];
5579
+ // Nothing in flight means the retry belongs to a request this query is not the one waiting on, and an
5580
+ // attempt already accounted for is the caller reporting the same one twice.
5581
+ if (!run || attempt <= run.attempts)
5582
+ return;
5583
+ stats.update((state) => ({ ...state, retries: state.retries + (attempt - run.attempts) }));
5584
+ runs.set(current.map((entry, index) => (index === pending ? { ...entry, attempts: attempt } : entry)));
5585
+ };
5586
+ const reset = () => {
5587
+ lastExecutionAt = null;
5588
+ lastUrl = null;
5589
+ runCounter = 0;
5590
+ stats.set(EMPTY_STATS);
5591
+ runs.set([]);
5592
+ };
5593
+ return {
5594
+ current: stats.asReadonly(),
5595
+ runs: runs.asReadonly(),
5596
+ reset,
5597
+ recordExecution,
5598
+ recordResponse,
5599
+ recordError,
5600
+ recordRetry,
5601
+ };
5602
+ };
5603
+ /**
5604
+ * Adds up the stats of several entries, for a stack's or a client's total. `lastDurationMs` is taken from
5605
+ * whichever entry responded most recently rather than summed.
5606
+ */
5607
+ const sumQueryDevtoolsStats = (handles) => {
5608
+ const total = { ...EMPTY_STATS };
5609
+ for (const handle of handles) {
5610
+ if (!handle)
5611
+ continue;
5612
+ const stats = handle.current();
5613
+ total.executions += stats.executions;
5614
+ total.requests += stats.requests;
5615
+ total.responses += stats.responses;
5616
+ total.errors += stats.errors;
5617
+ total.retries += stats.retries;
5618
+ total.receivedBytes += stats.receivedBytes;
5619
+ total.sentBytes += stats.sentBytes;
5620
+ total.totalDurationMs += stats.totalDurationMs;
5621
+ total.hasEstimatedBytes = total.hasEstimatedBytes || stats.hasEstimatedBytes;
5622
+ if (stats.firstExecutedAt !== null &&
5623
+ (total.firstExecutedAt === null || stats.firstExecutedAt < total.firstExecutedAt)) {
5624
+ total.firstExecutedAt = stats.firstExecutedAt;
5625
+ }
5626
+ if (stats.lastResponseAt !== null &&
5627
+ (total.lastResponseAt === null || stats.lastResponseAt > total.lastResponseAt)) {
5628
+ total.lastResponseAt = stats.lastResponseAt;
5629
+ total.lastDurationMs = stats.lastDurationMs;
5630
+ }
5631
+ }
5632
+ return total;
5633
+ };
5634
+
4948
5635
  const entries = /* @__PURE__ */ signal([], /* @ts-ignore */
4949
5636
  ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
4950
5637
  /**
@@ -4963,26 +5650,49 @@ const descriptorOf = (entry) => {
4963
5650
  return `auth-provider|${meta.name ?? ''}`;
4964
5651
  if (kind === 'ws-client')
4965
5652
  return `ws-client|${meta.name ?? ''}`;
5653
+ if (kind === 'query-form')
5654
+ return `query-form|${meta.name ?? ''}`;
4966
5655
  return kind;
4967
5656
  };
4968
5657
  /**
4969
- * Stringifies a query creator route. Function routes are invoked with an empty args object and any
4970
- * resulting `undefined` path segments are rendered as `:param`, matching the legacy devtools.
5658
+ * Wraps a path param name where a route function interpolated it. A NUL cannot occur in a route, so
5659
+ * a marked-up route stays unambiguously parseable.
5660
+ */
5661
+ const PARAM_MARKER = '\u0000';
5662
+ /**
5663
+ * Stands in for the path params object a route function expects, recording every param it reads as
5664
+ * `\0<name>\0` in the returned route. A route that transforms what it reads (`p.id.slice(2)`) still
5665
+ * yields a usable route - the recorded name is transformed along with it.
5666
+ */
5667
+ const paramRecorder = () => new Proxy({}, {
5668
+ get: (_, prop) => (typeof prop === 'string' ? `${PARAM_MARKER}${prop}${PARAM_MARKER}` : undefined),
5669
+ });
5670
+ /** Splits a recorded route into its literal chunks and the path params interleaved between them. */
5671
+ const parseRecordedRoute = (recorded) => recorded
5672
+ .split(PARAM_MARKER)
5673
+ // Markers are balanced, so every odd chunk is a param name and every even one is literal text.
5674
+ .map((text, index) => ({ text, param: index % 2 === 1 ? text : null }))
5675
+ .filter((part) => part.text !== '');
5676
+ /**
5677
+ * Parses a query creator route into its literal and path-param parts, so the devtools can show which
5678
+ * segments of a route are dynamic and fill them in with the values a query actually used.
4971
5679
  * @internal
4972
5680
  */
4973
- const stringifyQueryRoute = (route) => {
5681
+ const parseQueryRoute = (route) => {
4974
5682
  if (route === undefined || route === null)
4975
- return '';
5683
+ return [];
4976
5684
  try {
4977
5685
  if (typeof route === 'function') {
4978
- return route({}).replace(/undefined/g, ':param');
5686
+ return parseRecordedRoute(route(paramRecorder()));
4979
5687
  }
4980
- return String(route);
5688
+ return [{ text: String(route), param: null }];
4981
5689
  }
4982
5690
  catch {
4983
- return typeof route === 'function' ? '(dynamic route)' : String(route);
5691
+ return [{ text: typeof route === 'function' ? '(dynamic route)' : String(route), param: null }];
4984
5692
  }
4985
5693
  };
5694
+ /** Renders parsed route parts as a template, e.g. `/team/:teamId/players`. */
5695
+ const stringifyQueryRouteParts = (parts) => parts.map((part) => (part.param ? `:${part.param}` : part.text)).join('');
4986
5696
  /**
4987
5697
  * Extracts the human readable client name from a query client tuple. The client's DI token is named
4988
5698
  * `QueryClient_<name>`, so we strip the prefix to recover the original name.
@@ -4993,16 +5703,37 @@ const getQueryClientName = (client) => {
4993
5703
  const desc = token?.toString?.() ?? '';
4994
5704
  return desc.replace('InjectionToken ', '').replace('QueryClient_', '') || 'unknown';
4995
5705
  };
5706
+ /**
5707
+ * The provider name behind a bearer auth provider definition, recovered from its DI token the way
5708
+ * {@link getQueryClientName} recovers a client's.
5709
+ * @internal
5710
+ */
5711
+ const getBearerAuthProviderName = (ref) => {
5712
+ const desc = String(ref.token ?? '');
5713
+ return desc.replace('InjectionToken ', '').replace('BearerAuthProvider_', '') || 'unknown';
5714
+ };
4996
5715
  /**
4997
5716
  * Ids are derived deterministically from a stable descriptor + a per-descriptor sequence number, so
4998
5717
  * they survive a page reload (letting the UI restore the selected entry) instead of being random.
4999
5718
  */
5000
5719
  const registerEntry = (registration) => {
5001
5720
  const meta = { ...registration.meta };
5002
- if (registration.route !== undefined)
5003
- meta.route = stringifyQueryRoute(registration.route);
5721
+ if (registration.route !== undefined) {
5722
+ meta.routeParts = parseQueryRoute(registration.route);
5723
+ meta.route = stringifyQueryRouteParts(meta.routeParts);
5724
+ }
5004
5725
  if (registration.clientRef)
5005
5726
  meta.clientName = getQueryClientName(registration.clientRef);
5727
+ if (registration.authProviderRef) {
5728
+ meta.isSecure = true;
5729
+ meta.authProviderName = getBearerAuthProviderName(registration.authProviderRef);
5730
+ }
5731
+ if (registration.authQueries) {
5732
+ meta.authQueries = registration.authQueries.map((authQuery) => ({
5733
+ ...authQuery,
5734
+ route: stringifyQueryRouteParts(parseQueryRoute(authQuery.route)),
5735
+ }));
5736
+ }
5006
5737
  let id = registration.id;
5007
5738
  if (!id) {
5008
5739
  const descriptor = descriptorOf({ kind: registration.kind, meta });
@@ -5016,6 +5747,8 @@ const registerEntry = (registration) => {
5016
5747
  handle: registration.handle,
5017
5748
  meta,
5018
5749
  createdAt: Date.now(),
5750
+ stats: registration.stats,
5751
+ formLinks: registration.formLinks,
5019
5752
  };
5020
5753
  entries.update((list) => [...list, fullEntry]);
5021
5754
  return () => entries.update((list) => list.filter((e) => e.id !== fullEntry.id));
@@ -5037,6 +5770,9 @@ const registerEntry = (registration) => {
5037
5770
  */
5038
5771
  const provideQueryDevtools = () => {
5039
5772
  setQueryDevtoolsRegistrar(registerEntry);
5773
+ setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
5774
+ setQueryDevtoolsFormLinksFactory(createQueryDevtoolsFormLinks);
5775
+ setQueryDevtoolsFaultResolver(resolveQueryDevtoolsFaultForAttempt);
5040
5776
  if (!isDevMode()) {
5041
5777
  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.');
5042
5778
  }
@@ -5246,6 +5982,9 @@ const createGqlMutationViaPost = /* @__PURE__ */ createGqlCreatorTemplate('MUTAT
5246
5982
  /** A query creator that creates a secure GQL mutation where the payload is sent via POST (body) */
5247
5983
  const createSecureGqlMutationViaPost = /* @__PURE__ */ createSecureGqlCreatorTemplate('MUTATE', 'POST');
5248
5984
 
5985
+ /**
5986
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
5987
+ */
5249
5988
  class CustomHeaderAuthProvider {
5250
5989
  constructor(_config) {
5251
5990
  this._config = _config;
@@ -5258,6 +5997,9 @@ class CustomHeaderAuthProvider {
5258
5997
  }
5259
5998
  }
5260
5999
 
6000
+ /**
6001
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6002
+ */
5261
6003
  class BasicAuthProvider {
5262
6004
  constructor(_config) {
5263
6005
  this._config = _config;
@@ -5273,7 +6015,13 @@ class BasicAuthProvider {
5273
6015
  }
5274
6016
  }
5275
6017
 
6018
+ /**
6019
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6020
+ */
5276
6021
  const isRequestError = (error) => error instanceof Object && 'status' in error && 'statusText' in error && 'url' in error;
6022
+ /**
6023
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6024
+ */
5277
6025
  const v2ExtractExpiresInSeconds = (headers) => {
5278
6026
  const cacheControl = headers['cache-control'];
5279
6027
  const age = headers['age'];
@@ -5319,6 +6067,9 @@ const v2ExtractExpiresInSeconds = (headers) => {
5319
6067
  }
5320
6068
  return expiresIn;
5321
6069
  };
6070
+ /**
6071
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6072
+ */
5322
6073
  const serializeBody = (body) => {
5323
6074
  if (body === null || body === undefined) {
5324
6075
  return null;
@@ -5336,6 +6087,9 @@ const serializeBody = (body) => {
5336
6087
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5337
6088
  return body.toString();
5338
6089
  };
6090
+ /**
6091
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6092
+ */
5339
6093
  const transformMethod = (config) => {
5340
6094
  if (config.method === 'GQL_QUERY' || config.method === 'GQL_MUTATE') {
5341
6095
  if (!config.transferVia) {
@@ -5345,6 +6099,9 @@ const transformMethod = (config) => {
5345
6099
  }
5346
6100
  return config.method;
5347
6101
  };
6102
+ /**
6103
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6104
+ */
5348
6105
  const detectContentTypeHeader = (body) => {
5349
6106
  // An empty body has no content type.
5350
6107
  if (body === null) {
@@ -5375,14 +6132,23 @@ const detectContentTypeHeader = (body) => {
5375
6132
  // No type could be inferred.
5376
6133
  return null;
5377
6134
  };
6135
+ /**
6136
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6137
+ */
5378
6138
  const hasHeader = (headers, header) => {
5379
6139
  return Object.keys(headers).some((key) => key.toLowerCase() === header.toLowerCase());
5380
6140
  };
6141
+ /**
6142
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6143
+ */
5381
6144
  const forEachHeader = (headers, callback) => {
5382
6145
  Object.entries(headers).forEach(([key, value]) => {
5383
6146
  callback(key, value);
5384
6147
  });
5385
6148
  };
6149
+ /**
6150
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6151
+ */
5386
6152
  const parseAllXhrResponseHeaders = (xhr) => {
5387
6153
  const headers = xhr.getAllResponseHeaders();
5388
6154
  const parsedHeaders = {};
@@ -5397,6 +6163,9 @@ const parseAllXhrResponseHeaders = (xhr) => {
5397
6163
  }
5398
6164
  return parsedHeaders;
5399
6165
  };
6166
+ /**
6167
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6168
+ */
5400
6169
  const getResponseUrl = (xhr) => {
5401
6170
  if ('responseURL' in xhr && xhr.responseURL) {
5402
6171
  return xhr.responseURL;
@@ -5406,6 +6175,9 @@ const getResponseUrl = (xhr) => {
5406
6175
  }
5407
6176
  return null;
5408
6177
  };
6178
+ /**
6179
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6180
+ */
5409
6181
  const v2ShouldRetryRequest = (config) => {
5410
6182
  const defaultRetryDelay = 1000 + 1000 * config.currentRetryCount;
5411
6183
  if (config.currentRetryCount > 3) {
@@ -5448,6 +6220,9 @@ const v2ShouldRetryRequest = (config) => {
5448
6220
  };
5449
6221
 
5450
6222
  const XSSI_PREFIX = /^\)\]\}',?\n/;
6223
+ /**
6224
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6225
+ */
5451
6226
  const request = (config) => {
5452
6227
  const headers = config.headers || {};
5453
6228
  const responseType = config.responseType || 'json';
@@ -5653,11 +6428,16 @@ const request = (config) => {
5653
6428
  });
5654
6429
  };
5655
6430
 
6431
+ /**
6432
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6433
+ */
5656
6434
  const addQueryContainerHandling = (obs, valueFn, config) => {
5657
- assertInInjectionContext(addQueryContainerHandling);
6435
+ if (!config?.injector) {
6436
+ assertInInjectionContext(addQueryContainerHandling);
6437
+ }
5658
6438
  const { abortPrevious, abortOnDestroy, stopPreviousPolling } = config ?? {};
5659
- const injector = inject(Injector);
5660
- const destroy$ = createDestroy();
6439
+ const injector = config?.injector ?? inject(Injector);
6440
+ const destroy$ = runInInjectionContext(injector, () => createDestroy());
5661
6441
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5662
6442
  const tNode = injector._tNode;
5663
6443
  const componentId = tNode?.index ?? -1;
@@ -5722,12 +6502,18 @@ const addQueryContainerHandling = (obs, valueFn, config) => {
5722
6502
  }
5723
6503
  });
5724
6504
  };
6505
+ /**
6506
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6507
+ */
5725
6508
  function toQuerySignal(source, options) {
5726
6509
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5727
6510
  const s = toSignal(source, options);
5728
6511
  addQueryContainerHandling(source, () => s(), options);
5729
6512
  return s;
5730
6513
  }
6514
+ /**
6515
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6516
+ */
5731
6517
  function effectComputed(computation, injector) {
5732
6518
  let initialData = null;
5733
6519
  try {
@@ -5746,11 +6532,14 @@ function effectComputed(computation, injector) {
5746
6532
  return;
5747
6533
  lastResult.set(data);
5748
6534
  });
5749
- });
6535
+ }, { injector });
5750
6536
  return lastResult.asReadonly();
5751
6537
  }
6538
+ /**
6539
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6540
+ */
5752
6541
  function queryComputed(computation, options) {
5753
- const injector = inject(Injector);
6542
+ const injector = options?.injector ?? inject(Injector);
5754
6543
  const c = effectComputed(computation, injector);
5755
6544
  const obs = toObservable(c, options);
5756
6545
  addQueryContainerHandling(obs, () => c(), options);
@@ -5759,26 +6548,37 @@ function queryComputed(computation, options) {
5759
6548
  /**
5760
6549
  * Creates a signal that will only be reactive until the first query is created.
5761
6550
  * All subsequent changes inside the computation will be ignored.
6551
+ *
6552
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
5762
6553
  */
5763
6554
  function queryComputedTillTruthy(computation, options) {
5764
- const injector = inject(Injector);
6555
+ const injector = options?.injector ?? inject(Injector);
5765
6556
  const c = computedTillTruthy(effectComputed(computation, injector));
5766
6557
  const obs = toObservable(c, options);
5767
6558
  addQueryContainerHandling(obs, () => c(), options);
5768
6559
  return c;
5769
6560
  }
6561
+ /**
6562
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6563
+ */
5770
6564
  function queryArrayComputed(computation, options) {
5771
- const injector = inject(Injector);
6565
+ const injector = options?.injector ?? inject(Injector);
5772
6566
  const c = effectComputed(computation, injector);
5773
6567
  const obs = toObservable(c, options);
5774
6568
  addQueryContainerHandling(obs, () => c(), options);
5775
6569
  return c;
5776
6570
  }
6571
+ /**
6572
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6573
+ */
5777
6574
  function toQuerySubject(source, options) {
5778
6575
  const obs = toObservable(source, options);
5779
6576
  addQueryContainerHandling(obs, () => source(), options);
5780
6577
  return obs;
5781
6578
  }
6579
+ /**
6580
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6581
+ */
5782
6582
  function queryStateSignal(source, options) {
5783
6583
  const { cacheResponse } = options ?? {};
5784
6584
  const roSignal = toSignal(toObservable(source).pipe(switchMap((q) => extractQuery(q)?.state$ ?? of(null)), switchMap((state) => {
@@ -5793,6 +6593,9 @@ function queryStateSignal(source, options) {
5793
6593
  ...(ngDevMode ? [{ debugName: "rwSignal" }] : /* istanbul ignore next */ []));
5794
6594
  return rwSignal;
5795
6595
  }
6596
+ /**
6597
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6598
+ */
5796
6599
  function queryStateResponseSignal(source, options) {
5797
6600
  const s = queryStateSignal(source, options);
5798
6601
  const dataSignal = computed(() => {
@@ -5805,6 +6608,9 @@ function queryStateResponseSignal(source, options) {
5805
6608
  };
5806
6609
  return dataSignal;
5807
6610
  }
6611
+ /**
6612
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6613
+ */
5808
6614
  function queryStateErrorSignal(source) {
5809
6615
  const s = queryStateSignal(source);
5810
6616
  return computed(() => {
@@ -5812,6 +6618,9 @@ function queryStateErrorSignal(source) {
5812
6618
  return isQueryStateFailure(state) ? state.error : null;
5813
6619
  });
5814
6620
  }
6621
+ /**
6622
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6623
+ */
5815
6624
  function queryStateLoadingSignal(source) {
5816
6625
  const s = queryStateSignal(source);
5817
6626
  return computed(() => {
@@ -5820,6 +6629,9 @@ function queryStateLoadingSignal(source) {
5820
6629
  });
5821
6630
  }
5822
6631
 
6632
+ /**
6633
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6634
+ */
5823
6635
  const deepFreeze = (object) => {
5824
6636
  // check if object is actually an object or frozen
5825
6637
  if (!object || typeof object !== 'object' || Object.isFrozen(object)) {
@@ -5842,9 +6654,14 @@ const deepFreeze = (object) => {
5842
6654
  * **This function always returns null.**
5843
6655
  * @see https://github.com/microsoft/TypeScript/issues/26242
5844
6656
  * @returns null
6657
+ *
6658
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
5845
6659
  */
5846
6660
  const def = () => null;
5847
6661
 
6662
+ /**
6663
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6664
+ */
5848
6665
  class V2QueryCreator {
5849
6666
  constructor(_queryConfig, _client, _store) {
5850
6667
  this._queryConfig = _queryConfig;
@@ -5891,6 +6708,9 @@ class V2QueryCreator {
5891
6708
  }
5892
6709
  }
5893
6710
 
6711
+ /**
6712
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6713
+ */
5894
6714
  class QueryStore {
5895
6715
  constructor(_config) {
5896
6716
  this._config = _config;
@@ -6034,6 +6854,9 @@ class QueryStore {
6034
6854
  }
6035
6855
  }
6036
6856
 
6857
+ /**
6858
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6859
+ */
6037
6860
  class V2QueryClient {
6038
6861
  constructor(_clientConfig) {
6039
6862
  this._clientConfig = _clientConfig;
@@ -6111,9 +6934,15 @@ class V2QueryClient {
6111
6934
  }
6112
6935
  }
6113
6936
 
6937
+ /**
6938
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6939
+ */
6114
6940
  const v2ShouldCacheQuery = (method) => {
6115
6941
  return method === 'GET' || method === 'OPTIONS' || method === 'HEAD' || method === 'GQL_QUERY';
6116
6942
  };
6943
+ /**
6944
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6945
+ */
6117
6946
  const v2BuildQueryCacheKey = (route, args) => {
6118
6947
  const variables = JSON.stringify(args?.variables || {})
6119
6948
  // replace all curly braces with empty string
@@ -6131,6 +6960,64 @@ const v2BuildQueryCacheKey = (route, args) => {
6131
6960
  return hash.toString();
6132
6961
  };
6133
6962
 
6963
+ let fallbackInjector = null;
6964
+ /**
6965
+ * The stashed root injector, or `null` when the fallback was never provided (or was provided on the
6966
+ * server, where stashing is refused).
6967
+ *
6968
+ * @internal
6969
+ */
6970
+ const legacyPrepareFallbackInjector = () => fallbackInjector;
6971
+ /**
6972
+ * Lets `prepare()` fall back to the application's root injector when it is called outside any injection
6973
+ * context, instead of throwing `ET950`.
6974
+ *
6975
+ * Opt-in, and **browser only**. Two things degrade when the fallback is used, both matching v2, which had
6976
+ * no injector to scope to at all:
6977
+ *
6978
+ * - **The query's lifetime becomes the application's.** Nothing tears it down when a component dies, so
6979
+ * cleanup rests entirely on a query container or `config: { destroyOnResponse: true }`.
6980
+ * - **The devtools lose the host element**, so "inspect" cannot jump to the component. Already the case
6981
+ * for any query created from a root or environment injector.
6982
+ *
6983
+ * It refuses to stash anything on the server: a module-global injector would be shared across concurrent
6984
+ * requests, which is data bleed rather than a leak. Server-side renders keep throwing `ET950`, so a call
6985
+ * site that only works in the browser fails loudly where it matters.
6986
+ *
6987
+ * Prefer passing `injector` at the call site - it keeps the query scoped to whatever created it. Reach for
6988
+ * this when a migration has too many call sites to thread by hand.
6989
+ *
6990
+ * @example
6991
+ * ```ts
6992
+ * bootstrapApplication(AppComponent, {
6993
+ * providers: [provideLegacyPrepareFallback()],
6994
+ * });
6995
+ * ```
6996
+ *
6997
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6998
+ */
6999
+ const provideLegacyPrepareFallback = () => makeEnvironmentProviders([
7000
+ provideEnvironmentInitializer(() => {
7001
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) {
7002
+ if (isDevMode()) {
7003
+ console.warn('provideLegacyPrepareFallback() does nothing on the server: a module-global injector would be shared ' +
7004
+ 'between concurrent requests. prepare() calls without an injection context keep throwing ET950 there.');
7005
+ }
7006
+ return;
7007
+ }
7008
+ const injector = inject(EnvironmentInjector);
7009
+ fallbackInjector = injector;
7010
+ inject(DestroyRef).onDestroy(() => {
7011
+ if (fallbackInjector === injector) {
7012
+ fallbackInjector = null;
7013
+ }
7014
+ });
7015
+ }),
7016
+ ]);
7017
+
7018
+ /**
7019
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7020
+ */
6134
7021
  const transformExecStateToQueryState = (execState) => {
6135
7022
  const meta = { id: -1, triggeredVia: 'program' };
6136
7023
  switch (execState?.type) {
@@ -6182,6 +7069,9 @@ const transformExecStateToQueryState = (execState) => {
6182
7069
  }
6183
7070
  }
6184
7071
  };
7072
+ /**
7073
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7074
+ */
6185
7075
  const isLegacyQuery = (query) => {
6186
7076
  if (!query || typeof query !== 'object' || Array.isArray(query)) {
6187
7077
  return false;
@@ -6191,6 +7081,9 @@ const isLegacyQuery = (query) => {
6191
7081
  }
6192
7082
  return true;
6193
7083
  };
7084
+ /**
7085
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7086
+ */
6194
7087
  class LegacyQuery {
6195
7088
  constructor(newQuery, _arguments, entity,
6196
7089
  /**
@@ -6199,11 +7092,19 @@ class LegacyQuery {
6199
7092
  *
6200
7093
  * @internal
6201
7094
  */
6202
- _isInert = false) {
7095
+ _isInert = false,
7096
+ /**
7097
+ * Whether the underlying request is cacheable. Resolved by the creator, which is the only place that
7098
+ * knows the method before the first execution.
7099
+ *
7100
+ * @internal
7101
+ */
7102
+ _canBeCached = false) {
6203
7103
  this.newQuery = newQuery;
6204
7104
  this._arguments = _arguments;
6205
7105
  this.entity = entity;
6206
7106
  this._isInert = _isInert;
7107
+ this._canBeCached = _canBeCached;
6207
7108
  /**
6208
7109
  * @internal
6209
7110
  */
@@ -6238,17 +7139,22 @@ class LegacyQuery {
6238
7139
  }
6239
7140
  this.state$ = toObservable(this.newQuery.executionState, { injector: this.newQuery.subtle.injector }).pipe(map((execState) => transformExecStateToQueryState(execState)), switchMap((s) => this._transformState(s)), shareReplay({ bufferSize: 1, refCount: true }));
6240
7141
  this.storeSyncEffect = effect(() => {
6241
- const res = this.newQuery.response();
7142
+ // Gated on the execution state, not on the response being non-null: the effect runs once on
7143
+ // creation, and `response()` is null until the first one arrives - `id`/`set` are typed
7144
+ // non-nullable, so an unguarded write feeds `null` through them on every `prepare()`. A plain
7145
+ // null-check would be wrong the other way: a 204 is a success whose body is legitimately null.
7146
+ const execState = this.newQuery.executionState();
6242
7147
  untracked(() => {
6243
- if (this.entity?.set) {
6244
- const id = this.entity.id({ args: this._arguments, response: res });
6245
- this.entity.set({
6246
- args: this._arguments,
6247
- response: res,
6248
- id,
6249
- store: this.entity.store,
6250
- });
6251
- }
7148
+ if (execState?.type !== 'success' || !this.entity?.set)
7149
+ return;
7150
+ const res = execState.response;
7151
+ const id = this.entity.id({ args: this._arguments, response: res });
7152
+ this.entity.set({
7153
+ args: this._arguments,
7154
+ response: res,
7155
+ id,
7156
+ store: this.entity.store,
7157
+ });
6252
7158
  });
6253
7159
  }, /* @ts-ignore */
6254
7160
  ...(ngDevMode ? [{ debugName: "storeSyncEffect" }] : /* istanbul ignore next */ []));
@@ -6292,7 +7198,7 @@ class LegacyQuery {
6292
7198
  return null;
6293
7199
  }
6294
7200
  get canBeCached() {
6295
- return false;
7201
+ return this._canBeCached;
6296
7202
  }
6297
7203
  get _isInMockMode() {
6298
7204
  return false;
@@ -6484,32 +7390,61 @@ const isInjectorUsable = (injector) => {
6484
7390
  return false;
6485
7391
  }
6486
7392
  };
7393
+ /**
7394
+ * Angular's NG0203, thrown by `inject()` outside an injection context. Matched on the code rather than
7395
+ * the message: production builds strip the message, and NG0205 ("Injector has already been destroyed")
7396
+ * comes out of the same `inject()` call for a different reason entirely.
7397
+ */
7398
+ const isMissingInjectionContextError = (error) => typeof error === 'object' && error !== null && Math.abs(Number(error.code)) === 203;
7399
+ /**
7400
+ * What the creator was built with, for diagnostics - `GET /person`. Lets an unnamed wrapper still point
7401
+ * at an endpoint instead of "a legacy query creator". A route function cannot be described this way, but
7402
+ * those wrappers come from the generator, which always emits a `name`.
7403
+ */
7404
+ const describeCreator = (creator) => {
7405
+ const { method, route } = creator.subtle.creatorInternals;
7406
+ if (typeof route !== 'string')
7407
+ return undefined;
7408
+ return typeof method === 'string' ? `${method} ${route}` : route;
7409
+ };
7410
+ /**
7411
+ * Mirrors the cacheability decision the query repository makes per request: cacheable HTTP methods and
7412
+ * GraphQL queries, unless the creator opted in or out explicitly. The legacy container helpers branch on
7413
+ * `canBeCached` for their default cleanup, so a wrapper has to answer before the first execution.
7414
+ */
7415
+ const canCreatorBeCached = (creator) => {
7416
+ const { method } = creator.subtle.creatorInternals;
7417
+ const explicit = creator.subtle.creatorOptions?.subtle
7418
+ ?.useQueryRepositoryCache;
7419
+ if (explicit !== undefined)
7420
+ return explicit;
7421
+ if (method === 'QUERY')
7422
+ return true;
7423
+ if (method === 'MUTATE')
7424
+ return false;
7425
+ return typeof method === 'string' && shouldCacheQuery(method);
7426
+ };
7427
+ /**
7428
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7429
+ */
6487
7430
  class LegacyQueryCreator {
6488
7431
  constructor(options) {
6489
7432
  this.options = options;
6490
7433
  this.prepare = (args) => {
6491
- // v2's `prepare()` needed no injection context, so plenty of call sites are plain callbacks.
6492
- // Angular's raw NG0203 names neither the query nor the call site, which makes those a bisection
6493
- // exercise - replace it with an error that does.
6494
- const injector = args?.injector ??
6495
- (() => {
6496
- try {
6497
- return inject(Injector);
6498
- }
6499
- catch {
6500
- throw legacyPrepareWithoutInjectionContext(this.options.name);
6501
- }
6502
- })();
7434
+ const injector = args?.injector ?? this.resolveAmbientInjector('prepare');
6503
7435
  let headers = new HttpHeaders();
6504
7436
  if (args?.headers) {
6505
- Object.entries(args.headers).forEach(([key, value]) => {
6506
- if (value) {
6507
- headers = headers.set(key, value);
6508
- }
6509
- });
7437
+ for (const [key, value] of Object.entries(args.headers)) {
7438
+ // Only an absent value is skipped - an empty string is a header the caller asked to send.
7439
+ if (value === undefined || value === null)
7440
+ continue;
7441
+ headers = headers.set(key, value);
7442
+ }
6510
7443
  }
6511
7444
  const queryArgs = {
6512
- ...(args?.body ? { body: args.body } : {}),
7445
+ // Presence, not truthiness: `0`, `''` and `false` are bodies, and dropping them silently sends a
7446
+ // different request than the call site asked for.
7447
+ ...(args?.body !== undefined ? { body: args.body } : {}),
6513
7448
  ...(args?.pathParams ? { pathParams: args.pathParams } : {}),
6514
7449
  ...(args?.queryParams ? { queryParams: args.queryParams } : {}),
6515
7450
  headers,
@@ -6520,10 +7455,10 @@ class LegacyQueryCreator {
6520
7455
  // yields a query that does nothing instead of an NG0205 raised from the view's cleanup phase.
6521
7456
  if (!isInjectorUsable(injector)) {
6522
7457
  if (isDevMode()) {
6523
- console.warn(`${this.options.name ? `"${this.options.name}"` : 'A legacy query'}.prepare() was called with a destroyed injector, so an inert query was returned. ` +
7458
+ console.warn(`${this.label ? `"${this.label}"` : 'A legacy query'}.prepare() was called with a destroyed injector, so an inert query was returned. ` +
6524
7459
  `This usually means a callback outlived the component whose injector it captured - guard the call site with DestroyRef.onDestroy, or pass an injector that outlives it.`);
6525
7460
  }
6526
- return new LegacyQuery(createInertQuery(), queryArgs, this.options.entity, true);
7461
+ return new LegacyQuery(createInertQuery(), queryArgs, this.options.entity, true, this.canBeCached);
6527
7462
  }
6528
7463
  return runInInjectionContext(injector, () => {
6529
7464
  return untracked(() => {
@@ -6534,33 +7469,71 @@ class LegacyQueryCreator {
6534
7469
  // `LegacyQuery.execute()` forwards v2's `skipCache` as `allowCache` for every method, cacheable or not.
6535
7470
  silenceUncacheableAllowCacheError: true,
6536
7471
  });
6537
- const legacyQuery = new LegacyQuery(newQuery, queryArgs, this.options.entity);
7472
+ const legacyQuery = new LegacyQuery(newQuery, queryArgs, this.options.entity, false, this.canBeCached);
6538
7473
  if (args?.config?.destroyOnResponse) {
6539
- const destroyEffect = effect(() => {
6540
- if (newQuery.executionState()?.type === 'success' || newQuery.executionState()?.type === 'failure') {
7474
+ // Owned by the query's own injector, not the caller's. Call sites are told to pass an injector
7475
+ // that outlives them, so they hand over a root or environment one - and an effect that only
7476
+ // tears itself down on a terminal state then outlives an aborted or never-executed query for
7477
+ // the lifetime of the app. Tied to the query, it dies with the thing it exists to destroy.
7478
+ effect(() => {
7479
+ const type = newQuery.executionState()?.type;
7480
+ if (type === 'success' || type === 'failure') {
6541
7481
  legacyQuery.destroy();
6542
- destroyEffect.destroy();
6543
7482
  }
6544
- }, /* @ts-ignore */
6545
- ...(ngDevMode ? [{ debugName: "destroyEffect" }] : /* istanbul ignore next */ []));
7483
+ }, { injector: newQuery.subtle.injector });
6546
7484
  }
6547
7485
  return legacyQuery;
6548
7486
  });
6549
7487
  });
6550
7488
  };
6551
7489
  this.createSubject = (initialValue, config) => {
7490
+ const injector = config?.injector ?? this.resolveAmbientInjector('createSubject');
6552
7491
  const subject = new BehaviorSubject(initialValue ?? null);
6553
- addQueryContainerHandling(subject, () => subject.getValue(), config);
7492
+ addQueryContainerHandling(subject, () => subject.getValue(), { ...config, injector });
6554
7493
  return subject;
6555
7494
  };
6556
7495
  this.createSignal = (initialValue, config) => {
7496
+ const injector = config?.injector ?? this.resolveAmbientInjector('createSignal');
6557
7497
  const _signal = signal(initialValue ?? null, /* @ts-ignore */
6558
7498
  ...(ngDevMode ? [{ debugName: "_signal" }] : /* istanbul ignore next */ []));
6559
- addQueryContainerHandling(toObservable(_signal), () => _signal(), config);
7499
+ addQueryContainerHandling(toObservable(_signal, { injector }), () => _signal(), { ...config, injector });
6560
7500
  return _signal;
6561
7501
  };
6562
7502
  this.behaviorSubject = this.createSubject;
6563
7503
  }
7504
+ /**
7505
+ * Whether the underlying request is cacheable, which is what the legacy container helpers use to decide
7506
+ * their default cleanup. Resolved from the creator, so it answers before the first execution.
7507
+ */
7508
+ get canBeCached() {
7509
+ return canCreatorBeCached(this.options.creator);
7510
+ }
7511
+ get label() {
7512
+ return this.options.name ?? describeCreator(this.options.creator);
7513
+ }
7514
+ /**
7515
+ * v2 needed no injection context, so plenty of call sites are plain callbacks. Angular's raw NG0203
7516
+ * names neither the query nor the call site, which makes those a bisection exercise - replace it with
7517
+ * an error that does, and let every other DI failure (NG0205 from an injector mid-teardown, a missing
7518
+ * provider) through untouched.
7519
+ */
7520
+ resolveAmbientInjector(method) {
7521
+ try {
7522
+ return inject(Injector);
7523
+ }
7524
+ catch (error) {
7525
+ if (!isMissingInjectionContextError(error)) {
7526
+ throw error;
7527
+ }
7528
+ const fallback = legacyPrepareFallbackInjector();
7529
+ // A stale stash (the app injector is gone) is no injector at all - handing it over would build a
7530
+ // query that silently never runs.
7531
+ if (fallback && isInjectorUsable(fallback)) {
7532
+ return fallback;
7533
+ }
7534
+ throw legacyPrepareWithoutInjectionContext(this.label, method);
7535
+ }
7536
+ }
6564
7537
  }
6565
7538
  /**
6566
7539
  * Creates a legacy query creator.
@@ -6568,32 +7541,49 @@ class LegacyQueryCreator {
6568
7541
  * **NOTE**: This is a temporary solution to support legacy queries. It will be removed in the future.
6569
7542
  *
6570
7543
  * **DO NOT TURN NEW QUERY CREATORS INTO LEGACY ONES MANUALLY. THIS IS ONLY NEEDED FOR MIGRATION PURPOSES.**
7544
+ *
7545
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
6571
7546
  */
6572
7547
  const createLegacyQueryCreator = (options) => {
6573
7548
  return new LegacyQueryCreator(options);
6574
7549
  };
6575
7550
 
6576
7551
  /* eslint-disable @typescript-eslint/no-explicit-any */
7552
+ /**
7553
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7554
+ */
6577
7555
  function filterSuccess() {
6578
7556
  return function (source) {
6579
7557
  return source.pipe(filter((value) => isQueryStateSuccess(value)));
6580
7558
  };
6581
7559
  }
7560
+ /**
7561
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7562
+ */
6582
7563
  function ignoreAutoRefresh() {
6583
7564
  return function (source) {
6584
7565
  return source.pipe(filter((value) => !isAutoRefresh(value)));
6585
7566
  };
6586
7567
  }
7568
+ /**
7569
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7570
+ */
6587
7571
  function filterFailure() {
6588
7572
  return function (source) {
6589
7573
  return source.pipe(filter((value) => isQueryStateFailure(value)));
6590
7574
  };
6591
7575
  }
7576
+ /**
7577
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7578
+ */
6592
7579
  function filterQueryStates(allowedStates) {
6593
7580
  return function (source) {
6594
7581
  return source.pipe(filter((value) => value && allowedStates.includes(value.type)));
6595
7582
  };
6596
7583
  }
7584
+ /**
7585
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7586
+ */
6597
7587
  function takeUntilResponse(config) {
6598
7588
  return function (source) {
6599
7589
  return source.pipe(takeWhile((value) => {
@@ -6604,21 +7594,33 @@ function takeUntilResponse(config) {
6604
7594
  }, true));
6605
7595
  };
6606
7596
  }
7597
+ /**
7598
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7599
+ */
6607
7600
  function filterNull() {
6608
7601
  return function (source) {
6609
7602
  return source.pipe(filter((value) => value !== null));
6610
7603
  };
6611
7604
  }
7605
+ /**
7606
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7607
+ */
6612
7608
  function switchQueryState() {
6613
7609
  return function (source) {
6614
7610
  return source.pipe(switchMap((value) => value?.state$ ?? of(null)));
6615
7611
  };
6616
7612
  }
7613
+ /**
7614
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7615
+ */
6617
7616
  function switchQueryCollectionState() {
6618
7617
  return function (source) {
6619
7618
  return source.pipe(switchMap((value) => value?.query.state$ ?? of(null)));
6620
7619
  };
6621
7620
  }
7621
+ /**
7622
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7623
+ */
6622
7624
  const resetPageOnError = (config) => (source) => {
6623
7625
  const { queryForm, pageControlKey = 'page' } = config;
6624
7626
  if (isDevMode() && !queryForm.form.controls[pageControlKey]) {
@@ -6647,12 +7649,33 @@ const resetPageOnError = (config) => (source) => {
6647
7649
  }), takeUntilResponse());
6648
7650
  }));
6649
7651
  };
7652
+ /**
7653
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7654
+ */
6650
7655
  const isQueryStateLoading = (state) => state?.type === "LOADING" /* QueryStateType.Loading */;
7656
+ /**
7657
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7658
+ */
6651
7659
  const isQueryStateSuccess = (state) => state?.type === "SUCCESS" /* QueryStateType.Success */;
7660
+ /**
7661
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7662
+ */
6652
7663
  const isQueryStateFailure = (state) => state?.type === "FAILURE" /* QueryStateType.Failure */;
7664
+ /**
7665
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7666
+ */
6653
7667
  const isQueryStateCancelled = (state) => state?.type === "CANCELLED" /* QueryStateType.Cancelled */;
7668
+ /**
7669
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7670
+ */
6654
7671
  const isQueryStatePrepared = (state) => state?.type === "PREPARED" /* QueryStateType.Prepared */;
7672
+ /**
7673
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7674
+ */
6655
7675
  const isAutoRefresh = (state) => state?.meta.triggeredVia === 'auto';
7676
+ /**
7677
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7678
+ */
6656
7679
  const mergeHeaders = (...headers) => {
6657
7680
  return headers.reduce((acc, headers) => {
6658
7681
  if (!headers) {
@@ -6664,6 +7687,9 @@ const mergeHeaders = (...headers) => {
6664
7687
  };
6665
7688
  }, {});
6666
7689
  };
7690
+ /**
7691
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7692
+ */
6667
7693
  const isGqlQueryConfig = (config) => {
6668
7694
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
6669
7695
  return false;
@@ -6673,6 +7699,9 @@ const isGqlQueryConfig = (config) => {
6673
7699
  }
6674
7700
  return true;
6675
7701
  };
7702
+ /**
7703
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7704
+ */
6676
7705
  const isQuery = (query) => {
6677
7706
  if (!query || typeof query !== 'object' || Array.isArray(query)) {
6678
7707
  return false;
@@ -6682,6 +7711,9 @@ const isQuery = (query) => {
6682
7711
  }
6683
7712
  return true;
6684
7713
  };
7714
+ /**
7715
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7716
+ */
6685
7717
  const isQueryCollection = (query) => {
6686
7718
  if (!query || typeof query !== 'object' || Array.isArray(query)) {
6687
7719
  return false;
@@ -6691,9 +7723,15 @@ const isQueryCollection = (query) => {
6691
7723
  }
6692
7724
  return true;
6693
7725
  };
7726
+ /**
7727
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7728
+ */
6694
7729
  const createQueryCollection = (
6695
7730
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
6696
7731
  queryMap) => new BehaviorSubject(null);
7732
+ /**
7733
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7734
+ */
6697
7735
  const createQueryCollectionSubject = (
6698
7736
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
6699
7737
  queryMap, config) => {
@@ -6702,6 +7740,9 @@ queryMap, config) => {
6702
7740
  addQueryContainerHandling(subject.pipe(map((s) => s?.query ?? null)), () => subject.getValue()?.query ?? null, config);
6703
7741
  return subject;
6704
7742
  };
7743
+ /**
7744
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7745
+ */
6705
7746
  const createQueryCollectionSignal = (
6706
7747
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
6707
7748
  queryMap, config) => {
@@ -6711,7 +7752,13 @@ queryMap, config) => {
6711
7752
  addQueryContainerHandling(toObservable(_signal).pipe(map((s) => s?.query ?? null)), () => _signal()?.query ?? null, config);
6712
7753
  return _signal;
6713
7754
  };
7755
+ /**
7756
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7757
+ */
6714
7758
  const extractQuery = (v) => (isQuery(v) ? v : isLegacyQuery(v) ? v : v?.query) ?? null;
7759
+ /**
7760
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7761
+ */
6715
7762
  const getDefaultHeaders = (headers, method) => {
6716
7763
  if (!headers) {
6717
7764
  return {};
@@ -6721,6 +7768,9 @@ const getDefaultHeaders = (headers, method) => {
6721
7768
  }
6722
7769
  return headers;
6723
7770
  };
7771
+ /**
7772
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7773
+ */
6724
7774
  const castQueryCreatorTypes = (config) => {
6725
7775
  if (config.args?.pathParams) {
6726
7776
  console.error(config);
@@ -6728,6 +7778,9 @@ const castQueryCreatorTypes = (config) => {
6728
7778
  }
6729
7779
  return config.creator;
6730
7780
  };
7781
+ /**
7782
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7783
+ */
6731
7784
  const computeQueryMethod = (config) => {
6732
7785
  let method;
6733
7786
  if (isGqlQueryConfig(config.config)) {
@@ -6739,6 +7792,9 @@ const computeQueryMethod = (config) => {
6739
7792
  }
6740
7793
  return method;
6741
7794
  };
7795
+ /**
7796
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7797
+ */
6742
7798
  const computeQueryBody = (config) => {
6743
7799
  let body;
6744
7800
  if (config.method === 'GET' ||
@@ -6759,6 +7815,9 @@ const computeQueryBody = (config) => {
6759
7815
  }
6760
7816
  return body;
6761
7817
  };
7818
+ /**
7819
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7820
+ */
6762
7821
  const computeQueryAuthHeader = (config) => {
6763
7822
  let authHeader = null;
6764
7823
  if (config.config.secure) {
@@ -6774,11 +7833,17 @@ const computeQueryAuthHeader = (config) => {
6774
7833
  }
6775
7834
  return authHeader;
6776
7835
  };
7836
+ /**
7837
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7838
+ */
6777
7839
  const computeQueryHeaders = (config) => {
6778
7840
  const authHeader = computeQueryAuthHeader(config);
6779
7841
  const mergedHeaders = mergeHeaders(getDefaultHeaders(config.client.config.request?.headers, config.config.method), authHeader, config.args?.headers) || undefined;
6780
7842
  return mergedHeaders;
6781
7843
  };
7844
+ /**
7845
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7846
+ */
6782
7847
  const computeQueryQueryParams = (config) => {
6783
7848
  if (isGqlQueryConfig(config.config) &&
6784
7849
  (config.config.transferVia === 'GET' ||
@@ -6793,6 +7858,9 @@ const computeQueryQueryParams = (config) => {
6793
7858
  }
6794
7859
  return config.args?.queryParams;
6795
7860
  };
7861
+ /**
7862
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7863
+ */
6796
7864
  const paginatedEntityValueUpdater = (findFn) => ({ response, rawResponse, entity }) => {
6797
7865
  const indexOfEntityRaw = rawResponse.items.findIndex((item) => findFn(item, entity));
6798
7866
  const indexOfEntity = response.items.findIndex((item) => findFn(item, entity));
@@ -6818,6 +7886,9 @@ const paginatedEntityValueUpdater = (findFn) => ({ response, rawResponse, entity
6818
7886
  };
6819
7887
 
6820
7888
  let _nextQueryId = 0;
7889
+ /**
7890
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
7891
+ */
6821
7892
  class V2Query {
6822
7893
  constructor(_client,
6823
7894
  /**
@@ -7249,10 +8320,22 @@ class V2Query {
7249
8320
  }
7250
8321
  }
7251
8322
 
8323
+ /**
8324
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8325
+ */
7252
8326
  const isBasicAuthProvider = (authProvider) => authProvider instanceof BasicAuthProvider;
8327
+ /**
8328
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8329
+ */
7253
8330
  const isBearerAuthProvider = (authProvider) => authProvider instanceof V2BearerAuthProvider;
8331
+ /**
8332
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8333
+ */
7254
8334
  const isCustomHeaderAuthProvider = (authProvider) => authProvider instanceof CustomHeaderAuthProvider;
7255
8335
 
8336
+ /**
8337
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8338
+ */
7256
8339
  class V2BearerAuthProvider {
7257
8340
  constructor(_config) {
7258
8341
  this._config = _config;
@@ -7431,9 +8514,18 @@ const INFINITY_QUERY_RESPONSE_DELAY_DEF = /* @__PURE__ */ defineProvider(() => {
7431
8514
  enabled$,
7432
8515
  };
7433
8516
  }, { name: 'Infinity Query Response Delay' });
8517
+ /**
8518
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8519
+ */
7434
8520
  const provideInfinityQueryResponseDelay = /* @__PURE__ */ toProvideFn(INFINITY_QUERY_RESPONSE_DELAY_DEF);
8521
+ /**
8522
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8523
+ */
7435
8524
  const injectInfinityQueryResponseDelay = /* @__PURE__ */ toInjectFn(INFINITY_QUERY_RESPONSE_DELAY_DEF);
7436
8525
 
8526
+ /**
8527
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8528
+ */
7437
8529
  class InfinityQuery {
7438
8530
  constructor(_config, _destroy$) {
7439
8531
  this._config = _config;
@@ -7604,11 +8696,23 @@ class InfinityQuery {
7604
8696
  }
7605
8697
  }
7606
8698
 
8699
+ /**
8700
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8701
+ */
7607
8702
  const createInfinityQueryConfig = (config) => config;
8703
+ /**
8704
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8705
+ */
7608
8706
  const skipPaginationPageParamCalculator = ({ page, itemsPerPage }) => itemsPerPage * (page - 1);
7609
8707
 
7610
8708
  /* eslint-disable @typescript-eslint/no-explicit-any */
8709
+ /**
8710
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8711
+ */
7611
8712
  const INFINITY_QUERY_TOKEN = new InjectionToken('INFINITY_QUERY_TOKEN');
8713
+ /**
8714
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8715
+ */
7612
8716
  class InfinityQueryDirective {
7613
8717
  // TODO: Skipped for migration because:
7614
8718
  // Accessor inputs cannot be migrated as they are too complex.
@@ -7757,6 +8861,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7757
8861
  args: ['etInfinityQuery']
7758
8862
  }] } });
7759
8863
 
8864
+ /**
8865
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8866
+ */
7760
8867
  class InfinityQueryTriggerDirective {
7761
8868
  constructor() {
7762
8869
  this.elementRef = inject(ElementRef);
@@ -7812,6 +8919,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7812
8919
  }]
7813
8920
  }], propDecorators: { scrollContainerSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollContainerSelector", required: false }] }], infinityQueryOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "infinityQueryOverride", required: false }] }] } });
7814
8921
 
8922
+ /**
8923
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8924
+ */
7815
8925
  class QueryDirective {
7816
8926
  static ngTemplateContextGuard(_dir, _ctx) {
7817
8927
  return true;
@@ -7901,6 +9011,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7901
9011
  }]
7902
9012
  }], ctorParameters: () => [], propDecorators: { query: [{ type: i0.Input, args: [{ isSignal: true, alias: "etQuery", required: true }] }], cache: [{ type: i0.Input, args: [{ isSignal: true, alias: "etQueryCache", required: false }] }] } });
7903
9013
 
9014
+ /**
9015
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9016
+ */
7904
9017
  class QueryShortNamePipe {
7905
9018
  transform(value) {
7906
9019
  const route = value._routeWithParams;
@@ -7923,7 +9036,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7923
9036
  args: [{ name: 'queryShortName' }]
7924
9037
  }] });
7925
9038
 
9039
+ /**
9040
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9041
+ */
7926
9042
  const QUERY_CLIENT_DEVTOOLS_TOKEN = new InjectionToken('QUERY_CLIENT_DEVTOOLS_TOKEN');
9043
+ /**
9044
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9045
+ */
7927
9046
  const provideQueryClientForDevtools = (queryClient) => {
7928
9047
  if (!isDevMode()) {
7929
9048
  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.');
@@ -7935,6 +9054,9 @@ const provideQueryClientForDevtools = (queryClient) => {
7935
9054
  };
7936
9055
  };
7937
9056
 
9057
+ /**
9058
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9059
+ */
7938
9060
  class QueryDevtoolsComponent {
7939
9061
  constructor() {
7940
9062
  this.cdr = inject(ChangeDetectorRef);
@@ -8244,6 +9366,9 @@ class EntityStoreError extends Error {
8244
9366
  function flatten(arr) {
8245
9367
  return arr.reduce((acc, val) => acc.concat(val), []);
8246
9368
  }
9369
+ /**
9370
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9371
+ */
8247
9372
  class EntityStore {
8248
9373
  constructor(_config) {
8249
9374
  this._config = _config;
@@ -8360,12 +9485,17 @@ class EntityStore {
8360
9485
  }
8361
9486
  }
8362
9487
 
9488
+ /**
9489
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9490
+ */
8363
9491
  function mapToPaginated(response) {
8364
9492
  return map((items) => ({ ...response, items }));
8365
9493
  }
8366
9494
  // FIXME: This breaks if SubStoreType is an array
8367
9495
  /**
8368
9496
  * Use an other store to represent a property of the current entity.
9497
+ *
9498
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8369
9499
  */
8370
9500
  const insertFrom = (store, { for: key, id }) => switchMap((value) => {
8371
9501
  if (!value)
@@ -8380,6 +9510,8 @@ const insertFrom = (store, { for: key, id }) => switchMap((value) => {
8380
9510
  });
8381
9511
  /**
8382
9512
  * Remove entities from a store based on a where function.
9513
+ *
9514
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8383
9515
  */
8384
9516
  const removeFrom = (store, { where, id }) => {
8385
9517
  store
@@ -8388,9 +9520,21 @@ const removeFrom = (store, { where, id }) => {
8388
9520
  .subscribe();
8389
9521
  };
8390
9522
 
9523
+ /**
9524
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9525
+ */
8391
9526
  const queryStateAlreadyHasKeyError = (data) => new QueryError('004', 'The query state already contains the provided key', data);
9527
+ /**
9528
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9529
+ */
8392
9530
  const queryStateDoesNotContainKeyError = (data) => new QueryError('005', 'The query state does not contain the provided key', data);
9531
+ /**
9532
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9533
+ */
8393
9534
  const queryStateCannotTransformError = (data) => new QueryError('006', 'The query state cannot be transformed because it is not loading', data);
9535
+ /**
9536
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
9537
+ */
8394
9538
  const invalidBodyError = (data) => new QueryError('007', 'The body is not valid or upsupported', data);
8395
9539
 
8396
9540
  /**
@@ -8415,6 +9559,8 @@ const invalidBodyError = (data) => new QueryError('007', 'The body is not valid
8415
9559
  * });
8416
9560
  * });
8417
9561
  * ```
9562
+ *
9563
+ * @deprecated Part of the legacy (v2) query system. Migrate to the current query API - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2, and run `nx g @ethlete/query:migrate-to-query-v3` to rewrite the mechanical parts. Intent to remove in v7.
8418
9564
  */
8419
9565
  const validateWithV2Query = (path, config) => {
8420
9566
  applyQueryAsyncValidator(path, {
@@ -9556,6 +10702,16 @@ const createBranch = (fields, initial, injector) => {
9556
10702
  resetAllFieldsToDefault: () => model.set(clone(defaults)),
9557
10703
  };
9558
10704
  };
10705
+ /**
10706
+ * Per-name sequence behind the devtools id of a form, mirroring the registry's own scheme so the
10707
+ * panel can restore the selected form after a reload.
10708
+ */
10709
+ const devtoolsIdCounters = /* @__PURE__ */ new Map();
10710
+ const nextQueryFormDevtoolsId = (name) => {
10711
+ const seq = devtoolsIdCounters.get(name) ?? 0;
10712
+ devtoolsIdCounters.set(name, seq + 1);
10713
+ return `query-form|${name}#${seq}`;
10714
+ };
9559
10715
  /**
9560
10716
  * A signals-first query form: binds on-screen controls to typed fields, keeps a
9561
10717
  * debounced committed value, syncs to the URL query params, and resets dependent
@@ -9582,6 +10738,8 @@ class QueryFormSignals {
9582
10738
  this.observing = signal(false, /* @ts-ignore */
9583
10739
  ...(ngDevMode ? [{ debugName: "observing" }] : /* istanbul ignore next */ []));
9584
10740
  this.pendingTimer = null;
10741
+ this.commitPending = signal(false, /* @ts-ignore */
10742
+ ...(ngDevMode ? [{ debugName: "commitPending" }] : /* istanbul ignore next */ []));
9585
10743
  this.skipNextResets = false;
9586
10744
  assertInInjectionContext(QueryFormSignals);
9587
10745
  this._fields = config.fields;
@@ -9594,7 +10752,16 @@ class QueryFormSignals {
9594
10752
  this.committed = signal(clone(initial), /* @ts-ignore */
9595
10753
  ...(ngDevMode ? [{ debugName: "committed" }] : /* istanbul ignore next */ []));
9596
10754
  this.fields = form(this.model);
9597
- this.value = this.committed.asReadonly();
10755
+ const devtoolsName = config.name ?? (typeof this.prefix === 'string' ? this.prefix : 'form');
10756
+ const devtoolsId = isQueryDevtoolsEnabled() ? nextQueryFormDevtoolsId(devtoolsName) : null;
10757
+ // Reading `value` is how a query's args pick the form up, so the devtools learn which query a form
10758
+ // drives by noting the read - see `QueryDevtoolsFormLinksHandle`.
10759
+ this.value = devtoolsId
10760
+ ? computed(() => {
10761
+ noteQueryFormRead(devtoolsId);
10762
+ return this.committed();
10763
+ })
10764
+ : this.committed.asReadonly();
9598
10765
  this.previousValue = this.previous.asReadonly();
9599
10766
  this.changes = computed(() => ({ previousValue: this.previous(), currentValue: this.committed() }), /* @ts-ignore */
9600
10767
  ...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
@@ -9614,7 +10781,16 @@ class QueryFormSignals {
9614
10781
  this.applyFromUrl(changes);
9615
10782
  });
9616
10783
  });
9617
- inject(DestroyRef).onDestroy(() => this.cleanup());
10784
+ const destroyRef = inject(DestroyRef);
10785
+ destroyRef.onDestroy(() => this.cleanup());
10786
+ if (devtoolsId) {
10787
+ destroyRef.onDestroy(registerQueryDevtoolsEntry({
10788
+ id: devtoolsId,
10789
+ kind: 'query-form',
10790
+ handle: this.devtoolsHandle(),
10791
+ meta: { name: devtoolsName },
10792
+ }));
10793
+ }
9618
10794
  }
9619
10795
  observe(options) {
9620
10796
  if (this.observing()) {
@@ -9670,6 +10846,46 @@ class QueryFormSignals {
9670
10846
  branch() {
9671
10847
  return createBranch(this._fields, clone(this.committed()), this.injector);
9672
10848
  }
10849
+ /**
10850
+ * The live view `<et-query-devtools>` renders. Only built while the devtools are installed - it walks
10851
+ * every field on every change, which an app without them should not pay for.
10852
+ */
10853
+ devtoolsHandle() {
10854
+ const fields = computed(() => {
10855
+ const committed = this.committed();
10856
+ const live = this.model();
10857
+ return Object.entries(this._fields).map(([key, def]) => {
10858
+ const value = committed[key];
10859
+ return {
10860
+ key,
10861
+ paramKey: this.paramKey(key),
10862
+ value,
10863
+ liveValue: live[key],
10864
+ defaultValue: this.defaults[key],
10865
+ isDefault: equal(value, this.defaults[key]),
10866
+ queryParam: this.queryParamFor(key, def, value),
10867
+ debounceMs: def.debounce ?? null,
10868
+ isResetBy: def.isResetBy ?? [],
10869
+ countsAsFilter: !IGNORED_FILTER_COUNT_FIELDS.includes(key) && !def.skipInFilterCount,
10870
+ };
10871
+ });
10872
+ }, /* @ts-ignore */
10873
+ ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
10874
+ return {
10875
+ fields,
10876
+ // Not `this.value` - reading that from the panel would link the form to whichever query happens
10877
+ // to be building its args at the time.
10878
+ value: computed(() => this.committed()),
10879
+ previousValue: computed(() => this.previous() ?? null),
10880
+ defaultValue: this.defaults,
10881
+ activeFilterCount: this.activeFilterCount,
10882
+ isAtDefaults: computed(() => equal(this.committed(), this.defaults)),
10883
+ isCommitPending: this.commitPending.asReadonly(),
10884
+ isObserving: this.observing.asReadonly(),
10885
+ resetField: (key) => this.resetFieldToDefault(key),
10886
+ resetAll: () => this.resetAllFieldsToDefault(),
10887
+ };
10888
+ }
9673
10889
  onLiveChange(live) {
9674
10890
  if (!this.observing())
9675
10891
  return;
@@ -9682,6 +10898,7 @@ class QueryFormSignals {
9682
10898
  this.flush();
9683
10899
  }
9684
10900
  else {
10901
+ this.commitPending.set(true);
9685
10902
  this.pendingTimer = setTimeout(() => {
9686
10903
  this.pendingTimer = null;
9687
10904
  this.flush();
@@ -9782,23 +10999,21 @@ class QueryFormSignals {
9782
10999
  return raw;
9783
11000
  return autoCoerce(raw, resolveDefault(def));
9784
11001
  }
11002
+ /** What a field puts in the URL for a given value, or `undefined` when it writes nothing. */
11003
+ queryParamFor(key, def, value) {
11004
+ const isDefault = equal(value, this.defaults[key]);
11005
+ const writeToUrl = def.appendToUrl !== false;
11006
+ const writeDefault = def.appendDefaultValueToUrl === true;
11007
+ if (!writeToUrl || (isDefault && !writeDefault))
11008
+ return undefined;
11009
+ if (def.valueToQueryParam)
11010
+ return def.valueToQueryParam(value);
11011
+ return value === null ? ET_NULL_VALUE : value;
11012
+ }
9785
11013
  writeToUrl(value) {
9786
11014
  const queryParams = {};
9787
11015
  for (const [key, def] of Object.entries(this._fields)) {
9788
- const paramKey = this.paramKey(key);
9789
- const fieldValue = value[key];
9790
- const isDefault = equal(fieldValue, this.defaults[key]);
9791
- const writeToUrl = def.appendToUrl !== false;
9792
- const writeDefault = def.appendDefaultValueToUrl === true;
9793
- if (!writeToUrl || (isDefault && !writeDefault)) {
9794
- queryParams[paramKey] = undefined;
9795
- }
9796
- else if (def.valueToQueryParam) {
9797
- queryParams[paramKey] = def.valueToQueryParam(fieldValue);
9798
- }
9799
- else {
9800
- queryParams[paramKey] = fieldValue === null ? ET_NULL_VALUE : fieldValue;
9801
- }
11016
+ queryParams[this.paramKey(key)] = this.queryParamFor(key, def, value[key]);
9802
11017
  }
9803
11018
  queueMicrotask(() => {
9804
11019
  this.router.navigate([], {
@@ -9819,6 +11034,7 @@ class QueryFormSignals {
9819
11034
  clearTimeout(this.pendingTimer);
9820
11035
  this.pendingTimer = null;
9821
11036
  }
11037
+ this.commitPending.set(false);
9822
11038
  }
9823
11039
  cleanup() {
9824
11040
  if (!this.observing())
@@ -10071,5 +11287,5 @@ const createWebSocketClient = (options) => {
10071
11287
  * Generated bundle index. Do not edit.
10072
11288
  */
10073
11289
 
10074
- 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, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryFormSignals, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryForm, 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, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsRegistrar, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRoute, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
11290
+ export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryFormSignals, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsFaults, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryForm, 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, describeQueryDevtoolsFeatures, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsFaultInjectionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsRegistrar, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
10075
11291
  //# sourceMappingURL=ethlete-query.mjs.map