@ethlete/query 6.0.0-next.53 → 6.0.0-next.55

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,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, inject, DestroyRef, computed, effect, isDevMode, untracked, ApplicationRef, Injector, runInInjectionContext, InjectionToken, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, linkedSignal, PLATFORM_ID, resource, DOCUMENT, VERSION, provideEnvironmentInitializer, CSP_NONCE, afterNextRender, makeEnvironmentProviders, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
3
3
  import { UrlTree, Router, RedirectCommand, ActivatedRoute } from '@angular/router';
4
- import { Subject, Observable, defer, from, filter, Subscription, of, throwError, timer, switchMap, tap, retry, catchError, EMPTY as EMPTY$1, take, takeUntil, finalize, mergeMap, toArray, map, race, NEVER, merge, takeWhile, defaultIfEmpty, firstValueFrom, concatMap, fromEvent, throttleTime, interval, startWith, pairwise, BehaviorSubject, combineLatest, ReplaySubject, distinctUntilChanged, shareReplay, withLatestFrom, debounceTime } from 'rxjs';
5
- import { defineStaticProvider, RuntimeError, equal, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, setCookie, deleteCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
4
+ import { Subject, Observable, defer, from, ReplaySubject, filter, Subscription, of, throwError, timer, switchMap, tap, retry, catchError, EMPTY as EMPTY$1, take, takeUntil, finalize, mergeMap, toArray, map, race, NEVER, merge, takeWhile, share, defaultIfEmpty, firstValueFrom, concatMap, fromEvent, throttleTime, interval, startWith, pairwise, BehaviorSubject, combineLatest, distinctUntilChanged, shareReplay, withLatestFrom, debounceTime } from 'rxjs';
5
+ import { RuntimeError, defineStaticProvider, equal, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, setCookie, deleteCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, ET_PROPERTY_REMOVED } from '@ethlete/core';
6
+ import { HttpErrorResponse, HttpClient, HttpEventType, HttpHeaders, HttpContextToken, HttpContext, HttpResponse, HttpStatusCode } from '@angular/common/http';
6
7
  import { getActiveConsumer, setActiveConsumer, SIGNAL, createWatch } from '@angular/core/primitives/signals';
7
- import { HttpClient, HttpErrorResponse, HttpEventType, HttpHeaders, HttpContextToken, HttpContext, HttpResponse, HttpStatusCode } from '@angular/common/http';
8
- import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
8
+ import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
9
9
  import { isPlatformBrowser, isPlatformServer, NgClass, AsyncPipe, JsonPipe } from '@angular/common';
10
10
  import { validateAsync, form } from '@angular/forms/signals';
11
11
  import * as i1 from '@angular/forms';
@@ -703,6 +703,585 @@ const registerSessionEnd = (owner, sessionEnd) => {
703
703
  };
704
704
  const readSessionEnd = (owner) => sessionEnds.get(owner) ?? null;
705
705
 
706
+ const noop$2 = () => undefined;
707
+ let registrar = null;
708
+ /**
709
+ * Installs the real registry. Called by `provideQueryDevtools()`; nothing else may call it.
710
+ * @internal
711
+ */
712
+ const setQueryDevtoolsRegistrar = (fn) => {
713
+ registrar = fn;
714
+ };
715
+ /**
716
+ * Whether `provideQueryDevtools()` has been called on this page. When `false` every instrumentation
717
+ * site is a no-op, which is what a devtools UI gates itself on so a build that omits the provider
718
+ * renders nothing.
719
+ *
720
+ * It is answered by the `provideQueryDevtools()` call itself, which runs while the providers array is
721
+ * built - so it is already settled by the time an application bootstraps and needs no signal.
722
+ */
723
+ const isQueryDevtoolsEnabled = () => registrar !== null;
724
+ /**
725
+ * Registers an entry with the devtools registry. Returns an unregister callback. A no-op (returning
726
+ * a no-op callback) unless {@link provideQueryDevtools} has been called.
727
+ * @internal
728
+ */
729
+ const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$2;
730
+ let statsFactory = null;
731
+ /**
732
+ * Installs the stats recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
733
+ * @internal
734
+ */
735
+ const setQueryDevtoolsStatsFactory = (fn) => {
736
+ statsFactory = fn;
737
+ };
738
+ /**
739
+ * A stats recorder for one entry, or `null` unless {@link provideQueryDevtools} has been called.
740
+ *
741
+ * Instrumentation sites must go through this instead of importing `createQueryDevtoolsStats`: a static
742
+ * import pins the whole stats module (~1.4 kB) into every bundle that touches a query, and a runtime
743
+ * `isQueryDevtoolsEnabled()` guard at the call site does not undo that. Reached only from the provider,
744
+ * it is dead code in an app without devtools.
745
+ * @internal
746
+ */
747
+ const createQueryDevtoolsStatsRecorder = () => statsFactory?.() ?? null;
748
+ let formLinksFactory = null;
749
+ /**
750
+ * Installs the form-links recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
751
+ * @internal
752
+ */
753
+ const setQueryDevtoolsFormLinksFactory = (fn) => {
754
+ formLinksFactory = fn;
755
+ };
756
+ /**
757
+ * A form-links recorder for one query, or `null` unless {@link provideQueryDevtools} has been called.
758
+ * Reached only from the provider for the same reason {@link createQueryDevtoolsStatsRecorder} is.
759
+ * @internal
760
+ */
761
+ const createQueryDevtoolsFormLinksRecorder = () => formLinksFactory?.() ?? null;
762
+ let overridesFactory = null;
763
+ /**
764
+ * Installs the overrides recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
765
+ * @internal
766
+ */
767
+ const setQueryDevtoolsOverridesFactory = (fn) => {
768
+ overridesFactory = fn;
769
+ };
770
+ /**
771
+ * An overrides recorder for one query, or `null` unless {@link provideQueryDevtools} has been called.
772
+ * Reached only from the provider for the same reason {@link createQueryDevtoolsStatsRecorder} is.
773
+ * @internal
774
+ */
775
+ const createQueryDevtoolsOverridesRecorder = () => overridesFactory?.() ?? null;
776
+ /** The ids of the query forms read by the args build currently running, or `null` outside of one. */
777
+ let formLinkSink = null;
778
+ /**
779
+ * Runs `build` and reports which query forms it read. Nested builds are supported - the outer one
780
+ * resumes collecting once the inner one is done.
781
+ * @internal
782
+ */
783
+ const collectQueryFormLinks = (build) => {
784
+ const outer = formLinkSink;
785
+ const sink = new Set();
786
+ formLinkSink = sink;
787
+ try {
788
+ return { value: build(), ids: [...sink] };
789
+ }
790
+ finally {
791
+ formLinkSink = outer;
792
+ }
793
+ };
794
+ /**
795
+ * Records that a query form's committed value was read. The form calls this from its `value` signal, so
796
+ * a read that happens while a query builds its args is what links the two - no naming convention, and
797
+ * nothing to keep in sync when the args change.
798
+ * @internal
799
+ */
800
+ const noteQueryFormRead = (id) => {
801
+ formLinkSink?.add(id);
802
+ };
803
+ let faultResolver = null;
804
+ let mockResolver = null;
805
+ /**
806
+ * Installs the fault resolver. Called by `provideQueryDevtools()`; nothing else may call it.
807
+ * @internal
808
+ */
809
+ const setQueryDevtoolsFaultResolver = (fn) => {
810
+ faultResolver = fn;
811
+ };
812
+ /**
813
+ * Installs the mock resolver. Called by `provideQueryDevtools()`; nothing else may call it.
814
+ * @internal
815
+ */
816
+ const setQueryDevtoolsMockResolver = (fn) => {
817
+ mockResolver = fn;
818
+ };
819
+ /**
820
+ * Whether the devtools can do anything to a request at all - inject a fault, or serve a mock instead of
821
+ * sending it. Checked before building the wrapper that resolves both per attempt, so a request in an app
822
+ * without devtools keeps the pipeline it always had.
823
+ * @internal
824
+ */
825
+ const isQueryDevtoolsRequestInterceptionEnabled = () => faultResolver !== null || mockResolver !== null;
826
+ /**
827
+ * The mock to serve one upcoming attempt instead of sending it, or `null` to let it through.
828
+ * @internal
829
+ */
830
+ const resolveQueryDevtoolsMock = (target) => mockResolver?.(target) ?? null;
831
+ /**
832
+ * Resolves the fault to apply to one upcoming attempt, or `null` when nothing is armed. Consumes a
833
+ * `failNext` budget, so call it exactly once per attempt.
834
+ * @internal
835
+ */
836
+ const resolveQueryDevtoolsFault = (target) => faultResolver?.(target) ?? null;
837
+ let tokenPayloadPatcher = null;
838
+ /**
839
+ * Installs the access-token payload patcher. Called by `provideQueryDevtools()`; nothing else may call it.
840
+ * @internal
841
+ */
842
+ const setQueryDevtoolsTokenPayloadPatcher = (fn) => {
843
+ tokenPayloadPatcher = fn;
844
+ };
845
+ /**
846
+ * A decoded access token as the devtools want it seen, which today means with an overridden lifetime
847
+ * applied. Returns the payload as decoded unless `provideQueryDevtools()` installed a patcher.
848
+ *
849
+ * Auth code must reach the override through this rather than importing the registry it lives in: a
850
+ * static import would pin that module into every bundle that builds an auth provider, and a runtime
851
+ * guard at the call site does not undo that.
852
+ * @internal
853
+ */
854
+ const patchQueryDevtoolsTokenPayload = (options) => (tokenPayloadPatcher ? tokenPayloadPatcher(options) : options.payload);
855
+ /** The batch item being created right now, or `null` outside one. */
856
+ let batchOwner = null;
857
+ /**
858
+ * Runs `create` with every query it creates attributed to `owner`'s batch and item. The attribution has
859
+ * to be ambient: a batch's item query is built by the query creator the caller handed it, which takes no
860
+ * devtools argument - so there is nothing to thread the owner through. Nested runs restore the outer one.
861
+ * @internal
862
+ */
863
+ const runInQueryDevtoolsBatch = (owner, create) => {
864
+ const outer = batchOwner;
865
+ batchOwner = owner;
866
+ try {
867
+ return create();
868
+ }
869
+ finally {
870
+ batchOwner = outer;
871
+ }
872
+ };
873
+ /** The batch item a query being created belongs to, or `null` for one created outside a batch. @internal */
874
+ const currentQueryDevtoolsBatch = () => batchOwner;
875
+ let suppressStackRegistration = false;
876
+ /**
877
+ * Suppresses devtools registration for the next {@link createQueryStack} call. Used by
878
+ * `createPagedQueryStack` so its backing stack is not registered as a separate `query-stack` entry.
879
+ * @internal
880
+ */
881
+ const suppressNextQueryStackDevtools = () => {
882
+ suppressStackRegistration = true;
883
+ };
884
+ /**
885
+ * Reads and clears the suppress-next-stack flag. Always call this from `createQueryStack` (even when
886
+ * devtools are disabled) so a suppression request never leaks into a later stack.
887
+ * @internal
888
+ */
889
+ const consumeSuppressQueryStackDevtools = () => {
890
+ const value = suppressStackRegistration;
891
+ suppressStackRegistration = false;
892
+ return value;
893
+ };
894
+ let authProviderRegistrar = null;
895
+ /**
896
+ * Installs the session vault's provider registrar. Called by `provideQueryDevtools()`; nothing else may
897
+ * call it.
898
+ * @internal
899
+ */
900
+ const setQueryDevtoolsAuthProviderRegistrar = (fn) => {
901
+ authProviderRegistrar = fn;
902
+ };
903
+ /**
904
+ * Hands one auth provider to the devtools' session vault, which then keeps the live session's tokens in
905
+ * step with the stored one. A no-op without `provideQueryDevtools()`.
906
+ * @internal
907
+ */
908
+ const registerQueryDevtoolsAuthProvider = (registration) => authProviderRegistrar?.(registration) ?? (() => undefined);
909
+ let authSeedReader = null;
910
+ /**
911
+ * Installs the tab-local session reader. Called by `provideQueryDevtools()`; nothing else may call it.
912
+ * @internal
913
+ */
914
+ const setQueryDevtoolsAuthSeedReader = (fn) => {
915
+ authSeedReader = fn;
916
+ };
917
+ /**
918
+ * The session the panel put in this tab alone, or `null` for a tab on the shared one. Read once while a
919
+ * provider is built, before its features run: the tokens have to be in place before `withPersistentAuth`
920
+ * decides whether to spend its cookie.
921
+ * @internal
922
+ */
923
+ const readQueryDevtoolsAuthSeed = (providerName) => authSeedReader?.(providerName) ?? null;
924
+
925
+ // codes 0-999
926
+ const QueryRuntimeErrorCode = {
927
+ // Query
928
+ QUERY_FEATURE_USED_MULTIPLE_TIMES: 0,
929
+ QUERY_CREATED_IN_REACTIVE_CONTEXT: 1,
930
+ INVALIDATES_USED_ON_READ: 2,
931
+ QUERY_CREATED_OUTSIDE_INJECTION_CONTEXT: 3,
932
+ // Routes
933
+ INVALID_BASE_URL: 10,
934
+ INVALID_ROUTE: 11,
935
+ PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION: 12,
936
+ // Query features
937
+ WITH_ARGS_QUERY_FEATURE_MISSING_BUT_ROUTE_IS_FUNCTION: 100,
938
+ WITH_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 101,
939
+ WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
940
+ WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
941
+ SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT: 104,
942
+ WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 105,
943
+ WITH_LONG_POLLING_USED_WITH_POLLING: 106,
944
+ WITH_OPTIMISTIC_UPDATE_USED_ON_READ: 107,
945
+ // Auth provider
946
+ AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
947
+ AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
948
+ AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_REFRESH_TOKEN: 202,
949
+ AUTH_PROVIDER_FEATURE_USED_MULTIPLE_TIMES: 203,
950
+ AUTH_ACCESS_TOKEN_EXPIRY_UNREADABLE: 204,
951
+ // Query client
952
+ QUERY_CLIENT_FEATURE_USED_MULTIPLE_TIMES: 250,
953
+ // Query Repository
954
+ UNCACHEABLE_REQUEST_HAS_CACHE_KEY_PARAM: 300,
955
+ UNCACHEABLE_REQUEST_HAS_ALLOW_CACHE_PARAM: 301,
956
+ // Paged Query Stack
957
+ PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES: 400,
958
+ PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE: 401,
959
+ PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE: 402,
960
+ // Query Stack
961
+ QUERY_STACK_WITH_ARGS_USED: 500,
962
+ QUERY_STACK_WITH_RESPONSE_UPDATE_USED: 501,
963
+ // GQL
964
+ GQL_DATA_PROPERTY_MISSING_IN_RESPONSE: 600,
965
+ GQL_ERRORS_IN_RESPONSE: 601,
966
+ // Secure Execute
967
+ TOKENS_NOT_AVAILABLE_INSIDE_AUTH_AND_EXEC: 700,
968
+ INVALID_STATE_INSIDE_SECURE_EXECUTE_FACTORY: 701,
969
+ // Query Execution
970
+ CIRCULAR_QUERY_DEPENDENCY: 800,
971
+ // Query Sequence
972
+ QUERY_SEQUENCE_ALREADY_RUNNING: 900,
973
+ // Query Batch
974
+ QUERY_BATCH_ALREADY_RUNNING: 910,
975
+ QUERY_BATCH_WITH_ARGS_USED: 911,
976
+ // Legacy interop
977
+ LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT: 950,
978
+ LEGACY_QUERY_STATE_ALREADY_HAS_KEY: 951,
979
+ LEGACY_QUERY_STATE_DOES_NOT_CONTAIN_KEY: 952,
980
+ LEGACY_QUERY_STATE_CANNOT_TRANSFORM: 953,
981
+ LEGACY_INVALID_BODY: 954,
982
+ };
983
+ const queryFeatureUsedMultipleTimes = (type) => {
984
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_FEATURE_USED_MULTIPLE_TIMES, `The query feature "${type}()" was used multiple times.`);
985
+ };
986
+ const queryCreatedInReactiveContext = () => {
987
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_CREATED_IN_REACTIVE_CONTEXT, `A query was created inside a reactive context (a computed, an effect or a template). Every re-run would create another query that lives until its injector is destroyed. Create the query in a field initializer or constructor and change its args instead.`);
988
+ };
989
+ const invalidatesUsedOnRead = (method) => {
990
+ return new RuntimeError(QueryRuntimeErrorCode.INVALIDATES_USED_ON_READ, `This is a "${method}" request, "invalidates" is only supported on mutations. Give the read "tags" and invalidate them from the mutation.`);
991
+ };
992
+ const withOptimisticUpdateUsedOnRead = (method) => {
993
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_OPTIMISTIC_UPDATE_USED_ON_READ, `This is a "${method}" request, "withOptimisticUpdate()" is only supported on mutations.`);
994
+ };
995
+ const withArgsQueryFeatureMissingButRouteIsFunction = () => {
996
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_ARGS_QUERY_FEATURE_MISSING_BUT_ROUTE_IS_FUNCTION, `This queries route is a function, but a "withArgs()" feature is missing. Mutations need one too: create the query with "withArgs(() => ({ body: form.value() }))", then call ".execute()". The "silenceMissingWithArgsFeatureError" config is an escape hatch for args that only exist at call time.`);
997
+ };
998
+ const withPollingUsedOnUnsupportedHttpMethod = (method) => {
999
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1000
+ };
1001
+ const withAutoRefreshUsedOnUnsupportedHttpMethod = (method) => {
1002
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withAutoRefresh()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1003
+ };
1004
+ const withAutoRefreshUsedInManualQuery = () => {
1005
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY, `"withAutoRefresh()" has been used inside a query that has "onlyManualExecution" set to true.` +
1006
+ ` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
1007
+ };
1008
+ const withLongPollingUsedOnUnsupportedHttpMethod = (method) => {
1009
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withLongPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1010
+ };
1011
+ const withLongPollingUsedWithPolling = () => {
1012
+ return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_WITH_POLLING, `"withLongPolling()" and "withPolling()" cannot be used on the same query - both would drive its re-execution.` +
1013
+ ` Keep the long polling feature and drop the interval, or the other way around.`);
1014
+ };
1015
+ const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
1016
+ return new RuntimeError(QueryRuntimeErrorCode.SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT, `The "silenceMissingWithArgsFeatureError" config is set to true, but a "withArgs()" feature is present.`);
1017
+ };
1018
+ const authExtractTokensResponseNotObject = () => {
1019
+ return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT, `The auth token extractor expects the response to be an object.`);
1020
+ };
1021
+ const authExtractTokensResponseMissingAccessToken = () => {
1022
+ return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN, `The auth token extractor expects the response to contain a string "accessToken" property.`);
1023
+ };
1024
+ const authExtractTokensResponseMissingRefreshToken = () => {
1025
+ return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_REFRESH_TOKEN, `The auth token extractor expects the response to contain a string "refreshToken" property.`);
1026
+ };
1027
+ const authProviderFeatureUsedMultipleTimes = (type) => {
1028
+ return new RuntimeError(QueryRuntimeErrorCode.AUTH_PROVIDER_FEATURE_USED_MULTIPLE_TIMES, `Bearer auth feature "${type}" was used multiple times.`);
1029
+ };
1030
+ const authAccessTokenExpiryUnreadable = (claim) => {
1031
+ return new RuntimeError(QueryRuntimeErrorCode.AUTH_ACCESS_TOKEN_EXPIRY_UNREADABLE, `The access token has no numeric "${claim}" claim, neither in what bearerDecryptFn returned nor in its JWT payload, so it is never refreshed before it expires. Keep the claim in the bearerDecryptFn result, or set the refresh query's expiresInPropertyName to the claim that carries the expiry.`);
1032
+ };
1033
+ const queryClientFeatureUsedMultipleTimes = (type) => {
1034
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_CLIENT_FEATURE_USED_MULTIPLE_TIMES, `Query client feature "${type}" was used multiple times.`);
1035
+ };
1036
+ const uncacheableRequestHasCacheKeyParam = (key) => {
1037
+ return new RuntimeError(QueryRuntimeErrorCode.UNCACHEABLE_REQUEST_HAS_CACHE_KEY_PARAM, `This request is uncacheable, but a cache key was provided: ${key}. Please remove it.`);
1038
+ };
1039
+ const uncacheableRequestHasAllowCacheParam = () => {
1040
+ return new RuntimeError(QueryRuntimeErrorCode.UNCACHEABLE_REQUEST_HAS_ALLOW_CACHE_PARAM, `This request is uncacheable, but allowCache is set to true. Please remove it.`);
1041
+ };
1042
+ const pagedQueryStackPageBiggerThanTotalPages = (page, totalPages) => {
1043
+ return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES, `The page "${page}" is bigger than the total pages "${totalPages}".`);
1044
+ };
1045
+ const pagedQueryStackNextPageCalledWithoutPreviousPage = () => {
1046
+ return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE, `fetchNextPage() has been called but the current page is not yet loaded. Please call it after the previous page has been loaded.`);
1047
+ };
1048
+ const pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage = () => {
1049
+ return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE, `fetchPreviousPage() has been called but the current page is already the first page.`);
1050
+ };
1051
+ const queryStackWithArgsUsed = () => {
1052
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_STACK_WITH_ARGS_USED, `withArgs() has been used in a query stack or a paged query stack. This is not supported.`);
1053
+ };
1054
+ const queryStackWithResponseUpdateUsed = () => {
1055
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_STACK_WITH_RESPONSE_UPDATE_USED, `withResponseUpdate() has been used in a query stack or a paged query stack. This is not supported.`);
1056
+ };
1057
+ const gqlDataPropertyMissingInResponse = () => {
1058
+ return new RuntimeError(QueryRuntimeErrorCode.GQL_DATA_PROPERTY_MISSING_IN_RESPONSE, `The GraphQL response is missing the required "data" property. Please add a custom transformResponse param to the query creator to handle this response format.`);
1059
+ };
1060
+ const gqlErrorsInResponse = (errors) => {
1061
+ return new HttpErrorResponse({
1062
+ error: errors,
1063
+ status: 0,
1064
+ statusText: new RuntimeError(QueryRuntimeErrorCode.GQL_ERRORS_IN_RESPONSE, `The GraphQL response carries errors and no data. The server's errors are in raw.error.`).message,
1065
+ });
1066
+ };
1067
+ const tokensNotAvailableInsideAuthAndExec = () => {
1068
+ return new RuntimeError(QueryRuntimeErrorCode.TOKENS_NOT_AVAILABLE_INSIDE_AUTH_AND_EXEC, `Tokens are not available inside authAndExec.`);
1069
+ };
1070
+ const invalidStateInsideSecureExecuteFactory = () => {
1071
+ return new RuntimeError(QueryRuntimeErrorCode.INVALID_STATE_INSIDE_SECURE_EXECUTE_FACTORY, `An invalid state occurred inside the secure execute factory.`);
1072
+ };
1073
+ const circularQueryDependency = () => {
1074
+ return new RuntimeError(QueryRuntimeErrorCode.CIRCULAR_QUERY_DEPENDENCY, `The same query was executed with identical args more than 5 times in a row, less than 100ms apart. This is usually a sign of a circular dependency.`);
1075
+ };
1076
+ const queryExecutedAfterDestroyMessage = (route) => {
1077
+ const target = typeof route === 'string' ? `The query for "${route}"` : 'A query with a function route';
1078
+ return `${target} was executed after the scope that created it was destroyed. The call was ignored - drop the reference to the query when its component or injector goes away.`;
1079
+ };
1080
+ const queryExecutedWhileParkedMessage = (route) => {
1081
+ const target = typeof route === 'string' ? `The query for "${route}"` : 'A query with a function route';
1082
+ return `${target} was executed while its withArgs source returned null (parked). The call was ignored - pass args to execute(), or let the source return them.`;
1083
+ };
1084
+ const legacyPrepareWithoutInjectionContext = (creatorName, method = 'prepare') => {
1085
+ const label = creatorName ? `"${creatorName}"` : 'A legacy query creator';
1086
+ const example = method === 'prepare'
1087
+ ? ` search = (term: string) => myQuery.prepare({ queryParams: { term }, injector: this.injector });`
1088
+ : ` container = myQuery.${method}(null, { injector: this.injector });`;
1089
+ 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` +
1090
+ `Capture one where a context does exist (field initializer, constructor) and pass it along:\n\n` +
1091
+ ` private injector = inject(Injector);\n` +
1092
+ `${example}\n\n` +
1093
+ `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.`);
1094
+ };
1095
+ const querySequenceAlreadyRunning = () => {
1096
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_SEQUENCE_ALREADY_RUNNING, `run() was called on a query sequence that is already running. Wait for the current run to settle before starting another.`);
1097
+ };
1098
+ const queryBatchAlreadyRunning = () => {
1099
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_ALREADY_RUNNING, `run() or retryFailed() was called on a query batch that is already running. Await the current run, or call cancel() first.`);
1100
+ };
1101
+ const queryBatchWithArgsUsed = () => {
1102
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_WITH_ARGS_USED, `The "withArgs()" feature cannot be used inside a query batch. Each item's args come from the batch's "args" option.`);
1103
+ };
1104
+
1105
+ /**
1106
+ * Route building, query-string serialization and the token clock - shared by the current generation and
1107
+ * the legacy V2 client, which re-exports them under their original names.
1108
+ */
1109
+ const invalidBaseRouteError$1 = (base) => new RuntimeError(QueryRuntimeErrorCode.INVALID_BASE_URL, `The client base URL "${base}" must not end with "/". Drop the trailing slash.`);
1110
+ const invalidRouteError$1 = (route) => new RuntimeError(QueryRuntimeErrorCode.INVALID_ROUTE, `The route "${route}" must start with "/". Write it as "/${route}".`);
1111
+ const pathParamsMissingInRouteFunctionError$1 = (route) => new RuntimeError(QueryRuntimeErrorCode.PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION, `The route "${route}" is a function, but the query was executed without pathParams. Pass them through withArgs(() => ({ pathParams })) or execute({ args: { pathParams } }).`);
1112
+ const isNaN$2 = (value) => typeof value === 'number' && Number.isNaN(value);
1113
+ const isEmptyString$1 = (value) => typeof value === 'string' && value.trim() === '';
1114
+ const buildQueryString$1 = (params, config) => {
1115
+ const objectNotation = config?.objectNotation ?? 'bracket';
1116
+ const writeArrayIndexes = config?.writeArrayIndexes ?? false;
1117
+ const ignoredValues = config?.ignoredValues ?? [undefined, null, Infinity, -Infinity];
1118
+ const ignoredValuesFns = config?.ignoredValuesFns ?? [isNaN$2, isEmptyString$1];
1119
+ const queryParams = [];
1120
+ function processValue(key, rawValue) {
1121
+ if (rawValue instanceof Date && Number.isNaN(rawValue.getTime()))
1122
+ return false;
1123
+ const value = rawValue instanceof Date ? rawValue.toISOString() : rawValue;
1124
+ if (config?.objectNotation === 'json-stringify') {
1125
+ if (value === undefined) {
1126
+ return false;
1127
+ }
1128
+ if (ignoredValues.includes(value)) {
1129
+ return false;
1130
+ }
1131
+ if (ignoredValuesFns.some((fn) => fn(value))) {
1132
+ return false;
1133
+ }
1134
+ const encodedKey = encodeURIComponent(key);
1135
+ const val = typeof value === 'object' ? JSON.stringify(value) : value;
1136
+ const encodedValue = encodeURIComponent(val);
1137
+ queryParams.push(`${encodedKey}=${encodedValue}`);
1138
+ return true;
1139
+ }
1140
+ else if (Array.isArray(value)) {
1141
+ let currentFilteredIndex = 0;
1142
+ let didAddAnyValue = false;
1143
+ for (const arrayValue of value) {
1144
+ const nestedKey = writeArrayIndexes ? `${key}[${currentFilteredIndex}]` : `${key}[]`;
1145
+ const didAddValue = processValue(nestedKey, arrayValue);
1146
+ if (didAddValue) {
1147
+ currentFilteredIndex++;
1148
+ didAddAnyValue = true;
1149
+ }
1150
+ }
1151
+ return didAddAnyValue;
1152
+ }
1153
+ else if (typeof value === 'object' && value !== null) {
1154
+ let didAddAnyValue = false;
1155
+ for (const [objKey, val] of Object.entries(value)) {
1156
+ const nestedKey = objectNotation === 'dot' ? `${key}.${objKey}` : `${key}[${objKey}]`;
1157
+ didAddAnyValue = processValue(nestedKey, val) || didAddAnyValue;
1158
+ }
1159
+ return didAddAnyValue;
1160
+ }
1161
+ else {
1162
+ if (ignoredValues.includes(value)) {
1163
+ return false;
1164
+ }
1165
+ if (ignoredValuesFns.some((fn) => fn(value))) {
1166
+ return false;
1167
+ }
1168
+ const encodedKey = encodeURIComponent(key);
1169
+ const encodedValue = encodeURIComponent(value);
1170
+ queryParams.push(`${encodedKey}=${encodedValue}`);
1171
+ return true;
1172
+ }
1173
+ }
1174
+ for (const [key, val] of Object.entries(params)) {
1175
+ processValue(key, val);
1176
+ }
1177
+ return queryParams.length ? queryParams.join('&') : null;
1178
+ };
1179
+ const isPlainObject = (value) => {
1180
+ if (!value || typeof value !== 'object')
1181
+ return false;
1182
+ const proto = Object.getPrototypeOf(value);
1183
+ return proto === Object.prototype || proto === null;
1184
+ };
1185
+ /** Copies query params with every plain object's keys sorted, so key order never reaches a cache key. */
1186
+ const sortQueryParamKeys = (value) => {
1187
+ if (Array.isArray(value))
1188
+ return value.map(sortQueryParamKeys);
1189
+ if (!isPlainObject(value))
1190
+ return value;
1191
+ return Object.fromEntries(Object.keys(value)
1192
+ .sort()
1193
+ .map((key) => [key, sortQueryParamKeys(value[key])]));
1194
+ };
1195
+ const buildRoute$1 = (options) => {
1196
+ if (options.base.endsWith('/')) {
1197
+ throw invalidBaseRouteError$1(options.base);
1198
+ }
1199
+ let route;
1200
+ if (typeof options.route === 'function') {
1201
+ if (!options.pathParams) {
1202
+ throw pathParamsMissingInRouteFunctionError$1(options.route({}));
1203
+ }
1204
+ route = options.route(options.pathParams);
1205
+ }
1206
+ else {
1207
+ route = options.route ?? null;
1208
+ }
1209
+ if (route && !route.startsWith('/')) {
1210
+ throw invalidRouteError$1(route);
1211
+ }
1212
+ if (options.queryParams) {
1213
+ const queryString = buildQueryString$1(options.queryParams, options.queryParamConfig);
1214
+ if (queryString) {
1215
+ route = route ? `${route}${route.includes('?') ? '&' : '?'}${queryString}` : `/?${queryString}`;
1216
+ }
1217
+ }
1218
+ return `${options.base}${route ?? ''}`;
1219
+ };
1220
+ const buildTimestampFromSeconds$1 = (seconds) => {
1221
+ if (seconds === null || Number.isNaN(seconds)) {
1222
+ return null;
1223
+ }
1224
+ return new Date(Date.now() + seconds * 1000).getTime();
1225
+ };
1226
+ /** Reads a bearer token's payload. */
1227
+ const decryptBearer$1 = (token) => {
1228
+ try {
1229
+ const base64Url = token.split('.')[1];
1230
+ if (!base64Url) {
1231
+ return null;
1232
+ }
1233
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
1234
+ const jsonPayload = decodeURIComponent(atob(base64)
1235
+ .split('')
1236
+ .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
1237
+ .join(''));
1238
+ return JSON.parse(jsonPayload);
1239
+ }
1240
+ catch (error) {
1241
+ console.error('Invalid bearer token', error);
1242
+ return null;
1243
+ }
1244
+ };
1245
+
1246
+ const readClaim = (payload, claim) => {
1247
+ const value = payload?.[claim];
1248
+ return typeof value === 'number' ? value : null;
1249
+ };
1250
+ /**
1251
+ * The expiry and issue time of an access token. `decoded` (what `bearerDecryptFn` returned) is tried
1252
+ * first and the raw JWT payload second, so a `bearerDecryptFn` that maps the claims to an app user does
1253
+ * not hide the expiry.
1254
+ */
1255
+ const readAccessTokenTimes = (options) => {
1256
+ const { token, claim, providerName } = options;
1257
+ const patch = (payload) => patchQueryDevtoolsTokenPayload({ payload, providerName, expiresInPropertyName: claim });
1258
+ const candidates = [() => patch(options.decoded), () => patch(decryptBearer$1(token))];
1259
+ for (const candidate of candidates) {
1260
+ let payload;
1261
+ try {
1262
+ payload = candidate();
1263
+ }
1264
+ catch {
1265
+ continue;
1266
+ }
1267
+ const exp = readClaim(payload, claim);
1268
+ if (exp === null)
1269
+ continue;
1270
+ const iat = readClaim(payload, 'iat');
1271
+ return { expiresAtMs: exp * 1000, issuedAtMs: iat === null ? null : iat * 1000 };
1272
+ }
1273
+ return null;
1274
+ };
1275
+ const refreshBufferMs = (strategy, tokenLifetimeMs) => {
1276
+ if (typeof strategy === 'number') {
1277
+ return strategy >= 0 && strategy <= 1 ? tokenLifetimeMs * (1 - strategy) : Math.max(strategy, 0);
1278
+ }
1279
+ const percentage = strategy?.percentage ?? 0.75;
1280
+ const minBufferMs = strategy?.minBufferMs ?? 60_000;
1281
+ const maxBufferMs = strategy?.maxBufferMs ?? 600_000;
1282
+ return Math.max(minBufferMs, Math.min(maxBufferMs, tokenLifetimeMs * (1 - percentage)));
1283
+ };
1284
+
706
1285
  const DEFAULT_RETURN_URL_PARAM = 'returnUrl';
707
1286
  const DEFAULT_URL = '/';
708
1287
  const DEFAULT_RESTORE_TIMEOUT_MS = 10000;
@@ -907,244 +1486,25 @@ const formatQueryDevtoolsDuration = (ms) => {
907
1486
  return round(ms / 1000, 's');
908
1487
  if (ms < 3_600_000)
909
1488
  return round(ms / 60_000, 'm');
910
- return round(ms / 3_600_000, 'h');
911
- };
912
- /**
913
- * The name of a function passed as `options.<key>`, or `null` when it is an inline lambda. An inline
914
- * `{ handler: (e) => … }` inherits the name `handler` from the key it is assigned to, which tells the
915
- * reader nothing - a reference to a declared function does.
916
- */
917
- const queryDevtoolsFnName = (fn, key) => {
918
- const name = typeof fn === 'function' ? fn.name.replace(/^bound /, '') : '';
919
- return !name || name === key ? null : name;
920
- };
921
- /**
922
- * A detail for the function passed as `options.<key>`, omitted when it is an inline lambda.
923
- * @internal
924
- */
925
- const queryDevtoolsFnDetail = (fn, key) => {
926
- const name = queryDevtoolsFnName(fn, key);
927
- return name ? [{ label: key, value: name }] : [];
928
- };
929
-
930
- const noop$2 = () => undefined;
931
- let registrar = null;
932
- /**
933
- * Installs the real registry. Called by `provideQueryDevtools()`; nothing else may call it.
934
- * @internal
935
- */
936
- const setQueryDevtoolsRegistrar = (fn) => {
937
- registrar = fn;
938
- };
939
- /**
940
- * Whether `provideQueryDevtools()` has been called on this page. When `false` every instrumentation
941
- * site is a no-op, which is what a devtools UI gates itself on so a build that omits the provider
942
- * renders nothing.
943
- *
944
- * It is answered by the `provideQueryDevtools()` call itself, which runs while the providers array is
945
- * built - so it is already settled by the time an application bootstraps and needs no signal.
946
- */
947
- const isQueryDevtoolsEnabled = () => registrar !== null;
948
- /**
949
- * Registers an entry with the devtools registry. Returns an unregister callback. A no-op (returning
950
- * a no-op callback) unless {@link provideQueryDevtools} has been called.
951
- * @internal
952
- */
953
- const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$2;
954
- let statsFactory = null;
955
- /**
956
- * Installs the stats recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
957
- * @internal
958
- */
959
- const setQueryDevtoolsStatsFactory = (fn) => {
960
- statsFactory = fn;
961
- };
962
- /**
963
- * A stats recorder for one entry, or `null` unless {@link provideQueryDevtools} has been called.
964
- *
965
- * Instrumentation sites must go through this instead of importing `createQueryDevtoolsStats`: a static
966
- * import pins the whole stats module (~1.4 kB) into every bundle that touches a query, and a runtime
967
- * `isQueryDevtoolsEnabled()` guard at the call site does not undo that. Reached only from the provider,
968
- * it is dead code in an app without devtools.
969
- * @internal
970
- */
971
- const createQueryDevtoolsStatsRecorder = () => statsFactory?.() ?? null;
972
- let formLinksFactory = null;
973
- /**
974
- * Installs the form-links recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
975
- * @internal
976
- */
977
- const setQueryDevtoolsFormLinksFactory = (fn) => {
978
- formLinksFactory = fn;
979
- };
980
- /**
981
- * A form-links recorder for one query, or `null` unless {@link provideQueryDevtools} has been called.
982
- * Reached only from the provider for the same reason {@link createQueryDevtoolsStatsRecorder} is.
983
- * @internal
984
- */
985
- const createQueryDevtoolsFormLinksRecorder = () => formLinksFactory?.() ?? null;
986
- let overridesFactory = null;
987
- /**
988
- * Installs the overrides recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
989
- * @internal
990
- */
991
- const setQueryDevtoolsOverridesFactory = (fn) => {
992
- overridesFactory = fn;
993
- };
994
- /**
995
- * An overrides recorder for one query, or `null` unless {@link provideQueryDevtools} has been called.
996
- * Reached only from the provider for the same reason {@link createQueryDevtoolsStatsRecorder} is.
997
- * @internal
998
- */
999
- const createQueryDevtoolsOverridesRecorder = () => overridesFactory?.() ?? null;
1000
- /** The ids of the query forms read by the args build currently running, or `null` outside of one. */
1001
- let formLinkSink = null;
1002
- /**
1003
- * Runs `build` and reports which query forms it read. Nested builds are supported - the outer one
1004
- * resumes collecting once the inner one is done.
1005
- * @internal
1006
- */
1007
- const collectQueryFormLinks = (build) => {
1008
- const outer = formLinkSink;
1009
- const sink = new Set();
1010
- formLinkSink = sink;
1011
- try {
1012
- return { value: build(), ids: [...sink] };
1013
- }
1014
- finally {
1015
- formLinkSink = outer;
1016
- }
1017
- };
1018
- /**
1019
- * Records that a query form's committed value was read. The form calls this from its `value` signal, so
1020
- * a read that happens while a query builds its args is what links the two - no naming convention, and
1021
- * nothing to keep in sync when the args change.
1022
- * @internal
1023
- */
1024
- const noteQueryFormRead = (id) => {
1025
- formLinkSink?.add(id);
1026
- };
1027
- let faultResolver = null;
1028
- let mockResolver = null;
1029
- /**
1030
- * Installs the fault resolver. Called by `provideQueryDevtools()`; nothing else may call it.
1031
- * @internal
1032
- */
1033
- const setQueryDevtoolsFaultResolver = (fn) => {
1034
- faultResolver = fn;
1035
- };
1036
- /**
1037
- * Installs the mock resolver. Called by `provideQueryDevtools()`; nothing else may call it.
1038
- * @internal
1039
- */
1040
- const setQueryDevtoolsMockResolver = (fn) => {
1041
- mockResolver = fn;
1042
- };
1043
- /**
1044
- * Whether the devtools can do anything to a request at all - inject a fault, or serve a mock instead of
1045
- * sending it. Checked before building the wrapper that resolves both per attempt, so a request in an app
1046
- * without devtools keeps the pipeline it always had.
1047
- * @internal
1048
- */
1049
- const isQueryDevtoolsRequestInterceptionEnabled = () => faultResolver !== null || mockResolver !== null;
1050
- /**
1051
- * The mock to serve one upcoming attempt instead of sending it, or `null` to let it through.
1052
- * @internal
1053
- */
1054
- const resolveQueryDevtoolsMock = (target) => mockResolver?.(target) ?? null;
1055
- /**
1056
- * Resolves the fault to apply to one upcoming attempt, or `null` when nothing is armed. Consumes a
1057
- * `failNext` budget, so call it exactly once per attempt.
1058
- * @internal
1059
- */
1060
- const resolveQueryDevtoolsFault = (target) => faultResolver?.(target) ?? null;
1061
- let tokenPayloadPatcher = null;
1062
- /**
1063
- * Installs the access-token payload patcher. Called by `provideQueryDevtools()`; nothing else may call it.
1064
- * @internal
1065
- */
1066
- const setQueryDevtoolsTokenPayloadPatcher = (fn) => {
1067
- tokenPayloadPatcher = fn;
1068
- };
1069
- /**
1070
- * A decoded access token as the devtools want it seen, which today means with an overridden lifetime
1071
- * applied. Returns the payload as decoded unless `provideQueryDevtools()` installed a patcher.
1072
- *
1073
- * Auth code must reach the override through this rather than importing the registry it lives in: a
1074
- * static import would pin that module into every bundle that builds an auth provider, and a runtime
1075
- * guard at the call site does not undo that.
1076
- * @internal
1077
- */
1078
- const patchQueryDevtoolsTokenPayload = (options) => (tokenPayloadPatcher ? tokenPayloadPatcher(options) : options.payload);
1079
- /** The batch item being created right now, or `null` outside one. */
1080
- let batchOwner = null;
1081
- /**
1082
- * Runs `create` with every query it creates attributed to `owner`'s batch and item. The attribution has
1083
- * to be ambient: a batch's item query is built by the query creator the caller handed it, which takes no
1084
- * devtools argument - so there is nothing to thread the owner through. Nested runs restore the outer one.
1085
- * @internal
1086
- */
1087
- const runInQueryDevtoolsBatch = (owner, create) => {
1088
- const outer = batchOwner;
1089
- batchOwner = owner;
1090
- try {
1091
- return create();
1092
- }
1093
- finally {
1094
- batchOwner = outer;
1095
- }
1096
- };
1097
- /** The batch item a query being created belongs to, or `null` for one created outside a batch. @internal */
1098
- const currentQueryDevtoolsBatch = () => batchOwner;
1099
- let suppressStackRegistration = false;
1100
- /**
1101
- * Suppresses devtools registration for the next {@link createQueryStack} call. Used by
1102
- * `createPagedQueryStack` so its backing stack is not registered as a separate `query-stack` entry.
1103
- * @internal
1104
- */
1105
- const suppressNextQueryStackDevtools = () => {
1106
- suppressStackRegistration = true;
1107
- };
1108
- /**
1109
- * Reads and clears the suppress-next-stack flag. Always call this from `createQueryStack` (even when
1110
- * devtools are disabled) so a suppression request never leaks into a later stack.
1111
- * @internal
1112
- */
1113
- const consumeSuppressQueryStackDevtools = () => {
1114
- const value = suppressStackRegistration;
1115
- suppressStackRegistration = false;
1116
- return value;
1117
- };
1118
- let authProviderRegistrar = null;
1119
- /**
1120
- * Installs the session vault's provider registrar. Called by `provideQueryDevtools()`; nothing else may
1121
- * call it.
1122
- * @internal
1123
- */
1124
- const setQueryDevtoolsAuthProviderRegistrar = (fn) => {
1125
- authProviderRegistrar = fn;
1126
- };
1127
- /**
1128
- * Hands one auth provider to the devtools' session vault, which then keeps the live session's tokens in
1129
- * step with the stored one. A no-op without `provideQueryDevtools()`.
1130
- * @internal
1131
- */
1132
- const registerQueryDevtoolsAuthProvider = (registration) => authProviderRegistrar?.(registration) ?? (() => undefined);
1133
- let authSeedReader = null;
1489
+ return round(ms / 3_600_000, 'h');
1490
+ };
1134
1491
  /**
1135
- * Installs the tab-local session reader. Called by `provideQueryDevtools()`; nothing else may call it.
1136
- * @internal
1492
+ * The name of a function passed as `options.<key>`, or `null` when it is an inline lambda. An inline
1493
+ * `{ handler: (e) => … }` inherits the name `handler` from the key it is assigned to, which tells the
1494
+ * reader nothing - a reference to a declared function does.
1137
1495
  */
1138
- const setQueryDevtoolsAuthSeedReader = (fn) => {
1139
- authSeedReader = fn;
1496
+ const queryDevtoolsFnName = (fn, key) => {
1497
+ const name = typeof fn === 'function' ? fn.name.replace(/^bound /, '') : '';
1498
+ return !name || name === key ? null : name;
1140
1499
  };
1141
1500
  /**
1142
- * The session the panel put in this tab alone, or `null` for a tab on the shared one. Read once while a
1143
- * provider is built, before its features run: the tokens have to be in place before `withPersistentAuth`
1144
- * decides whether to spend its cookie.
1501
+ * A detail for the function passed as `options.<key>`, omitted when it is an inline lambda.
1145
1502
  * @internal
1146
1503
  */
1147
- const readQueryDevtoolsAuthSeed = (providerName) => authSeedReader?.(providerName) ?? null;
1504
+ const queryDevtoolsFnDetail = (fn, key) => {
1505
+ const name = queryDevtoolsFnName(fn, key);
1506
+ return name ? [{ label: key, value: name }] : [];
1507
+ };
1148
1508
 
1149
1509
  /**
1150
1510
  * Whether a create-query options object came from the gql layer - only gql's `creatorInternals` carries
@@ -1153,21 +1513,105 @@ const readQueryDevtoolsAuthSeed = (providerName) => authSeedReader?.(providerNam
1153
1513
  */
1154
1514
  const isCreateGqlQueryOptions = (options) => 'transport' in options.creatorInternals;
1155
1515
 
1156
- const wrapAsObservableSignal = (source, defaultInjector) => {
1157
- const default$ = toObservable(source, { injector: defaultInjector });
1158
- const overrides = new WeakMap();
1159
- const asObservable = (options) => {
1160
- const injector = options?.injector;
1161
- if (!injector)
1516
+ const createObservableSignalWatcher = (injector) => {
1517
+ const destroyRef = injector.get(DestroyRef);
1518
+ const watched = [];
1519
+ const registrations = signal(0, /* @ts-ignore */
1520
+ ...(ngDevMode ? [{ debugName: "registrations" }] : /* istanbul ignore next */ []));
1521
+ let watching = false;
1522
+ let destroyed = false;
1523
+ const push = (entry) => {
1524
+ if (entry.subject.closed || entry.subject.isStopped)
1525
+ return;
1526
+ let value;
1527
+ try {
1528
+ value = entry.source();
1529
+ }
1530
+ catch (error) {
1531
+ untracked(() => entry.subject.error(error));
1532
+ return;
1533
+ }
1534
+ if (entry.hasValue && Object.is(value, entry.last))
1535
+ return;
1536
+ entry.hasValue = true;
1537
+ entry.last = value;
1538
+ untracked(() => entry.subject.next(value));
1539
+ };
1540
+ const startWatching = () => {
1541
+ if (watching)
1542
+ return;
1543
+ watching = true;
1544
+ effect(() => {
1545
+ registrations();
1546
+ for (const entry of watched)
1547
+ push(entry);
1548
+ }, { injector });
1549
+ destroyRef.onDestroy(() => {
1550
+ destroyed = true;
1551
+ for (const entry of watched)
1552
+ entry.subject.complete();
1553
+ });
1554
+ };
1555
+ const watch = (source) => {
1556
+ const entry = { source, subject: new ReplaySubject(1), hasValue: false, last: undefined };
1557
+ if (destroyed || destroyRef.destroyed) {
1558
+ push(entry);
1559
+ entry.subject.complete();
1560
+ return entry;
1561
+ }
1562
+ startWatching();
1563
+ watched.push(entry);
1564
+ push(entry);
1565
+ registrations.update((count) => count + 1);
1566
+ return entry;
1567
+ };
1568
+ const wrap = (source) => {
1569
+ let default$ = null;
1570
+ const overrides = new WeakMap();
1571
+ const getDefault = () => {
1572
+ if (default$)
1573
+ return default$;
1574
+ const entry = untracked(() => watch(source));
1575
+ default$ = new Observable((subscriber) => {
1576
+ untracked(() => push(entry));
1577
+ return entry.subject.subscribe(subscriber);
1578
+ });
1162
1579
  return default$;
1163
- const existing = overrides.get(injector);
1164
- if (existing)
1165
- return existing;
1166
- const override$ = untracked(() => toObservable(source, { injector }).pipe(takeUntilDestroyed(defaultInjector.get(DestroyRef))));
1167
- overrides.set(injector, override$);
1168
- return override$;
1580
+ };
1581
+ const asObservable = (options) => {
1582
+ const overrideInjector = options?.injector;
1583
+ if (!overrideInjector)
1584
+ return getDefault();
1585
+ const existing = overrides.get(overrideInjector);
1586
+ if (existing)
1587
+ return existing;
1588
+ const override$ = getDefault().pipe(takeUntilDestroyed(overrideInjector.get(DestroyRef)));
1589
+ overrides.set(overrideInjector, override$);
1590
+ return override$;
1591
+ };
1592
+ return Object.assign(computed(() => source()), { asObservable });
1169
1593
  };
1170
- return Object.assign(computed(() => source()), { asObservable });
1594
+ return { wrap };
1595
+ };
1596
+
1597
+ // Angular has no public check for an injection context; `inject()` outside one throws NG0203 (`code: -203`).
1598
+ const MISSING_INJECTION_CONTEXT = -203;
1599
+ const queryCreatedOutsideInjectionContext = (kind) => {
1600
+ const fix = kind === 'query'
1601
+ ? 'or pass an injector as the first creator argument: getPost({ injector: this.injector }, withArgs(...))'
1602
+ : 'or wrap the call in runInInjectionContext(this.injector, () => ...)';
1603
+ return new RuntimeError(QueryRuntimeErrorCode.QUERY_CREATED_OUTSIDE_INJECTION_CONTEXT, `A ${kind} was created outside an injection context (in ngOnInit, an event handler or a timer). Create it in a field initializer or constructor, ${fix}.`);
1604
+ };
1605
+ const injectInQueryContext = (token, kind) => {
1606
+ try {
1607
+ return inject(token);
1608
+ }
1609
+ catch (error) {
1610
+ if (error?.code === MISSING_INJECTION_CONTEXT) {
1611
+ throw queryCreatedOutsideInjectionContext(kind);
1612
+ }
1613
+ throw error;
1614
+ }
1171
1615
  };
1172
1616
 
1173
1617
  const QUERY_CONTEXT_TOKEN = new InjectionToken('QUERY_CONTEXT');
@@ -1181,12 +1625,12 @@ const injectQueryContext = () => inject(QUERY_CONTEXT_TOKEN);
1181
1625
 
1182
1626
  /** @internal */
1183
1627
  const setupQueryDependencies = (options) => {
1184
- const hostInjector = options.queryConfig?.injector ?? inject(Injector);
1628
+ const hostInjector = options.queryConfig?.injector ?? injectInQueryContext(Injector, 'query');
1185
1629
  const environmentInjector = options.queryConfig?.injector?.get(EnvironmentInjector) ?? hostInjector.get(EnvironmentInjector);
1186
1630
  const clientToken = options.client.token;
1187
1631
  const dependencies = {
1188
1632
  destroyRef: undefined, // Will be set after injector creation
1189
- scopeDestroyRef: hostInjector.get(DestroyRef),
1633
+ scopeDestroyRef: options.queryConfig?.scopeDestroyRef ?? hostInjector.get(DestroyRef),
1190
1634
  client: hostInjector.get(clientToken),
1191
1635
  injector: undefined, // Will be set after injector creation
1192
1636
  ngErrorHandler: hostInjector.get(ErrorHandler),
@@ -1223,165 +1667,6 @@ const setupQueryDependencies = (options) => {
1223
1667
  return dependencies;
1224
1668
  };
1225
1669
 
1226
- // codes 0-999
1227
- const QueryRuntimeErrorCode = {
1228
- // Query
1229
- QUERY_FEATURE_USED_MULTIPLE_TIMES: 0,
1230
- QUERY_CREATED_IN_REACTIVE_CONTEXT: 1,
1231
- // Query features
1232
- WITH_ARGS_QUERY_FEATURE_MISSING_BUT_ROUTE_IS_FUNCTION: 100,
1233
- WITH_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 101,
1234
- WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
1235
- WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
1236
- SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT: 104,
1237
- WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 105,
1238
- WITH_LONG_POLLING_USED_WITH_POLLING: 106,
1239
- // Auth provider
1240
- AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
1241
- AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
1242
- AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_REFRESH_TOKEN: 202,
1243
- AUTH_PROVIDER_FEATURE_USED_MULTIPLE_TIMES: 203,
1244
- // Query client
1245
- QUERY_CLIENT_FEATURE_USED_MULTIPLE_TIMES: 250,
1246
- // Query Repository
1247
- UNCACHEABLE_REQUEST_HAS_CACHE_KEY_PARAM: 300,
1248
- UNCACHEABLE_REQUEST_HAS_ALLOW_CACHE_PARAM: 301,
1249
- // Paged Query Stack
1250
- PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES: 400,
1251
- PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE: 401,
1252
- PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE: 402,
1253
- // Query Stack
1254
- QUERY_STACK_WITH_ARGS_USED: 500,
1255
- QUERY_STACK_WITH_RESPONSE_UPDATE_USED: 501,
1256
- // GQL
1257
- GQL_DATA_PROPERTY_MISSING_IN_RESPONSE: 600,
1258
- GQL_ERRORS_IN_RESPONSE: 601,
1259
- // Secure Execute
1260
- TOKENS_NOT_AVAILABLE_INSIDE_AUTH_AND_EXEC: 700,
1261
- INVALID_STATE_INSIDE_SECURE_EXECUTE_FACTORY: 701,
1262
- // Query Execution
1263
- CIRCULAR_QUERY_DEPENDENCY: 800,
1264
- // Query Sequence
1265
- QUERY_SEQUENCE_ALREADY_RUNNING: 900,
1266
- // Query Batch
1267
- QUERY_BATCH_ALREADY_RUNNING: 910,
1268
- QUERY_BATCH_WITH_ARGS_USED: 911,
1269
- // Legacy interop
1270
- LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT: 950,
1271
- LEGACY_QUERY_STATE_ALREADY_HAS_KEY: 951,
1272
- LEGACY_QUERY_STATE_DOES_NOT_CONTAIN_KEY: 952,
1273
- LEGACY_QUERY_STATE_CANNOT_TRANSFORM: 953,
1274
- LEGACY_INVALID_BODY: 954,
1275
- };
1276
- const queryFeatureUsedMultipleTimes = (type) => {
1277
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_FEATURE_USED_MULTIPLE_TIMES, `The query feature "${type}()" was used multiple times.`);
1278
- };
1279
- const queryCreatedInReactiveContext = () => {
1280
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_CREATED_IN_REACTIVE_CONTEXT, `A query was created inside a reactive context (a computed, an effect or a template). Every re-run would create another query that lives until its injector is destroyed. Create the query in a field initializer or constructor and change its args instead.`);
1281
- };
1282
- const withArgsQueryFeatureMissingButRouteIsFunction = () => {
1283
- return new RuntimeError(QueryRuntimeErrorCode.WITH_ARGS_QUERY_FEATURE_MISSING_BUT_ROUTE_IS_FUNCTION, `This queries route is a function, but a "withArgs()" feature is missing. Mutations need one too: create the query with "withArgs(() => ({ body: form.value() }))", then call ".execute()". The "silenceMissingWithArgsFeatureError" config is an escape hatch for args that only exist at call time.`);
1284
- };
1285
- const withPollingUsedOnUnsupportedHttpMethod = (method) => {
1286
- return new RuntimeError(QueryRuntimeErrorCode.WITH_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1287
- };
1288
- const withAutoRefreshUsedOnUnsupportedHttpMethod = (method) => {
1289
- return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withAutoRefresh()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1290
- };
1291
- const withAutoRefreshUsedInManualQuery = () => {
1292
- return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY, `"withAutoRefresh()" has been used inside a query that has "onlyManualExecution" set to true.` +
1293
- ` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
1294
- };
1295
- const withLongPollingUsedOnUnsupportedHttpMethod = (method) => {
1296
- return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withLongPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
1297
- };
1298
- const withLongPollingUsedWithPolling = () => {
1299
- return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_WITH_POLLING, `"withLongPolling()" and "withPolling()" cannot be used on the same query - both would drive its re-execution.` +
1300
- ` Keep the long polling feature and drop the interval, or the other way around.`);
1301
- };
1302
- const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
1303
- return new RuntimeError(QueryRuntimeErrorCode.SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT, `The "silenceMissingWithArgsFeatureError" config is set to true, but a "withArgs()" feature is present.`);
1304
- };
1305
- const authExtractTokensResponseNotObject = () => {
1306
- return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT, `The auth token extractor expects the response to be an object.`);
1307
- };
1308
- const authExtractTokensResponseMissingAccessToken = () => {
1309
- return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN, `The auth token extractor expects the response to contain a string "accessToken" property.`);
1310
- };
1311
- const authExtractTokensResponseMissingRefreshToken = () => {
1312
- return new RuntimeError(QueryRuntimeErrorCode.AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_REFRESH_TOKEN, `The auth token extractor expects the response to contain a string "refreshToken" property.`);
1313
- };
1314
- const authProviderFeatureUsedMultipleTimes = (type) => {
1315
- return new RuntimeError(QueryRuntimeErrorCode.AUTH_PROVIDER_FEATURE_USED_MULTIPLE_TIMES, `Bearer auth feature "${type}" was used multiple times.`);
1316
- };
1317
- const queryClientFeatureUsedMultipleTimes = (type) => {
1318
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_CLIENT_FEATURE_USED_MULTIPLE_TIMES, `Query client feature "${type}" was used multiple times.`);
1319
- };
1320
- const uncacheableRequestHasCacheKeyParam = (key) => {
1321
- return new RuntimeError(QueryRuntimeErrorCode.UNCACHEABLE_REQUEST_HAS_CACHE_KEY_PARAM, `This request is uncacheable, but a cache key was provided: ${key}. Please remove it.`);
1322
- };
1323
- const uncacheableRequestHasAllowCacheParam = () => {
1324
- return new RuntimeError(QueryRuntimeErrorCode.UNCACHEABLE_REQUEST_HAS_ALLOW_CACHE_PARAM, `This request is uncacheable, but allowCache is set to true. Please remove it.`);
1325
- };
1326
- const pagedQueryStackPageBiggerThanTotalPages = (page, totalPages) => {
1327
- return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_PAGE_BIGGER_THAN_TOTAL_PAGES, `The page "${page}" is bigger than the total pages "${totalPages}".`);
1328
- };
1329
- const pagedQueryStackNextPageCalledWithoutPreviousPage = () => {
1330
- return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_NEXT_PAGE_CALLED_WITHOUT_PREVIOUS_PAGE, `fetchNextPage() has been called but the current page is not yet loaded. Please call it after the previous page has been loaded.`);
1331
- };
1332
- const pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage = () => {
1333
- return new RuntimeError(QueryRuntimeErrorCode.PAGED_QUERY_STACK_PREVIOUS_PAGE_CALLED_BUT_ALREADY_AT_FIRST_PAGE, `fetchPreviousPage() has been called but the current page is already the first page.`);
1334
- };
1335
- const queryStackWithArgsUsed = () => {
1336
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_STACK_WITH_ARGS_USED, `withArgs() has been used in a query stack or a paged query stack. This is not supported.`);
1337
- };
1338
- const queryStackWithResponseUpdateUsed = () => {
1339
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_STACK_WITH_RESPONSE_UPDATE_USED, `withResponseUpdate() has been used in a query stack or a paged query stack. This is not supported.`);
1340
- };
1341
- const gqlDataPropertyMissingInResponse = () => {
1342
- return new RuntimeError(QueryRuntimeErrorCode.GQL_DATA_PROPERTY_MISSING_IN_RESPONSE, `The GraphQL response is missing the required "data" property. Please add a custom transformResponse param to the query creator to handle this response format.`);
1343
- };
1344
- const gqlErrorsInResponse = (errors) => {
1345
- return new HttpErrorResponse({
1346
- error: errors,
1347
- status: 0,
1348
- statusText: new RuntimeError(QueryRuntimeErrorCode.GQL_ERRORS_IN_RESPONSE, `The GraphQL response carries errors and no data. The server's errors are in raw.error.`).message,
1349
- });
1350
- };
1351
- const tokensNotAvailableInsideAuthAndExec = () => {
1352
- return new RuntimeError(QueryRuntimeErrorCode.TOKENS_NOT_AVAILABLE_INSIDE_AUTH_AND_EXEC, `Tokens are not available inside authAndExec.`);
1353
- };
1354
- const invalidStateInsideSecureExecuteFactory = () => {
1355
- return new RuntimeError(QueryRuntimeErrorCode.INVALID_STATE_INSIDE_SECURE_EXECUTE_FACTORY, `An invalid state occurred inside the secure execute factory.`);
1356
- };
1357
- const circularQueryDependency = () => {
1358
- return new RuntimeError(QueryRuntimeErrorCode.CIRCULAR_QUERY_DEPENDENCY, `The same query was executed with identical args more than 5 times in a row, less than 100ms apart. This is usually a sign of a circular dependency.`);
1359
- };
1360
- const queryExecutedAfterDestroyMessage = (route) => {
1361
- const target = typeof route === 'string' ? `The query for "${route}"` : 'A query with a function route';
1362
- return `${target} was executed after the scope that created it was destroyed. The call was ignored - drop the reference to the query when its component or injector goes away.`;
1363
- };
1364
- const legacyPrepareWithoutInjectionContext = (creatorName, method = 'prepare') => {
1365
- const label = creatorName ? `"${creatorName}"` : 'A legacy query creator';
1366
- const example = method === 'prepare'
1367
- ? ` search = (term: string) => myQuery.prepare({ queryParams: { term }, injector: this.injector });`
1368
- : ` container = myQuery.${method}(null, { injector: this.injector });`;
1369
- 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` +
1370
- `Capture one where a context does exist (field initializer, constructor) and pass it along:\n\n` +
1371
- ` private injector = inject(Injector);\n` +
1372
- `${example}\n\n` +
1373
- `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.`);
1374
- };
1375
- const querySequenceAlreadyRunning = () => {
1376
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_SEQUENCE_ALREADY_RUNNING, `run() was called on a query sequence that is already running. Wait for the current run to settle before starting another.`);
1377
- };
1378
- const queryBatchAlreadyRunning = () => {
1379
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_ALREADY_RUNNING, `run() or retryFailed() was called on a query batch that is already running. Await the current run, or call cancel() first.`);
1380
- };
1381
- const queryBatchWithArgsUsed = () => {
1382
- return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_WITH_ARGS_USED, `The "withArgs()" feature cannot be used inside a query batch. Each item's args come from the batch's "args" option.`);
1383
- };
1384
-
1385
1670
  const isClassValidatorError = (error) => {
1386
1671
  return (typeof error === 'object' &&
1387
1672
  error !== null &&
@@ -1448,6 +1733,7 @@ const QueryFeatureType = {
1448
1733
  WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
1449
1734
  WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
1450
1735
  WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
1736
+ WITH_OPTIMISTIC_UPDATE: 'WITH_OPTIMISTIC_UPDATE',
1451
1737
  };
1452
1738
  const createQueryFeature = (config) => {
1453
1739
  return config;
@@ -1457,7 +1743,9 @@ const createQueryFeature = (config) => {
1457
1743
  * The arguments are read within a computed function, so you can use reactive values.
1458
1744
  *
1459
1745
  * Return `null` to park the query: it is reset (`response()` and `executionState()` become `null`),
1460
- * and polling and auto refresh pause until args are set again.
1746
+ * and polling and auto refresh pause until args are set again. A mutation that is still in flight
1747
+ * finishes first and the query parks once it has settled. `execute({ args })` runs and keeps other args
1748
+ * until this function produces new non-null ones.
1461
1749
  *
1462
1750
  * Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
1463
1751
  */
@@ -1474,6 +1762,8 @@ const withArgs = (args) => {
1474
1762
  nestedEffect(() => {
1475
1763
  const currArgsNow = context.state.args();
1476
1764
  if (currArgsNow === null) {
1765
+ if (!context.flags.shouldAutoExecuteMethod && context.state.loading() !== null)
1766
+ return;
1477
1767
  untracked(() => {
1478
1768
  if (context.state.subtle.request() !== null || context.state.loading() !== null)
1479
1769
  context.execute.reset();
@@ -1481,6 +1771,8 @@ const withArgs = (args) => {
1481
1771
  return;
1482
1772
  }
1483
1773
  untracked(() => {
1774
+ if (context.state.subtle.isExplicitArgs(currArgsNow))
1775
+ return;
1484
1776
  const guard = context.state.subtle.autoExecuteGuard();
1485
1777
  if (context.flags.shouldAutoExecute && (guard === null || guard()))
1486
1778
  context.execute({ args: currArgsNow });
@@ -1590,9 +1882,9 @@ const withPolling = (options) => {
1590
1882
  }, Math.max(0, firstDelay));
1591
1883
  };
1592
1884
  const remainingDelay = () => lastTickAt + currentInterval() - Date.now();
1593
- // Without an args source, `state.args` only changes when `execute({ args })` records them - and
1594
- // that execution already sent the request.
1595
- const wasJustExecutedWith = (args) => !context.state.subtle.hasArgsSource() && args !== null && equal(context.state.subtle.request()?.args, args);
1885
+ // Args an `execute({ args })` recorded already went out with that execution.
1886
+ const wasJustExecutedWith = (args) => context.state.subtle.isExplicitArgs(args) ||
1887
+ (!context.state.subtle.hasArgsSource() && args !== null && equal(context.state.subtle.request()?.args, args));
1596
1888
  nestedEffect(() => {
1597
1889
  const args = context.state.args();
1598
1890
  untracked(() => {
@@ -1895,7 +2187,7 @@ const withResponseUpdate = (options) => {
1895
2187
  if (response === null)
1896
2188
  return;
1897
2189
  untracked(() => {
1898
- context.state.rawResponse.set(response);
2190
+ context.state.response.set(response);
1899
2191
  });
1900
2192
  }, { injector: context.deps.injector });
1901
2193
  },
@@ -1960,6 +2252,62 @@ const withPageResetOnError = (options) => {
1960
2252
  });
1961
2253
  };
1962
2254
 
2255
+ /**
2256
+ * Resolves an {@link QueryInvalidationOptions.url} to the absolute form request URLs are built in, so
2257
+ * the comparison - and the message the other tabs receive - never depends on who resolves it.
2258
+ */
2259
+ const resolveInvalidationUrl = (baseUrl, url) => {
2260
+ const absolute = url.startsWith('/') ? `${baseUrl}${url}` : url;
2261
+ // A trailing slash would put the boundary check below one character past where the URL actually
2262
+ // ends, which is the one way `/players/` could fail to match `/players/1`.
2263
+ return absolute.endsWith('/') ? absolute.slice(0, -1) : absolute;
2264
+ };
2265
+ /**
2266
+ * Whether a query URL is the invalidated one or sits below it. The check on what follows the prefix
2267
+ * is what keeps `/players` from matching `/players-archive`, which a `startsWith` alone would.
2268
+ */
2269
+ const isUnderInvalidatedUrl = (queryUrl, invalidatedUrl) => {
2270
+ if (!queryUrl.startsWith(invalidatedUrl))
2271
+ return false;
2272
+ const rest = queryUrl.slice(invalidatedUrl.length);
2273
+ return rest === '' || rest.startsWith('/') || rest.startsWith('?') || rest.startsWith('#');
2274
+ };
2275
+ /**
2276
+ * Turns an invalidation into the filter the repository refreshes by, or `undefined` when nothing
2277
+ * narrows it - "everything in use" is the repository's own cheapest path.
2278
+ */
2279
+ const createQueryInvalidationFilter = (options) => {
2280
+ const { url, tag, filter } = options;
2281
+ if (!url && !tag && !filter)
2282
+ return undefined;
2283
+ return (request, tags) => {
2284
+ if (url && !isUnderInvalidatedUrl(request.url, url))
2285
+ return false;
2286
+ if (tag && !tags?.includes(tag))
2287
+ return false;
2288
+ return filter ? filter({ method: request.method, url: request.url }) : true;
2289
+ };
2290
+ };
2291
+ /** @internal */
2292
+ const resolveQueryTags = (tags, args) => (typeof tags === 'function' ? tags({ args: args ?? {} }) : tags);
2293
+ /** @internal */
2294
+ const invalidateOnSuccess = (options) => {
2295
+ const { invalidates, state, deps } = options;
2296
+ const subscription = state.events$.subscribe((event) => {
2297
+ if (event.type !== HttpEventType.Response)
2298
+ return;
2299
+ const targets = typeof invalidates === 'function'
2300
+ ? invalidates({
2301
+ args: state.subtle.request()?.args ?? {},
2302
+ response: state.response(),
2303
+ })
2304
+ : invalidates;
2305
+ for (const target of targets)
2306
+ deps.client.invalidateQueries(target);
2307
+ });
2308
+ deps.destroyRef.onDestroy(() => subscription.unsubscribe());
2309
+ };
2310
+
1963
2311
  const parsers = [];
1964
2312
  /**
1965
2313
  * Adds a parser to the pipeline `createQueryErrorResponse` runs, ahead of the built-in ladder.
@@ -2016,7 +2364,11 @@ const createDefaultRetryFn = (options = {}) => {
2016
2364
  const jitter = clamp(options.jitter ?? DEFAULT_JITTER, 0, 1);
2017
2365
  const retryableStatusCodes = options.retryableStatusCodes;
2018
2366
  const retryNonIdempotent = options.retryNonIdempotent ?? false;
2019
- const isRetryableStatus = (status) => retryableStatusCodes ? retryableStatusCodes.includes(status) : isRetryableByDefault(status);
2367
+ const isRetryableStatus = (status) => {
2368
+ if (typeof retryableStatusCodes === 'function')
2369
+ return retryableStatusCodes(status, isRetryableByDefault(status));
2370
+ return retryableStatusCodes ? retryableStatusCodes.includes(status) : isRetryableByDefault(status);
2371
+ };
2020
2372
  const backoff = (retryCount) => {
2021
2373
  const exponential = baseDelayMs * Math.pow(2, retryCount);
2022
2374
  const spread = jitter === 0 ? 1 : 1 + (Math.random() * 2 - 1) * jitter;
@@ -2066,6 +2418,7 @@ const isQueryErrorResponse = (error) => {
2066
2418
  /**
2067
2419
  * Every message carried by an error, flattened into a plain list - one entry for a single error,
2068
2420
  * one per violation for a list (a form response with several field violations), empty for `null`.
2421
+ * Blank messages are dropped; an error left with none yields the raw response's message instead.
2069
2422
  *
2070
2423
  * The single/list split exists because that is how APIs answer; UI almost never wants to branch on
2071
2424
  * it. Reach for this when rendering, and keep {@link QueryErrorResponse} for anything that needs
@@ -2081,7 +2434,9 @@ const isQueryErrorResponse = (error) => {
2081
2434
  const queryErrorMessages = (error) => {
2082
2435
  if (!error)
2083
2436
  return [];
2084
- return error.isList ? error.errors.map((item) => item.message) : [error.error.message];
2437
+ const items = error.isList ? error.errors : [error.error];
2438
+ const messages = items.map((item) => item.message).filter((message) => message.trim());
2439
+ return messages.length ? messages : [error.raw.message];
2085
2440
  };
2086
2441
  /**
2087
2442
  * The first message of an error, or `null` when there is none - the single-line counterpart to
@@ -2210,14 +2565,24 @@ const setupQueryState = (options) => {
2210
2565
  // set is overridden again the next time the source's dependencies change.
2211
2566
  const argsSource = signal(() => null, /* @ts-ignore */
2212
2567
  ...(ngDevMode ? [{ debugName: "argsSource" }] : /* istanbul ignore next */ []));
2213
- const args = linkedSignal(() => argsSource()(), /* @ts-ignore */
2214
- ...(ngDevMode ? [{ debugName: "args" }] : /* istanbul ignore next */ []));
2568
+ let explicitArgs = null;
2569
+ const args = linkedSignal({ ...(ngDevMode ? { debugName: "args" } : /* istanbul ignore next */ {}), source: () => argsSource()(),
2570
+ computation: (next, previous) => {
2571
+ if (next === null && explicitArgs !== null && previous)
2572
+ return previous.value;
2573
+ explicitArgs = null;
2574
+ return next;
2575
+ } });
2215
2576
  const hasArgsSource = signal(false, /* @ts-ignore */
2216
2577
  ...(ngDevMode ? [{ debugName: "hasArgsSource" }] : /* istanbul ignore next */ []));
2217
2578
  const setArgsSource = (source) => {
2218
2579
  argsSource.set(source);
2219
2580
  hasArgsSource.set(true);
2220
2581
  };
2582
+ const setExplicitArgs = (value) => {
2583
+ explicitArgs = value;
2584
+ args.set(value);
2585
+ };
2221
2586
  const lastTimeExecutedAt = signal(null, /* @ts-ignore */
2222
2587
  ...(ngDevMode ? [{ debugName: "lastTimeExecutedAt" }] : /* istanbul ignore next */ []));
2223
2588
  const lastTriggeredBy = signal(null, /* @ts-ignore */
@@ -2236,6 +2601,8 @@ const setupQueryState = (options) => {
2236
2601
  ...(ngDevMode ? [{ debugName: "defaultRunOptions" }] : /* istanbul ignore next */ []));
2237
2602
  const autoExecuteGuard = signal(null, /* @ts-ignore */
2238
2603
  ...(ngDevMode ? [{ debugName: "autoExecuteGuard" }] : /* istanbul ignore next */ []));
2604
+ const beforeExecute = signal(null, /* @ts-ignore */
2605
+ ...(ngDevMode ? [{ debugName: "beforeExecute" }] : /* istanbul ignore next */ []));
2239
2606
  options.destroyRef?.onDestroy(() => {
2240
2607
  requestEventsSubscription.unsubscribe();
2241
2608
  requestEvents$.complete();
@@ -2306,8 +2673,11 @@ const setupQueryState = (options) => {
2306
2673
  unbindRequestEvents,
2307
2674
  defaultRunOptions,
2308
2675
  autoExecuteGuard,
2676
+ beforeExecute,
2309
2677
  setArgsSource,
2310
2678
  hasArgsSource: hasArgsSource.asReadonly(),
2679
+ setExplicitArgs,
2680
+ isExplicitArgs: (value) => value !== null && value === explicitArgs,
2311
2681
  devtoolsStats: options.devtoolsStats ?? null,
2312
2682
  devtoolsFormLinks: options.devtoolsFormLinks ?? null,
2313
2683
  },
@@ -2406,17 +2776,18 @@ const createQuerySnapshotFn = (options) => {
2406
2776
  // snapshot has to settle itself - otherwise `executeUntilSettled` resolves with a snapshot still
2407
2777
  // reporting the execution as loading.
2408
2778
  unregisterScopeListener = context.deps.destroyRef.onDestroy(cancel);
2779
+ const watcher = createObservableSignalWatcher(context.deps.injector);
2409
2780
  const snapshot = {
2410
- args: wrapAsObservableSignal(snapshotState.args.asReadonly(), context.deps.injector),
2411
- response: wrapAsObservableSignal(snapshotState.response, context.deps.injector),
2412
- latestHttpEvent: wrapAsObservableSignal(snapshotState.latestHttpEvent.asReadonly(), context.deps.injector),
2413
- loading: wrapAsObservableSignal(snapshotState.loading.asReadonly(), context.deps.injector),
2414
- error: wrapAsObservableSignal(snapshotState.error.asReadonly(), context.deps.injector),
2415
- lastTimeExecutedAt: wrapAsObservableSignal(snapshotState.lastTimeExecutedAt.asReadonly(), context.deps.injector),
2416
- triggeredBy: wrapAsObservableSignal(snapshotState.lastTriggeredBy.asReadonly(), context.deps.injector),
2417
- isAlive: wrapAsObservableSignal(isAlive.asReadonly(), context.deps.injector),
2418
- id: wrapAsObservableSignal(options.execute.currentRepositoryKey, context.deps.injector),
2419
- executionState: wrapAsObservableSignal(snapshotState.executionState, context.deps.injector),
2781
+ args: watcher.wrap(snapshotState.args.asReadonly()),
2782
+ response: watcher.wrap(snapshotState.response),
2783
+ latestHttpEvent: watcher.wrap(snapshotState.latestHttpEvent.asReadonly()),
2784
+ loading: watcher.wrap(snapshotState.loading.asReadonly()),
2785
+ error: watcher.wrap(snapshotState.error.asReadonly()),
2786
+ lastTimeExecutedAt: watcher.wrap(snapshotState.lastTimeExecutedAt.asReadonly()),
2787
+ triggeredBy: watcher.wrap(snapshotState.lastTriggeredBy.asReadonly()),
2788
+ isAlive: watcher.wrap(isAlive.asReadonly()),
2789
+ id: watcher.wrap(options.execute.currentRepositoryKey),
2790
+ executionState: watcher.wrap(snapshotState.executionState),
2420
2791
  };
2421
2792
  return snapshot;
2422
2793
  });
@@ -2495,22 +2866,27 @@ const trackRefreshedExecutions = (options) => {
2495
2866
  };
2496
2867
  const createQueryObject = (options) => {
2497
2868
  const { state, execute, deps } = options;
2498
- const destroy = () => deps.injector.destroy();
2869
+ const destroy = () => {
2870
+ if (!deps.injector.destroyed)
2871
+ deps.injector.destroy();
2872
+ };
2499
2873
  const setResponse = (response) => state.response.set(response);
2500
2874
  const setLoading = (loading) => state.loading.set(loading);
2501
2875
  const setError = (error) => state.error.set(error);
2502
2876
  const createSnapshot = createQuerySnapshotFn({ state, execute, deps });
2503
2877
  // Pre-wrap all public signals once so both `query` and `roQuery` share the same instances.
2504
- const wrappedArgs = wrapAsObservableSignal(state.args.asReadonly(), deps.injector);
2505
- const wrappedResponse = wrapAsObservableSignal(state.response, deps.injector);
2506
- const wrappedLatestHttpEvent = wrapAsObservableSignal(state.latestHttpEvent.asReadonly(), deps.injector);
2507
- const wrappedLoading = wrapAsObservableSignal(state.loading.asReadonly(), deps.injector);
2508
- const wrappedError = wrapAsObservableSignal(state.error.asReadonly(), deps.injector);
2509
- const wrappedLastTimeExecutedAt = wrapAsObservableSignal(state.lastTimeExecutedAt.asReadonly(), deps.injector);
2510
- const wrappedTriggeredBy = wrapAsObservableSignal(state.lastTriggeredBy.asReadonly(), deps.injector);
2511
- const wrappedId = wrapAsObservableSignal(execute.currentRepositoryKey, deps.injector);
2512
- const wrappedExecutionState = wrapAsObservableSignal(state.executionState, deps.injector);
2513
- const wrappedRequest = wrapAsObservableSignal(state.subtle.request.asReadonly(), deps.injector);
2878
+ const watcher = createObservableSignalWatcher(deps.injector);
2879
+ const wrappedArgs = watcher.wrap(state.args.asReadonly());
2880
+ const wrappedResponse = watcher.wrap(state.response);
2881
+ const wrappedLatestHttpEvent = watcher.wrap(state.latestHttpEvent.asReadonly());
2882
+ const wrappedLoading = watcher.wrap(state.loading.asReadonly());
2883
+ const wrappedError = watcher.wrap(state.error.asReadonly());
2884
+ const wrappedLastTimeExecutedAt = watcher.wrap(state.lastTimeExecutedAt.asReadonly());
2885
+ const wrappedTriggeredBy = watcher.wrap(state.lastTriggeredBy.asReadonly());
2886
+ const wrappedId = watcher.wrap(execute.currentRepositoryKey);
2887
+ const wrappedExecutionState = watcher.wrap(state.executionState);
2888
+ const wrappedRequest = watcher.wrap(state.subtle.request.asReadonly());
2889
+ const wrappedUrl = watcher.wrap(computed(() => state.subtle.request()?.url ?? null));
2514
2890
  const asReadonly = () => {
2515
2891
  const roQuery = {
2516
2892
  args: wrappedArgs,
@@ -2522,6 +2898,7 @@ const createQueryObject = (options) => {
2522
2898
  triggeredBy: wrappedTriggeredBy,
2523
2899
  id: wrappedId,
2524
2900
  createSnapshot,
2901
+ url: wrappedUrl,
2525
2902
  executionState: wrappedExecutionState,
2526
2903
  };
2527
2904
  return roQuery;
@@ -2537,6 +2914,7 @@ const createQueryObject = (options) => {
2537
2914
  triggeredBy: wrappedTriggeredBy,
2538
2915
  id: wrappedId,
2539
2916
  createSnapshot,
2917
+ url: wrappedUrl,
2540
2918
  reset: execute.reset,
2541
2919
  abort: execute.abort,
2542
2920
  asReadonly,
@@ -2587,6 +2965,12 @@ const createBaseQuery = (options) => {
2587
2965
  deps,
2588
2966
  };
2589
2967
  applyQueryFeatures(options.features, featureFnContext);
2968
+ const invalidates = options.creator?.invalidates;
2969
+ if (invalidates) {
2970
+ if (flags.shouldAutoExecuteMethod)
2971
+ throw invalidatesUsedOnRead(flags.method);
2972
+ invalidateOnSuccess({ invalidates, state, deps });
2973
+ }
2590
2974
  maybeExecute({ execute, flags, state });
2591
2975
  trackRefreshedExecutions({ state, deps });
2592
2976
  const query = createQueryObject({ state, execute, deps });
@@ -2698,7 +3082,7 @@ const extractExpiresInSeconds = (headers) => {
2698
3082
  if (cacheControl && /(?:^|,)\s*no-(?:cache|store)(?:\s|,|$)/i.test(cacheControl)) {
2699
3083
  return null;
2700
3084
  }
2701
- const maxAgeMatch = cacheControl?.match(/(?:^|,)\s*(?:s-maxage|max-age)=(\d+)/i);
3085
+ const maxAgeMatch = cacheControl?.match(/(?:^|,)\s*max-age=(\d+)/i) ?? cacheControl?.match(/(?:^|,)\s*s-maxage=(\d+)/i);
2702
3086
  if (maxAgeMatch?.[1]) {
2703
3087
  maxAge = parseInt(maxAgeMatch[1]);
2704
3088
  }
@@ -2794,6 +3178,7 @@ const queryExecute = (options) => {
2794
3178
  }
2795
3179
  const defaultRunOptions = state.subtle.defaultRunOptions();
2796
3180
  const runQueryOptions = defaultRunOptions || options.options ? { ...defaultRunOptions, ...options.options } : undefined;
3181
+ const onRequest = state.subtle.beforeExecute()?.(args);
2797
3182
  const { key, request, executed } = deps.client.repository.request({
2798
3183
  route: creatorInternals.route,
2799
3184
  method: creatorInternals.method,
@@ -2807,25 +3192,44 @@ const queryExecute = (options) => {
2807
3192
  isSecure,
2808
3193
  isRefreshable,
2809
3194
  silenceUncacheableAllowCacheError: queryConfig.silenceUncacheableAllowCacheError,
3195
+ tags: creator?.tags ? resolveQueryTags(creator.tags, args) : undefined,
2810
3196
  });
2811
3197
  executeState.previousKey.set(key);
2812
3198
  state.lastTimeExecutedAt.set(Date.now());
2813
3199
  state.lastTriggeredBy.set(runQueryOptions?.triggeredBy ?? null);
2814
3200
  state.subtle.request.set(request);
3201
+ onRequest?.(request);
2815
3202
  state.subtle.bindRequestEvents(request);
2816
3203
  state.subtle.devtoolsStats?.recordExecution({ didRequest: executed, body: args?.body, url: request.url });
2817
3204
  };
3205
+ /**
3206
+ * A query over a `withArgs` source that returns `null` is parked. Running it would send `null` args, so the
3207
+ * call is dropped with a dev-mode warning.
3208
+ */
3209
+ const skipParkedExecution = (state, args, route) => {
3210
+ if ((args !== null && args !== undefined) || !state.subtle.hasArgsSource())
3211
+ return false;
3212
+ if (isDevMode())
3213
+ console.warn(queryExecutedWhileParkedMessage(route));
3214
+ return true;
3215
+ };
2818
3216
  /**
2819
3217
  * Writes the args an execution runs with to `state.args`, so a later bare `execute()` re-sends them.
2820
- * A `withArgs` source owns `state.args`, and a `withLongPolling` round must not look like a new
2821
- * args value, so neither is written.
3218
+ * A `withLongPolling` round must not look like a new args value, so it is not written. Over a `withArgs`
3219
+ * source only the caller's own non-null args are written, never what a feature re-sends.
2822
3220
  */
2823
3221
  const recordExecutionArgs = (state, args, options) => {
2824
- if (state.subtle.hasArgsSource() || options?.triggeredBy === LONG_POLLING_TRIGGER)
3222
+ const triggeredBy = options?.triggeredBy;
3223
+ if (triggeredBy === LONG_POLLING_TRIGGER)
3224
+ return;
3225
+ if (!state.subtle.hasArgsSource())
3226
+ return state.args.set(args);
3227
+ if (args === null || args === state.args() || (triggeredBy && FEATURE_TRIGGERS.includes(triggeredBy)))
2825
3228
  return;
2826
- state.args.set(args);
3229
+ state.subtle.setExplicitArgs(args);
2827
3230
  };
2828
3231
  const LONG_POLLING_TRIGGER = 'long-polling';
3232
+ const FEATURE_TRIGGERS = ['polling', 'auto-refresh'];
2829
3233
  const CIRCULAR_DEPENDENCY_WINDOW_MS = 100;
2830
3234
  const CIRCULAR_DEPENDENCY_MAX_REPEATS = 5;
2831
3235
  /**
@@ -2862,6 +3266,8 @@ const createExecuteFn = (executeOptions) => {
2862
3266
  const reset = () => untracked(() => resetExecuteState({ executeState, executeOptions }));
2863
3267
  const exec = (executeArgs) => untracked(() => {
2864
3268
  const { args = executeOptions.state.args(), options } = executeArgs ?? {};
3269
+ if (skipParkedExecution(executeOptions.state, args, executeOptions.creatorInternals.route))
3270
+ return;
2865
3271
  circularChecker.check(args);
2866
3272
  aborter.capture();
2867
3273
  recordExecutionArgs(executeOptions.state, args, options);
@@ -2943,153 +3349,6 @@ const createBaseQueryCreator = (config) => {
2943
3349
  return queryCreator;
2944
3350
  };
2945
3351
 
2946
- /**
2947
- * Route building, query-string serialization and the token clock - shared by the current generation and
2948
- * the legacy V2 client, which re-exports them under their original names.
2949
- */
2950
- // codes 1-99 of the shared 0-999 `@ethlete/query` range - see `query-errors.ts` for the rest
2951
- const RouteRuntimeErrorCode = {
2952
- INVALID_BASE_ROUTE: 1,
2953
- INVALID_ROUTE: 2,
2954
- PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION: 3,
2955
- };
2956
- const invalidBaseRouteError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.INVALID_BASE_ROUTE, 'The baseRoute must not end with a slash', data);
2957
- const invalidRouteError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.INVALID_ROUTE, 'The route must start with a slash', data);
2958
- const pathParamsMissingInRouteFunctionError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION, 'The route is a function but pathParams are missing', data);
2959
- const isNaN$2 = (value) => typeof value === 'number' && Number.isNaN(value);
2960
- const isEmptyString$1 = (value) => typeof value === 'string' && value.trim() === '';
2961
- const buildQueryString$1 = (params, config) => {
2962
- const objectNotation = config?.objectNotation ?? 'bracket';
2963
- const writeArrayIndexes = config?.writeArrayIndexes ?? false;
2964
- const ignoredValues = config?.ignoredValues ?? [undefined, null, Infinity, -Infinity];
2965
- const ignoredValuesFns = config?.ignoredValuesFns ?? [isNaN$2, isEmptyString$1];
2966
- const queryParams = [];
2967
- function processValue(key, rawValue) {
2968
- if (rawValue instanceof Date && Number.isNaN(rawValue.getTime()))
2969
- return false;
2970
- const value = rawValue instanceof Date ? rawValue.toISOString() : rawValue;
2971
- if (config?.objectNotation === 'json-stringify') {
2972
- if (value === undefined) {
2973
- return false;
2974
- }
2975
- if (ignoredValues.includes(value)) {
2976
- return false;
2977
- }
2978
- if (ignoredValuesFns.some((fn) => fn(value))) {
2979
- return false;
2980
- }
2981
- const encodedKey = encodeURIComponent(key);
2982
- const val = typeof value === 'object' ? JSON.stringify(value) : value;
2983
- const encodedValue = encodeURIComponent(val);
2984
- queryParams.push(`${encodedKey}=${encodedValue}`);
2985
- return true;
2986
- }
2987
- else if (Array.isArray(value)) {
2988
- let currentFilteredIndex = 0;
2989
- let didAddAnyValue = false;
2990
- for (const arrayValue of value) {
2991
- const nestedKey = writeArrayIndexes ? `${key}[${currentFilteredIndex}]` : `${key}[]`;
2992
- const didAddValue = processValue(nestedKey, arrayValue);
2993
- if (didAddValue) {
2994
- currentFilteredIndex++;
2995
- didAddAnyValue = true;
2996
- }
2997
- }
2998
- return didAddAnyValue;
2999
- }
3000
- else if (typeof value === 'object' && value !== null) {
3001
- let didAddAnyValue = false;
3002
- for (const [objKey, val] of Object.entries(value)) {
3003
- const nestedKey = objectNotation === 'dot' ? `${key}.${objKey}` : `${key}[${objKey}]`;
3004
- didAddAnyValue = processValue(nestedKey, val) || didAddAnyValue;
3005
- }
3006
- return didAddAnyValue;
3007
- }
3008
- else {
3009
- if (ignoredValues.includes(value)) {
3010
- return false;
3011
- }
3012
- if (ignoredValuesFns.some((fn) => fn(value))) {
3013
- return false;
3014
- }
3015
- const encodedKey = encodeURIComponent(key);
3016
- const encodedValue = encodeURIComponent(value);
3017
- queryParams.push(`${encodedKey}=${encodedValue}`);
3018
- return true;
3019
- }
3020
- }
3021
- for (const [key, val] of Object.entries(params)) {
3022
- processValue(key, val);
3023
- }
3024
- return queryParams.length ? queryParams.join('&') : null;
3025
- };
3026
- const isPlainObject = (value) => {
3027
- if (!value || typeof value !== 'object')
3028
- return false;
3029
- const proto = Object.getPrototypeOf(value);
3030
- return proto === Object.prototype || proto === null;
3031
- };
3032
- /** Copies query params with every plain object's keys sorted, so key order never reaches a cache key. */
3033
- const sortQueryParamKeys = (value) => {
3034
- if (Array.isArray(value))
3035
- return value.map(sortQueryParamKeys);
3036
- if (!isPlainObject(value))
3037
- return value;
3038
- return Object.fromEntries(Object.keys(value)
3039
- .sort()
3040
- .map((key) => [key, sortQueryParamKeys(value[key])]));
3041
- };
3042
- const buildRoute$1 = (options) => {
3043
- if (options.base.endsWith('/')) {
3044
- throw invalidBaseRouteError$1(options.base);
3045
- }
3046
- let route;
3047
- if (typeof options.route === 'function') {
3048
- if (!options.pathParams) {
3049
- throw pathParamsMissingInRouteFunctionError$1(options.route({}));
3050
- }
3051
- route = options.route(options.pathParams);
3052
- }
3053
- else {
3054
- route = options.route ?? null;
3055
- }
3056
- if (route && !route.startsWith('/')) {
3057
- throw invalidRouteError$1(route);
3058
- }
3059
- if (options.queryParams) {
3060
- const queryString = buildQueryString$1(options.queryParams, options.queryParamConfig);
3061
- if (queryString) {
3062
- route = route ? `${route}?${queryString}` : `/?${queryString}`;
3063
- }
3064
- }
3065
- return `${options.base}${route ?? ''}`;
3066
- };
3067
- const buildTimestampFromSeconds$1 = (seconds) => {
3068
- if (seconds === null) {
3069
- return null;
3070
- }
3071
- return new Date(Date.now() + seconds * 1000).getTime();
3072
- };
3073
- /** Reads a bearer token's payload. */
3074
- const decryptBearer$1 = (token) => {
3075
- try {
3076
- const base64Url = token.split('.')[1];
3077
- if (!base64Url) {
3078
- return null;
3079
- }
3080
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
3081
- const jsonPayload = decodeURIComponent(atob(base64)
3082
- .split('')
3083
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
3084
- .join(''));
3085
- return JSON.parse(jsonPayload);
3086
- }
3087
- catch (error) {
3088
- console.error('Invalid bearer token', error);
3089
- return null;
3090
- }
3091
- };
3092
-
3093
3352
  /**
3094
3353
  * `true` on every request `@ethlete/query` sends through `HttpClient`, so an app interceptor can skip
3095
3354
  * query requests: `if (req.context.get(IS_QUERY_REQUEST)) return next(req);`.
@@ -3443,11 +3702,18 @@ const createHttpRequest = (options) => {
3443
3702
  retryState: retryState.asReadonly(),
3444
3703
  lastDurationMs: lastDurationMs.asReadonly(),
3445
3704
  abort,
3705
+ markStale: () => {
3706
+ if (!isStale())
3707
+ expiresIn.set(Date.now());
3708
+ },
3709
+ setResponse: (body) => response.set(body),
3446
3710
  },
3447
3711
  };
3448
3712
  return httpRequest;
3449
3713
  };
3450
3714
 
3715
+ const wrapAsObservableSignal = (source, defaultInjector) => createObservableSignalWatcher(defaultInjector).wrap(source);
3716
+
3451
3717
  /* eslint-disable @typescript-eslint/no-explicit-any */
3452
3718
  /** Transforms an array of arrays into a single array and filters out `null` values. */
3453
3719
  const transformArrayResponse = (responses) => responses.filter((r) => !!r).flatMap((r) => r);
@@ -3459,7 +3725,7 @@ const createQueryStack = (options) => {
3459
3725
  const lastQuery = newQueries[newQueries.length - 1] ?? oldQueries[oldQueries.length - 1] ?? null;
3460
3726
  return { queries, lastQuery };
3461
3727
  }, transform, argsKeyFn, deduplicateArgs, maxQueries = Infinity, removeStrategy = 'oldest', } = options ?? {};
3462
- const injector = inject(Injector);
3728
+ const injector = injectInQueryContext(Injector, 'query stack');
3463
3729
  const queries = signal([], /* @ts-ignore */
3464
3730
  ...(ngDevMode ? [{ debugName: "queries" }] : /* istanbul ignore next */ []));
3465
3731
  const lastQuery = signal(null, /* @ts-ignore */
@@ -3467,11 +3733,11 @@ const createQueryStack = (options) => {
3467
3733
  const firstQuery = computed(() => queries()[0] ?? null, /* @ts-ignore */
3468
3734
  ...(ngDevMode ? [{ debugName: "firstQuery" }] : /* istanbul ignore next */ []));
3469
3735
  const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
3470
- const hasWithOptimisticUpdateFeature = features.some((f) => f.type === QueryFeatureType.WITH_RESPONSE_UPDATE);
3736
+ const hasWithResponseUpdateFeature = features.some((f) => f.type === QueryFeatureType.WITH_RESPONSE_UPDATE);
3471
3737
  if (hasWithArgsFeature) {
3472
3738
  throw queryStackWithArgsUsed();
3473
3739
  }
3474
- if (hasWithOptimisticUpdateFeature) {
3740
+ if (hasWithResponseUpdateFeature) {
3475
3741
  throw queryStackWithResponseUpdateUsed();
3476
3742
  }
3477
3743
  const runWithArgs = (args) => {
@@ -3724,6 +3990,7 @@ const createPagedQueryStack = (options) => {
3724
3990
  const args = options.args(initialPage(), []);
3725
3991
  untracked(() => {
3726
3992
  stack.clear();
3993
+ pageDirection.set('next');
3727
3994
  currentPageArgs.set(args);
3728
3995
  loadedMinPage.set(initialPage());
3729
3996
  loadedMaxPage.set(initialPage());
@@ -4499,7 +4766,8 @@ const createIndexedDbQueryPersistenceAdapter = (options) => {
4499
4766
  */
4500
4767
  const createQueryBatch = (options) => {
4501
4768
  const { queryCreator, args: mapArgs, stopOnError, allowCache, onItemSettled, features = [] } = options;
4502
- const concurrency = Math.max(1, Math.floor(options.concurrency ?? 4));
4769
+ const requestedConcurrency = options.concurrency ?? 4;
4770
+ const concurrency = Number.isNaN(requestedConcurrency) ? 4 : Math.max(1, Math.floor(requestedConcurrency));
4503
4771
  if (features.some((f) => f.type === QueryFeatureType.WITH_ARGS)) {
4504
4772
  throw queryBatchWithArgsUsed();
4505
4773
  }
@@ -4780,41 +5048,6 @@ const isQueryDevtoolsRepositoryLive = (repository) => {
4780
5048
  return live$1.has(repository);
4781
5049
  };
4782
5050
 
4783
- /**
4784
- * Resolves an {@link QueryInvalidationOptions.url} to the absolute form request URLs are built in, so
4785
- * the comparison - and the message the other tabs receive - never depends on who resolves it.
4786
- */
4787
- const resolveInvalidationUrl = (baseUrl, url) => {
4788
- const absolute = url.startsWith('/') ? `${baseUrl}${url}` : url;
4789
- // A trailing slash would put the boundary check below one character past where the URL actually
4790
- // ends, which is the one way `/players/` could fail to match `/players/1`.
4791
- return absolute.endsWith('/') ? absolute.slice(0, -1) : absolute;
4792
- };
4793
- /**
4794
- * Whether a query URL is the invalidated one or sits below it. The check on what follows the prefix
4795
- * is what keeps `/players` from matching `/players-archive`, which a `startsWith` alone would.
4796
- */
4797
- const isUnderInvalidatedUrl = (queryUrl, invalidatedUrl) => {
4798
- if (!queryUrl.startsWith(invalidatedUrl))
4799
- return false;
4800
- const rest = queryUrl.slice(invalidatedUrl.length);
4801
- return rest === '' || rest.startsWith('/') || rest.startsWith('?') || rest.startsWith('#');
4802
- };
4803
- /**
4804
- * Turns an invalidation into the filter the repository refreshes by, or `undefined` when nothing
4805
- * narrows it - "everything in use" is the repository's own cheapest path.
4806
- */
4807
- const createQueryInvalidationFilter = (options) => {
4808
- const { url, filter } = options;
4809
- if (!url && !filter)
4810
- return undefined;
4811
- return (request) => {
4812
- if (url && !isUnderInvalidatedUrl(request.url, url))
4813
- return false;
4814
- return filter ? filter({ method: request.method, url: request.url }) : true;
4815
- };
4816
- };
4817
-
4818
5051
  /**
4819
5052
  * An error body can be an HTML page instead of JSON: a proxy's 502, a load balancer's maintenance page, a
4820
5053
  * platform's "service temporarily unavailable". The markup is never worth showing, but the sentence inside it
@@ -4996,6 +5229,14 @@ const createQuerySyncEngine = (options) => {
4996
5229
  // response sharing would just make a tab stop updating. Enforced here rather than documented.
4997
5230
  const isPollingDedupeEnabled = (config.dedupePolling ?? true) && syncResponses && transport.isSupported && lockManager.isSupported;
4998
5231
  const mutationFilter = typeof refreshOnMutation === 'object' ? refreshOnMutation.filter : null;
5232
+ let outgoingBatch = null;
5233
+ const currentOutgoingBatch = () => {
5234
+ if (outgoingBatch === null) {
5235
+ outgoingBatch = randomId();
5236
+ queueMicrotask(() => (outgoingBatch = null));
5237
+ }
5238
+ return outgoingBatch;
5239
+ };
4999
5240
  const broadcast = (event) => {
5000
5241
  if (event.type !== 'request-success' || !event.isMultiTabSyncEnabled)
5001
5242
  return;
@@ -5017,7 +5258,12 @@ const createQuerySyncEngine = (options) => {
5017
5258
  // worth telling the other tabs about - and an uncacheable read has no shared key to send anyway.
5018
5259
  if (!refreshOnMutation || shouldCacheQuery(event.request.method))
5019
5260
  return;
5020
- transport.post({ type: 'mutation', method: event.request.method, url: event.request.url });
5261
+ transport.post({
5262
+ type: 'mutation',
5263
+ method: event.request.method,
5264
+ url: event.request.url,
5265
+ batch: currentOutgoingBatch(),
5266
+ });
5021
5267
  };
5022
5268
  const apply = (message) => {
5023
5269
  if (message.type === 'response') {
@@ -5029,23 +5275,20 @@ const createQuerySyncEngine = (options) => {
5029
5275
  // An invalidation is something the other tab's app code asked for explicitly, so unlike the
5030
5276
  // mutation heuristic it is not something `refreshOnMutation: false` opts out of.
5031
5277
  if (message.type === 'invalidate') {
5032
- repository.refreshInUse(createQueryInvalidationFilter({ url: message.url }), {
5278
+ repository.refreshInUse(createQueryInvalidationFilter({ url: message.url, tag: message.tag }), {
5033
5279
  type: 'invalidation',
5034
5280
  url: message.url,
5035
5281
  otherTab: true,
5036
- });
5282
+ ...(message.tag && { tag: message.tag }),
5283
+ }, message.batch);
5037
5284
  return;
5038
5285
  }
5039
5286
  if (!refreshOnMutation)
5040
5287
  return;
5041
5288
  const mutation = { method: message.method, url: message.url };
5042
- repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined, {
5043
- type: 'mutation',
5044
- url: message.url,
5045
- otherTab: true,
5046
- });
5289
+ repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined, { type: 'mutation', url: message.url, otherTab: true }, message.batch);
5047
5290
  };
5048
- const postInvalidation = (url) => transport.post({ type: 'invalidate', url });
5291
+ const postInvalidation = (url, tag) => transport.post({ type: 'invalidate', url, ...(tag && { tag }), batch: currentOutgoingBatch() });
5049
5292
  const eventSubscription = repository.events$.subscribe(broadcast);
5050
5293
  const unlisten = transport.listen(apply);
5051
5294
  const destroy = () => {
@@ -5098,11 +5341,13 @@ const unwrapQuerySyncMessage = (data) => {
5098
5341
  expiresAt: typeof expiresAt === 'number' ? expiresAt : null,
5099
5342
  };
5100
5343
  }
5344
+ const batch = typeof raw['batch'] === 'string' ? { batch: raw['batch'] } : {};
5101
5345
  if (type === 'mutation' && typeof method === 'string' && typeof url === 'string') {
5102
- return { type: 'mutation', method: method, url };
5346
+ return { type: 'mutation', method: method, url, ...batch };
5103
5347
  }
5104
5348
  if (type === 'invalidate' && (typeof url === 'string' || url === null)) {
5105
- return { type: 'invalidate', url };
5349
+ const tag = raw['tag'];
5350
+ return { type: 'invalidate', url, tag: typeof tag === 'string' ? tag : null, ...batch };
5106
5351
  }
5107
5352
  return null;
5108
5353
  };
@@ -5364,6 +5609,16 @@ const withEthleteApiErrors = (options) => () => {
5364
5609
  };
5365
5610
  };
5366
5611
 
5612
+ const functionIds = /* @__PURE__ */ new WeakMap();
5613
+ let nextFunctionId = 1;
5614
+ const functionId = (fn) => {
5615
+ let id = functionIds.get(fn);
5616
+ if (id === undefined) {
5617
+ id = nextFunctionId++;
5618
+ functionIds.set(fn, id);
5619
+ }
5620
+ return id;
5621
+ };
5367
5622
  const generateUuid = () => randomId();
5368
5623
  /** @see CreateQueryClientConfigOptions.keepUnusedFor */
5369
5624
  const DEFAULT_KEEP_UNUSED_FOR = 300_000;
@@ -5415,8 +5670,17 @@ const createQueryRepository = (config) => {
5415
5670
  queryParamConfig: config.queryString,
5416
5671
  })
5417
5672
  : route;
5673
+ const wireKey = [
5674
+ creatorOptions?.responseType && creatorOptions.responseType !== 'json' ? creatorOptions.responseType : '',
5675
+ creatorOptions?.withCredentials ? 'credentials' : '',
5676
+ creatorOptions?.reportErrors === false ? 'silent' : '',
5677
+ creatorOptions?.reportProgress ? 'progress' : '',
5678
+ options.retryFn ? `retry${functionId(options.retryFn)}` : '',
5679
+ ]
5680
+ .filter(Boolean)
5681
+ .join('_');
5418
5682
  const cacheKey = shouldCache
5419
- ? buildQueryCacheKey(`${options.key ? options.key + '_' : ''}${keyRoute}`, {
5683
+ ? buildQueryCacheKey(`${wireKey ? wireKey + '_' : ''}${options.key ? options.key + '_' : ''}${keyRoute}`, {
5420
5684
  body: args?.body,
5421
5685
  queryParams: args?.queryParams,
5422
5686
  pathParams: args?.pathParams,
@@ -5427,6 +5691,7 @@ const createQueryRepository = (config) => {
5427
5691
  const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
5428
5692
  const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
5429
5693
  const isSecure = options.isSecure ?? false;
5694
+ const tags = options.tags ?? [];
5430
5695
  // A secure response needs an explicit `persistence: true` to reach the disk: leaving an
5431
5696
  // authenticated user's data there is a decision per endpoint, not a default. Everything else
5432
5697
  // persists unless it opted out.
@@ -5451,6 +5716,7 @@ const createQueryRepository = (config) => {
5451
5716
  isMultiTabSyncEnabled,
5452
5717
  isPersistEnabled,
5453
5718
  keepUnusedFor,
5719
+ tags,
5454
5720
  });
5455
5721
  const executed = !runQueryOptions?.allowCache || cacheEntry.request.isStale()
5456
5722
  ? cacheEntry.request.execute({ allowCache: runQueryOptions?.allowCache })
@@ -5483,6 +5749,7 @@ const createQueryRepository = (config) => {
5483
5749
  isMultiTabSyncEnabled,
5484
5750
  isPersistEnabled,
5485
5751
  keepUnusedFor,
5752
+ tags,
5486
5753
  });
5487
5754
  // Announced after the entry is bound and its request is on its way, because the one thing that acts
5488
5755
  // on it - hydration from the persisted store - is only ever allowed to fill a gap this request has
@@ -5544,6 +5811,7 @@ const createQueryRepository = (config) => {
5544
5811
  cacheEntry.isPersistEnabled = bindings.some((binding) => binding.isPersistEnabled);
5545
5812
  cacheEntry.isMultiTabSyncEnabled = bindings.every((binding) => binding.isMultiTabSyncEnabled);
5546
5813
  cacheEntry.keepUnusedFor = Math.min(...bindings.map((binding) => binding.keepUnusedFor));
5814
+ cacheEntry.tags = bindings.flatMap((binding) => binding.tags);
5547
5815
  };
5548
5816
  const unbind = (key, consumerDestroyRef) => {
5549
5817
  if (key === null)
@@ -5586,10 +5854,30 @@ const createQueryRepository = (config) => {
5586
5854
  bumpCacheVersion();
5587
5855
  eventsSubject.next({ type: 'unbind-all-secure' });
5588
5856
  };
5589
- const refreshInUse = (filter, cause) => {
5857
+ let restartedBatch = null;
5858
+ let turnBatchKey = null;
5859
+ const currentTurnBatchKey = () => {
5860
+ if (turnBatchKey === null) {
5861
+ turnBatchKey = generateUuid();
5862
+ queueMicrotask(() => (turnBatchKey = null));
5863
+ }
5864
+ return turnBatchKey;
5865
+ };
5866
+ const resolveRestartedBatch = (cause, batch) => {
5867
+ if (!cause || cause.type === 'refresh')
5868
+ return null;
5869
+ const key = batch ?? currentTurnBatchKey();
5870
+ if (restartedBatch?.key !== key)
5871
+ restartedBatch = { key, requests: new WeakSet() };
5872
+ return restartedBatch.requests;
5873
+ };
5874
+ const refreshInUse = (filter, cause, batch) => {
5590
5875
  const refreshed = [];
5876
+ const marksUnusedStale = cause?.type === 'invalidation';
5877
+ const restartedInBatch = resolveRestartedBatch(cause, batch);
5591
5878
  for (const cacheEntry of cache.values()) {
5592
- if (cacheEntry.consumers.size === 0)
5879
+ const isUnused = cacheEntry.consumers.size === 0;
5880
+ if (isUnused && !marksUnusedStale)
5593
5881
  continue;
5594
5882
  // Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed:
5595
5883
  // a cacheable method, or a GQL query transported via POST. Opting into the cache alone does not
@@ -5597,8 +5885,15 @@ const createQueryRepository = (config) => {
5597
5885
  // end the session.
5598
5886
  if (!cacheEntry.isRefreshable)
5599
5887
  continue;
5600
- if (filter && !filter(cacheEntry.request))
5888
+ if (filter && !filter(cacheEntry.request, cacheEntry.tags))
5601
5889
  continue;
5890
+ if (isUnused) {
5891
+ cacheEntry.request.subtle.markStale();
5892
+ continue;
5893
+ }
5894
+ if (restartedInBatch?.has(cacheEntry.request))
5895
+ continue;
5896
+ restartedInBatch?.add(cacheEntry.request);
5602
5897
  refreshed.push(cacheEntry.request);
5603
5898
  cacheEntry.request.execute({ force: true });
5604
5899
  }
@@ -5629,7 +5924,7 @@ const createQueryRepository = (config) => {
5629
5924
  return true;
5630
5925
  };
5631
5926
  const bind = (options) => {
5632
- const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
5927
+ const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, tags, } = options;
5633
5928
  const cacheEntry = cache.get(key);
5634
5929
  if (cacheEntry && isCached) {
5635
5930
  const existingBinding = cacheEntry.consumers.get(consumerDestroyRef);
@@ -5639,6 +5934,7 @@ const createQueryRepository = (config) => {
5639
5934
  existingBinding.isMultiTabSyncEnabled = isMultiTabSyncEnabled;
5640
5935
  existingBinding.isPersistEnabled = isPersistEnabled;
5641
5936
  existingBinding.keepUnusedFor = keepUnusedFor;
5937
+ existingBinding.tags = tags;
5642
5938
  }
5643
5939
  else {
5644
5940
  cacheEntry.consumers.set(consumerDestroyRef, {
@@ -5648,6 +5944,7 @@ const createQueryRepository = (config) => {
5648
5944
  isMultiTabSyncEnabled,
5649
5945
  isPersistEnabled,
5650
5946
  keepUnusedFor,
5947
+ tags,
5651
5948
  });
5652
5949
  }
5653
5950
  mergeConsumerPolicies(cacheEntry);
@@ -5663,6 +5960,7 @@ const createQueryRepository = (config) => {
5663
5960
  isMultiTabSyncEnabled,
5664
5961
  isPersistEnabled,
5665
5962
  keepUnusedFor,
5963
+ tags,
5666
5964
  },
5667
5965
  ],
5668
5966
  ]);
@@ -5709,6 +6007,7 @@ const createQueryRepository = (config) => {
5709
6007
  isRefreshable,
5710
6008
  isMultiTabSyncEnabled,
5711
6009
  isPersistEnabled,
6010
+ tags,
5712
6011
  });
5713
6012
  }
5714
6013
  bumpCacheVersion();
@@ -5718,6 +6017,8 @@ const createQueryRepository = (config) => {
5718
6017
  isSecure: entry.isSecure,
5719
6018
  consumerCount: entry.consumers.size,
5720
6019
  isUnused: entry.consumers.size === 0,
6020
+ isRefreshable: entry.isRefreshable,
6021
+ tags: entry.tags,
5721
6022
  request: entry.request,
5722
6023
  }));
5723
6024
  const evict = (key, cause = 'manual') => {
@@ -5744,7 +6045,21 @@ const createQueryRepository = (config) => {
5744
6045
  return repository;
5745
6046
  };
5746
6047
 
6048
+ const clientNamesByInjector = /* @__PURE__ */ new WeakMap();
6049
+ const warnOnDuplicateClientName = (name, destroyRef) => {
6050
+ const environmentInjector = inject(EnvironmentInjector);
6051
+ const names = clientNamesByInjector.get(environmentInjector) ?? new Set();
6052
+ clientNamesByInjector.set(environmentInjector, names);
6053
+ if (names.has(name)) {
6054
+ console.warn(`Two query clients are named "${name}". The name keys multi-tab sync, persistence and the devtools, so both clients share them. Give each createQueryClient() a unique name.`);
6055
+ return;
6056
+ }
6057
+ names.add(name);
6058
+ destroyRef.onDestroy(() => names.delete(name));
6059
+ };
5747
6060
  const createQueryClient = (options) => defineRootProvider(() => {
6061
+ if (options.baseUrl.endsWith('/'))
6062
+ throw invalidBaseRouteError$1(options.baseUrl);
5748
6063
  const httpClient = inject(HttpClient);
5749
6064
  const ngErrorHandler = inject(ErrorHandler);
5750
6065
  const injector = inject(Injector);
@@ -5762,6 +6077,8 @@ const createQueryClient = (options) => defineRootProvider(() => {
5762
6077
  dependencies: { httpClient, ngErrorHandler, injector },
5763
6078
  });
5764
6079
  const destroyRef = inject(DestroyRef);
6080
+ if (isDevMode())
6081
+ warnOnDuplicateClientName(options.name, destroyRef);
5765
6082
  destroyRef.onDestroy(() => {
5766
6083
  for (const entry of repository.subtle.cacheEntries()) {
5767
6084
  if (entry.isUnused)
@@ -5801,15 +6118,17 @@ const createQueryClient = (options) => defineRootProvider(() => {
5801
6118
  refreshQueriesInUse: () => repository.refreshInUse(),
5802
6119
  invalidateQueries: (invalidation) => {
5803
6120
  const url = invalidation?.url ? resolveInvalidationUrl(options.baseUrl, invalidation.url) : null;
5804
- repository.refreshInUse(createQueryInvalidationFilter({ url, filter: invalidation?.filter }), {
6121
+ const tag = invalidation?.tag ?? null;
6122
+ repository.refreshInUse(createQueryInvalidationFilter({ url, tag, filter: invalidation?.filter }), {
5805
6123
  type: 'invalidation',
5806
6124
  url,
5807
6125
  otherTab: false,
6126
+ ...(tag && { tag }),
5808
6127
  });
5809
6128
  // The resolved URL is what travels: the other tabs are the same client, so they would
5810
6129
  // resolve it identically, and a message that needs no interpretation cannot drift.
5811
6130
  if (invalidation?.otherTabs ?? true)
5812
- sync?.postInvalidation(url);
6131
+ sync?.postInvalidation(url, tag);
5813
6132
  },
5814
6133
  clearPersistedQueries: () => persistenceEngine?.clear() ?? Promise.resolve(),
5815
6134
  whenPersistenceReady: persistenceEngine?.whenReady ?? Promise.resolve(),
@@ -5953,6 +6272,8 @@ const createSecureExecuteFactory = (options) => {
5953
6272
  args: executeArgs?.args ?? options.state.args(),
5954
6273
  options: executeArgs?.options,
5955
6274
  };
6275
+ if (skipParkedExecution(options.state, execArgsWithDefaults.args, options.route))
6276
+ return;
5956
6277
  circularChecker.check(execArgsWithDefaults.args);
5957
6278
  hasExecuted = true;
5958
6279
  lastExecuteArgs = execArgsWithDefaults;
@@ -6042,6 +6363,7 @@ const createSecureExecuteFn = (executeOptions) => {
6042
6363
  authProvider,
6043
6364
  deps: executeOptions.deps,
6044
6365
  state: executeOptions.state,
6366
+ route: executeOptions.creatorInternals.route,
6045
6367
  autoExecutes: shouldAutoExecuteQuery(executeOptions.creatorInternals.method) && !executeOptions.queryConfig.onlyManualExecution,
6046
6368
  transformAuthAndExec: (executeArgs, executeState) => {
6047
6369
  queryExecute({
@@ -6104,6 +6426,181 @@ const createPatchQuery = /* @__PURE__ */ createQueryTemplate('PATCH');
6104
6426
  /** A query creator that creates a secure PATCH query (requires authentication) */
6105
6427
  const createSecurePatchQuery = /* @__PURE__ */ createSecureQueryTemplate('PATCH');
6106
6428
 
6429
+ /**
6430
+ * Groups a fixed set of queries of which the one executed last wins. The group fits `etQueryButton` and
6431
+ * `et-query-error`'s `[query]`, which retries the member that ran last. Call it in an injection context.
6432
+ *
6433
+ * @example
6434
+ * readonly actions = createQueryGroup({
6435
+ * accept: postAcceptInvitation(withArgs(() => ({ pathParams: { id: this.id() } }))),
6436
+ * decline: postDeclineInvitation(withArgs(() => ({ pathParams: { id: this.id() } }))),
6437
+ * });
6438
+ *
6439
+ * accept() {
6440
+ * this.actions.members.accept.execute();
6441
+ * }
6442
+ */
6443
+ const createQueryGroup = (members) => {
6444
+ const executions = new Subject();
6445
+ const latestMember = signal(null, /* @ts-ignore */
6446
+ ...(ngDevMode ? [{ debugName: "latestMember" }] : /* istanbul ignore next */ []));
6447
+ injectInQueryContext(DestroyRef, 'query group').onDestroy(() => executions.complete());
6448
+ const wrap = (key, query) => {
6449
+ const member = {
6450
+ key,
6451
+ query,
6452
+ execute: (executeArgs) => {
6453
+ query.execute(executeArgs);
6454
+ latestMember.set(member);
6455
+ executions.next(member);
6456
+ },
6457
+ };
6458
+ return { ...query, execute: member.execute };
6459
+ };
6460
+ const wrappedMembers = Object.fromEntries(Object.entries(members).map(([key, query]) => [key, wrap(key, query)]));
6461
+ const queries = Object.values(members);
6462
+ const loading = computed(() => latestMember()?.query.loading() ??
6463
+ queries.map((query) => query.loading()).find((state) => state !== null) ??
6464
+ null, /* @ts-ignore */
6465
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
6466
+ const error = computed(() => latestMember()?.query.error() ?? null, /* @ts-ignore */
6467
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
6468
+ const latest = computed(() => {
6469
+ const member = latestMember();
6470
+ return member ? { key: member.key, response: member.query.response() } : null;
6471
+ }, /* @ts-ignore */
6472
+ ...(ngDevMode ? [{ debugName: "latest" }] : /* istanbul ignore next */ []));
6473
+ const execute = (executeArgs) => untracked(latestMember)?.execute(executeArgs);
6474
+ const succeeded$ = executions.pipe(mergeMap(({ key, query }) => {
6475
+ const snapshot = query.createSnapshot();
6476
+ return snapshot.isAlive.asObservable().pipe(filter((isAlive) => !isAlive), take(1), map(() => untracked(() => snapshot.executionState())), filter((state) => state?.type === 'success'), map((state) => ({ key, response: state.response })));
6477
+ }), share());
6478
+ return { members: wrappedMembers, loading, error, latest, execute, succeeded$ };
6479
+ };
6480
+
6481
+ const optimisticLayers = /* @__PURE__ */ new WeakMap();
6482
+ const liveLayer = (request) => {
6483
+ const layer = optimisticLayers.get(request);
6484
+ if (layer && layer.written === request.response())
6485
+ return layer;
6486
+ optimisticLayers.delete(request);
6487
+ return null;
6488
+ };
6489
+ const rewrite = (request, layer) => {
6490
+ layer.written = layer.ops.reduce((current, op) => op.apply(current) ?? current, layer.base);
6491
+ request.subtle.setResponse(layer.written);
6492
+ if (layer.ops.every((op) => op.settled))
6493
+ optimisticLayers.delete(request);
6494
+ };
6495
+ const applyOp$1 = (request, op) => {
6496
+ const current = request.response();
6497
+ if (current === null)
6498
+ return false;
6499
+ const layer = liveLayer(request) ?? { base: current, written: current, ops: [] };
6500
+ const next = op.apply(layer.written);
6501
+ if (next === null)
6502
+ return false;
6503
+ layer.ops.push(op);
6504
+ layer.written = next;
6505
+ optimisticLayers.set(request, layer);
6506
+ request.subtle.setResponse(next);
6507
+ return true;
6508
+ };
6509
+ const settleOp = (request, op, confirmed) => {
6510
+ const layer = liveLayer(request);
6511
+ const index = layer?.ops.indexOf(op) ?? -1;
6512
+ if (!layer || index === -1)
6513
+ return;
6514
+ if (confirmed) {
6515
+ op.apply = confirmed;
6516
+ op.settled = true;
6517
+ }
6518
+ else {
6519
+ layer.ops.splice(index, 1);
6520
+ }
6521
+ rewrite(request, layer);
6522
+ };
6523
+ const findTargetRequests = (deps, targets) => {
6524
+ const filters = targets.map((target) => createQueryInvalidationFilter('url' in target
6525
+ ? { url: resolveInvalidationUrl(deps.client.baseUrl, target.url) }
6526
+ : { url: null, tag: target.tag }));
6527
+ return deps.client.repository.subtle
6528
+ .cacheEntries()
6529
+ .filter((entry) => entry.isRefreshable && filters.some((filter) => filter?.(entry.request, entry.tags)))
6530
+ .map((entry) => entry.request);
6531
+ };
6532
+ /**
6533
+ * Writes the expected result of a mutation into the cached responses of the reads `target` matches before
6534
+ * the request leaves, and rolls those writes back if it fails or is cancelled. Pair it with the creator's
6535
+ * `invalidates`, whose refetch corrects a wrong guess. The writes stay in this tab; the invalidation reaches
6536
+ * the others.
6537
+ *
6538
+ * A rollback leaves an entry alone that a refetch or another tab wrote in the meantime; other optimistic
6539
+ * updates of the entry still pending are re-applied on top of what it held before.
6540
+ *
6541
+ * @example
6542
+ * patchOpportunityPerson(
6543
+ * withArgs(() => ({ pathParams: { uuid, peopleUuid }, body })),
6544
+ * withOptimisticUpdate({
6545
+ * read: getOpportunity,
6546
+ * target: ({ args }) => ({ tag: `opportunity:${args.pathParams.uuid}` }),
6547
+ * update: ({ current, args }) => ({ ...current, people: toggle(current.people, args.pathParams.peopleUuid) }),
6548
+ * }),
6549
+ * );
6550
+ *
6551
+ * @throws If the query is a read (e.g. a GET request)
6552
+ */
6553
+ const withOptimisticUpdate = (options) => {
6554
+ const update = options.update;
6555
+ return createQueryFeature({
6556
+ type: QueryFeatureType.WITH_OPTIMISTIC_UPDATE,
6557
+ devtools: () => queryDevtoolsFnDetail(options.update, 'update'),
6558
+ fn: (context) => {
6559
+ if (context.flags.shouldAutoExecuteMethod)
6560
+ throw withOptimisticUpdateUsedOnRead(context.flags.method);
6561
+ context.state.subtle.beforeExecute.set((executeArgs) => {
6562
+ const args = executeArgs ?? {};
6563
+ const target = options.target({ args });
6564
+ const targets = Array.isArray(target) ? target : [target];
6565
+ const optimistic = (current) => update({ current, args });
6566
+ const applied = findTargetRequests(context.deps, targets)
6567
+ .map((request) => ({ request, op: { apply: optimistic, settled: false } }))
6568
+ .filter(({ request, op }) => applyOp$1(request, op));
6569
+ if (!applied.length)
6570
+ return null;
6571
+ const settle = (confirmed) => {
6572
+ for (const { request, op } of applied)
6573
+ settleOp(request, op, confirmed);
6574
+ };
6575
+ return (request) => {
6576
+ if (!request.loading())
6577
+ return settle(null);
6578
+ let settled = false;
6579
+ const subscription = request.events$.subscribe({
6580
+ next: (event) => {
6581
+ if (event.type === HttpEventType.Response) {
6582
+ const response = context.state.subtle.request() === request ? context.state.response() : null;
6583
+ settled = true;
6584
+ settle((current) => update({ current, args, response }) ?? optimistic(current));
6585
+ }
6586
+ else if (event.type === 'error' || event.type === 'cancel') {
6587
+ settled = true;
6588
+ settle(null);
6589
+ }
6590
+ if (settled)
6591
+ subscription.unsubscribe();
6592
+ },
6593
+ complete: () => {
6594
+ if (!settled)
6595
+ settle(null);
6596
+ },
6597
+ });
6598
+ };
6599
+ });
6600
+ },
6601
+ });
6602
+ };
6603
+
6107
6604
  const isAbortedSnapshot = (snapshot) => snapshot.latestHttpEvent()?.type === 'cancel' && snapshot.error() === null;
6108
6605
  const executeAndSettle$ = (query, executeArgs, emitAborted) => defer(() => {
6109
6606
  query.execute(executeArgs);
@@ -6743,16 +7240,18 @@ const createBearerAuthProviderImpl = (config) => {
6743
7240
  const refreshToken = signal(null, /* @ts-ignore */
6744
7241
  ...(ngDevMode ? [{ debugName: "refreshToken" }] : /* istanbul ignore next */ []));
6745
7242
  const afterTokenRefresh$ = new Subject();
7243
+ const expiresInPropertyName = signal('exp', /* @ts-ignore */
7244
+ ...(ngDevMode ? [{ debugName: "expiresInPropertyName" }] : /* istanbul ignore next */ []));
6746
7245
  const bearerData = computed(() => {
6747
7246
  const token = accessToken();
6748
7247
  if (!token)
6749
7248
  return null;
6750
7249
  try {
6751
- const decoded = config.bearerDecryptFn?.(token) ?? decryptBearer$1(token);
7250
+ const decoded = config.bearerDecryptFn ? config.bearerDecryptFn(token) : decryptBearer$1(token);
6752
7251
  return patchQueryDevtoolsTokenPayload({
6753
7252
  payload: decoded,
6754
7253
  providerName: config.name,
6755
- expiresInPropertyName: 'exp',
7254
+ expiresInPropertyName: expiresInPropertyName(),
6756
7255
  });
6757
7256
  }
6758
7257
  catch (error) {
@@ -6885,6 +7384,7 @@ const createBearerAuthProviderImpl = (config) => {
6885
7384
  reportAccessTokenExpiry: (isExpired) => {
6886
7385
  readAccessTokenExpiry = isExpired;
6887
7386
  },
7387
+ reportExpiresInPropertyName: (claim) => expiresInPropertyName.set(claim),
6888
7388
  };
6889
7389
  for (const builder of config.queries) {
6890
7390
  builder.setup?.(querySetupContext);
@@ -7075,6 +7575,7 @@ const withRefreshQuery = (key, config) => {
7075
7575
  const buildArgs = (refreshToken) => (config.buildArgs?.(refreshToken) ?? { body: { token: refreshToken } });
7076
7576
  const setup = (context) => {
7077
7577
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
7578
+ context.reportExpiresInPropertyName(expiresInPropertyName);
7078
7579
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
7079
7580
  const refreshIfExpired = config.refreshIfExpired ?? true;
7080
7581
  const destroyRef = inject(DestroyRef);
@@ -7259,48 +7760,51 @@ const withRefreshQuery = (key, config) => {
7259
7760
  case 'notLeader':
7260
7761
  return Math.min(Math.max(msUntilAccessTokenIsStale() ?? minRefreshInterval, rescheduleDelayMs), maxTimerDelayMs);
7261
7762
  }
7262
- };
7263
- const calculateRefreshBuffer = (tokenLifetimeMs) => {
7264
- if (typeof config.refreshStrategy === 'number') {
7265
- return config.refreshStrategy >= 0 && config.refreshStrategy <= 1
7266
- ? tokenLifetimeMs * (1 - config.refreshStrategy)
7267
- : Math.max(config.refreshStrategy, 0);
7763
+ };
7764
+ let expiryWarnedFor = null;
7765
+ const tokenTimes = (token) => {
7766
+ let decoded;
7767
+ try {
7768
+ decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer$1(token);
7769
+ }
7770
+ catch {
7771
+ decoded = null;
7772
+ }
7773
+ const times = readAccessTokenTimes({
7774
+ token,
7775
+ decoded,
7776
+ claim: expiresInPropertyName,
7777
+ providerName: context.name,
7778
+ });
7779
+ if (!times && isDevMode() && expiryWarnedFor !== token) {
7780
+ expiryWarnedFor = token;
7781
+ console.warn(authAccessTokenExpiryUnreadable(expiresInPropertyName).message);
7268
7782
  }
7269
- const percentage = config.refreshStrategy?.percentage ?? 0.75;
7270
- const minBufferMs = config.refreshStrategy?.minBufferMs ?? 60000; // 1 minute
7271
- const maxBufferMs = config.refreshStrategy?.maxBufferMs ?? 600000; // 10 minutes
7272
- const calculatedBuffer = tokenLifetimeMs * (1 - percentage);
7273
- return Math.max(minBufferMs, Math.min(maxBufferMs, calculatedBuffer));
7783
+ return times;
7274
7784
  };
7275
7785
  /** When this token expires, as a timestamp, or `null` if the expiry cannot be read off it. */
7276
- const tokenExpiresAt = (token) => {
7277
- try {
7278
- const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer$1(token);
7279
- const bearerDataValue = patchQueryDevtoolsTokenPayload({
7280
- payload: decoded,
7281
- providerName: context.name,
7282
- expiresInPropertyName,
7283
- });
7284
- const expiresIn = bearerDataValue?.[expiresInPropertyName];
7285
- if (typeof expiresIn !== 'number') {
7286
- if (isDevMode()) {
7287
- console.warn(`Token does not contain valid ${expiresInPropertyName} property for auto-refresh`);
7288
- }
7289
- return null;
7290
- }
7291
- return expiresIn * 1000;
7786
+ const tokenExpiresAt = (token) => tokenTimes(token)?.expiresAtMs ?? null;
7787
+ let measuredLifetime = null;
7788
+ /**
7789
+ * The whole lifetime of a token, not what is left of it: a schedule recomputed later (the tab came
7790
+ * back to the foreground) must land at the same instant as the first one.
7791
+ */
7792
+ const tokenLifetimeMs = (token, times) => {
7793
+ if (times.issuedAtMs !== null && times.expiresAtMs > times.issuedAtMs) {
7794
+ return times.expiresAtMs - times.issuedAtMs;
7292
7795
  }
7293
- catch {
7294
- return null;
7796
+ if (measuredLifetime?.token !== token) {
7797
+ measuredLifetime = { token, lifetimeMs: times.expiresAtMs - Date.now() };
7295
7798
  }
7799
+ return measuredLifetime.lifetimeMs;
7296
7800
  };
7297
7801
  /** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
7298
7802
  const scheduledRefreshDelay = (token) => {
7299
- const expiresAt = tokenExpiresAt(token);
7300
- if (expiresAt === null)
7803
+ const times = tokenTimes(token);
7804
+ if (times === null)
7301
7805
  return null;
7302
- const tokenLifetimeMs = expiresAt - Date.now();
7303
- if (tokenLifetimeMs <= 0) {
7806
+ const remainingMs = times.expiresAtMs - Date.now();
7807
+ if (remainingMs <= 0) {
7304
7808
  if (!refreshIfExpired)
7305
7809
  return null;
7306
7810
  if (isDevMode()) {
@@ -7308,7 +7812,7 @@ const withRefreshQuery = (key, config) => {
7308
7812
  }
7309
7813
  return 0;
7310
7814
  }
7311
- return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
7815
+ return Math.max(remainingMs - refreshBufferMs(config.refreshStrategy, tokenLifetimeMs(token, times)), 0);
7312
7816
  };
7313
7817
  /**
7314
7818
  * How long this tab can still work with the access token it holds, or `null` without a readable
@@ -7909,40 +8413,59 @@ const buildPersistentAuthFeature = (config, context) => {
7909
8413
  */
7910
8414
  const createPersistentAuthFeature = (config, context) => runInInjectionContext(context.injector, () => buildPersistentAuthFeature(config, context));
7911
8415
 
8416
+ const RENEWAL_GRACE_MS = 10_000;
7912
8417
  const withTokenExpirationWarning = (config = {}) => {
7913
8418
  return (context) => {
7914
8419
  const warningThreshold = config.warningThreshold ?? 5 * 60 * 1000;
7915
8420
  const checkInterval = config.checkInterval ?? 1000;
7916
8421
  const refreshBuilder = context.queryBuilders.find((builder) => builder._type === 'tokenRefreshQuery');
7917
- const expiresInPropertyName = config.expiresInPropertyName ??
7918
- (refreshBuilder?._type === 'tokenRefreshQuery' ? refreshBuilder.config.expiresInPropertyName : undefined) ??
7919
- 'exp';
7920
- const expiresAt = computed(() => {
7921
- const decoded = context.bearerData();
7922
- if (!decoded)
8422
+ const refreshConfig = refreshBuilder?._type === 'tokenRefreshQuery' ? refreshBuilder.config : null;
8423
+ const expiresInPropertyName = config.expiresInPropertyName ?? refreshConfig?.expiresInPropertyName ?? 'exp';
8424
+ let measuredLifetime = null;
8425
+ const tokenTimes = computed(() => {
8426
+ const token = context.accessToken();
8427
+ if (!token)
7923
8428
  return null;
7924
- const exp = decoded[expiresInPropertyName];
7925
- if (typeof exp !== 'number')
8429
+ const times = readAccessTokenTimes({
8430
+ token,
8431
+ decoded: context.bearerData(),
8432
+ claim: expiresInPropertyName,
8433
+ providerName: context.name,
8434
+ });
8435
+ if (!times)
7926
8436
  return null;
7927
- return new Date(exp * 1000);
8437
+ if (times.issuedAtMs !== null && times.expiresAtMs > times.issuedAtMs) {
8438
+ return { ...times, lifetimeMs: times.expiresAtMs - times.issuedAtMs };
8439
+ }
8440
+ if (measuredLifetime?.token !== token) {
8441
+ measuredLifetime = { token, lifetimeMs: times.expiresAtMs - Date.now() };
8442
+ }
8443
+ return { ...times, lifetimeMs: measuredLifetime.lifetimeMs };
8444
+ }, /* @ts-ignore */
8445
+ ...(ngDevMode ? [{ debugName: "tokenTimes" }] : /* istanbul ignore next */ []));
8446
+ const expiresAt = computed(() => {
8447
+ const times = tokenTimes();
8448
+ return times ? new Date(times.expiresAtMs) : null;
7928
8449
  }, /* @ts-ignore */
7929
8450
  ...(ngDevMode ? [{ debugName: "expiresAt" }] : /* istanbul ignore next */ []));
7930
- const expiresIn$ = toObservable(context.bearerData).pipe(switchMap((bearerData) => bearerData
8451
+ const expiresIn = toSignal(toObservable(tokenTimes).pipe(switchMap((times) => times
7931
8452
  ? timer(0, checkInterval).pipe(map(() => {
7932
- const expiry = expiresAt();
7933
- if (!expiry)
7934
- return null;
7935
- const msUntilExpiry = expiry.getTime() - Date.now();
8453
+ const msUntilExpiry = times.expiresAtMs - Date.now();
7936
8454
  return msUntilExpiry > 0 ? msUntilExpiry : null;
7937
8455
  }))
7938
- : of(null)));
7939
- const isExpiringSoon$ = expiresIn$.pipe(map((msUntilExpiry) => {
7940
- if (msUntilExpiry === null)
8456
+ : of(null))), { initialValue: null });
8457
+ const isExpiringSoon = computed(() => {
8458
+ const msUntilExpiry = expiresIn();
8459
+ const times = tokenTimes();
8460
+ if (msUntilExpiry === null || !times || msUntilExpiry > warningThreshold)
7941
8461
  return false;
7942
- return msUntilExpiry <= warningThreshold && msUntilExpiry > 0;
7943
- }));
7944
- const expiresIn = toSignal(expiresIn$, { initialValue: null });
7945
- const isExpiringSoon = toSignal(isExpiringSoon$, { initialValue: false });
8462
+ if (!refreshConfig || !context.refreshToken())
8463
+ return true;
8464
+ const bufferMs = refreshBufferMs(refreshConfig.refreshStrategy, times.lifetimeMs);
8465
+ const msOverdue = bufferMs - msUntilExpiry;
8466
+ return msOverdue >= Math.min(RENEWAL_GRACE_MS, bufferMs / 2);
8467
+ }, /* @ts-ignore */
8468
+ ...(ngDevMode ? [{ debugName: "isExpiringSoon" }] : /* istanbul ignore next */ []));
7946
8469
  const instance = {
7947
8470
  expiresAt,
7948
8471
  expiresIn,
@@ -7995,10 +8518,11 @@ const withTokenRevocation = (config) => {
7995
8518
  ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
7996
8519
  const dropped = signal(false, /* @ts-ignore */
7997
8520
  ...(ngDevMode ? [{ debugName: "dropped" }] : /* istanbul ignore next */ []));
7998
- const follow = (read) => wrapAsObservableSignal(computed(() => {
8521
+ const watcher = createObservableSignalWatcher(context.injector);
8522
+ const follow = (read) => watcher.wrap(computed(() => {
7999
8523
  const snapshot = target();
8000
8524
  return snapshot ? read(snapshot) : null;
8001
- }), context.injector);
8525
+ }));
8002
8526
  const snapshot = {
8003
8527
  args: follow((s) => s.args()),
8004
8528
  response: follow((s) => s.response()),
@@ -8009,7 +8533,7 @@ const withTokenRevocation = (config) => {
8009
8533
  triggeredBy: follow((s) => s.triggeredBy()),
8010
8534
  id: follow((s) => s.id()),
8011
8535
  executionState: follow((s) => s.executionState()),
8012
- isAlive: wrapAsObservableSignal(computed(() => target()?.isAlive() ?? !(dropped() || queueDropped())), context.injector),
8536
+ isAlive: watcher.wrap(computed(() => target()?.isAlive() ?? !(dropped() || queueDropped()))),
8013
8537
  };
8014
8538
  const queued = {
8015
8539
  tokens,
@@ -8146,19 +8670,18 @@ const buildTrackingFeature = (context, config) => {
8146
8670
  }
8147
8671
  }
8148
8672
  };
8149
- const fireHandlers = (event, data) => {
8150
- const eventHandlers = handlers.get(event);
8151
- if (eventHandlers) {
8152
- eventHandlers.forEach((handler) => {
8153
- try {
8154
- handler(data);
8155
- }
8156
- catch (error) {
8157
- console.error(`Error in tracking event handler for "${event}":`, error);
8158
- }
8159
- });
8160
- }
8161
- };
8673
+ // Handlers run outside the reactive context of the effects that fire them, so one that ends the
8674
+ // session or creates a query does not throw NG0602.
8675
+ const fireHandlers = (event, data) => untracked(() => {
8676
+ handlers.get(event)?.forEach((handler) => {
8677
+ try {
8678
+ handler(data);
8679
+ }
8680
+ catch (error) {
8681
+ console.error(`Error in tracking event handler for "${event}":`, error);
8682
+ }
8683
+ });
8684
+ });
8162
8685
  const pendingForwardedMessages = [];
8163
8686
  const forwardingChannel = context.leaderElection && typeof BroadcastChannel !== 'undefined'
8164
8687
  ? new BroadcastChannel(`ethlete-auth-tracking:${context.name}`)
@@ -8292,6 +8815,10 @@ const buildTrackingFeature = (context, config) => {
8292
8815
  */
8293
8816
  const createTrackingFeature = (context, config) => runInInjectionContext(context.injector, () => buildTrackingFeature(context, config));
8294
8817
 
8818
+ // Generated from package.json by tools/scripts/generate-version.js - do not edit.
8819
+ /** The version of `@ethlete/query` this build was cut from. */
8820
+ const QUERY_VERSION = '6.0.0-next.55';
8821
+
8295
8822
  const ethlete = {};
8296
8823
  let app = null;
8297
8824
  /** What `window.ethlete` exposes - the same object {@link queryDevtoolsAbout} returns. */
@@ -9315,12 +9842,23 @@ const sanitizeSession = (value) => {
9315
9842
  savedAt: typeof session.savedAt === 'number' ? session.savedAt : 0,
9316
9843
  };
9317
9844
  };
9845
+ const isLocalAccount = (value) => {
9846
+ const account = value;
9847
+ return (typeof account === 'object' &&
9848
+ account !== null &&
9849
+ typeof account.id === 'string' &&
9850
+ typeof account.provider === 'string' &&
9851
+ typeof account.label === 'string' &&
9852
+ typeof account.loginQuery === 'string' &&
9853
+ typeof account.scope === 'string' &&
9854
+ (account.fields === undefined || Array.isArray(account.fields)));
9855
+ };
9318
9856
  const sanitize = (value) => ({
9319
9857
  sessions: Array.isArray(value?.sessions)
9320
9858
  ? value.sessions.map(sanitizeSession).filter((session) => session !== null)
9321
9859
  : [],
9322
9860
  credentials: typeof value?.credentials === 'object' && value.credentials !== null ? value.credentials : {},
9323
- accounts: Array.isArray(value?.accounts) ? value.accounts : [],
9861
+ accounts: Array.isArray(value?.accounts) ? value.accounts.filter(isLocalAccount) : [],
9324
9862
  });
9325
9863
  /**
9326
9864
  * The vault is one key every tab writes, so a tab holding a stale copy of it would drop whatever another
@@ -9993,246 +10531,39 @@ const resolveQueryDevtoolsFaultForAttempt = (target) => {
9993
10531
  const noteQueryFormReads = (formId, source) => {
9994
10532
  const memoized = computed(() => source(), /* @ts-ignore */
9995
10533
  ...(ngDevMode ? [{ debugName: "memoized" }] : /* istanbul ignore next */ []));
9996
- // The read must be noted per read, not per recomputation of the value: the second query to build
9997
- // args reads a memoized value, and a note that only fires on a recomputation never reaches its
9998
- // collection window. Carrying the memo's signal node over keeps this function a real `Signal`.
9999
- const read = () => {
10000
- noteQueryFormRead(formId);
10001
- return memoized();
10002
- };
10003
- return Object.assign(read, { [SIGNAL]: memoized[SIGNAL] });
10004
- };
10005
- /**
10006
- * Records which query forms feed one query's args, so the devtools can answer "which query does this
10007
- * form drive?" from the reads themselves rather than from a naming convention.
10008
- * @internal
10009
- */
10010
- const createQueryDevtoolsFormLinks = () => {
10011
- const ids = signal([], /* @ts-ignore */
10012
- ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
10013
- return {
10014
- ids: ids.asReadonly(),
10015
- track: (build) => {
10016
- const collected = collectQueryFormLinks(build);
10017
- // Args are built inside a reactive computation, which may not write to a signal - `untracked`
10018
- // lifts that restriction, and skipping the write unless something is new keeps the panel from
10019
- // being notified on every evaluation.
10020
- untracked(() => {
10021
- const known = ids();
10022
- const added = collected.ids.filter((id) => !known.includes(id));
10023
- if (added.length)
10024
- ids.set([...known, ...added]);
10025
- });
10026
- return collected.value;
10027
- },
10028
- };
10029
- };
10030
-
10031
- /**
10032
- * The designed library. Which of them is **armed** is a separate store under a separate scope - see
10033
- * {@link queryDevtoolsArmedMocks}.
10034
- */
10035
- const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
10036
- const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
10037
- const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
10038
- ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
10039
- const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
10040
- ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
10041
- const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
10042
- ...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
10043
- const scope$1 = () => queryDevtoolsSettings().mocks;
10044
- const armedScope = () => queryDevtoolsSettings().armedMocks;
10045
- /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
10046
- const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
10047
- /**
10048
- * The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
10049
- * an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
10050
- */
10051
- const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
10052
- /**
10053
- * Whether what is armed right now came back from a previous page load rather than being armed by hand.
10054
- * The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
10055
- */
10056
- const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
10057
- /**
10058
- * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
10059
- * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
10060
- */
10061
- const queryDevtoolsMockId = (parts) => `${parts.clientName}|${parts.method}|${parts.pattern}${parts.query ? `?${parts.query}` : ''}`;
10062
- const write = () => {
10063
- if (scope$1() === 'none') {
10064
- clearQueryDevtoolsStore(STORAGE_KEY$1);
10065
- return;
10066
- }
10067
- writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
10068
- };
10069
- const writeArmed = () => {
10070
- if (armedScope() === 'none') {
10071
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10072
- return;
10073
- }
10074
- writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
10075
- };
10076
- /** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
10077
- const setArmed = (next) => {
10078
- armed.set(next);
10079
- armedRestored.set(false);
10080
- writeArmed();
10081
- };
10082
- const isMock = (value) => {
10083
- const mock = value;
10084
- return (!!mock &&
10085
- typeof mock.id === 'string' &&
10086
- typeof mock.clientName === 'string' &&
10087
- typeof mock.method === 'string' &&
10088
- typeof mock.pattern === 'string' &&
10089
- typeof mock.status === 'number');
10090
- };
10091
- /**
10092
- * Reads the designed library, so a mock can be armed without authoring it again. Called by
10093
- * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
10094
- * @internal
10095
- */
10096
- const initQueryDevtoolsMocks = () => {
10097
- const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
10098
- // `query` was added after the first version of the store, so an entry written before it still loads.
10099
- mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
10100
- const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
10101
- const library = new Set(mocks().map((mock) => mock.id));
10102
- // An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
10103
- // mock that may never be designed again.
10104
- const inherited = Array.isArray(kept)
10105
- ? kept.filter((id) => typeof id === 'string' && library.has(id))
10106
- : [];
10107
- armed.set(new Set(inherited));
10108
- armedRestored.set(inherited.length > 0);
10109
- };
10110
- /**
10111
- * The path of a request URL relative to the client's `baseUrl` (base path included), without its query
10112
- * string - what a pattern is matched against. A URL outside `baseUrl`, or no `baseUrl` at all, falls
10113
- * back to the path without its origin.
10114
- */
10115
- const queryDevtoolsRequestPath = (url, baseUrl) => {
10116
- const withoutQuery = url.split('?')[0] ?? '';
10117
- const base = baseUrl?.replace(/\/+$/, '');
10118
- if (base && withoutQuery.startsWith(base)) {
10119
- const rest = withoutQuery.slice(base.length);
10120
- if (!rest)
10121
- return '/';
10122
- if (rest.startsWith('/'))
10123
- return rest;
10124
- }
10125
- const schemeEnd = withoutQuery.indexOf('://');
10126
- if (schemeEnd === -1)
10127
- return withoutQuery;
10128
- const pathStart = withoutQuery.indexOf('/', schemeEnd + 3);
10129
- return pathStart === -1 ? '/' : withoutQuery.slice(pathStart);
10130
- };
10131
- const segmentsOf$1 = (path) => path.split('/').filter(Boolean);
10132
- /** The query string of a request URL, without the `?`. */
10133
- const queryOf = (url) => url.split('?')[1] ?? '';
10134
- /**
10135
- * Whether a request carries every query parameter a mock declares. Extra parameters are ignored - a mock
10136
- * says what the request must ask for, not everything it may.
10137
- */
10138
- const matchesQueryDevtoolsMockQuery = (declared, url) => {
10139
- if (!declared)
10140
- return true;
10141
- const asked = new URLSearchParams(queryOf(url));
10142
- return [...new URLSearchParams(declared)].every(([key, value]) => asked.get(key) === value);
10143
- };
10144
- /**
10145
- * Whether a request path is the route a pattern describes. A `:param` segment matches any single
10146
- * non-empty segment, which is the whole point: a mock is armed for a route, not for the one URL a query
10147
- * happened to build.
10148
- */
10149
- const matchesQueryDevtoolsMockPattern = (pattern, path) => {
10150
- const patternSegments = segmentsOf$1(pattern);
10151
- const pathSegments = segmentsOf$1(path);
10152
- if (patternSegments.length !== pathSegments.length)
10153
- return false;
10154
- return patternSegments.every((segment, index) => segment.startsWith(':') || segment === pathSegments[index]);
10155
- };
10156
- /**
10157
- * Adds a designed mock, or replaces the one already under its id. Arming is untouched: re-designing a
10158
- * body while it is being served changes what the next request gets.
10159
- */
10160
- const saveQueryDevtoolsMock = (mock) => {
10161
- mocks.update((current) => {
10162
- const index = current.findIndex((entry) => entry.id === mock.id);
10163
- if (index === -1)
10164
- return [...current, mock];
10165
- return current.map((entry) => (entry.id === mock.id ? mock : entry));
10166
- });
10167
- write();
10168
- };
10169
- /** Removes a designed mock, disarming it on the way out. */
10170
- const deleteQueryDevtoolsMock = (id) => {
10171
- mocks.update((current) => current.filter((entry) => entry.id !== id));
10172
- armQueryDevtoolsMock(id, false);
10173
- write();
10174
- };
10175
- /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
10176
- const armQueryDevtoolsMock = (id, isArmed) => {
10177
- if (isArmed && !mocks().some((mock) => mock.id === id))
10178
- return;
10179
- const next = new Set(armed());
10180
- if (isArmed)
10181
- next.add(id);
10182
- else
10183
- next.delete(id);
10184
- setArmed(next);
10185
- };
10186
- /** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
10187
- const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
10188
- /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
10189
- const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
10190
- /**
10191
- * Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
10192
- * the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
10193
- * exactly what it was already serving.
10194
- */
10195
- const setQueryDevtoolsArmedMocksScope = (next) => {
10196
- if (next === armedScope())
10197
- return;
10198
- // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
10199
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10200
- setQueryDevtoolsSettings({ armedMocks: next });
10201
- writeArmed();
10202
- };
10203
- /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
10204
- const clearQueryDevtoolsMockStore = () => {
10205
- mocks.set([]);
10206
- armed.set(new Set());
10207
- armedRestored.set(false);
10208
- clearQueryDevtoolsStore(STORAGE_KEY$1);
10209
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10534
+ // The read must be noted per read, not per recomputation of the value: the second query to build
10535
+ // args reads a memoized value, and a note that only fires on a recomputation never reaches its
10536
+ // collection window. Carrying the memo's signal node over keeps this function a real `Signal`.
10537
+ const read = () => {
10538
+ noteQueryFormRead(formId);
10539
+ return memoized();
10540
+ };
10541
+ return Object.assign(read, { [SIGNAL]: memoized[SIGNAL] });
10210
10542
  };
10211
- const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
10212
10543
  /**
10213
- * The mock to serve one upcoming attempt, or `null` to let it reach the network. The armed mock declaring
10214
- * the most query parameters wins, so a special case armed on top of a general one is what answers.
10215
- *
10216
- * Installed as the mock resolver by `provideQueryDevtools()`; nothing else may call it.
10544
+ * Records which query forms feed one query's args, so the devtools can answer "which query does this
10545
+ * form drive?" from the reads themselves rather than from a naming convention.
10217
10546
  * @internal
10218
10547
  */
10219
- const resolveQueryDevtoolsMockForAttempt = (target) => {
10220
- const ids = armed();
10221
- if (!ids.size)
10222
- return null;
10223
- const path = queryDevtoolsRequestPath(target.url, target.baseUrl);
10224
- const mock = mocks()
10225
- .filter((candidate) => ids.has(candidate.id) &&
10226
- candidate.clientName === target.clientName &&
10227
- candidate.method === target.method &&
10228
- matchesQueryDevtoolsMockPattern(candidate.pattern, path) &&
10229
- matchesQueryDevtoolsMockQuery(candidate.query, target.url))
10230
- // The most specific armed mock answers: one that names `page=2` beats one that takes any query, so
10231
- // arming a general mock does not shadow the special case someone armed on top of it.
10232
- .sort((a, b) => declaredParamCount(b) - declaredParamCount(a))[0];
10233
- if (!mock)
10234
- return null;
10235
- return { status: mock.status, body: mock.body, latencyMs: mock.latencyMs };
10548
+ const createQueryDevtoolsFormLinks = () => {
10549
+ const ids = signal([], /* @ts-ignore */
10550
+ ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
10551
+ return {
10552
+ ids: ids.asReadonly(),
10553
+ track: (build) => {
10554
+ const collected = collectQueryFormLinks(build);
10555
+ // Args are built inside a reactive computation, which may not write to a signal - `untracked`
10556
+ // lifts that restriction, and skipping the write unless something is new keeps the panel from
10557
+ // being notified on every evaluation.
10558
+ untracked(() => {
10559
+ const known = ids();
10560
+ const added = collected.ids.filter((id) => !known.includes(id));
10561
+ if (added.length)
10562
+ ids.set([...known, ...added]);
10563
+ });
10564
+ return collected.value;
10565
+ },
10566
+ };
10236
10567
  };
10237
10568
 
10238
10569
  const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
@@ -10469,19 +10800,19 @@ const resizePagination = (value, shape, mode, amount) => {
10469
10800
  }
10470
10801
  return result;
10471
10802
  };
10472
- const STRING_PRESETS = {
10803
+ const STRING_PRESETS$1 = {
10473
10804
  short: 'Ab',
10474
10805
  long: /* @__PURE__ */ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt. '.repeat(4),
10475
10806
  longWord: 'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
10476
10807
  unicode: 'مرحبا بالعالم 👋 日本語テスト 🎉 Ñoño',
10477
10808
  };
10478
- const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS[preset]);
10479
- const NUMBER_PRESETS = {
10809
+ const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS$1[preset]);
10810
+ const NUMBER_PRESETS$1 = {
10480
10811
  zero: 0,
10481
10812
  negative: -1,
10482
10813
  huge: Number.MAX_SAFE_INTEGER,
10483
10814
  };
10484
- const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS[preset]);
10815
+ const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS$1[preset]);
10485
10816
  const pickRandom = (values) => values[Math.floor(Math.random() * values.length)];
10486
10817
  const randomIntBetween = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
10487
10818
  const randomHex = (length) => {
@@ -10697,6 +11028,30 @@ const OP_TYPES = /* @__PURE__ */ new Set([
10697
11028
  'reset',
10698
11029
  ]);
10699
11030
  const isJsonPath = (value) => Array.isArray(value) && value.every((step) => typeof step === 'string' || typeof step === 'number');
11031
+ const STRING_PRESETS = ['short', 'long', 'longWord', 'unicode', 'custom'];
11032
+ const NUMBER_PRESETS = ['zero', 'negative', 'huge', 'custom'];
11033
+ const DATE_PRESETS = ['now', 'plusDay', 'minusDay', 'farFuture', 'farPast', 'invalid'];
11034
+ const isIndex = (value) => Number.isInteger(value) && value >= 0;
11035
+ const hasReplayableFields = (op) => {
11036
+ switch (op['type']) {
11037
+ case 'set':
11038
+ return 'value' in op;
11039
+ case 'stringPreset':
11040
+ return STRING_PRESETS.includes(op['preset']) && (op['custom'] === undefined || typeof op['custom'] === 'string');
11041
+ case 'numberPreset':
11042
+ return NUMBER_PRESETS.includes(op['preset']) && (op['custom'] === undefined || Number.isFinite(op['custom']));
11043
+ case 'datePreset':
11044
+ return DATE_PRESETS.includes(op['preset']);
11045
+ case 'duplicateArrayItem':
11046
+ return isIndex(op['index']);
11047
+ case 'pasteArrayItem':
11048
+ return 'value' in op && (op['index'] === undefined || isIndex(op['index']));
11049
+ case 'paginationResize':
11050
+ return (op['mode'] === 'shrink' || op['mode'] === 'extend') && isIndex(op['amount']);
11051
+ default:
11052
+ return true;
11053
+ }
11054
+ };
10700
11055
  /**
10701
11056
  * Whether a value parsed from outside - a paste, a stored set - is an op this build can replay.
10702
11057
  * @internal
@@ -10705,7 +11060,7 @@ const isOverrideOp = (value) => {
10705
11060
  if (!value || typeof value !== 'object' || Array.isArray(value))
10706
11061
  return false;
10707
11062
  const op = value;
10708
- return typeof op.type === 'string' && OP_TYPES.has(op.type) && isJsonPath(op.path);
11063
+ return (typeof op['type'] === 'string' && OP_TYPES.has(op['type']) && isJsonPath(op['path']) && hasReplayableFields(op));
10709
11064
  };
10710
11065
  /**
10711
11066
  * Serializes the ops armed on one query into clipboard text. `reset` never reaches a recorder's list, so
@@ -10771,12 +11126,12 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
10771
11126
  * debugging the wrong thing. `local` is offered in Settings for the dev who has a reason, and the
10772
11127
  * restored-overrides bar names the scope whatever it is.
10773
11128
  */
10774
- const STORAGE_KEY = 'ethlete:query:devtools:overrides:v1';
11129
+ const STORAGE_KEY$1 = 'ethlete:query:devtools:overrides:v1';
10775
11130
  const restored = /* @__PURE__ */ signal([], /* @ts-ignore */
10776
11131
  ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
10777
11132
  const restoredScope = /* @__PURE__ */ signal(null, /* @ts-ignore */
10778
11133
  ...(ngDevMode ? [{ debugName: "restoredScope" }] : /* istanbul ignore next */ []));
10779
- const scope = () => queryDevtoolsSettings().overrides;
11134
+ const scope$1 = () => queryDevtoolsSettings().overrides;
10780
11135
  /** Where "Keep across reloads" puts them back when it is switched on again. */
10781
11136
  let lastPersistentScope = 'session';
10782
11137
  /** Every recorder that currently holds at least one op, so switching persistence on can capture them. */
@@ -10787,7 +11142,7 @@ const carriedOver = /* @__PURE__ */ new Map();
10787
11142
  const replayed = /* @__PURE__ */ new Set();
10788
11143
  let ops = {};
10789
11144
  const readStore = () => {
10790
- const parsed = readQueryDevtoolsStore(scope(), STORAGE_KEY);
11145
+ const parsed = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
10791
11146
  const stored = parsed?.ops;
10792
11147
  const usable = !!stored && typeof stored === 'object' && !Array.isArray(stored);
10793
11148
  const valid = {};
@@ -10799,11 +11154,11 @@ const readStore = () => {
10799
11154
  return { enabled: parsed?.enabled === true, ops: valid };
10800
11155
  };
10801
11156
  const writeStore = () => {
10802
- if (scope() === 'none') {
10803
- clearQueryDevtoolsStore(STORAGE_KEY);
11157
+ if (scope$1() === 'none') {
11158
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
10804
11159
  return;
10805
11160
  }
10806
- writeQueryDevtoolsStore(scope(), STORAGE_KEY, { enabled: true, ops });
11161
+ writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, { enabled: true, ops });
10807
11162
  };
10808
11163
  const publishRestored = () => restored.set([...carriedOver].map(([id, count]) => ({ id, count, armed: replayed.has(id) })));
10809
11164
  const forgetCarriedOver = () => {
@@ -10836,7 +11191,7 @@ const initQueryDevtoolsOverridePersistence = () => {
10836
11191
  if (list.length)
10837
11192
  carriedOver.set(id, list.length);
10838
11193
  }
10839
- const current = scope();
11194
+ const current = scope$1();
10840
11195
  if (current !== 'none')
10841
11196
  lastPersistentScope = current;
10842
11197
  restoredScope.set(carriedOver.size ? current : null);
@@ -10846,7 +11201,7 @@ const initQueryDevtoolsOverridePersistence = () => {
10846
11201
  * Whether armed response overrides are stored and replayed on the next page load - that is, whether
10847
11202
  * their scope is anything other than `none`. Off by default.
10848
11203
  */
10849
- const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope() !== 'none', /* @ts-ignore */
11204
+ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope$1() !== 'none', /* @ts-ignore */
10850
11205
  ...(ngDevMode ? [{ debugName: "queryDevtoolsOverridePersistence" }] : /* istanbul ignore next */ []));
10851
11206
  /**
10852
11207
  * Where armed response overrides are kept. Switching to a scope that keeps them captures whatever is
@@ -10855,11 +11210,11 @@ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope()
10855
11210
  * op exactly where it is for the rest of this page.
10856
11211
  */
10857
11212
  const setQueryDevtoolsOverridesScope = (next) => {
10858
- const previous = scope();
11213
+ const previous = scope$1();
10859
11214
  if (next === previous)
10860
11215
  return;
10861
11216
  // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
10862
- clearQueryDevtoolsStore(STORAGE_KEY);
11217
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
10863
11218
  setQueryDevtoolsSettings({ overrides: next });
10864
11219
  if (next === 'none') {
10865
11220
  ops = {};
@@ -10878,106 +11233,309 @@ const setQueryDevtoolsOverridesScope = (next) => {
10878
11233
  writeStore();
10879
11234
  };
10880
11235
  /**
10881
- * Turns persistence on or off - the panel's "Keep across reloads" toggle. On restores the last scope
10882
- * that kept them (`session` unless Settings picked `local`).
11236
+ * Turns persistence on or off - the panel's "Keep across reloads" toggle. On restores the last scope
11237
+ * that kept them (`session` unless Settings picked `local`).
11238
+ */
11239
+ const setQueryDevtoolsOverridePersistence = (value) => {
11240
+ setQueryDevtoolsOverridesScope(value ? lastPersistentScope : 'none');
11241
+ };
11242
+ /**
11243
+ * Empties the override store in both browser stores and disarms everything a reload brought back,
11244
+ * without changing where overrides are kept - the panel's "Reset devtools".
11245
+ */
11246
+ const clearQueryDevtoolsOverrideStore = () => {
11247
+ clearRestoredQueryDevtoolsOverrides();
11248
+ ops = {};
11249
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
11250
+ };
11251
+ /**
11252
+ * The ops this page load inherited from the previous one, and for each id whether a live query has taken
11253
+ * it. Overrides that outlive the page that armed them have to announce themselves, because an app being
11254
+ * lied to looks exactly like an app that is broken.
11255
+ */
11256
+ const restoredQueryDevtoolsOverrides =
11257
+ /* @__PURE__ */ restored.asReadonly();
11258
+ /**
11259
+ * The scope those ops came back from, or `null` when this page inherited none. `local` is the one worth
11260
+ * naming in the banner: it means they outlived closing the tab, not just a reload.
11261
+ */
11262
+ const queryDevtoolsRestoredOverridesScope =
11263
+ /* @__PURE__ */ restoredScope.asReadonly();
11264
+ /** Disarms every op this page load inherited and empties the store - the banner's one-click way out. */
11265
+ const clearRestoredQueryDevtoolsOverrides = () => {
11266
+ for (const id of replayed) {
11267
+ armedRecorders.get(id)?.clearAll();
11268
+ armedRecorders.delete(id);
11269
+ }
11270
+ for (const id of carriedOver.keys())
11271
+ writeOps(id, []);
11272
+ forgetCarriedOver();
11273
+ };
11274
+ /**
11275
+ * Forgets a destroyed query's recorder. Ops already persisted stay in the store on purpose - they are
11276
+ * keyed by an id the next page load re-derives, which is what "Keep across reloads" replays them from.
11277
+ * @internal
11278
+ */
11279
+ const releaseQueryDevtoolsOverridePersistence = (id) => {
11280
+ armedRecorders.delete(id);
11281
+ if (scope$1() !== 'none')
11282
+ return;
11283
+ const { [id]: _dropped, ...rest } = ops;
11284
+ ops = rest;
11285
+ };
11286
+ /**
11287
+ * Wraps a query's overrides recorder so its armed ops are stored and replayed. Replay happens here, at
11288
+ * registration, rather than in the panel - a query can run long before the panel is ever opened.
11289
+ *
11290
+ * Ids are reload-deterministic (descriptor + per-descriptor counter), so the same queries created in the
11291
+ * same order re-arm correctly; an id the store holds that nobody claims is reported by
11292
+ * {@link restoredQueryDevtoolsOverrides} rather than guessed at.
11293
+ * @internal
11294
+ */
11295
+ const withQueryDevtoolsOverridePersistence = (id, recorder) => {
11296
+ const inherited = ops[id];
11297
+ if (scope$1() !== 'none' && inherited?.length) {
11298
+ for (const op of inherited)
11299
+ recorder.arm(op);
11300
+ armedRecorders.set(id, recorder);
11301
+ replayed.add(id);
11302
+ publishRestored();
11303
+ }
11304
+ const sync = () => {
11305
+ const list = recorder.list().map((entry) => entry.op);
11306
+ if (list.length)
11307
+ armedRecorders.set(id, recorder);
11308
+ else
11309
+ armedRecorders.delete(id);
11310
+ if (scope$1() !== 'none')
11311
+ writeOps(id, list);
11312
+ };
11313
+ return {
11314
+ ...recorder,
11315
+ arm: (op) => {
11316
+ recorder.arm(op);
11317
+ sync();
11318
+ },
11319
+ clear: (opId) => {
11320
+ recorder.clear(opId);
11321
+ sync();
11322
+ },
11323
+ clearAll: () => {
11324
+ recorder.clearAll();
11325
+ if (replayed.delete(id))
11326
+ publishRestored();
11327
+ sync();
11328
+ },
11329
+ };
11330
+ };
11331
+
11332
+ /**
11333
+ * The designed library. Which of them is **armed** is a separate store under a separate scope - see
11334
+ * {@link queryDevtoolsArmedMocks}.
11335
+ */
11336
+ const STORAGE_KEY = 'ethlete:query:devtools:mocks:v1';
11337
+ const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
11338
+ const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
11339
+ ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
11340
+ const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
11341
+ ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
11342
+ const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
11343
+ ...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
11344
+ const scope = () => queryDevtoolsSettings().mocks;
11345
+ const armedScope = () => queryDevtoolsSettings().armedMocks;
11346
+ /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
11347
+ const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
11348
+ /**
11349
+ * The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
11350
+ * an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
11351
+ */
11352
+ const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
11353
+ /**
11354
+ * Whether what is armed right now came back from a previous page load rather than being armed by hand.
11355
+ * The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
11356
+ */
11357
+ const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
11358
+ /**
11359
+ * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
11360
+ * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
11361
+ */
11362
+ const queryDevtoolsMockId = (parts) => `${parts.clientName}|${parts.method}|${parts.pattern}${parts.query ? `?${parts.query}` : ''}`;
11363
+ const write = () => {
11364
+ if (scope() === 'none') {
11365
+ clearQueryDevtoolsStore(STORAGE_KEY);
11366
+ return;
11367
+ }
11368
+ writeQueryDevtoolsStore(scope(), STORAGE_KEY, mocks());
11369
+ };
11370
+ const writeArmed = () => {
11371
+ if (armedScope() === 'none') {
11372
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
11373
+ return;
11374
+ }
11375
+ writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
11376
+ };
11377
+ /** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
11378
+ const setArmed = (next) => {
11379
+ armed.set(next);
11380
+ armedRestored.set(false);
11381
+ writeArmed();
11382
+ };
11383
+ const isMock = (value) => {
11384
+ const mock = value;
11385
+ return (!!mock &&
11386
+ typeof mock.id === 'string' &&
11387
+ typeof mock.clientName === 'string' &&
11388
+ typeof mock.method === 'string' &&
11389
+ typeof mock.pattern === 'string' &&
11390
+ typeof mock.status === 'number');
11391
+ };
11392
+ /**
11393
+ * Reads the designed library, so a mock can be armed without authoring it again. Called by
11394
+ * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
11395
+ * @internal
11396
+ */
11397
+ const initQueryDevtoolsMocks = () => {
11398
+ const stored = readQueryDevtoolsStore(scope(), STORAGE_KEY);
11399
+ // `query` was added after the first version of the store, so an entry written before it still loads.
11400
+ mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
11401
+ const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
11402
+ const library = new Set(mocks().map((mock) => mock.id));
11403
+ // An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
11404
+ // mock that may never be designed again.
11405
+ const inherited = Array.isArray(kept)
11406
+ ? kept.filter((id) => typeof id === 'string' && library.has(id))
11407
+ : [];
11408
+ armed.set(new Set(inherited));
11409
+ armedRestored.set(inherited.length > 0);
11410
+ };
11411
+ /**
11412
+ * The path of a request URL relative to the client's `baseUrl` (base path included), without its query
11413
+ * string - what a pattern is matched against. A URL outside `baseUrl`, or no `baseUrl` at all, falls
11414
+ * back to the path without its origin.
11415
+ */
11416
+ const queryDevtoolsRequestPath = (url, baseUrl) => {
11417
+ const withoutQuery = url.split('?')[0] ?? '';
11418
+ const base = baseUrl?.replace(/\/+$/, '');
11419
+ if (base && withoutQuery.startsWith(base)) {
11420
+ const rest = withoutQuery.slice(base.length);
11421
+ if (!rest)
11422
+ return '/';
11423
+ if (rest.startsWith('/'))
11424
+ return rest;
11425
+ }
11426
+ const schemeEnd = withoutQuery.indexOf('://');
11427
+ if (schemeEnd === -1)
11428
+ return withoutQuery;
11429
+ const pathStart = withoutQuery.indexOf('/', schemeEnd + 3);
11430
+ return pathStart === -1 ? '/' : withoutQuery.slice(pathStart);
11431
+ };
11432
+ const segmentsOf$1 = (path) => path.split('/').filter(Boolean);
11433
+ /** The query string of a request URL, without the `?`. */
11434
+ const queryOf = (url) => url.split('?')[1] ?? '';
11435
+ /**
11436
+ * Whether a request carries every query parameter a mock declares. Extra parameters are ignored - a mock
11437
+ * says what the request must ask for, not everything it may.
10883
11438
  */
10884
- const setQueryDevtoolsOverridePersistence = (value) => {
10885
- setQueryDevtoolsOverridesScope(value ? lastPersistentScope : 'none');
11439
+ const matchesQueryDevtoolsMockQuery = (declared, url) => {
11440
+ if (!declared)
11441
+ return true;
11442
+ const asked = new URLSearchParams(queryOf(url));
11443
+ return [...new URLSearchParams(declared)].every(([key, value]) => asked.getAll(key).includes(value));
10886
11444
  };
10887
11445
  /**
10888
- * Empties the override store in both browser stores and disarms everything a reload brought back,
10889
- * without changing where overrides are kept - the panel's "Reset devtools".
11446
+ * Whether a request path is the route a pattern describes. A `:param` segment matches any single
11447
+ * non-empty segment, which is the whole point: a mock is armed for a route, not for the one URL a query
11448
+ * happened to build.
10890
11449
  */
10891
- const clearQueryDevtoolsOverrideStore = () => {
10892
- clearRestoredQueryDevtoolsOverrides();
10893
- ops = {};
10894
- clearQueryDevtoolsStore(STORAGE_KEY);
11450
+ const matchesQueryDevtoolsMockPattern = (pattern, path) => {
11451
+ const patternSegments = segmentsOf$1(pattern);
11452
+ const pathSegments = segmentsOf$1(path);
11453
+ if (patternSegments.length !== pathSegments.length)
11454
+ return false;
11455
+ return patternSegments.every((segment, index) => segment.startsWith(':') || segment === pathSegments[index]);
10895
11456
  };
10896
11457
  /**
10897
- * The ops this page load inherited from the previous one, and for each id whether a live query has taken
10898
- * it. Overrides that outlive the page that armed them have to announce themselves, because an app being
10899
- * lied to looks exactly like an app that is broken.
10900
- */
10901
- const restoredQueryDevtoolsOverrides =
10902
- /* @__PURE__ */ restored.asReadonly();
10903
- /**
10904
- * The scope those ops came back from, or `null` when this page inherited none. `local` is the one worth
10905
- * naming in the banner: it means they outlived closing the tab, not just a reload.
11458
+ * Adds a designed mock, or replaces the one already under its id. Arming is untouched: re-designing a
11459
+ * body while it is being served changes what the next request gets.
10906
11460
  */
10907
- const queryDevtoolsRestoredOverridesScope =
10908
- /* @__PURE__ */ restoredScope.asReadonly();
10909
- /** Disarms every op this page load inherited and empties the store - the banner's one-click way out. */
10910
- const clearRestoredQueryDevtoolsOverrides = () => {
10911
- for (const id of replayed) {
10912
- armedRecorders.get(id)?.clearAll();
10913
- armedRecorders.delete(id);
10914
- }
10915
- for (const id of carriedOver.keys())
10916
- writeOps(id, []);
10917
- forgetCarriedOver();
11461
+ const saveQueryDevtoolsMock = (mock) => {
11462
+ mocks.update((current) => {
11463
+ const index = current.findIndex((entry) => entry.id === mock.id);
11464
+ if (index === -1)
11465
+ return [...current, mock];
11466
+ return current.map((entry) => (entry.id === mock.id ? mock : entry));
11467
+ });
11468
+ write();
11469
+ };
11470
+ /** Removes a designed mock, disarming it on the way out. */
11471
+ const deleteQueryDevtoolsMock = (id) => {
11472
+ mocks.update((current) => current.filter((entry) => entry.id !== id));
11473
+ armQueryDevtoolsMock(id, false);
11474
+ write();
10918
11475
  };
11476
+ /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
11477
+ const armQueryDevtoolsMock = (id, isArmed) => {
11478
+ if (isArmed && !mocks().some((mock) => mock.id === id))
11479
+ return;
11480
+ const next = new Set(armed());
11481
+ if (isArmed)
11482
+ next.add(id);
11483
+ else
11484
+ next.delete(id);
11485
+ setArmed(next);
11486
+ };
11487
+ /** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
11488
+ const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
11489
+ /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
11490
+ const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
10919
11491
  /**
10920
- * Forgets a destroyed query's recorder. Ops already persisted stay in the store on purpose - they are
10921
- * keyed by an id the next page load re-derives, which is what "Keep across reloads" replays them from.
10922
- * @internal
11492
+ * Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
11493
+ * the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
11494
+ * exactly what it was already serving.
10923
11495
  */
10924
- const releaseQueryDevtoolsOverridePersistence = (id) => {
10925
- armedRecorders.delete(id);
10926
- if (scope() !== 'none')
11496
+ const setQueryDevtoolsArmedMocksScope = (next) => {
11497
+ if (next === armedScope())
10927
11498
  return;
10928
- const { [id]: _dropped, ...rest } = ops;
10929
- ops = rest;
11499
+ // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
11500
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
11501
+ setQueryDevtoolsSettings({ armedMocks: next });
11502
+ writeArmed();
11503
+ };
11504
+ /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
11505
+ const clearQueryDevtoolsMockStore = () => {
11506
+ mocks.set([]);
11507
+ armed.set(new Set());
11508
+ armedRestored.set(false);
11509
+ clearQueryDevtoolsStore(STORAGE_KEY);
11510
+ clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10930
11511
  };
11512
+ const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
10931
11513
  /**
10932
- * Wraps a query's overrides recorder so its armed ops are stored and replayed. Replay happens here, at
10933
- * registration, rather than in the panel - a query can run long before the panel is ever opened.
11514
+ * The mock to serve one upcoming attempt, or `null` to let it reach the network. The armed mock declaring
11515
+ * the most query parameters wins, so a special case armed on top of a general one is what answers.
10934
11516
  *
10935
- * Ids are reload-deterministic (descriptor + per-descriptor counter), so the same queries created in the
10936
- * same order re-arm correctly; an id the store holds that nobody claims is reported by
10937
- * {@link restoredQueryDevtoolsOverrides} rather than guessed at.
11517
+ * Installed as the mock resolver by `provideQueryDevtools()`; nothing else may call it.
10938
11518
  * @internal
10939
11519
  */
10940
- const withQueryDevtoolsOverridePersistence = (id, recorder) => {
10941
- const inherited = ops[id];
10942
- if (scope() !== 'none' && inherited?.length) {
10943
- for (const op of inherited)
10944
- recorder.arm(op);
10945
- armedRecorders.set(id, recorder);
10946
- replayed.add(id);
10947
- publishRestored();
10948
- }
10949
- const sync = () => {
10950
- const list = recorder.list().map((entry) => entry.op);
10951
- if (list.length)
10952
- armedRecorders.set(id, recorder);
10953
- else
10954
- armedRecorders.delete(id);
10955
- if (scope() !== 'none')
10956
- writeOps(id, list);
10957
- };
10958
- return {
10959
- ...recorder,
10960
- arm: (op) => {
10961
- recorder.arm(op);
10962
- sync();
10963
- },
10964
- clear: (opId) => {
10965
- recorder.clear(opId);
10966
- sync();
10967
- },
10968
- clearAll: () => {
10969
- recorder.clearAll();
10970
- if (replayed.delete(id))
10971
- publishRestored();
10972
- sync();
10973
- },
10974
- };
11520
+ const resolveQueryDevtoolsMockForAttempt = (target) => {
11521
+ const ids = armed();
11522
+ if (!ids.size)
11523
+ return null;
11524
+ const path = queryDevtoolsRequestPath(target.url, target.baseUrl);
11525
+ const mock = mocks()
11526
+ .filter((candidate) => ids.has(candidate.id) &&
11527
+ candidate.clientName === target.clientName &&
11528
+ candidate.method === target.method &&
11529
+ matchesQueryDevtoolsMockPattern(candidate.pattern, path) &&
11530
+ matchesQueryDevtoolsMockQuery(candidate.query, target.url))
11531
+ // The most specific armed mock answers: one that names `page=2` beats one that takes any query, so
11532
+ // arming a general mock does not shadow the special case someone armed on top of it.
11533
+ .sort((a, b) => declaredParamCount(b) - declaredParamCount(a))[0];
11534
+ if (!mock)
11535
+ return null;
11536
+ return { status: mock.status, body: mock.body, latencyMs: mock.latencyMs };
10975
11537
  };
10976
11538
 
10977
- // Generated from package.json by tools/scripts/generate-version.js - do not edit.
10978
- /** The version of `@ethlete/query` this build was cut from. */
10979
- const QUERY_VERSION = '6.0.0-next.53';
10980
-
10981
11539
  /** How deep a schema is expanded before a nested branch is given up on. */
10982
11540
  const MAX_DEPTH = 12;
10983
11541
  /** How many elements are generated for an array, whatever its `minItems` says. */
@@ -11196,9 +11754,7 @@ const stringSample = (node, schema, ctx) => {
11196
11754
  if (ctx.style === 'stress')
11197
11755
  return fitLength(generateQueryDevtoolsStringPreset(pickStressStringPreset()), schema);
11198
11756
  const title = schema['title'];
11199
- if (typeof title === 'string' && title)
11200
- return title;
11201
- return node.key ?? 'string';
11757
+ return fitLength(typeof title === 'string' && title ? title : (node.key ?? 'string'), schema);
11202
11758
  };
11203
11759
  const boundsOf = (schema) => {
11204
11760
  const exclusiveMin = finiteNumber(schema['exclusiveMinimum']);
@@ -11218,9 +11774,9 @@ const clampToBounds = (value, bounds) => {
11218
11774
  const numberSample = (schema, ctx) => {
11219
11775
  const bounds = boundsOf(schema);
11220
11776
  if (ctx.style === 'placeholder')
11221
- return bounds.min ?? 0;
11777
+ return clampToBounds(bounds.min ?? 0, bounds);
11222
11778
  if (ctx.style === 'realistic') {
11223
- return generateQueryDevtoolsSampleNumber({ ...bounds, fractional: typeOf(schema) === 'number' });
11779
+ return clampToBounds(generateQueryDevtoolsSampleNumber({ ...bounds, fractional: typeOf(schema) === 'number' }), bounds);
11224
11780
  }
11225
11781
  const allowsNegative = bounds.min === null || bounds.min < 0;
11226
11782
  return clampToBounds(generateQueryDevtoolsNumberPreset(allowsNegative && Math.random() < 0.5 ? 'negative' : 'huge'), bounds);
@@ -11661,7 +12217,8 @@ const queryDevtoolsResponseHistory = () => queryDevtoolsSettings().responseHisto
11661
12217
  * @internal
11662
12218
  */
11663
12219
  const setQueryDevtoolsResponseHistory = (count) => {
11664
- providedResponseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
12220
+ providedResponseHistory =
12221
+ count === undefined || Number.isNaN(count) ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
11665
12222
  };
11666
12223
  /** The index of the newest run still in flight, or -1 when none is. */
11667
12224
  const lastPendingRunIndex = (runs) => {
@@ -12347,7 +12904,7 @@ const provideQueryDevtools = (options) => {
12347
12904
  return makeEnvironmentProviders([settleThePillsOnFirstRender()]);
12348
12905
  };
12349
12906
 
12350
- const getOpName = /\b(?:query|mutation)\s+([\w-]+)(?:\s*\([^)]*\))?\s*\{/;
12907
+ const getOpName = /\b(?:query|mutation)\s+([\w-]+)(?:\s*\([^)]*\))?(?:\s*@\w+(?:\s*\([^)]*\))?)*\s*\{/;
12351
12908
  const STRING_LITERAL = '"""[\\s\\S]*?"""|"(?:[^"\\\\\\n]|\\\\.)*"';
12352
12909
  const commentOutsideString = /* @__PURE__ */ new RegExp(`(${STRING_LITERAL})|#[^\\n\\r]*`, 'g');
12353
12910
  const whitespaceOutsideString = /* @__PURE__ */ new RegExp(`(${STRING_LITERAL})|\\s+`, 'g');
@@ -12355,6 +12912,19 @@ const minifyGql = (document) => document
12355
12912
  .replace(commentOutsideString, (_match, literal) => literal ?? '')
12356
12913
  .replace(whitespaceOutsideString, (_match, literal) => literal ?? ' ')
12357
12914
  .trim();
12915
+ /** Blanks everything inside braces, so a field named `query` or `mutation` cannot pass for an operation. */
12916
+ const topLevelOnly = (document) => {
12917
+ let depth = 0;
12918
+ let result = '';
12919
+ for (const char of document) {
12920
+ if (char === '}')
12921
+ depth = Math.max(depth - 1, 0);
12922
+ result += depth === 0 ? char : ' ';
12923
+ if (char === '{')
12924
+ depth++;
12925
+ }
12926
+ return result;
12927
+ };
12358
12928
  /**
12359
12929
  * Builds the GraphQL-over-HTTP payload of a document. `variables` comes back as a JSON string for
12360
12930
  * the `GET` transport (the default), where the payload travels as query parameters, and as the map
@@ -12362,7 +12932,7 @@ const minifyGql = (document) => document
12362
12932
  */
12363
12933
  const transformGql = (str) => {
12364
12934
  const normalizedStr = Array.isArray(str) ? str.join('') : str;
12365
- const operationName = getOpName.exec(normalizedStr.replace(commentOutsideString, (_match, literal) => (literal ? '""' : '')))?.[1];
12935
+ const operationName = getOpName.exec(topLevelOnly(normalizedStr.replace(commentOutsideString, (_match, literal) => (literal ? '""' : ''))))?.[1];
12366
12936
  let minified;
12367
12937
  return (variables, transport = 'GET') => {
12368
12938
  const data = { query: isDevMode() ? normalizedStr : (minified ??= minifyGql(normalizedStr)) };
@@ -12403,9 +12973,11 @@ const createGqlExecuteFn = (executeOptions) => {
12403
12973
  const executeState = setupQueryExecuteState();
12404
12974
  const circularChecker = circularQueryDependencyChecker();
12405
12975
  const aborter = createQueryExecutionAborter(executeOptions.state);
12406
- const reset = () => resetExecuteState({ executeState, executeOptions });
12407
- const exec = (executeArgs) => {
12976
+ const reset = () => untracked(() => resetExecuteState({ executeState, executeOptions }));
12977
+ const exec = (executeArgs) => untracked(() => {
12408
12978
  const { args = executeOptions.state.args(), options } = executeArgs ?? {};
12979
+ if (skipParkedExecution(executeOptions.state, args, executeOptions.creator?.route))
12980
+ return;
12409
12981
  circularChecker.check(args);
12410
12982
  let gqlParams = gqlTransformerFor(executeOptions.creatorInternals)(args?.variables, executeOptions.creatorInternals.transport);
12411
12983
  if (args?.queryParams && executeOptions.creatorInternals.transport === 'GET') {
@@ -12445,7 +13017,7 @@ const createGqlExecuteFn = (executeOptions) => {
12445
13017
  options,
12446
13018
  isRefreshable: executeOptions.creatorInternals.method === 'QUERY',
12447
13019
  });
12448
- };
13020
+ });
12449
13021
  exec['reset'] = reset;
12450
13022
  exec['abort'] = () => aborter.abort();
12451
13023
  exec['aborted$'] = aborter.aborted$;
@@ -12495,6 +13067,7 @@ const createSecureGqlExecuteFn = (executeOptions) => {
12495
13067
  authProvider,
12496
13068
  deps: executeOptions.deps,
12497
13069
  state: executeOptions.state,
13070
+ route: executeOptions.creator?.route,
12498
13071
  autoExecutes: shouldAutoExecuteGqlQuery(executeOptions.creatorInternals.method) &&
12499
13072
  !executeOptions.queryConfig.onlyManualExecution,
12500
13073
  transformAuthAndExec: (executeArgs, executeState) => {
@@ -13059,6 +13632,15 @@ const request = (config) => {
13059
13632
  });
13060
13633
  };
13061
13634
 
13635
+ /**
13636
+ * Destroying a legacy query tears down the underlying query's injector, which cancels its request - so
13637
+ * a superseded or released query that was not aborted (a mutation the server may already have accepted)
13638
+ * is torn down only once it has settled. `state$` completes with the query, so this ends either way.
13639
+ */
13640
+ const destroyOnceSettled = (query) => {
13641
+ query.state$.pipe(takeUntilResponse()).subscribe({ complete: () => query.destroy() });
13642
+ };
13643
+
13062
13644
  let nextQueryContainerOwnerId = 0;
13063
13645
  const queryContainerOwnerIds = /* @__PURE__ */ new WeakMap();
13064
13646
  const queryContainerOwnerId = (injector) => {
@@ -13070,14 +13652,6 @@ const queryContainerOwnerId = (injector) => {
13070
13652
  queryContainerOwnerIds.set(injector, id);
13071
13653
  return id;
13072
13654
  };
13073
- /**
13074
- * Destroying a legacy query tears down the underlying query's injector, which cancels its request - so
13075
- * a superseded query that was not aborted (a mutation the server may already have accepted) is torn
13076
- * down only once it has settled. `state$` completes with the query, so this ends either way.
13077
- */
13078
- const destroyOnceSettled = (query) => {
13079
- query.state$.pipe(takeUntilResponse()).subscribe({ complete: () => query.destroy() });
13080
- };
13081
13655
  /**
13082
13656
  * @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.
13083
13657
  */
@@ -13153,7 +13727,14 @@ const addQueryContainerHandling = (obs, valueFn, config) => {
13153
13727
  if ((q?.canBeCached && abortOnDestroy === undefined) || abortOnDestroy) {
13154
13728
  q?.abort();
13155
13729
  }
13156
- q?.destroy?.();
13730
+ const legacyQuery = q;
13731
+ if (!legacyQuery?.destroy)
13732
+ return;
13733
+ if (isQueryStateLoading(q?.rawState)) {
13734
+ destroyOnceSettled(legacyQuery);
13735
+ return;
13736
+ }
13737
+ legacyQuery.destroy();
13157
13738
  };
13158
13739
  if (isQuery(query)) {
13159
13740
  handleQuery(query);
@@ -13918,6 +14499,10 @@ class LegacyQuery {
13918
14499
  // NG0205 ("Injector has already been destroyed") from inside the view's cleanup phase.
13919
14500
  this.newQuery.subtle.destroyRef.onDestroy(() => this.teardown());
13920
14501
  }
14502
+ /** The full URL of the request the underlying query is bound to, `null` before the first execution. */
14503
+ get url() {
14504
+ return this.newQuery.url();
14505
+ }
13921
14506
  get rawState() {
13922
14507
  return this.toQueryState(this.newQuery.executionState());
13923
14508
  }
@@ -14176,9 +14761,10 @@ const createInertQuery = () => {
14176
14761
  ...base,
14177
14762
  execute: noop,
14178
14763
  createSnapshot,
14764
+ url: inertSignal(null),
14179
14765
  reset: noop,
14180
14766
  abort: () => false,
14181
- asReadonly: () => ({ ...base, createSnapshot }),
14767
+ asReadonly: () => ({ ...base, createSnapshot, url: inertSignal(null) }),
14182
14768
  subtle: {
14183
14769
  destroy: noop,
14184
14770
  setResponse: noop,
@@ -14204,6 +14790,35 @@ const isInjectorUsable = (injector) => {
14204
14790
  return false;
14205
14791
  }
14206
14792
  };
14793
+ /**
14794
+ * The scope ends when its owner dies with no request in flight, when that request settles, or when the
14795
+ * owner's nearest environment injector dies - a destroyed injector does not destroy the environment
14796
+ * injectors made with `createEnvironmentInjector` below it.
14797
+ */
14798
+ const createSettleScope = (owner) => {
14799
+ const anchor = owner.get(EnvironmentInjector);
14800
+ const scope = createEnvironmentInjector([], anchor);
14801
+ const end = () => {
14802
+ if (!scope.destroyed)
14803
+ scope.destroy();
14804
+ };
14805
+ const bind = (query, queryInjector) => {
14806
+ const stopOnAnchorDestroy = anchor.get(DestroyRef).onDestroy(end);
14807
+ const stopOnOwnerDestroy = owner.get(DestroyRef).onDestroy(() => {
14808
+ if (isQueryStateLoading(untracked(() => query.rawState))) {
14809
+ query.state$.pipe(takeUntilResponse()).subscribe({ complete: end });
14810
+ return;
14811
+ }
14812
+ end();
14813
+ });
14814
+ queryInjector.get(DestroyRef).onDestroy(() => {
14815
+ stopOnAnchorDestroy();
14816
+ stopOnOwnerDestroy();
14817
+ end();
14818
+ });
14819
+ };
14820
+ return { destroyRef: scope.get(DestroyRef), bind };
14821
+ };
14207
14822
  /**
14208
14823
  * Angular's NG0203, thrown by `inject()` outside an injection context. Matched on the code rather than
14209
14824
  * the message: production builds strip the message, and NG0205 ("Injector has already been destroyed")
@@ -14278,14 +14893,17 @@ class LegacyQueryCreator {
14278
14893
  }
14279
14894
  return runInInjectionContext(injector, () => {
14280
14895
  return untracked(() => {
14896
+ const settleScope = this.canBeCached ? null : createSettleScope(injector);
14281
14897
  const newQuery = this.options.creator({
14282
14898
  onlyManualExecution: true,
14283
14899
  injector,
14900
+ ...(settleScope ? { scopeDestroyRef: settleScope.destroyRef } : {}),
14284
14901
  silenceMissingWithArgsFeatureError: true,
14285
14902
  // `LegacyQuery.execute()` forwards v2's `skipCache` as `allowCache` for every method, cacheable or not.
14286
14903
  silenceUncacheableAllowCacheError: true,
14287
14904
  });
14288
14905
  const legacyQuery = new LegacyQuery(newQuery, queryArgs, this.options.entity, false, this.canBeCached);
14906
+ settleScope?.bind(legacyQuery, newQuery.subtle.injector);
14289
14907
  if (args?.config?.destroyOnResponse) {
14290
14908
  // Owned by the query's own injector, not the caller's. Call sites are told to pass an injector
14291
14909
  // that outlives them, so they hand over a root or environment one - and an effect that only
@@ -15490,9 +16108,16 @@ class InfinityQuery {
15490
16108
  destroyQueries() {
15491
16109
  for (const query of this.queries$.value) {
15492
16110
  query.stopPolling();
15493
- query.abort();
15494
- if ('destroy' in query && typeof query.destroy === 'function')
16111
+ if (query.canBeCached)
16112
+ query.abort();
16113
+ if (!('destroy' in query) || typeof query.destroy !== 'function')
16114
+ continue;
16115
+ if (!query.canBeCached && isQueryStateLoading(query.rawState)) {
16116
+ destroyOnceSettled(query);
16117
+ }
16118
+ else {
15495
16119
  query.destroy();
16120
+ }
15496
16121
  }
15497
16122
  }
15498
16123
  prepareArgs(config, page) {
@@ -15818,7 +16443,8 @@ class QueryDirective {
15818
16443
  this.queryState = queryStateSignal(this.query);
15819
16444
  inject(ViewContainerRef).createEmbeddedView(inject(TemplateRef), this.viewContext);
15820
16445
  effect(() => {
15821
- const query = extractQuery(this.query());
16446
+ const rawQuery = this.query();
16447
+ const query = extractQuery(rawQuery);
15822
16448
  untracked(() => {
15823
16449
  if (!query) {
15824
16450
  this.viewContext.$implicit = null;
@@ -15834,7 +16460,7 @@ class QueryDirective {
15834
16460
  if (isQueryStatePrepared(query.rawState) || isQueryStateCancelled(query.rawState)) {
15835
16461
  query.execute();
15836
16462
  }
15837
- this.viewContext.scope = isQueryCollection(query) ? query.type : null;
16463
+ this.viewContext.scope = isQueryCollection(rawQuery) ? rawQuery.type : null;
15838
16464
  this.viewContext.query = query;
15839
16465
  });
15840
16466
  });
@@ -16904,13 +17530,13 @@ const transformToStringArray = (value) => {
16904
17530
  };
16905
17531
  const transformToNumber = (value) => {
16906
17532
  if (typeof value === 'number') {
16907
- return value;
17533
+ return Number.isFinite(value) ? value : null;
16908
17534
  }
16909
17535
  if (typeof value === 'string') {
16910
17536
  if (!value.trim())
16911
17537
  return null;
16912
17538
  const number = Number(value);
16913
- return Number.isNaN(number) ? null : number;
17539
+ return Number.isFinite(number) ? number : null;
16914
17540
  }
16915
17541
  return null;
16916
17542
  };
@@ -16946,7 +17572,7 @@ const transformToBooleanArray = (value) => {
16946
17572
  const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
16947
17573
  const transformToDate = (value) => {
16948
17574
  if (value instanceof Date) {
16949
- return value;
17575
+ return Number.isNaN(value.getTime()) ? null : value;
16950
17576
  }
16951
17577
  if (typeof value === 'string') {
16952
17578
  const date = new Date(value);
@@ -16973,7 +17599,9 @@ const transformToDateArray = (value) => {
16973
17599
  };
16974
17600
  const transformToSort = (value) => {
16975
17601
  if (typeof value === 'string') {
16976
- const [active, direction] = value.split(':');
17602
+ const separator = value.lastIndexOf(':');
17603
+ const active = separator === -1 ? value : value.slice(0, separator);
17604
+ const direction = separator === -1 ? '' : value.slice(separator + 1);
16977
17605
  if (!active) {
16978
17606
  return null;
16979
17607
  }
@@ -17676,6 +18304,8 @@ const MAX_RESET_PASSES = 10;
17676
18304
  const changedKeysBetween = (previous, current) => Object.keys(current).filter((key) => !equal(previous?.[key], current[key]));
17677
18305
  /** Reads a URL string into the type of the field's default; `null` means the default's type cannot hold it. */
17678
18306
  const autoCoerce = (raw, defaultValue) => {
18307
+ if (Array.isArray(raw))
18308
+ return Array.isArray(defaultValue) ? raw : null;
17679
18309
  if (typeof raw !== 'string')
17680
18310
  return raw;
17681
18311
  if (Array.isArray(defaultValue))
@@ -17703,10 +18333,13 @@ const resolveDebounce = (fieldDefs, changedKeys, live) => {
17703
18333
  }
17704
18334
  return Math.min(...times);
17705
18335
  };
18336
+ const isUnserializable = (value) => value === undefined ||
18337
+ (typeof value === 'number' && Number.isNaN(value)) ||
18338
+ (value instanceof Date && Number.isNaN(value.getTime()));
17706
18339
  /**
17707
18340
  * A value that cannot be told apart from the default once serialized - a cleared text (`''`), an emptied list
17708
- * (`[]`) or one whose `valueToQueryParam` yields nothing - commits as the default, so the URL reproduces the
17709
- * committed state and the filter count does not report it.
18341
+ * (`[]`), `undefined`, `NaN`, an invalid `Date` or one whose `valueToQueryParam` yields nothing - commits as the
18342
+ * default, so the URL reproduces the committed state and the filter count does not report it.
17710
18343
  */
17711
18344
  const normalizeLive = (fieldDefs, rawLive, defaults) => {
17712
18345
  const live = { ...rawLive };
@@ -17717,7 +18350,7 @@ const normalizeLive = (fieldDefs, rawLive, defaults) => {
17717
18350
  const serialized = def.valueToQueryParam?.(value);
17718
18351
  const serializedToNothing = !!def.valueToQueryParam && (serialized === null || serialized === undefined);
17719
18352
  const clearedNullable = defaults[key] === null && (value === '' || (Array.isArray(value) && value.length === 0));
17720
- if (serializedToNothing || clearedNullable) {
18353
+ if (serializedToNothing || clearedNullable || isUnserializable(value)) {
17721
18354
  live[key] = defaults[key];
17722
18355
  }
17723
18356
  }
@@ -18208,6 +18841,13 @@ const defineQueryForm = (config) => {
18208
18841
  }
18209
18842
  observeOptions = options;
18210
18843
  observing.set(true);
18844
+ if (isDevMode()) {
18845
+ for (const key of options?.persistence?.fields ?? []) {
18846
+ if (!(key in fieldDefs)) {
18847
+ console.warn(`defineQueryForm: persistence.fields references unknown field "${key}". Is it a typo?`);
18848
+ }
18849
+ }
18850
+ }
18211
18851
  changesSeenAtObserve = untracked(queryParamChanges);
18212
18852
  if (options?.persistence) {
18213
18853
  restoreFromStorage(options.persistence, route.snapshot.queryParams);
@@ -18334,6 +18974,11 @@ const normalizeConfig = (config) => {
18334
18974
  isResetBy: isResetBy === undefined ? undefined : Array.isArray(isResetBy) ? isResetBy : [isResetBy],
18335
18975
  };
18336
18976
  };
18977
+ const dateToQueryParam = (value) => {
18978
+ if (value === null)
18979
+ return ET_NULL_VALUE;
18980
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value.toISOString() : value;
18981
+ };
18337
18982
  function queryField(config) {
18338
18983
  return {
18339
18984
  defaultValue: null,
@@ -18410,6 +19055,7 @@ function dateQueryField(config) {
18410
19055
  return {
18411
19056
  defaultValue: null,
18412
19057
  queryParamToValue: transformToDate,
19058
+ valueToQueryParam: dateToQueryParam,
18413
19059
  ...normalizeConfig(rest),
18414
19060
  };
18415
19061
  }
@@ -18417,6 +19063,11 @@ function dateQueryField(config) {
18417
19063
  const dateArrayQueryField = (config) => ({
18418
19064
  defaultValue: null,
18419
19065
  queryParamToValue: transformToDateArray,
19066
+ valueToQueryParam: (value) => {
19067
+ if (value === null)
19068
+ return ET_NULL_VALUE;
19069
+ return value.length ? value.map(dateToQueryParam) : ET_EMPTY_ARRAY_VALUE;
19070
+ },
18420
19071
  ...normalizeConfig(config),
18421
19072
  });
18422
19073
 
@@ -18425,6 +19076,7 @@ const WebSocketRuntimeErrorCode = {
18425
19076
  // Web Socket Client
18426
19077
  ROOM_NOT_JOINED: 1000,
18427
19078
  MESSAGE_MALFORMED: 1001,
19079
+ AUTH_AND_AUTH_PROVIDER: 1002,
18428
19080
  };
18429
19081
  const roomNotJoined = (room) => {
18430
19082
  return new RuntimeError(WebSocketRuntimeErrorCode.ROOM_NOT_JOINED, `Tried leaving the room "${room}" but it has not been joined.`);
@@ -18432,6 +19084,9 @@ const roomNotJoined = (room) => {
18432
19084
  const messageMalformed = () => {
18433
19085
  return new RuntimeError(WebSocketRuntimeErrorCode.MESSAGE_MALFORMED, 'A message has been received but it is malformed. Expected a JSON string shaped like { room: string; event: string; data: unknown }.');
18434
19086
  };
19087
+ const authAndAuthProvider = (name) => {
19088
+ return new RuntimeError(WebSocketRuntimeErrorCode.AUTH_AND_AUTH_PROVIDER, `The web socket client "${name}" sets both \`auth\` and \`authProvider\`. \`authProvider\` sends the access token itself, so \`auth\` is ignored. Remove one of them.`);
19089
+ };
18435
19090
 
18436
19091
  const MAX_DEVTOOLS_MESSAGES = 100;
18437
19092
  const RECONNECT_BASE_DELAY = 1000;
@@ -18441,7 +19096,10 @@ const createWebSocketClient = (options) => {
18441
19096
  return defineRootProvider(() => {
18442
19097
  if (isPlatformServer(inject(PLATFORM_ID)))
18443
19098
  return createServerWebSocketClient();
18444
- const auth = options.auth;
19099
+ if (options.auth && options.authProvider && isDevMode())
19100
+ throw authAndAuthProvider(options.name);
19101
+ const authProvider = options.authProvider?.inject();
19102
+ const auth = authProvider ? () => ({ token: authProvider.accessToken() }) : options.auth;
18445
19103
  const socket = options.io(options.url, {
18446
19104
  withCredentials: options.withCredentials ?? true,
18447
19105
  autoConnect: false,
@@ -18449,6 +19107,7 @@ const createWebSocketClient = (options) => {
18449
19107
  auth: auth ? (cb) => cb(typeof auth === 'function' ? auth() : auth) : undefined,
18450
19108
  });
18451
19109
  const rooms = new Map();
19110
+ const endedRoomHolders = new Map();
18452
19111
  const joinsDeliveredThisConnection = new Set();
18453
19112
  const joinsDeliveredToClosedConnection = new Set();
18454
19113
  const isConnected = signal(false, /* @ts-ignore */
@@ -18479,50 +19138,54 @@ const createWebSocketClient = (options) => {
18479
19138
  direction: 'out',
18480
19139
  });
18481
19140
  };
19141
+ const join = (name) => {
19142
+ const existingRoom = rooms.get(name);
19143
+ if (existingRoom) {
19144
+ existingRoom.joinCount++;
19145
+ return existingRoom;
19146
+ }
19147
+ emit({ event: 'join-room', data: name, room: name });
19148
+ joinsDeliveredToClosedConnection.delete(name);
19149
+ const messages = new Subject();
19150
+ const newRoom = {
19151
+ latestMessage: signal(null),
19152
+ messages,
19153
+ messages$: messages.asObservable(),
19154
+ joinCount: 1,
19155
+ };
19156
+ rooms.set(name, newRoom);
19157
+ syncDevtoolsRooms();
19158
+ return newRoom;
19159
+ };
19160
+ const joinStaticRoom = (name) => {
19161
+ const destroyRef = inject(DestroyRef);
19162
+ const roomData = signal(join(name), /* @ts-ignore */
19163
+ ...(ngDevMode ? [{ debugName: "roomData" }] : /* istanbul ignore next */ []));
19164
+ destroyRef.onDestroy(() => {
19165
+ leaveRoom(name);
19166
+ roomData.set(null);
19167
+ });
19168
+ return roomData.asReadonly();
19169
+ };
18482
19170
  const joinRoom = (room) => {
18483
- const roomFn = typeof room === 'function' ? room : () => room;
18484
- // Must stay above the effect below: `previousSignalValue` registers a `toObservable` effect that has to run
18485
- // first in the same flush. Registered later, it leaves `pre()` on the room from the flush before, and the
18486
- // effect joins the new room without ever leaving the old one.
18487
- const pre = previousSignalValue(computed(() => roomFn()));
19171
+ if (typeof room === 'string')
19172
+ return joinStaticRoom(room);
19173
+ const roomFn = room;
18488
19174
  const roomData = signal(null, /* @ts-ignore */
18489
19175
  ...(ngDevMode ? [{ debugName: "roomData" }] : /* istanbul ignore next */ []));
18490
19176
  let joinedRoomName = null;
18491
- const join = (name) => {
18492
- const existingRoom = rooms.get(name);
18493
- if (existingRoom) {
18494
- existingRoom.joinCount++;
18495
- return existingRoom;
18496
- }
18497
- emit({ event: 'join-room', data: name, room: name });
18498
- joinsDeliveredToClosedConnection.delete(name);
18499
- const messages = new Subject();
18500
- const newRoom = {
18501
- latestMessage: signal(null),
18502
- messages,
18503
- messages$: messages.asObservable(),
18504
- joinCount: 1,
18505
- };
18506
- rooms.set(name, newRoom);
18507
- syncDevtoolsRooms();
18508
- return newRoom;
18509
- };
18510
19177
  effect(() => {
18511
- const current = roomFn();
19178
+ const current = roomFn() || null;
18512
19179
  untracked(() => {
18513
- const previous = pre();
18514
- if (previous === current)
19180
+ if (current === joinedRoomName)
18515
19181
  return;
18516
- if (previous) {
18517
- leaveRoom(previous);
19182
+ if (joinedRoomName) {
19183
+ leaveRoom(joinedRoomName);
18518
19184
  joinedRoomName = null;
18519
19185
  }
18520
19186
  if (current) {
18521
- const joinedRoom = join(current);
18522
- if (joinedRoom) {
18523
- joinedRoomName = current;
18524
- roomData.set(joinedRoom);
18525
- }
19187
+ joinedRoomName = current;
19188
+ roomData.set(join(current));
18526
19189
  }
18527
19190
  else {
18528
19191
  roomData.set(null);
@@ -18539,6 +19202,14 @@ const createWebSocketClient = (options) => {
18539
19202
  return roomData.asReadonly();
18540
19203
  };
18541
19204
  const leaveRoom = (room) => {
19205
+ const endedHolders = endedRoomHolders.get(room);
19206
+ if (endedHolders !== undefined) {
19207
+ if (endedHolders > 1)
19208
+ endedRoomHolders.set(room, endedHolders - 1);
19209
+ else
19210
+ endedRoomHolders.delete(room);
19211
+ return;
19212
+ }
18542
19213
  const joinedRoom = rooms.get(room);
18543
19214
  if (!joinedRoom) {
18544
19215
  if (isDevMode())
@@ -18559,9 +19230,10 @@ const createWebSocketClient = (options) => {
18559
19230
  let reconnectTimer = null;
18560
19231
  let connectedAt = null;
18561
19232
  let destroyed = false;
19233
+ let suspended = false;
18562
19234
  /** socket.io stops reconnecting after a server disconnect or a rejected handshake, so the client takes over. */
18563
19235
  const scheduleReconnect = () => {
18564
- if (reconnectTimer !== null || destroyed)
19236
+ if (reconnectTimer !== null || destroyed || suspended)
18565
19237
  return;
18566
19238
  const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** reconnectAttempt, RECONNECT_MAX_DELAY);
18567
19239
  reconnectAttempt++;
@@ -18651,7 +19323,62 @@ const createWebSocketClient = (options) => {
18651
19323
  });
18652
19324
  setupWebSocketConnectionListener();
18653
19325
  setupWebSocketListener();
18654
- socket.connect();
19326
+ const endSession = () => {
19327
+ suspended = true;
19328
+ if (reconnectTimer !== null) {
19329
+ clearTimeout(reconnectTimer);
19330
+ reconnectTimer = null;
19331
+ }
19332
+ socket.disconnect();
19333
+ for (const [name, room] of rooms) {
19334
+ endedRoomHolders.set(name, (endedRoomHolders.get(name) ?? 0) + room.joinCount);
19335
+ room.messages.complete();
19336
+ }
19337
+ rooms.clear();
19338
+ syncDevtoolsRooms();
19339
+ };
19340
+ const startConnection = () => {
19341
+ suspended = false;
19342
+ reconnectAttempt = 0;
19343
+ socket.connect();
19344
+ };
19345
+ if (authProvider) {
19346
+ let hasConnected = false;
19347
+ let wasAuthenticated = false;
19348
+ let handledLogin = null;
19349
+ effect(() => {
19350
+ const status = authProvider.sessionStatus();
19351
+ const execution = authProvider.executionState();
19352
+ untracked(() => {
19353
+ if (destroyed || status === 'unknown' || status === 'restoring')
19354
+ return;
19355
+ if (status === 'anonymous') {
19356
+ if (wasAuthenticated)
19357
+ endSession();
19358
+ else if (!hasConnected)
19359
+ startConnection();
19360
+ wasAuthenticated = false;
19361
+ hasConnected = true;
19362
+ return;
19363
+ }
19364
+ const login = execution?.type === 'login' && execution.state === 'success' ? execution : null;
19365
+ const startedSession = !wasAuthenticated || (login !== null && login !== handledLogin);
19366
+ if (login)
19367
+ handledLogin = login;
19368
+ wasAuthenticated = true;
19369
+ if (!hasConnected)
19370
+ startConnection();
19371
+ else if (startedSession) {
19372
+ socket.disconnect();
19373
+ startConnection();
19374
+ }
19375
+ hasConnected = true;
19376
+ });
19377
+ });
19378
+ }
19379
+ else {
19380
+ socket.connect();
19381
+ }
18655
19382
  const client = {
18656
19383
  joinRoom,
18657
19384
  isConnected: isConnected.asReadonly(),
@@ -18698,5 +19425,5 @@ const createServerWebSocketClient = () => {
18698
19425
  * Generated bundle index. Do not edit.
18699
19426
  */
18700
19427
 
18701
- export { AuthBearerRefreshStrategy, BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, IS_QUERY_REQUEST, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_BATCH_TOMBSTONES, MAX_QUERY_BATCH_TOMBSTONE_BUCKETS, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_DEVTOOLS_TOKEN_TTL_LIMIT, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStateType, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, addQueryDevtoolsAuthAccount, applyQueryDevtoolsOverrides, applyQueryDevtoolsTokenTtl, applyQueryFeatures, armAllQueryDevtoolsMocks, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, canOverrideQueryDevtoolsTokenTtl, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsAuthCredentials, clearQueryDevtoolsAuthSessions, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTokenTtl, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryDevtoolsSchemaComponents, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDefaultRetryFn, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHeadQuery, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createOptionsQuery, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryBatch, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySubmission, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecureHeadQuery, createSecureOptionsQuery, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, currentQueryDevtoolsBatch, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, executeUntilSettled$, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, forgetQueryDevtoolsAuthSession, forgetQueryDevtoolsAuthSessionsFor, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsSampleNumber, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, gqlErrorsInResponse, gqlTransformerFor, hasDefaultQueryRetry, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsAuthSessions, initQueryDevtoolsFaults, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isIdempotentQueryMethod, isLegacyQuery, isNaN$1 as isNaN, isOverrideOp, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRepositoryLive, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, loginQueryDevtoolsAuthAccount, logoutQueryDevtoolsAuthSession, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, noteQueryFormReads, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, patchQueryDevtoolsTokenPayload, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryBatchAlreadyRunning, queryBatchWithArgsUsed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryCreatedInReactiveContext, queryDevtoolsAbout, queryDevtoolsAllowsLocalAuthSessions, queryDevtoolsApiEnvIds, queryDevtoolsApiEnvIsProduction, queryDevtoolsApiEnvScope, queryDevtoolsApiEnvValues, queryDevtoolsApiEnvs, queryDevtoolsArmedMocks, queryDevtoolsArmedMocksRestored, queryDevtoolsAuthAccountsFor, queryDevtoolsAuthActive, queryDevtoolsAuthFieldsFor, queryDevtoolsAuthOtherScopeCount, queryDevtoolsAuthProviders, queryDevtoolsAuthSessions, queryDevtoolsAuthSessionsFor, queryDevtoolsAuthTabLocal, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFaultsRestored, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryDevtoolsTokenTtls, queryErrorMessage, queryErrorMessages, queryExecute, queryExecutedAfterDestroyMessage, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsAuthSeed, readQueryDevtoolsAuthSeedFor, readQueryDevtoolsStore, recordExecutionArgs, registerEthleteVersion, registerLiveQueryDevtoolsRepository, registerQueryDevtoolsAuthProvider, registerQueryDevtoolsEntry, registerQueryErrorParser, releaseQueryDevtoolsOverridePersistence, removeFrom, removeQueryDevtoolsAuthAccount, renameQueryDevtoolsAuthSession, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, resolveQueryHeaders, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runInQueryDevtoolsBatch, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsApiEnv, setQueryDevtoolsApiEnvs, setQueryDevtoolsAppInfo, setQueryDevtoolsArmedMocksScope, setQueryDevtoolsAuthCredentials, setQueryDevtoolsAuthProviderRegistrar, setQueryDevtoolsAuthSeedReader, setQueryDevtoolsAuthTabLocal, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFaultsScope, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setQueryDevtoolsTokenPayloadPatcher, setQueryDevtoolsTokenTtl, setQueryDevtoolsUiMounted, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryDevtoolsAuthSession, switchQueryState, symfonyQueryErrorParser, tableSortQueryField, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, trackQueryDevtoolsAuthProvider, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, transformToTableSort, transformToTableSortQueryParam, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapGqlResponseWithErrors, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withLongPolling, withLongPollingUsedOnUnsupportedHttpMethod, withLongPollingUsedWithPolling, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
19428
+ export { AuthBearerRefreshStrategy, BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, IS_QUERY_REQUEST, 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, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStateType, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authAccessTokenExpiryUnreadable, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsTokenTtl, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, contentfulGqlLikePaginationAdapter, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDefaultRetryFn, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHeadQuery, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createOptionsQuery, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryBatch, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryGroup, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySubmission, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecureHeadQuery, createSecureOptionsQuery, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, executeUntilSettled$, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, gqlErrorsInResponse, gqlTransformerFor, hasDefaultQueryRetry, hasHeader, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, invalidateOnSuccess, invalidatesUsedOnRead, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isIdempotentQueryMethod, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryBatchAlreadyRunning, queryBatchWithArgsUsed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryCreatedInReactiveContext, queryErrorMessage, queryErrorMessages, queryExecute, queryExecutedAfterDestroyMessage, queryExecutedWhileParkedMessage, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, recordExecutionArgs, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryHeaders, resolveQueryTags, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsTokenTtl, setQueryDevtoolsUiMounted, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, skipParkedExecution, sortQueryField, splitQueryConfig, stringArrayQueryField, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, tableSortQueryField, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, transformToTableSort, transformToTableSortQueryParam, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapGqlResponseWithErrors, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withLongPolling, withLongPollingUsedOnUnsupportedHttpMethod, withLongPollingUsedWithPolling, withMultiTabSync, withOptimisticUpdate, withOptimisticUpdateUsedOnRead, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, EMPTY_QUERY_DEVTOOLS_FAULT as ɵEMPTY_QUERY_DEVTOOLS_FAULT, MAX_QUERY_BATCH_TOMBSTONES as ɵMAX_QUERY_BATCH_TOMBSTONES, MAX_QUERY_BATCH_TOMBSTONE_BUCKETS as ɵMAX_QUERY_BATCH_TOMBSTONE_BUCKETS, MAX_QUERY_DEVTOOLS_TOMBSTONES as ɵMAX_QUERY_DEVTOOLS_TOMBSTONES, QUERY_DEVTOOLS_FAULT_STATUSES as ɵQUERY_DEVTOOLS_FAULT_STATUSES, QUERY_DEVTOOLS_TOKEN_TTL_LIMIT as ɵQUERY_DEVTOOLS_TOKEN_TTL_LIMIT, addQueryDevtoolsAuthAccount as ɵaddQueryDevtoolsAuthAccount, applyQueryDevtoolsOverrides as ɵapplyQueryDevtoolsOverrides, applyQueryDevtoolsTokenTtl as ɵapplyQueryDevtoolsTokenTtl, armAllQueryDevtoolsMocks as ɵarmAllQueryDevtoolsMocks, armQueryDevtoolsMock as ɵarmQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer as ɵarmQueryDevtoolsOverrideTransfer, canOverrideQueryDevtoolsTokenTtl as ɵcanOverrideQueryDevtoolsTokenTtl, clearQueryDevtoolsArmedMocks as ɵclearQueryDevtoolsArmedMocks, clearQueryDevtoolsAuthCredentials as ɵclearQueryDevtoolsAuthCredentials, clearQueryDevtoolsAuthSessions as ɵclearQueryDevtoolsAuthSessions, clearQueryDevtoolsFaults as ɵclearQueryDevtoolsFaults, clearQueryDevtoolsMockStore as ɵclearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore as ɵclearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore as ɵclearQueryDevtoolsStore, clearQueryDevtoolsTombstones as ɵclearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides as ɵclearRestoredQueryDevtoolsOverrides, collectLeafPaths as ɵcollectLeafPaths, collectQueryDevtoolsSchemaComponents as ɵcollectQueryDevtoolsSchemaComponents, countUnresolvedQueryDevtoolsOverrides as ɵcountUnresolvedQueryDevtoolsOverrides, createQueryDevtoolsOverrides as ɵcreateQueryDevtoolsOverrides, deleteQueryDevtoolsMock as ɵdeleteQueryDevtoolsMock, detectPaginationShape as ɵdetectPaginationShape, forgetQueryDevtoolsAuthSession as ɵforgetQueryDevtoolsAuthSession, forgetQueryDevtoolsAuthSessionsFor as ɵforgetQueryDevtoolsAuthSessionsFor, generateQueryDevtoolsNumberPreset as ɵgenerateQueryDevtoolsNumberPreset, generateQueryDevtoolsSampleNumber as ɵgenerateQueryDevtoolsSampleNumber, generateQueryDevtoolsStringPreset as ɵgenerateQueryDevtoolsStringPreset, hasQueryDevtoolsOverridesAtPath as ɵhasQueryDevtoolsOverridesAtPath, isDateShapedLeaf as ɵisDateShapedLeaf, isQueryDevtoolsEnabled as ɵisQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed as ɵisQueryDevtoolsFaultArmed, isQueryDevtoolsRepositoryLive as ɵisQueryDevtoolsRepositoryLive, loadQueryDevtoolsSchema as ɵloadQueryDevtoolsSchema, loginQueryDevtoolsAuthAccount as ɵloginQueryDevtoolsAuthAccount, logoutQueryDevtoolsAuthSession as ɵlogoutQueryDevtoolsAuthSession, matchesQueryDevtoolsMockPattern as ɵmatchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery as ɵmatchesQueryDevtoolsMockQuery, measureQueryDevtoolsPayload as ɵmeasureQueryDevtoolsPayload, parseQueryDevtoolsOverrideTransfer as ɵparseQueryDevtoolsOverrideTransfer, queryDevtoolsAbout as ɵqueryDevtoolsAbout, queryDevtoolsAllowsLocalAuthSessions as ɵqueryDevtoolsAllowsLocalAuthSessions, queryDevtoolsApiEnvIds as ɵqueryDevtoolsApiEnvIds, queryDevtoolsApiEnvIsProduction as ɵqueryDevtoolsApiEnvIsProduction, queryDevtoolsApiEnvScope as ɵqueryDevtoolsApiEnvScope, queryDevtoolsApiEnvValues as ɵqueryDevtoolsApiEnvValues, queryDevtoolsApiEnvs as ɵqueryDevtoolsApiEnvs, queryDevtoolsArmedMocks as ɵqueryDevtoolsArmedMocks, queryDevtoolsArmedMocksRestored as ɵqueryDevtoolsArmedMocksRestored, queryDevtoolsAuthAccountsFor as ɵqueryDevtoolsAuthAccountsFor, queryDevtoolsAuthActive as ɵqueryDevtoolsAuthActive, queryDevtoolsAuthFieldsFor as ɵqueryDevtoolsAuthFieldsFor, queryDevtoolsAuthOtherScopeCount as ɵqueryDevtoolsAuthOtherScopeCount, queryDevtoolsAuthSessions as ɵqueryDevtoolsAuthSessions, queryDevtoolsAuthSessionsFor as ɵqueryDevtoolsAuthSessionsFor, queryDevtoolsAuthTabLocal as ɵqueryDevtoolsAuthTabLocal, queryDevtoolsEntries as ɵqueryDevtoolsEntries, queryDevtoolsFaults as ɵqueryDevtoolsFaults, queryDevtoolsFaultsRestored as ɵqueryDevtoolsFaultsRestored, queryDevtoolsMockId as ɵqueryDevtoolsMockId, queryDevtoolsMocks as ɵqueryDevtoolsMocks, queryDevtoolsOverridePersistence as ɵqueryDevtoolsOverridePersistence, queryDevtoolsRequestPath as ɵqueryDevtoolsRequestPath, queryDevtoolsResponseHistory as ɵqueryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope as ɵqueryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames as ɵqueryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes as ɵqueryDevtoolsSchemaRoutes, queryDevtoolsSchemaState as ɵqueryDevtoolsSchemaState, queryDevtoolsSettings as ɵqueryDevtoolsSettings, queryDevtoolsStorage as ɵqueryDevtoolsStorage, queryDevtoolsTokenTtls as ɵqueryDevtoolsTokenTtls, readQueryDevtoolsStore as ɵreadQueryDevtoolsStore, registerEthleteVersion as ɵregisterEthleteVersion, removeQueryDevtoolsAuthAccount as ɵremoveQueryDevtoolsAuthAccount, renameQueryDevtoolsAuthSession as ɵrenameQueryDevtoolsAuthSession, restoredQueryDevtoolsOverrides as ɵrestoredQueryDevtoolsOverrides, saveQueryDevtoolsMock as ɵsaveQueryDevtoolsMock, seedQueryDevtoolsSchemaBody as ɵseedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute as ɵseedQueryDevtoolsSchemaRoute, serializeQueryDevtoolsOverrideTransfer as ɵserializeQueryDevtoolsOverrideTransfer, setQueryDevtoolsApiEnv as ɵsetQueryDevtoolsApiEnv, setQueryDevtoolsAppInfo as ɵsetQueryDevtoolsAppInfo, setQueryDevtoolsArmedMocksScope as ɵsetQueryDevtoolsArmedMocksScope, setQueryDevtoolsAuthCredentials as ɵsetQueryDevtoolsAuthCredentials, setQueryDevtoolsAuthTabLocal as ɵsetQueryDevtoolsAuthTabLocal, setQueryDevtoolsFault as ɵsetQueryDevtoolsFault, setQueryDevtoolsFaultsScope as ɵsetQueryDevtoolsFaultsScope, setQueryDevtoolsOverridePersistence as ɵsetQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesScope as ɵsetQueryDevtoolsOverridesScope, setQueryDevtoolsSettings as ɵsetQueryDevtoolsSettings, smartDuplicateArrayItem as ɵsmartDuplicateArrayItem, snapshotQueryDevtoolsHandle as ɵsnapshotQueryDevtoolsHandle, stringifyQueryRouteParts as ɵstringifyQueryRouteParts, sumQueryDevtoolsStats as ɵsumQueryDevtoolsStats, switchQueryDevtoolsAuthSession as ɵswitchQueryDevtoolsAuthSession, tombstoneOf as ɵtombstoneOf, writeQueryDevtoolsStore as ɵwriteQueryDevtoolsStore };
18702
19429
  //# sourceMappingURL=ethlete-query.mjs.map