@ethlete/query 6.0.0-next.54 → 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;
@@ -903,248 +1482,29 @@ const formatQueryDevtoolsDuration = (ms) => {
903
1482
  if (ms < 1000)
904
1483
  return `${ms}ms`;
905
1484
  const round = (value, unit) => `${Number(value.toFixed(1))}${unit}`;
906
- if (ms < 60_000)
907
- return round(ms / 1000, 's');
908
- if (ms < 3_600_000)
909
- 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;
1485
+ if (ms < 60_000)
1486
+ return round(ms / 1000, 's');
1487
+ if (ms < 3_600_000)
1488
+ return round(ms / 60_000, 'm');
1489
+ return round(ms / 3_600_000, 'h');
1126
1490
  };
1127
1491
  /**
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;
1134
- /**
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,7 +1625,7 @@ 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 = {
@@ -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;
@@ -1458,7 +1744,8 @@ const createQueryFeature = (config) => {
1458
1744
  *
1459
1745
  * Return `null` to park the query: it is reset (`response()` and `executionState()` become `null`),
1460
1746
  * and polling and auto refresh pause until args are set again. A mutation that is still in flight
1461
- * finishes first and the query parks once it has settled.
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.
1462
1749
  *
1463
1750
  * Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
1464
1751
  */
@@ -1484,6 +1771,8 @@ const withArgs = (args) => {
1484
1771
  return;
1485
1772
  }
1486
1773
  untracked(() => {
1774
+ if (context.state.subtle.isExplicitArgs(currArgsNow))
1775
+ return;
1487
1776
  const guard = context.state.subtle.autoExecuteGuard();
1488
1777
  if (context.flags.shouldAutoExecute && (guard === null || guard()))
1489
1778
  context.execute({ args: currArgsNow });
@@ -1593,9 +1882,9 @@ const withPolling = (options) => {
1593
1882
  }, Math.max(0, firstDelay));
1594
1883
  };
1595
1884
  const remainingDelay = () => lastTickAt + currentInterval() - Date.now();
1596
- // Without an args source, `state.args` only changes when `execute({ args })` records them - and
1597
- // that execution already sent the request.
1598
- 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));
1599
1888
  nestedEffect(() => {
1600
1889
  const args = context.state.args();
1601
1890
  untracked(() => {
@@ -1898,7 +2187,7 @@ const withResponseUpdate = (options) => {
1898
2187
  if (response === null)
1899
2188
  return;
1900
2189
  untracked(() => {
1901
- context.state.rawResponse.set(response);
2190
+ context.state.response.set(response);
1902
2191
  });
1903
2192
  }, { injector: context.deps.injector });
1904
2193
  },
@@ -1963,6 +2252,62 @@ const withPageResetOnError = (options) => {
1963
2252
  });
1964
2253
  };
1965
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
+
1966
2311
  const parsers = [];
1967
2312
  /**
1968
2313
  * Adds a parser to the pipeline `createQueryErrorResponse` runs, ahead of the built-in ladder.
@@ -2019,7 +2364,11 @@ const createDefaultRetryFn = (options = {}) => {
2019
2364
  const jitter = clamp(options.jitter ?? DEFAULT_JITTER, 0, 1);
2020
2365
  const retryableStatusCodes = options.retryableStatusCodes;
2021
2366
  const retryNonIdempotent = options.retryNonIdempotent ?? false;
2022
- 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
+ };
2023
2372
  const backoff = (retryCount) => {
2024
2373
  const exponential = baseDelayMs * Math.pow(2, retryCount);
2025
2374
  const spread = jitter === 0 ? 1 : 1 + (Math.random() * 2 - 1) * jitter;
@@ -2069,6 +2418,7 @@ const isQueryErrorResponse = (error) => {
2069
2418
  /**
2070
2419
  * Every message carried by an error, flattened into a plain list - one entry for a single error,
2071
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.
2072
2422
  *
2073
2423
  * The single/list split exists because that is how APIs answer; UI almost never wants to branch on
2074
2424
  * it. Reach for this when rendering, and keep {@link QueryErrorResponse} for anything that needs
@@ -2084,7 +2434,9 @@ const isQueryErrorResponse = (error) => {
2084
2434
  const queryErrorMessages = (error) => {
2085
2435
  if (!error)
2086
2436
  return [];
2087
- 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];
2088
2440
  };
2089
2441
  /**
2090
2442
  * The first message of an error, or `null` when there is none - the single-line counterpart to
@@ -2213,14 +2565,24 @@ const setupQueryState = (options) => {
2213
2565
  // set is overridden again the next time the source's dependencies change.
2214
2566
  const argsSource = signal(() => null, /* @ts-ignore */
2215
2567
  ...(ngDevMode ? [{ debugName: "argsSource" }] : /* istanbul ignore next */ []));
2216
- const args = linkedSignal(() => argsSource()(), /* @ts-ignore */
2217
- ...(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
+ } });
2218
2576
  const hasArgsSource = signal(false, /* @ts-ignore */
2219
2577
  ...(ngDevMode ? [{ debugName: "hasArgsSource" }] : /* istanbul ignore next */ []));
2220
2578
  const setArgsSource = (source) => {
2221
2579
  argsSource.set(source);
2222
2580
  hasArgsSource.set(true);
2223
2581
  };
2582
+ const setExplicitArgs = (value) => {
2583
+ explicitArgs = value;
2584
+ args.set(value);
2585
+ };
2224
2586
  const lastTimeExecutedAt = signal(null, /* @ts-ignore */
2225
2587
  ...(ngDevMode ? [{ debugName: "lastTimeExecutedAt" }] : /* istanbul ignore next */ []));
2226
2588
  const lastTriggeredBy = signal(null, /* @ts-ignore */
@@ -2239,6 +2601,8 @@ const setupQueryState = (options) => {
2239
2601
  ...(ngDevMode ? [{ debugName: "defaultRunOptions" }] : /* istanbul ignore next */ []));
2240
2602
  const autoExecuteGuard = signal(null, /* @ts-ignore */
2241
2603
  ...(ngDevMode ? [{ debugName: "autoExecuteGuard" }] : /* istanbul ignore next */ []));
2604
+ const beforeExecute = signal(null, /* @ts-ignore */
2605
+ ...(ngDevMode ? [{ debugName: "beforeExecute" }] : /* istanbul ignore next */ []));
2242
2606
  options.destroyRef?.onDestroy(() => {
2243
2607
  requestEventsSubscription.unsubscribe();
2244
2608
  requestEvents$.complete();
@@ -2309,8 +2673,11 @@ const setupQueryState = (options) => {
2309
2673
  unbindRequestEvents,
2310
2674
  defaultRunOptions,
2311
2675
  autoExecuteGuard,
2676
+ beforeExecute,
2312
2677
  setArgsSource,
2313
2678
  hasArgsSource: hasArgsSource.asReadonly(),
2679
+ setExplicitArgs,
2680
+ isExplicitArgs: (value) => value !== null && value === explicitArgs,
2314
2681
  devtoolsStats: options.devtoolsStats ?? null,
2315
2682
  devtoolsFormLinks: options.devtoolsFormLinks ?? null,
2316
2683
  },
@@ -2409,17 +2776,18 @@ const createQuerySnapshotFn = (options) => {
2409
2776
  // snapshot has to settle itself - otherwise `executeUntilSettled` resolves with a snapshot still
2410
2777
  // reporting the execution as loading.
2411
2778
  unregisterScopeListener = context.deps.destroyRef.onDestroy(cancel);
2779
+ const watcher = createObservableSignalWatcher(context.deps.injector);
2412
2780
  const snapshot = {
2413
- args: wrapAsObservableSignal(snapshotState.args.asReadonly(), context.deps.injector),
2414
- response: wrapAsObservableSignal(snapshotState.response, context.deps.injector),
2415
- latestHttpEvent: wrapAsObservableSignal(snapshotState.latestHttpEvent.asReadonly(), context.deps.injector),
2416
- loading: wrapAsObservableSignal(snapshotState.loading.asReadonly(), context.deps.injector),
2417
- error: wrapAsObservableSignal(snapshotState.error.asReadonly(), context.deps.injector),
2418
- lastTimeExecutedAt: wrapAsObservableSignal(snapshotState.lastTimeExecutedAt.asReadonly(), context.deps.injector),
2419
- triggeredBy: wrapAsObservableSignal(snapshotState.lastTriggeredBy.asReadonly(), context.deps.injector),
2420
- isAlive: wrapAsObservableSignal(isAlive.asReadonly(), context.deps.injector),
2421
- id: wrapAsObservableSignal(options.execute.currentRepositoryKey, context.deps.injector),
2422
- 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),
2423
2791
  };
2424
2792
  return snapshot;
2425
2793
  });
@@ -2507,16 +2875,18 @@ const createQueryObject = (options) => {
2507
2875
  const setError = (error) => state.error.set(error);
2508
2876
  const createSnapshot = createQuerySnapshotFn({ state, execute, deps });
2509
2877
  // Pre-wrap all public signals once so both `query` and `roQuery` share the same instances.
2510
- const wrappedArgs = wrapAsObservableSignal(state.args.asReadonly(), deps.injector);
2511
- const wrappedResponse = wrapAsObservableSignal(state.response, deps.injector);
2512
- const wrappedLatestHttpEvent = wrapAsObservableSignal(state.latestHttpEvent.asReadonly(), deps.injector);
2513
- const wrappedLoading = wrapAsObservableSignal(state.loading.asReadonly(), deps.injector);
2514
- const wrappedError = wrapAsObservableSignal(state.error.asReadonly(), deps.injector);
2515
- const wrappedLastTimeExecutedAt = wrapAsObservableSignal(state.lastTimeExecutedAt.asReadonly(), deps.injector);
2516
- const wrappedTriggeredBy = wrapAsObservableSignal(state.lastTriggeredBy.asReadonly(), deps.injector);
2517
- const wrappedId = wrapAsObservableSignal(execute.currentRepositoryKey, deps.injector);
2518
- const wrappedExecutionState = wrapAsObservableSignal(state.executionState, deps.injector);
2519
- 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));
2520
2890
  const asReadonly = () => {
2521
2891
  const roQuery = {
2522
2892
  args: wrappedArgs,
@@ -2528,6 +2898,7 @@ const createQueryObject = (options) => {
2528
2898
  triggeredBy: wrappedTriggeredBy,
2529
2899
  id: wrappedId,
2530
2900
  createSnapshot,
2901
+ url: wrappedUrl,
2531
2902
  executionState: wrappedExecutionState,
2532
2903
  };
2533
2904
  return roQuery;
@@ -2543,6 +2914,7 @@ const createQueryObject = (options) => {
2543
2914
  triggeredBy: wrappedTriggeredBy,
2544
2915
  id: wrappedId,
2545
2916
  createSnapshot,
2917
+ url: wrappedUrl,
2546
2918
  reset: execute.reset,
2547
2919
  abort: execute.abort,
2548
2920
  asReadonly,
@@ -2593,6 +2965,12 @@ const createBaseQuery = (options) => {
2593
2965
  deps,
2594
2966
  };
2595
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
+ }
2596
2974
  maybeExecute({ execute, flags, state });
2597
2975
  trackRefreshedExecutions({ state, deps });
2598
2976
  const query = createQueryObject({ state, execute, deps });
@@ -2704,7 +3082,7 @@ const extractExpiresInSeconds = (headers) => {
2704
3082
  if (cacheControl && /(?:^|,)\s*no-(?:cache|store)(?:\s|,|$)/i.test(cacheControl)) {
2705
3083
  return null;
2706
3084
  }
2707
- 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);
2708
3086
  if (maxAgeMatch?.[1]) {
2709
3087
  maxAge = parseInt(maxAgeMatch[1]);
2710
3088
  }
@@ -2800,6 +3178,7 @@ const queryExecute = (options) => {
2800
3178
  }
2801
3179
  const defaultRunOptions = state.subtle.defaultRunOptions();
2802
3180
  const runQueryOptions = defaultRunOptions || options.options ? { ...defaultRunOptions, ...options.options } : undefined;
3181
+ const onRequest = state.subtle.beforeExecute()?.(args);
2803
3182
  const { key, request, executed } = deps.client.repository.request({
2804
3183
  route: creatorInternals.route,
2805
3184
  method: creatorInternals.method,
@@ -2813,25 +3192,44 @@ const queryExecute = (options) => {
2813
3192
  isSecure,
2814
3193
  isRefreshable,
2815
3194
  silenceUncacheableAllowCacheError: queryConfig.silenceUncacheableAllowCacheError,
3195
+ tags: creator?.tags ? resolveQueryTags(creator.tags, args) : undefined,
2816
3196
  });
2817
3197
  executeState.previousKey.set(key);
2818
3198
  state.lastTimeExecutedAt.set(Date.now());
2819
3199
  state.lastTriggeredBy.set(runQueryOptions?.triggeredBy ?? null);
2820
3200
  state.subtle.request.set(request);
3201
+ onRequest?.(request);
2821
3202
  state.subtle.bindRequestEvents(request);
2822
3203
  state.subtle.devtoolsStats?.recordExecution({ didRequest: executed, body: args?.body, url: request.url });
2823
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
+ };
2824
3216
  /**
2825
3217
  * Writes the args an execution runs with to `state.args`, so a later bare `execute()` re-sends them.
2826
- * A `withArgs` source owns `state.args`, and a `withLongPolling` round must not look like a new
2827
- * 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.
2828
3220
  */
2829
3221
  const recordExecutionArgs = (state, args, options) => {
2830
- if (state.subtle.hasArgsSource() || options?.triggeredBy === LONG_POLLING_TRIGGER)
3222
+ const triggeredBy = options?.triggeredBy;
3223
+ if (triggeredBy === LONG_POLLING_TRIGGER)
2831
3224
  return;
2832
- state.args.set(args);
3225
+ if (!state.subtle.hasArgsSource())
3226
+ return state.args.set(args);
3227
+ if (args === null || args === state.args() || (triggeredBy && FEATURE_TRIGGERS.includes(triggeredBy)))
3228
+ return;
3229
+ state.subtle.setExplicitArgs(args);
2833
3230
  };
2834
3231
  const LONG_POLLING_TRIGGER = 'long-polling';
3232
+ const FEATURE_TRIGGERS = ['polling', 'auto-refresh'];
2835
3233
  const CIRCULAR_DEPENDENCY_WINDOW_MS = 100;
2836
3234
  const CIRCULAR_DEPENDENCY_MAX_REPEATS = 5;
2837
3235
  /**
@@ -2868,6 +3266,8 @@ const createExecuteFn = (executeOptions) => {
2868
3266
  const reset = () => untracked(() => resetExecuteState({ executeState, executeOptions }));
2869
3267
  const exec = (executeArgs) => untracked(() => {
2870
3268
  const { args = executeOptions.state.args(), options } = executeArgs ?? {};
3269
+ if (skipParkedExecution(executeOptions.state, args, executeOptions.creatorInternals.route))
3270
+ return;
2871
3271
  circularChecker.check(args);
2872
3272
  aborter.capture();
2873
3273
  recordExecutionArgs(executeOptions.state, args, options);
@@ -2949,153 +3349,6 @@ const createBaseQueryCreator = (config) => {
2949
3349
  return queryCreator;
2950
3350
  };
2951
3351
 
2952
- /**
2953
- * Route building, query-string serialization and the token clock - shared by the current generation and
2954
- * the legacy V2 client, which re-exports them under their original names.
2955
- */
2956
- // codes 1-99 of the shared 0-999 `@ethlete/query` range - see `query-errors.ts` for the rest
2957
- const RouteRuntimeErrorCode = {
2958
- INVALID_BASE_ROUTE: 1,
2959
- INVALID_ROUTE: 2,
2960
- PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION: 3,
2961
- };
2962
- const invalidBaseRouteError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.INVALID_BASE_ROUTE, 'The baseRoute must not end with a slash', data);
2963
- const invalidRouteError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.INVALID_ROUTE, 'The route must start with a slash', data);
2964
- const pathParamsMissingInRouteFunctionError$1 = (data) => new RuntimeError(RouteRuntimeErrorCode.PATH_PARAMS_MISSING_IN_ROUTE_FUNCTION, 'The route is a function but pathParams are missing', data);
2965
- const isNaN$2 = (value) => typeof value === 'number' && Number.isNaN(value);
2966
- const isEmptyString$1 = (value) => typeof value === 'string' && value.trim() === '';
2967
- const buildQueryString$1 = (params, config) => {
2968
- const objectNotation = config?.objectNotation ?? 'bracket';
2969
- const writeArrayIndexes = config?.writeArrayIndexes ?? false;
2970
- const ignoredValues = config?.ignoredValues ?? [undefined, null, Infinity, -Infinity];
2971
- const ignoredValuesFns = config?.ignoredValuesFns ?? [isNaN$2, isEmptyString$1];
2972
- const queryParams = [];
2973
- function processValue(key, rawValue) {
2974
- if (rawValue instanceof Date && Number.isNaN(rawValue.getTime()))
2975
- return false;
2976
- const value = rawValue instanceof Date ? rawValue.toISOString() : rawValue;
2977
- if (config?.objectNotation === 'json-stringify') {
2978
- if (value === undefined) {
2979
- return false;
2980
- }
2981
- if (ignoredValues.includes(value)) {
2982
- return false;
2983
- }
2984
- if (ignoredValuesFns.some((fn) => fn(value))) {
2985
- return false;
2986
- }
2987
- const encodedKey = encodeURIComponent(key);
2988
- const val = typeof value === 'object' ? JSON.stringify(value) : value;
2989
- const encodedValue = encodeURIComponent(val);
2990
- queryParams.push(`${encodedKey}=${encodedValue}`);
2991
- return true;
2992
- }
2993
- else if (Array.isArray(value)) {
2994
- let currentFilteredIndex = 0;
2995
- let didAddAnyValue = false;
2996
- for (const arrayValue of value) {
2997
- const nestedKey = writeArrayIndexes ? `${key}[${currentFilteredIndex}]` : `${key}[]`;
2998
- const didAddValue = processValue(nestedKey, arrayValue);
2999
- if (didAddValue) {
3000
- currentFilteredIndex++;
3001
- didAddAnyValue = true;
3002
- }
3003
- }
3004
- return didAddAnyValue;
3005
- }
3006
- else if (typeof value === 'object' && value !== null) {
3007
- let didAddAnyValue = false;
3008
- for (const [objKey, val] of Object.entries(value)) {
3009
- const nestedKey = objectNotation === 'dot' ? `${key}.${objKey}` : `${key}[${objKey}]`;
3010
- didAddAnyValue = processValue(nestedKey, val) || didAddAnyValue;
3011
- }
3012
- return didAddAnyValue;
3013
- }
3014
- else {
3015
- if (ignoredValues.includes(value)) {
3016
- return false;
3017
- }
3018
- if (ignoredValuesFns.some((fn) => fn(value))) {
3019
- return false;
3020
- }
3021
- const encodedKey = encodeURIComponent(key);
3022
- const encodedValue = encodeURIComponent(value);
3023
- queryParams.push(`${encodedKey}=${encodedValue}`);
3024
- return true;
3025
- }
3026
- }
3027
- for (const [key, val] of Object.entries(params)) {
3028
- processValue(key, val);
3029
- }
3030
- return queryParams.length ? queryParams.join('&') : null;
3031
- };
3032
- const isPlainObject = (value) => {
3033
- if (!value || typeof value !== 'object')
3034
- return false;
3035
- const proto = Object.getPrototypeOf(value);
3036
- return proto === Object.prototype || proto === null;
3037
- };
3038
- /** Copies query params with every plain object's keys sorted, so key order never reaches a cache key. */
3039
- const sortQueryParamKeys = (value) => {
3040
- if (Array.isArray(value))
3041
- return value.map(sortQueryParamKeys);
3042
- if (!isPlainObject(value))
3043
- return value;
3044
- return Object.fromEntries(Object.keys(value)
3045
- .sort()
3046
- .map((key) => [key, sortQueryParamKeys(value[key])]));
3047
- };
3048
- const buildRoute$1 = (options) => {
3049
- if (options.base.endsWith('/')) {
3050
- throw invalidBaseRouteError$1(options.base);
3051
- }
3052
- let route;
3053
- if (typeof options.route === 'function') {
3054
- if (!options.pathParams) {
3055
- throw pathParamsMissingInRouteFunctionError$1(options.route({}));
3056
- }
3057
- route = options.route(options.pathParams);
3058
- }
3059
- else {
3060
- route = options.route ?? null;
3061
- }
3062
- if (route && !route.startsWith('/')) {
3063
- throw invalidRouteError$1(route);
3064
- }
3065
- if (options.queryParams) {
3066
- const queryString = buildQueryString$1(options.queryParams, options.queryParamConfig);
3067
- if (queryString) {
3068
- route = route ? `${route}?${queryString}` : `/?${queryString}`;
3069
- }
3070
- }
3071
- return `${options.base}${route ?? ''}`;
3072
- };
3073
- const buildTimestampFromSeconds$1 = (seconds) => {
3074
- if (seconds === null) {
3075
- return null;
3076
- }
3077
- return new Date(Date.now() + seconds * 1000).getTime();
3078
- };
3079
- /** Reads a bearer token's payload. */
3080
- const decryptBearer$1 = (token) => {
3081
- try {
3082
- const base64Url = token.split('.')[1];
3083
- if (!base64Url) {
3084
- return null;
3085
- }
3086
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
3087
- const jsonPayload = decodeURIComponent(atob(base64)
3088
- .split('')
3089
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
3090
- .join(''));
3091
- return JSON.parse(jsonPayload);
3092
- }
3093
- catch (error) {
3094
- console.error('Invalid bearer token', error);
3095
- return null;
3096
- }
3097
- };
3098
-
3099
3352
  /**
3100
3353
  * `true` on every request `@ethlete/query` sends through `HttpClient`, so an app interceptor can skip
3101
3354
  * query requests: `if (req.context.get(IS_QUERY_REQUEST)) return next(req);`.
@@ -3449,11 +3702,18 @@ const createHttpRequest = (options) => {
3449
3702
  retryState: retryState.asReadonly(),
3450
3703
  lastDurationMs: lastDurationMs.asReadonly(),
3451
3704
  abort,
3705
+ markStale: () => {
3706
+ if (!isStale())
3707
+ expiresIn.set(Date.now());
3708
+ },
3709
+ setResponse: (body) => response.set(body),
3452
3710
  },
3453
3711
  };
3454
3712
  return httpRequest;
3455
3713
  };
3456
3714
 
3715
+ const wrapAsObservableSignal = (source, defaultInjector) => createObservableSignalWatcher(defaultInjector).wrap(source);
3716
+
3457
3717
  /* eslint-disable @typescript-eslint/no-explicit-any */
3458
3718
  /** Transforms an array of arrays into a single array and filters out `null` values. */
3459
3719
  const transformArrayResponse = (responses) => responses.filter((r) => !!r).flatMap((r) => r);
@@ -3465,7 +3725,7 @@ const createQueryStack = (options) => {
3465
3725
  const lastQuery = newQueries[newQueries.length - 1] ?? oldQueries[oldQueries.length - 1] ?? null;
3466
3726
  return { queries, lastQuery };
3467
3727
  }, transform, argsKeyFn, deduplicateArgs, maxQueries = Infinity, removeStrategy = 'oldest', } = options ?? {};
3468
- const injector = inject(Injector);
3728
+ const injector = injectInQueryContext(Injector, 'query stack');
3469
3729
  const queries = signal([], /* @ts-ignore */
3470
3730
  ...(ngDevMode ? [{ debugName: "queries" }] : /* istanbul ignore next */ []));
3471
3731
  const lastQuery = signal(null, /* @ts-ignore */
@@ -3473,11 +3733,11 @@ const createQueryStack = (options) => {
3473
3733
  const firstQuery = computed(() => queries()[0] ?? null, /* @ts-ignore */
3474
3734
  ...(ngDevMode ? [{ debugName: "firstQuery" }] : /* istanbul ignore next */ []));
3475
3735
  const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
3476
- const hasWithOptimisticUpdateFeature = features.some((f) => f.type === QueryFeatureType.WITH_RESPONSE_UPDATE);
3736
+ const hasWithResponseUpdateFeature = features.some((f) => f.type === QueryFeatureType.WITH_RESPONSE_UPDATE);
3477
3737
  if (hasWithArgsFeature) {
3478
3738
  throw queryStackWithArgsUsed();
3479
3739
  }
3480
- if (hasWithOptimisticUpdateFeature) {
3740
+ if (hasWithResponseUpdateFeature) {
3481
3741
  throw queryStackWithResponseUpdateUsed();
3482
3742
  }
3483
3743
  const runWithArgs = (args) => {
@@ -3730,6 +3990,7 @@ const createPagedQueryStack = (options) => {
3730
3990
  const args = options.args(initialPage(), []);
3731
3991
  untracked(() => {
3732
3992
  stack.clear();
3993
+ pageDirection.set('next');
3733
3994
  currentPageArgs.set(args);
3734
3995
  loadedMinPage.set(initialPage());
3735
3996
  loadedMaxPage.set(initialPage());
@@ -4505,7 +4766,8 @@ const createIndexedDbQueryPersistenceAdapter = (options) => {
4505
4766
  */
4506
4767
  const createQueryBatch = (options) => {
4507
4768
  const { queryCreator, args: mapArgs, stopOnError, allowCache, onItemSettled, features = [] } = options;
4508
- 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));
4509
4771
  if (features.some((f) => f.type === QueryFeatureType.WITH_ARGS)) {
4510
4772
  throw queryBatchWithArgsUsed();
4511
4773
  }
@@ -4786,41 +5048,6 @@ const isQueryDevtoolsRepositoryLive = (repository) => {
4786
5048
  return live$1.has(repository);
4787
5049
  };
4788
5050
 
4789
- /**
4790
- * Resolves an {@link QueryInvalidationOptions.url} to the absolute form request URLs are built in, so
4791
- * the comparison - and the message the other tabs receive - never depends on who resolves it.
4792
- */
4793
- const resolveInvalidationUrl = (baseUrl, url) => {
4794
- const absolute = url.startsWith('/') ? `${baseUrl}${url}` : url;
4795
- // A trailing slash would put the boundary check below one character past where the URL actually
4796
- // ends, which is the one way `/players/` could fail to match `/players/1`.
4797
- return absolute.endsWith('/') ? absolute.slice(0, -1) : absolute;
4798
- };
4799
- /**
4800
- * Whether a query URL is the invalidated one or sits below it. The check on what follows the prefix
4801
- * is what keeps `/players` from matching `/players-archive`, which a `startsWith` alone would.
4802
- */
4803
- const isUnderInvalidatedUrl = (queryUrl, invalidatedUrl) => {
4804
- if (!queryUrl.startsWith(invalidatedUrl))
4805
- return false;
4806
- const rest = queryUrl.slice(invalidatedUrl.length);
4807
- return rest === '' || rest.startsWith('/') || rest.startsWith('?') || rest.startsWith('#');
4808
- };
4809
- /**
4810
- * Turns an invalidation into the filter the repository refreshes by, or `undefined` when nothing
4811
- * narrows it - "everything in use" is the repository's own cheapest path.
4812
- */
4813
- const createQueryInvalidationFilter = (options) => {
4814
- const { url, filter } = options;
4815
- if (!url && !filter)
4816
- return undefined;
4817
- return (request) => {
4818
- if (url && !isUnderInvalidatedUrl(request.url, url))
4819
- return false;
4820
- return filter ? filter({ method: request.method, url: request.url }) : true;
4821
- };
4822
- };
4823
-
4824
5051
  /**
4825
5052
  * An error body can be an HTML page instead of JSON: a proxy's 502, a load balancer's maintenance page, a
4826
5053
  * platform's "service temporarily unavailable". The markup is never worth showing, but the sentence inside it
@@ -5002,6 +5229,14 @@ const createQuerySyncEngine = (options) => {
5002
5229
  // response sharing would just make a tab stop updating. Enforced here rather than documented.
5003
5230
  const isPollingDedupeEnabled = (config.dedupePolling ?? true) && syncResponses && transport.isSupported && lockManager.isSupported;
5004
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
+ };
5005
5240
  const broadcast = (event) => {
5006
5241
  if (event.type !== 'request-success' || !event.isMultiTabSyncEnabled)
5007
5242
  return;
@@ -5023,7 +5258,12 @@ const createQuerySyncEngine = (options) => {
5023
5258
  // worth telling the other tabs about - and an uncacheable read has no shared key to send anyway.
5024
5259
  if (!refreshOnMutation || shouldCacheQuery(event.request.method))
5025
5260
  return;
5026
- 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
+ });
5027
5267
  };
5028
5268
  const apply = (message) => {
5029
5269
  if (message.type === 'response') {
@@ -5035,23 +5275,20 @@ const createQuerySyncEngine = (options) => {
5035
5275
  // An invalidation is something the other tab's app code asked for explicitly, so unlike the
5036
5276
  // mutation heuristic it is not something `refreshOnMutation: false` opts out of.
5037
5277
  if (message.type === 'invalidate') {
5038
- repository.refreshInUse(createQueryInvalidationFilter({ url: message.url }), {
5278
+ repository.refreshInUse(createQueryInvalidationFilter({ url: message.url, tag: message.tag }), {
5039
5279
  type: 'invalidation',
5040
5280
  url: message.url,
5041
5281
  otherTab: true,
5042
- });
5282
+ ...(message.tag && { tag: message.tag }),
5283
+ }, message.batch);
5043
5284
  return;
5044
5285
  }
5045
5286
  if (!refreshOnMutation)
5046
5287
  return;
5047
5288
  const mutation = { method: message.method, url: message.url };
5048
- repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined, {
5049
- type: 'mutation',
5050
- url: message.url,
5051
- otherTab: true,
5052
- });
5289
+ repository.refreshInUse(mutationFilter ? (request) => mutationFilter(mutation, request) : undefined, { type: 'mutation', url: message.url, otherTab: true }, message.batch);
5053
5290
  };
5054
- const postInvalidation = (url) => transport.post({ type: 'invalidate', url });
5291
+ const postInvalidation = (url, tag) => transport.post({ type: 'invalidate', url, ...(tag && { tag }), batch: currentOutgoingBatch() });
5055
5292
  const eventSubscription = repository.events$.subscribe(broadcast);
5056
5293
  const unlisten = transport.listen(apply);
5057
5294
  const destroy = () => {
@@ -5104,11 +5341,13 @@ const unwrapQuerySyncMessage = (data) => {
5104
5341
  expiresAt: typeof expiresAt === 'number' ? expiresAt : null,
5105
5342
  };
5106
5343
  }
5344
+ const batch = typeof raw['batch'] === 'string' ? { batch: raw['batch'] } : {};
5107
5345
  if (type === 'mutation' && typeof method === 'string' && typeof url === 'string') {
5108
- return { type: 'mutation', method: method, url };
5346
+ return { type: 'mutation', method: method, url, ...batch };
5109
5347
  }
5110
5348
  if (type === 'invalidate' && (typeof url === 'string' || url === null)) {
5111
- return { type: 'invalidate', url };
5349
+ const tag = raw['tag'];
5350
+ return { type: 'invalidate', url, tag: typeof tag === 'string' ? tag : null, ...batch };
5112
5351
  }
5113
5352
  return null;
5114
5353
  };
@@ -5370,6 +5609,16 @@ const withEthleteApiErrors = (options) => () => {
5370
5609
  };
5371
5610
  };
5372
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
+ };
5373
5622
  const generateUuid = () => randomId();
5374
5623
  /** @see CreateQueryClientConfigOptions.keepUnusedFor */
5375
5624
  const DEFAULT_KEEP_UNUSED_FOR = 300_000;
@@ -5421,8 +5670,17 @@ const createQueryRepository = (config) => {
5421
5670
  queryParamConfig: config.queryString,
5422
5671
  })
5423
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('_');
5424
5682
  const cacheKey = shouldCache
5425
- ? buildQueryCacheKey(`${options.key ? options.key + '_' : ''}${keyRoute}`, {
5683
+ ? buildQueryCacheKey(`${wireKey ? wireKey + '_' : ''}${options.key ? options.key + '_' : ''}${keyRoute}`, {
5426
5684
  body: args?.body,
5427
5685
  queryParams: args?.queryParams,
5428
5686
  pathParams: args?.pathParams,
@@ -5433,6 +5691,7 @@ const createQueryRepository = (config) => {
5433
5691
  const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
5434
5692
  const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
5435
5693
  const isSecure = options.isSecure ?? false;
5694
+ const tags = options.tags ?? [];
5436
5695
  // A secure response needs an explicit `persistence: true` to reach the disk: leaving an
5437
5696
  // authenticated user's data there is a decision per endpoint, not a default. Everything else
5438
5697
  // persists unless it opted out.
@@ -5457,6 +5716,7 @@ const createQueryRepository = (config) => {
5457
5716
  isMultiTabSyncEnabled,
5458
5717
  isPersistEnabled,
5459
5718
  keepUnusedFor,
5719
+ tags,
5460
5720
  });
5461
5721
  const executed = !runQueryOptions?.allowCache || cacheEntry.request.isStale()
5462
5722
  ? cacheEntry.request.execute({ allowCache: runQueryOptions?.allowCache })
@@ -5489,6 +5749,7 @@ const createQueryRepository = (config) => {
5489
5749
  isMultiTabSyncEnabled,
5490
5750
  isPersistEnabled,
5491
5751
  keepUnusedFor,
5752
+ tags,
5492
5753
  });
5493
5754
  // Announced after the entry is bound and its request is on its way, because the one thing that acts
5494
5755
  // on it - hydration from the persisted store - is only ever allowed to fill a gap this request has
@@ -5550,6 +5811,7 @@ const createQueryRepository = (config) => {
5550
5811
  cacheEntry.isPersistEnabled = bindings.some((binding) => binding.isPersistEnabled);
5551
5812
  cacheEntry.isMultiTabSyncEnabled = bindings.every((binding) => binding.isMultiTabSyncEnabled);
5552
5813
  cacheEntry.keepUnusedFor = Math.min(...bindings.map((binding) => binding.keepUnusedFor));
5814
+ cacheEntry.tags = bindings.flatMap((binding) => binding.tags);
5553
5815
  };
5554
5816
  const unbind = (key, consumerDestroyRef) => {
5555
5817
  if (key === null)
@@ -5592,10 +5854,30 @@ const createQueryRepository = (config) => {
5592
5854
  bumpCacheVersion();
5593
5855
  eventsSubject.next({ type: 'unbind-all-secure' });
5594
5856
  };
5595
- 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) => {
5596
5875
  const refreshed = [];
5876
+ const marksUnusedStale = cause?.type === 'invalidation';
5877
+ const restartedInBatch = resolveRestartedBatch(cause, batch);
5597
5878
  for (const cacheEntry of cache.values()) {
5598
- if (cacheEntry.consumers.size === 0)
5879
+ const isUnused = cacheEntry.consumers.size === 0;
5880
+ if (isUnused && !marksUnusedStale)
5599
5881
  continue;
5600
5882
  // Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed:
5601
5883
  // a cacheable method, or a GQL query transported via POST. Opting into the cache alone does not
@@ -5603,8 +5885,15 @@ const createQueryRepository = (config) => {
5603
5885
  // end the session.
5604
5886
  if (!cacheEntry.isRefreshable)
5605
5887
  continue;
5606
- if (filter && !filter(cacheEntry.request))
5888
+ if (filter && !filter(cacheEntry.request, cacheEntry.tags))
5889
+ continue;
5890
+ if (isUnused) {
5891
+ cacheEntry.request.subtle.markStale();
5607
5892
  continue;
5893
+ }
5894
+ if (restartedInBatch?.has(cacheEntry.request))
5895
+ continue;
5896
+ restartedInBatch?.add(cacheEntry.request);
5608
5897
  refreshed.push(cacheEntry.request);
5609
5898
  cacheEntry.request.execute({ force: true });
5610
5899
  }
@@ -5635,7 +5924,7 @@ const createQueryRepository = (config) => {
5635
5924
  return true;
5636
5925
  };
5637
5926
  const bind = (options) => {
5638
- const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
5927
+ const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, tags, } = options;
5639
5928
  const cacheEntry = cache.get(key);
5640
5929
  if (cacheEntry && isCached) {
5641
5930
  const existingBinding = cacheEntry.consumers.get(consumerDestroyRef);
@@ -5645,6 +5934,7 @@ const createQueryRepository = (config) => {
5645
5934
  existingBinding.isMultiTabSyncEnabled = isMultiTabSyncEnabled;
5646
5935
  existingBinding.isPersistEnabled = isPersistEnabled;
5647
5936
  existingBinding.keepUnusedFor = keepUnusedFor;
5937
+ existingBinding.tags = tags;
5648
5938
  }
5649
5939
  else {
5650
5940
  cacheEntry.consumers.set(consumerDestroyRef, {
@@ -5654,6 +5944,7 @@ const createQueryRepository = (config) => {
5654
5944
  isMultiTabSyncEnabled,
5655
5945
  isPersistEnabled,
5656
5946
  keepUnusedFor,
5947
+ tags,
5657
5948
  });
5658
5949
  }
5659
5950
  mergeConsumerPolicies(cacheEntry);
@@ -5669,6 +5960,7 @@ const createQueryRepository = (config) => {
5669
5960
  isMultiTabSyncEnabled,
5670
5961
  isPersistEnabled,
5671
5962
  keepUnusedFor,
5963
+ tags,
5672
5964
  },
5673
5965
  ],
5674
5966
  ]);
@@ -5715,6 +6007,7 @@ const createQueryRepository = (config) => {
5715
6007
  isRefreshable,
5716
6008
  isMultiTabSyncEnabled,
5717
6009
  isPersistEnabled,
6010
+ tags,
5718
6011
  });
5719
6012
  }
5720
6013
  bumpCacheVersion();
@@ -5724,6 +6017,8 @@ const createQueryRepository = (config) => {
5724
6017
  isSecure: entry.isSecure,
5725
6018
  consumerCount: entry.consumers.size,
5726
6019
  isUnused: entry.consumers.size === 0,
6020
+ isRefreshable: entry.isRefreshable,
6021
+ tags: entry.tags,
5727
6022
  request: entry.request,
5728
6023
  }));
5729
6024
  const evict = (key, cause = 'manual') => {
@@ -5750,7 +6045,21 @@ const createQueryRepository = (config) => {
5750
6045
  return repository;
5751
6046
  };
5752
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
+ };
5753
6060
  const createQueryClient = (options) => defineRootProvider(() => {
6061
+ if (options.baseUrl.endsWith('/'))
6062
+ throw invalidBaseRouteError$1(options.baseUrl);
5754
6063
  const httpClient = inject(HttpClient);
5755
6064
  const ngErrorHandler = inject(ErrorHandler);
5756
6065
  const injector = inject(Injector);
@@ -5768,6 +6077,8 @@ const createQueryClient = (options) => defineRootProvider(() => {
5768
6077
  dependencies: { httpClient, ngErrorHandler, injector },
5769
6078
  });
5770
6079
  const destroyRef = inject(DestroyRef);
6080
+ if (isDevMode())
6081
+ warnOnDuplicateClientName(options.name, destroyRef);
5771
6082
  destroyRef.onDestroy(() => {
5772
6083
  for (const entry of repository.subtle.cacheEntries()) {
5773
6084
  if (entry.isUnused)
@@ -5807,15 +6118,17 @@ const createQueryClient = (options) => defineRootProvider(() => {
5807
6118
  refreshQueriesInUse: () => repository.refreshInUse(),
5808
6119
  invalidateQueries: (invalidation) => {
5809
6120
  const url = invalidation?.url ? resolveInvalidationUrl(options.baseUrl, invalidation.url) : null;
5810
- repository.refreshInUse(createQueryInvalidationFilter({ url, filter: invalidation?.filter }), {
6121
+ const tag = invalidation?.tag ?? null;
6122
+ repository.refreshInUse(createQueryInvalidationFilter({ url, tag, filter: invalidation?.filter }), {
5811
6123
  type: 'invalidation',
5812
6124
  url,
5813
6125
  otherTab: false,
6126
+ ...(tag && { tag }),
5814
6127
  });
5815
6128
  // The resolved URL is what travels: the other tabs are the same client, so they would
5816
6129
  // resolve it identically, and a message that needs no interpretation cannot drift.
5817
6130
  if (invalidation?.otherTabs ?? true)
5818
- sync?.postInvalidation(url);
6131
+ sync?.postInvalidation(url, tag);
5819
6132
  },
5820
6133
  clearPersistedQueries: () => persistenceEngine?.clear() ?? Promise.resolve(),
5821
6134
  whenPersistenceReady: persistenceEngine?.whenReady ?? Promise.resolve(),
@@ -5959,6 +6272,8 @@ const createSecureExecuteFactory = (options) => {
5959
6272
  args: executeArgs?.args ?? options.state.args(),
5960
6273
  options: executeArgs?.options,
5961
6274
  };
6275
+ if (skipParkedExecution(options.state, execArgsWithDefaults.args, options.route))
6276
+ return;
5962
6277
  circularChecker.check(execArgsWithDefaults.args);
5963
6278
  hasExecuted = true;
5964
6279
  lastExecuteArgs = execArgsWithDefaults;
@@ -6048,6 +6363,7 @@ const createSecureExecuteFn = (executeOptions) => {
6048
6363
  authProvider,
6049
6364
  deps: executeOptions.deps,
6050
6365
  state: executeOptions.state,
6366
+ route: executeOptions.creatorInternals.route,
6051
6367
  autoExecutes: shouldAutoExecuteQuery(executeOptions.creatorInternals.method) && !executeOptions.queryConfig.onlyManualExecution,
6052
6368
  transformAuthAndExec: (executeArgs, executeState) => {
6053
6369
  queryExecute({
@@ -6110,6 +6426,181 @@ const createPatchQuery = /* @__PURE__ */ createQueryTemplate('PATCH');
6110
6426
  /** A query creator that creates a secure PATCH query (requires authentication) */
6111
6427
  const createSecurePatchQuery = /* @__PURE__ */ createSecureQueryTemplate('PATCH');
6112
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
+
6113
6604
  const isAbortedSnapshot = (snapshot) => snapshot.latestHttpEvent()?.type === 'cancel' && snapshot.error() === null;
6114
6605
  const executeAndSettle$ = (query, executeArgs, emitAborted) => defer(() => {
6115
6606
  query.execute(executeArgs);
@@ -6749,16 +7240,18 @@ const createBearerAuthProviderImpl = (config) => {
6749
7240
  const refreshToken = signal(null, /* @ts-ignore */
6750
7241
  ...(ngDevMode ? [{ debugName: "refreshToken" }] : /* istanbul ignore next */ []));
6751
7242
  const afterTokenRefresh$ = new Subject();
7243
+ const expiresInPropertyName = signal('exp', /* @ts-ignore */
7244
+ ...(ngDevMode ? [{ debugName: "expiresInPropertyName" }] : /* istanbul ignore next */ []));
6752
7245
  const bearerData = computed(() => {
6753
7246
  const token = accessToken();
6754
7247
  if (!token)
6755
7248
  return null;
6756
7249
  try {
6757
- const decoded = config.bearerDecryptFn?.(token) ?? decryptBearer$1(token);
7250
+ const decoded = config.bearerDecryptFn ? config.bearerDecryptFn(token) : decryptBearer$1(token);
6758
7251
  return patchQueryDevtoolsTokenPayload({
6759
7252
  payload: decoded,
6760
7253
  providerName: config.name,
6761
- expiresInPropertyName: 'exp',
7254
+ expiresInPropertyName: expiresInPropertyName(),
6762
7255
  });
6763
7256
  }
6764
7257
  catch (error) {
@@ -6891,6 +7384,7 @@ const createBearerAuthProviderImpl = (config) => {
6891
7384
  reportAccessTokenExpiry: (isExpired) => {
6892
7385
  readAccessTokenExpiry = isExpired;
6893
7386
  },
7387
+ reportExpiresInPropertyName: (claim) => expiresInPropertyName.set(claim),
6894
7388
  };
6895
7389
  for (const builder of config.queries) {
6896
7390
  builder.setup?.(querySetupContext);
@@ -7081,6 +7575,7 @@ const withRefreshQuery = (key, config) => {
7081
7575
  const buildArgs = (refreshToken) => (config.buildArgs?.(refreshToken) ?? { body: { token: refreshToken } });
7082
7576
  const setup = (context) => {
7083
7577
  const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
7578
+ context.reportExpiresInPropertyName(expiresInPropertyName);
7084
7579
  const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
7085
7580
  const refreshIfExpired = config.refreshIfExpired ?? true;
7086
7581
  const destroyRef = inject(DestroyRef);
@@ -7266,47 +7761,50 @@ const withRefreshQuery = (key, config) => {
7266
7761
  return Math.min(Math.max(msUntilAccessTokenIsStale() ?? minRefreshInterval, rescheduleDelayMs), maxTimerDelayMs);
7267
7762
  }
7268
7763
  };
7269
- const calculateRefreshBuffer = (tokenLifetimeMs) => {
7270
- if (typeof config.refreshStrategy === 'number') {
7271
- return config.refreshStrategy >= 0 && config.refreshStrategy <= 1
7272
- ? tokenLifetimeMs * (1 - config.refreshStrategy)
7273
- : Math.max(config.refreshStrategy, 0);
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);
7274
7782
  }
7275
- const percentage = config.refreshStrategy?.percentage ?? 0.75;
7276
- const minBufferMs = config.refreshStrategy?.minBufferMs ?? 60000; // 1 minute
7277
- const maxBufferMs = config.refreshStrategy?.maxBufferMs ?? 600000; // 10 minutes
7278
- const calculatedBuffer = tokenLifetimeMs * (1 - percentage);
7279
- return Math.max(minBufferMs, Math.min(maxBufferMs, calculatedBuffer));
7783
+ return times;
7280
7784
  };
7281
7785
  /** When this token expires, as a timestamp, or `null` if the expiry cannot be read off it. */
7282
- const tokenExpiresAt = (token) => {
7283
- try {
7284
- const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer$1(token);
7285
- const bearerDataValue = patchQueryDevtoolsTokenPayload({
7286
- payload: decoded,
7287
- providerName: context.name,
7288
- expiresInPropertyName,
7289
- });
7290
- const expiresIn = bearerDataValue?.[expiresInPropertyName];
7291
- if (typeof expiresIn !== 'number') {
7292
- if (isDevMode()) {
7293
- console.warn(`Token does not contain valid ${expiresInPropertyName} property for auto-refresh`);
7294
- }
7295
- return null;
7296
- }
7297
- 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;
7298
7795
  }
7299
- catch {
7300
- return null;
7796
+ if (measuredLifetime?.token !== token) {
7797
+ measuredLifetime = { token, lifetimeMs: times.expiresAtMs - Date.now() };
7301
7798
  }
7799
+ return measuredLifetime.lifetimeMs;
7302
7800
  };
7303
7801
  /** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
7304
7802
  const scheduledRefreshDelay = (token) => {
7305
- const expiresAt = tokenExpiresAt(token);
7306
- if (expiresAt === null)
7803
+ const times = tokenTimes(token);
7804
+ if (times === null)
7307
7805
  return null;
7308
- const tokenLifetimeMs = expiresAt - Date.now();
7309
- if (tokenLifetimeMs <= 0) {
7806
+ const remainingMs = times.expiresAtMs - Date.now();
7807
+ if (remainingMs <= 0) {
7310
7808
  if (!refreshIfExpired)
7311
7809
  return null;
7312
7810
  if (isDevMode()) {
@@ -7314,7 +7812,7 @@ const withRefreshQuery = (key, config) => {
7314
7812
  }
7315
7813
  return 0;
7316
7814
  }
7317
- return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
7815
+ return Math.max(remainingMs - refreshBufferMs(config.refreshStrategy, tokenLifetimeMs(token, times)), 0);
7318
7816
  };
7319
7817
  /**
7320
7818
  * How long this tab can still work with the access token it holds, or `null` without a readable
@@ -7915,40 +8413,59 @@ const buildPersistentAuthFeature = (config, context) => {
7915
8413
  */
7916
8414
  const createPersistentAuthFeature = (config, context) => runInInjectionContext(context.injector, () => buildPersistentAuthFeature(config, context));
7917
8415
 
8416
+ const RENEWAL_GRACE_MS = 10_000;
7918
8417
  const withTokenExpirationWarning = (config = {}) => {
7919
8418
  return (context) => {
7920
8419
  const warningThreshold = config.warningThreshold ?? 5 * 60 * 1000;
7921
8420
  const checkInterval = config.checkInterval ?? 1000;
7922
8421
  const refreshBuilder = context.queryBuilders.find((builder) => builder._type === 'tokenRefreshQuery');
7923
- const expiresInPropertyName = config.expiresInPropertyName ??
7924
- (refreshBuilder?._type === 'tokenRefreshQuery' ? refreshBuilder.config.expiresInPropertyName : undefined) ??
7925
- 'exp';
7926
- const expiresAt = computed(() => {
7927
- const decoded = context.bearerData();
7928
- 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)
7929
8428
  return null;
7930
- const exp = decoded[expiresInPropertyName];
7931
- 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)
7932
8436
  return null;
7933
- 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;
7934
8449
  }, /* @ts-ignore */
7935
8450
  ...(ngDevMode ? [{ debugName: "expiresAt" }] : /* istanbul ignore next */ []));
7936
- const expiresIn$ = toObservable(context.bearerData).pipe(switchMap((bearerData) => bearerData
8451
+ const expiresIn = toSignal(toObservable(tokenTimes).pipe(switchMap((times) => times
7937
8452
  ? timer(0, checkInterval).pipe(map(() => {
7938
- const expiry = expiresAt();
7939
- if (!expiry)
7940
- return null;
7941
- const msUntilExpiry = expiry.getTime() - Date.now();
8453
+ const msUntilExpiry = times.expiresAtMs - Date.now();
7942
8454
  return msUntilExpiry > 0 ? msUntilExpiry : null;
7943
8455
  }))
7944
- : of(null)));
7945
- const isExpiringSoon$ = expiresIn$.pipe(map((msUntilExpiry) => {
7946
- 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)
7947
8461
  return false;
7948
- return msUntilExpiry <= warningThreshold && msUntilExpiry > 0;
7949
- }));
7950
- const expiresIn = toSignal(expiresIn$, { initialValue: null });
7951
- 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 */ []));
7952
8469
  const instance = {
7953
8470
  expiresAt,
7954
8471
  expiresIn,
@@ -8001,10 +8518,11 @@ const withTokenRevocation = (config) => {
8001
8518
  ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
8002
8519
  const dropped = signal(false, /* @ts-ignore */
8003
8520
  ...(ngDevMode ? [{ debugName: "dropped" }] : /* istanbul ignore next */ []));
8004
- const follow = (read) => wrapAsObservableSignal(computed(() => {
8521
+ const watcher = createObservableSignalWatcher(context.injector);
8522
+ const follow = (read) => watcher.wrap(computed(() => {
8005
8523
  const snapshot = target();
8006
8524
  return snapshot ? read(snapshot) : null;
8007
- }), context.injector);
8525
+ }));
8008
8526
  const snapshot = {
8009
8527
  args: follow((s) => s.args()),
8010
8528
  response: follow((s) => s.response()),
@@ -8015,7 +8533,7 @@ const withTokenRevocation = (config) => {
8015
8533
  triggeredBy: follow((s) => s.triggeredBy()),
8016
8534
  id: follow((s) => s.id()),
8017
8535
  executionState: follow((s) => s.executionState()),
8018
- isAlive: wrapAsObservableSignal(computed(() => target()?.isAlive() ?? !(dropped() || queueDropped())), context.injector),
8536
+ isAlive: watcher.wrap(computed(() => target()?.isAlive() ?? !(dropped() || queueDropped()))),
8019
8537
  };
8020
8538
  const queued = {
8021
8539
  tokens,
@@ -8152,19 +8670,18 @@ const buildTrackingFeature = (context, config) => {
8152
8670
  }
8153
8671
  }
8154
8672
  };
8155
- const fireHandlers = (event, data) => {
8156
- const eventHandlers = handlers.get(event);
8157
- if (eventHandlers) {
8158
- eventHandlers.forEach((handler) => {
8159
- try {
8160
- handler(data);
8161
- }
8162
- catch (error) {
8163
- console.error(`Error in tracking event handler for "${event}":`, error);
8164
- }
8165
- });
8166
- }
8167
- };
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
+ });
8168
8685
  const pendingForwardedMessages = [];
8169
8686
  const forwardingChannel = context.leaderElection && typeof BroadcastChannel !== 'undefined'
8170
8687
  ? new BroadcastChannel(`ethlete-auth-tracking:${context.name}`)
@@ -8298,6 +8815,10 @@ const buildTrackingFeature = (context, config) => {
8298
8815
  */
8299
8816
  const createTrackingFeature = (context, config) => runInInjectionContext(context.injector, () => buildTrackingFeature(context, config));
8300
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
+
8301
8822
  const ethlete = {};
8302
8823
  let app = null;
8303
8824
  /** What `window.ethlete` exposes - the same object {@link queryDevtoolsAbout} returns. */
@@ -9321,12 +9842,23 @@ const sanitizeSession = (value) => {
9321
9842
  savedAt: typeof session.savedAt === 'number' ? session.savedAt : 0,
9322
9843
  };
9323
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
+ };
9324
9856
  const sanitize = (value) => ({
9325
9857
  sessions: Array.isArray(value?.sessions)
9326
9858
  ? value.sessions.map(sanitizeSession).filter((session) => session !== null)
9327
9859
  : [],
9328
9860
  credentials: typeof value?.credentials === 'object' && value.credentials !== null ? value.credentials : {},
9329
- accounts: Array.isArray(value?.accounts) ? value.accounts : [],
9861
+ accounts: Array.isArray(value?.accounts) ? value.accounts.filter(isLocalAccount) : [],
9330
9862
  });
9331
9863
  /**
9332
9864
  * The vault is one key every tab writes, so a tab holding a stale copy of it would drop whatever another
@@ -9979,266 +10511,59 @@ const resolveQueryDevtoolsFaultForAttempt = (target) => {
9979
10511
  if (fault.failNext > 0) {
9980
10512
  shouldFail = true;
9981
10513
  // Not the public setter: spending the budget is the armed fault doing its job, not someone re-arming
9982
- // it, so it must not clear what the panel's bar says about where the fault came from.
9983
- patchFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
9984
- }
9985
- else if (fault.failRate > 0) {
9986
- shouldFail = Math.random() * 100 < fault.failRate;
9987
- }
9988
- return {
9989
- latencyMs: fault.latencyMs,
9990
- status: shouldFail ? fault.status : null,
9991
- };
9992
- };
9993
-
9994
- /**
9995
- * Wraps the committed value of the query form `formId` in a signal that reports every read to the
9996
- * devtools.
9997
- * @internal
9998
- */
9999
- const noteQueryFormReads = (formId, source) => {
10000
- const memoized = computed(() => source(), /* @ts-ignore */
10001
- ...(ngDevMode ? [{ debugName: "memoized" }] : /* istanbul ignore next */ []));
10002
- // The read must be noted per read, not per recomputation of the value: the second query to build
10003
- // args reads a memoized value, and a note that only fires on a recomputation never reaches its
10004
- // collection window. Carrying the memo's signal node over keeps this function a real `Signal`.
10005
- const read = () => {
10006
- noteQueryFormRead(formId);
10007
- return memoized();
10008
- };
10009
- return Object.assign(read, { [SIGNAL]: memoized[SIGNAL] });
10010
- };
10011
- /**
10012
- * Records which query forms feed one query's args, so the devtools can answer "which query does this
10013
- * form drive?" from the reads themselves rather than from a naming convention.
10014
- * @internal
10015
- */
10016
- const createQueryDevtoolsFormLinks = () => {
10017
- const ids = signal([], /* @ts-ignore */
10018
- ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
10019
- return {
10020
- ids: ids.asReadonly(),
10021
- track: (build) => {
10022
- const collected = collectQueryFormLinks(build);
10023
- // Args are built inside a reactive computation, which may not write to a signal - `untracked`
10024
- // lifts that restriction, and skipping the write unless something is new keeps the panel from
10025
- // being notified on every evaluation.
10026
- untracked(() => {
10027
- const known = ids();
10028
- const added = collected.ids.filter((id) => !known.includes(id));
10029
- if (added.length)
10030
- ids.set([...known, ...added]);
10031
- });
10032
- return collected.value;
10033
- },
10034
- };
10035
- };
10036
-
10037
- /**
10038
- * The designed library. Which of them is **armed** is a separate store under a separate scope - see
10039
- * {@link queryDevtoolsArmedMocks}.
10040
- */
10041
- const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
10042
- const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
10043
- const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
10044
- ...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
10045
- const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
10046
- ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
10047
- const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
10048
- ...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
10049
- const scope$1 = () => queryDevtoolsSettings().mocks;
10050
- const armedScope = () => queryDevtoolsSettings().armedMocks;
10051
- /** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
10052
- const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
10053
- /**
10054
- * The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
10055
- * an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
10056
- */
10057
- const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
10058
- /**
10059
- * Whether what is armed right now came back from a previous page load rather than being armed by hand.
10060
- * The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
10061
- */
10062
- const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
10063
- /**
10064
- * The identity of a designed mock: one per client, method, route **and** declared query. The query is part
10065
- * of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
10066
- */
10067
- const queryDevtoolsMockId = (parts) => `${parts.clientName}|${parts.method}|${parts.pattern}${parts.query ? `?${parts.query}` : ''}`;
10068
- const write = () => {
10069
- if (scope$1() === 'none') {
10070
- clearQueryDevtoolsStore(STORAGE_KEY$1);
10071
- return;
10072
- }
10073
- writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
10074
- };
10075
- const writeArmed = () => {
10076
- if (armedScope() === 'none') {
10077
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10078
- return;
10079
- }
10080
- writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
10081
- };
10082
- /** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
10083
- const setArmed = (next) => {
10084
- armed.set(next);
10085
- armedRestored.set(false);
10086
- writeArmed();
10087
- };
10088
- const isMock = (value) => {
10089
- const mock = value;
10090
- return (!!mock &&
10091
- typeof mock.id === 'string' &&
10092
- typeof mock.clientName === 'string' &&
10093
- typeof mock.method === 'string' &&
10094
- typeof mock.pattern === 'string' &&
10095
- typeof mock.status === 'number');
10096
- };
10097
- /**
10098
- * Reads the designed library, so a mock can be armed without authoring it again. Called by
10099
- * `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
10100
- * @internal
10101
- */
10102
- const initQueryDevtoolsMocks = () => {
10103
- const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
10104
- // `query` was added after the first version of the store, so an entry written before it still loads.
10105
- mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
10106
- const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
10107
- const library = new Set(mocks().map((mock) => mock.id));
10108
- // An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
10109
- // mock that may never be designed again.
10110
- const inherited = Array.isArray(kept)
10111
- ? kept.filter((id) => typeof id === 'string' && library.has(id))
10112
- : [];
10113
- armed.set(new Set(inherited));
10114
- armedRestored.set(inherited.length > 0);
10115
- };
10116
- /**
10117
- * The path of a request URL relative to the client's `baseUrl` (base path included), without its query
10118
- * string - what a pattern is matched against. A URL outside `baseUrl`, or no `baseUrl` at all, falls
10119
- * back to the path without its origin.
10120
- */
10121
- const queryDevtoolsRequestPath = (url, baseUrl) => {
10122
- const withoutQuery = url.split('?')[0] ?? '';
10123
- const base = baseUrl?.replace(/\/+$/, '');
10124
- if (base && withoutQuery.startsWith(base)) {
10125
- const rest = withoutQuery.slice(base.length);
10126
- if (!rest)
10127
- return '/';
10128
- if (rest.startsWith('/'))
10129
- return rest;
10130
- }
10131
- const schemeEnd = withoutQuery.indexOf('://');
10132
- if (schemeEnd === -1)
10133
- return withoutQuery;
10134
- const pathStart = withoutQuery.indexOf('/', schemeEnd + 3);
10135
- return pathStart === -1 ? '/' : withoutQuery.slice(pathStart);
10136
- };
10137
- const segmentsOf$1 = (path) => path.split('/').filter(Boolean);
10138
- /** The query string of a request URL, without the `?`. */
10139
- const queryOf = (url) => url.split('?')[1] ?? '';
10140
- /**
10141
- * Whether a request carries every query parameter a mock declares. Extra parameters are ignored - a mock
10142
- * says what the request must ask for, not everything it may.
10143
- */
10144
- const matchesQueryDevtoolsMockQuery = (declared, url) => {
10145
- if (!declared)
10146
- return true;
10147
- const asked = new URLSearchParams(queryOf(url));
10148
- return [...new URLSearchParams(declared)].every(([key, value]) => asked.get(key) === value);
10149
- };
10150
- /**
10151
- * Whether a request path is the route a pattern describes. A `:param` segment matches any single
10152
- * non-empty segment, which is the whole point: a mock is armed for a route, not for the one URL a query
10153
- * happened to build.
10154
- */
10155
- const matchesQueryDevtoolsMockPattern = (pattern, path) => {
10156
- const patternSegments = segmentsOf$1(pattern);
10157
- const pathSegments = segmentsOf$1(path);
10158
- if (patternSegments.length !== pathSegments.length)
10159
- return false;
10160
- return patternSegments.every((segment, index) => segment.startsWith(':') || segment === pathSegments[index]);
10161
- };
10162
- /**
10163
- * Adds a designed mock, or replaces the one already under its id. Arming is untouched: re-designing a
10164
- * body while it is being served changes what the next request gets.
10165
- */
10166
- const saveQueryDevtoolsMock = (mock) => {
10167
- mocks.update((current) => {
10168
- const index = current.findIndex((entry) => entry.id === mock.id);
10169
- if (index === -1)
10170
- return [...current, mock];
10171
- return current.map((entry) => (entry.id === mock.id ? mock : entry));
10172
- });
10173
- write();
10174
- };
10175
- /** Removes a designed mock, disarming it on the way out. */
10176
- const deleteQueryDevtoolsMock = (id) => {
10177
- mocks.update((current) => current.filter((entry) => entry.id !== id));
10178
- armQueryDevtoolsMock(id, false);
10179
- write();
10180
- };
10181
- /** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
10182
- const armQueryDevtoolsMock = (id, isArmed) => {
10183
- if (isArmed && !mocks().some((mock) => mock.id === id))
10184
- return;
10185
- const next = new Set(armed());
10186
- if (isArmed)
10187
- next.add(id);
10188
- else
10189
- next.delete(id);
10190
- setArmed(next);
10514
+ // it, so it must not clear what the panel's bar says about where the fault came from.
10515
+ patchFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
10516
+ }
10517
+ else if (fault.failRate > 0) {
10518
+ shouldFail = Math.random() * 100 < fault.failRate;
10519
+ }
10520
+ return {
10521
+ latencyMs: fault.latencyMs,
10522
+ status: shouldFail ? fault.status : null,
10523
+ };
10191
10524
  };
10192
- /** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
10193
- const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
10194
- /** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
10195
- const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
10525
+
10196
10526
  /**
10197
- * Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
10198
- * the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
10199
- * exactly what it was already serving.
10527
+ * Wraps the committed value of the query form `formId` in a signal that reports every read to the
10528
+ * devtools.
10529
+ * @internal
10200
10530
  */
10201
- const setQueryDevtoolsArmedMocksScope = (next) => {
10202
- if (next === armedScope())
10203
- return;
10204
- // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
10205
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10206
- setQueryDevtoolsSettings({ armedMocks: next });
10207
- writeArmed();
10208
- };
10209
- /** Empties the designed library and disarms everything - the panel's "Reset devtools". */
10210
- const clearQueryDevtoolsMockStore = () => {
10211
- mocks.set([]);
10212
- armed.set(new Set());
10213
- armedRestored.set(false);
10214
- clearQueryDevtoolsStore(STORAGE_KEY$1);
10215
- clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
10531
+ const noteQueryFormReads = (formId, source) => {
10532
+ const memoized = computed(() => source(), /* @ts-ignore */
10533
+ ...(ngDevMode ? [{ debugName: "memoized" }] : /* istanbul ignore next */ []));
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] });
10216
10542
  };
10217
- const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
10218
10543
  /**
10219
- * The mock to serve one upcoming attempt, or `null` to let it reach the network. The armed mock declaring
10220
- * the most query parameters wins, so a special case armed on top of a general one is what answers.
10221
- *
10222
- * 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.
10223
10546
  * @internal
10224
10547
  */
10225
- const resolveQueryDevtoolsMockForAttempt = (target) => {
10226
- const ids = armed();
10227
- if (!ids.size)
10228
- return null;
10229
- const path = queryDevtoolsRequestPath(target.url, target.baseUrl);
10230
- const mock = mocks()
10231
- .filter((candidate) => ids.has(candidate.id) &&
10232
- candidate.clientName === target.clientName &&
10233
- candidate.method === target.method &&
10234
- matchesQueryDevtoolsMockPattern(candidate.pattern, path) &&
10235
- matchesQueryDevtoolsMockQuery(candidate.query, target.url))
10236
- // The most specific armed mock answers: one that names `page=2` beats one that takes any query, so
10237
- // arming a general mock does not shadow the special case someone armed on top of it.
10238
- .sort((a, b) => declaredParamCount(b) - declaredParamCount(a))[0];
10239
- if (!mock)
10240
- return null;
10241
- 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
+ };
10242
10567
  };
10243
10568
 
10244
10569
  const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
@@ -10475,19 +10800,19 @@ const resizePagination = (value, shape, mode, amount) => {
10475
10800
  }
10476
10801
  return result;
10477
10802
  };
10478
- const STRING_PRESETS = {
10803
+ const STRING_PRESETS$1 = {
10479
10804
  short: 'Ab',
10480
10805
  long: /* @__PURE__ */ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt. '.repeat(4),
10481
10806
  longWord: 'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
10482
10807
  unicode: 'مرحبا بالعالم 👋 日本語テスト 🎉 Ñoño',
10483
10808
  };
10484
- const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS[preset]);
10485
- const NUMBER_PRESETS = {
10809
+ const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS$1[preset]);
10810
+ const NUMBER_PRESETS$1 = {
10486
10811
  zero: 0,
10487
10812
  negative: -1,
10488
10813
  huge: Number.MAX_SAFE_INTEGER,
10489
10814
  };
10490
- const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS[preset]);
10815
+ const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS$1[preset]);
10491
10816
  const pickRandom = (values) => values[Math.floor(Math.random() * values.length)];
10492
10817
  const randomIntBetween = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
10493
10818
  const randomHex = (length) => {
@@ -10703,6 +11028,30 @@ const OP_TYPES = /* @__PURE__ */ new Set([
10703
11028
  'reset',
10704
11029
  ]);
10705
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
+ };
10706
11055
  /**
10707
11056
  * Whether a value parsed from outside - a paste, a stored set - is an op this build can replay.
10708
11057
  * @internal
@@ -10711,7 +11060,7 @@ const isOverrideOp = (value) => {
10711
11060
  if (!value || typeof value !== 'object' || Array.isArray(value))
10712
11061
  return false;
10713
11062
  const op = value;
10714
- 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));
10715
11064
  };
10716
11065
  /**
10717
11066
  * Serializes the ops armed on one query into clipboard text. `reset` never reaches a recorder's list, so
@@ -10777,12 +11126,12 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
10777
11126
  * debugging the wrong thing. `local` is offered in Settings for the dev who has a reason, and the
10778
11127
  * restored-overrides bar names the scope whatever it is.
10779
11128
  */
10780
- const STORAGE_KEY = 'ethlete:query:devtools:overrides:v1';
11129
+ const STORAGE_KEY$1 = 'ethlete:query:devtools:overrides:v1';
10781
11130
  const restored = /* @__PURE__ */ signal([], /* @ts-ignore */
10782
11131
  ...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
10783
11132
  const restoredScope = /* @__PURE__ */ signal(null, /* @ts-ignore */
10784
11133
  ...(ngDevMode ? [{ debugName: "restoredScope" }] : /* istanbul ignore next */ []));
10785
- const scope = () => queryDevtoolsSettings().overrides;
11134
+ const scope$1 = () => queryDevtoolsSettings().overrides;
10786
11135
  /** Where "Keep across reloads" puts them back when it is switched on again. */
10787
11136
  let lastPersistentScope = 'session';
10788
11137
  /** Every recorder that currently holds at least one op, so switching persistence on can capture them. */
@@ -10793,7 +11142,7 @@ const carriedOver = /* @__PURE__ */ new Map();
10793
11142
  const replayed = /* @__PURE__ */ new Set();
10794
11143
  let ops = {};
10795
11144
  const readStore = () => {
10796
- const parsed = readQueryDevtoolsStore(scope(), STORAGE_KEY);
11145
+ const parsed = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
10797
11146
  const stored = parsed?.ops;
10798
11147
  const usable = !!stored && typeof stored === 'object' && !Array.isArray(stored);
10799
11148
  const valid = {};
@@ -10805,11 +11154,11 @@ const readStore = () => {
10805
11154
  return { enabled: parsed?.enabled === true, ops: valid };
10806
11155
  };
10807
11156
  const writeStore = () => {
10808
- if (scope() === 'none') {
10809
- clearQueryDevtoolsStore(STORAGE_KEY);
11157
+ if (scope$1() === 'none') {
11158
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
10810
11159
  return;
10811
11160
  }
10812
- writeQueryDevtoolsStore(scope(), STORAGE_KEY, { enabled: true, ops });
11161
+ writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, { enabled: true, ops });
10813
11162
  };
10814
11163
  const publishRestored = () => restored.set([...carriedOver].map(([id, count]) => ({ id, count, armed: replayed.has(id) })));
10815
11164
  const forgetCarriedOver = () => {
@@ -10842,7 +11191,7 @@ const initQueryDevtoolsOverridePersistence = () => {
10842
11191
  if (list.length)
10843
11192
  carriedOver.set(id, list.length);
10844
11193
  }
10845
- const current = scope();
11194
+ const current = scope$1();
10846
11195
  if (current !== 'none')
10847
11196
  lastPersistentScope = current;
10848
11197
  restoredScope.set(carriedOver.size ? current : null);
@@ -10852,7 +11201,7 @@ const initQueryDevtoolsOverridePersistence = () => {
10852
11201
  * Whether armed response overrides are stored and replayed on the next page load - that is, whether
10853
11202
  * their scope is anything other than `none`. Off by default.
10854
11203
  */
10855
- const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope() !== 'none', /* @ts-ignore */
11204
+ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope$1() !== 'none', /* @ts-ignore */
10856
11205
  ...(ngDevMode ? [{ debugName: "queryDevtoolsOverridePersistence" }] : /* istanbul ignore next */ []));
10857
11206
  /**
10858
11207
  * Where armed response overrides are kept. Switching to a scope that keeps them captures whatever is
@@ -10861,11 +11210,11 @@ const queryDevtoolsOverridePersistence = /* @__PURE__ */ computed(() => scope()
10861
11210
  * op exactly where it is for the rest of this page.
10862
11211
  */
10863
11212
  const setQueryDevtoolsOverridesScope = (next) => {
10864
- const previous = scope();
11213
+ const previous = scope$1();
10865
11214
  if (next === previous)
10866
11215
  return;
10867
11216
  // Off both stores first: whichever the previous scope was, its copy must not outlive the change.
10868
- clearQueryDevtoolsStore(STORAGE_KEY);
11217
+ clearQueryDevtoolsStore(STORAGE_KEY$1);
10869
11218
  setQueryDevtoolsSettings({ overrides: next });
10870
11219
  if (next === 'none') {
10871
11220
  ops = {};
@@ -10884,106 +11233,309 @@ const setQueryDevtoolsOverridesScope = (next) => {
10884
11233
  writeStore();
10885
11234
  };
10886
11235
  /**
10887
- * Turns persistence on or off - the panel's "Keep across reloads" toggle. On restores the last scope
10888
- * 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
10889
11396
  */
10890
- const setQueryDevtoolsOverridePersistence = (value) => {
10891
- setQueryDevtoolsOverridesScope(value ? lastPersistentScope : 'none');
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);
10892
11410
  };
10893
11411
  /**
10894
- * Empties the override store in both browser stores and disarms everything a reload brought back,
10895
- * without changing where overrides are kept - the panel's "Reset devtools".
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.
10896
11415
  */
10897
- const clearQueryDevtoolsOverrideStore = () => {
10898
- clearRestoredQueryDevtoolsOverrides();
10899
- ops = {};
10900
- clearQueryDevtoolsStore(STORAGE_KEY);
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);
10901
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] ?? '';
10902
11435
  /**
10903
- * The ops this page load inherited from the previous one, and for each id whether a live query has taken
10904
- * it. Overrides that outlive the page that armed them have to announce themselves, because an app being
10905
- * lied to looks exactly like an app that is broken.
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.
10906
11438
  */
10907
- const restoredQueryDevtoolsOverrides =
10908
- /* @__PURE__ */ restored.asReadonly();
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));
11444
+ };
10909
11445
  /**
10910
- * The scope those ops came back from, or `null` when this page inherited none. `local` is the one worth
10911
- * naming in the banner: it means they outlived closing the tab, not just a reload.
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.
10912
11449
  */
10913
- const queryDevtoolsRestoredOverridesScope =
10914
- /* @__PURE__ */ restoredScope.asReadonly();
10915
- /** Disarms every op this page load inherited and empties the store - the banner's one-click way out. */
10916
- const clearRestoredQueryDevtoolsOverrides = () => {
10917
- for (const id of replayed) {
10918
- armedRecorders.get(id)?.clearAll();
10919
- armedRecorders.delete(id);
10920
- }
10921
- for (const id of carriedOver.keys())
10922
- writeOps(id, []);
10923
- forgetCarriedOver();
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]);
10924
11456
  };
10925
11457
  /**
10926
- * Forgets a destroyed query's recorder. Ops already persisted stay in the store on purpose - they are
10927
- * keyed by an id the next page load re-derives, which is what "Keep across reloads" replays them from.
10928
- * @internal
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.
10929
11460
  */
10930
- const releaseQueryDevtoolsOverridePersistence = (id) => {
10931
- armedRecorders.delete(id);
10932
- if (scope() !== 'none')
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();
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))
10933
11479
  return;
10934
- const { [id]: _dropped, ...rest } = ops;
10935
- ops = rest;
11480
+ const next = new Set(armed());
11481
+ if (isArmed)
11482
+ next.add(id);
11483
+ else
11484
+ next.delete(id);
11485
+ setArmed(next);
10936
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());
10937
11491
  /**
10938
- * Wraps a query's overrides recorder so its armed ops are stored and replayed. Replay happens here, at
10939
- * registration, rather than in the panel - a query can run long before the panel is ever opened.
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.
11495
+ */
11496
+ const setQueryDevtoolsArmedMocksScope = (next) => {
11497
+ if (next === armedScope())
11498
+ return;
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);
11511
+ };
11512
+ const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
11513
+ /**
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.
10940
11516
  *
10941
- * Ids are reload-deterministic (descriptor + per-descriptor counter), so the same queries created in the
10942
- * same order re-arm correctly; an id the store holds that nobody claims is reported by
10943
- * {@link restoredQueryDevtoolsOverrides} rather than guessed at.
11517
+ * Installed as the mock resolver by `provideQueryDevtools()`; nothing else may call it.
10944
11518
  * @internal
10945
11519
  */
10946
- const withQueryDevtoolsOverridePersistence = (id, recorder) => {
10947
- const inherited = ops[id];
10948
- if (scope() !== 'none' && inherited?.length) {
10949
- for (const op of inherited)
10950
- recorder.arm(op);
10951
- armedRecorders.set(id, recorder);
10952
- replayed.add(id);
10953
- publishRestored();
10954
- }
10955
- const sync = () => {
10956
- const list = recorder.list().map((entry) => entry.op);
10957
- if (list.length)
10958
- armedRecorders.set(id, recorder);
10959
- else
10960
- armedRecorders.delete(id);
10961
- if (scope() !== 'none')
10962
- writeOps(id, list);
10963
- };
10964
- return {
10965
- ...recorder,
10966
- arm: (op) => {
10967
- recorder.arm(op);
10968
- sync();
10969
- },
10970
- clear: (opId) => {
10971
- recorder.clear(opId);
10972
- sync();
10973
- },
10974
- clearAll: () => {
10975
- recorder.clearAll();
10976
- if (replayed.delete(id))
10977
- publishRestored();
10978
- sync();
10979
- },
10980
- };
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 };
10981
11537
  };
10982
11538
 
10983
- // Generated from package.json by tools/scripts/generate-version.js - do not edit.
10984
- /** The version of `@ethlete/query` this build was cut from. */
10985
- const QUERY_VERSION = '6.0.0-next.54';
10986
-
10987
11539
  /** How deep a schema is expanded before a nested branch is given up on. */
10988
11540
  const MAX_DEPTH = 12;
10989
11541
  /** How many elements are generated for an array, whatever its `minItems` says. */
@@ -11202,9 +11754,7 @@ const stringSample = (node, schema, ctx) => {
11202
11754
  if (ctx.style === 'stress')
11203
11755
  return fitLength(generateQueryDevtoolsStringPreset(pickStressStringPreset()), schema);
11204
11756
  const title = schema['title'];
11205
- if (typeof title === 'string' && title)
11206
- return title;
11207
- return node.key ?? 'string';
11757
+ return fitLength(typeof title === 'string' && title ? title : (node.key ?? 'string'), schema);
11208
11758
  };
11209
11759
  const boundsOf = (schema) => {
11210
11760
  const exclusiveMin = finiteNumber(schema['exclusiveMinimum']);
@@ -11224,9 +11774,9 @@ const clampToBounds = (value, bounds) => {
11224
11774
  const numberSample = (schema, ctx) => {
11225
11775
  const bounds = boundsOf(schema);
11226
11776
  if (ctx.style === 'placeholder')
11227
- return bounds.min ?? 0;
11777
+ return clampToBounds(bounds.min ?? 0, bounds);
11228
11778
  if (ctx.style === 'realistic') {
11229
- return generateQueryDevtoolsSampleNumber({ ...bounds, fractional: typeOf(schema) === 'number' });
11779
+ return clampToBounds(generateQueryDevtoolsSampleNumber({ ...bounds, fractional: typeOf(schema) === 'number' }), bounds);
11230
11780
  }
11231
11781
  const allowsNegative = bounds.min === null || bounds.min < 0;
11232
11782
  return clampToBounds(generateQueryDevtoolsNumberPreset(allowsNegative && Math.random() < 0.5 ? 'negative' : 'huge'), bounds);
@@ -11667,7 +12217,8 @@ const queryDevtoolsResponseHistory = () => queryDevtoolsSettings().responseHisto
11667
12217
  * @internal
11668
12218
  */
11669
12219
  const setQueryDevtoolsResponseHistory = (count) => {
11670
- 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));
11671
12222
  };
11672
12223
  /** The index of the newest run still in flight, or -1 when none is. */
11673
12224
  const lastPendingRunIndex = (runs) => {
@@ -12353,7 +12904,7 @@ const provideQueryDevtools = (options) => {
12353
12904
  return makeEnvironmentProviders([settleThePillsOnFirstRender()]);
12354
12905
  };
12355
12906
 
12356
- const getOpName = /\b(?:query|mutation)\s+([\w-]+)(?:\s*\([^)]*\))?\s*\{/;
12907
+ const getOpName = /\b(?:query|mutation)\s+([\w-]+)(?:\s*\([^)]*\))?(?:\s*@\w+(?:\s*\([^)]*\))?)*\s*\{/;
12357
12908
  const STRING_LITERAL = '"""[\\s\\S]*?"""|"(?:[^"\\\\\\n]|\\\\.)*"';
12358
12909
  const commentOutsideString = /* @__PURE__ */ new RegExp(`(${STRING_LITERAL})|#[^\\n\\r]*`, 'g');
12359
12910
  const whitespaceOutsideString = /* @__PURE__ */ new RegExp(`(${STRING_LITERAL})|\\s+`, 'g');
@@ -12361,6 +12912,19 @@ const minifyGql = (document) => document
12361
12912
  .replace(commentOutsideString, (_match, literal) => literal ?? '')
12362
12913
  .replace(whitespaceOutsideString, (_match, literal) => literal ?? ' ')
12363
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
+ };
12364
12928
  /**
12365
12929
  * Builds the GraphQL-over-HTTP payload of a document. `variables` comes back as a JSON string for
12366
12930
  * the `GET` transport (the default), where the payload travels as query parameters, and as the map
@@ -12368,7 +12932,7 @@ const minifyGql = (document) => document
12368
12932
  */
12369
12933
  const transformGql = (str) => {
12370
12934
  const normalizedStr = Array.isArray(str) ? str.join('') : str;
12371
- const operationName = getOpName.exec(normalizedStr.replace(commentOutsideString, (_match, literal) => (literal ? '""' : '')))?.[1];
12935
+ const operationName = getOpName.exec(topLevelOnly(normalizedStr.replace(commentOutsideString, (_match, literal) => (literal ? '""' : ''))))?.[1];
12372
12936
  let minified;
12373
12937
  return (variables, transport = 'GET') => {
12374
12938
  const data = { query: isDevMode() ? normalizedStr : (minified ??= minifyGql(normalizedStr)) };
@@ -12409,9 +12973,11 @@ const createGqlExecuteFn = (executeOptions) => {
12409
12973
  const executeState = setupQueryExecuteState();
12410
12974
  const circularChecker = circularQueryDependencyChecker();
12411
12975
  const aborter = createQueryExecutionAborter(executeOptions.state);
12412
- const reset = () => resetExecuteState({ executeState, executeOptions });
12413
- const exec = (executeArgs) => {
12976
+ const reset = () => untracked(() => resetExecuteState({ executeState, executeOptions }));
12977
+ const exec = (executeArgs) => untracked(() => {
12414
12978
  const { args = executeOptions.state.args(), options } = executeArgs ?? {};
12979
+ if (skipParkedExecution(executeOptions.state, args, executeOptions.creator?.route))
12980
+ return;
12415
12981
  circularChecker.check(args);
12416
12982
  let gqlParams = gqlTransformerFor(executeOptions.creatorInternals)(args?.variables, executeOptions.creatorInternals.transport);
12417
12983
  if (args?.queryParams && executeOptions.creatorInternals.transport === 'GET') {
@@ -12451,7 +13017,7 @@ const createGqlExecuteFn = (executeOptions) => {
12451
13017
  options,
12452
13018
  isRefreshable: executeOptions.creatorInternals.method === 'QUERY',
12453
13019
  });
12454
- };
13020
+ });
12455
13021
  exec['reset'] = reset;
12456
13022
  exec['abort'] = () => aborter.abort();
12457
13023
  exec['aborted$'] = aborter.aborted$;
@@ -12501,6 +13067,7 @@ const createSecureGqlExecuteFn = (executeOptions) => {
12501
13067
  authProvider,
12502
13068
  deps: executeOptions.deps,
12503
13069
  state: executeOptions.state,
13070
+ route: executeOptions.creator?.route,
12504
13071
  autoExecutes: shouldAutoExecuteGqlQuery(executeOptions.creatorInternals.method) &&
12505
13072
  !executeOptions.queryConfig.onlyManualExecution,
12506
13073
  transformAuthAndExec: (executeArgs, executeState) => {
@@ -13932,6 +14499,10 @@ class LegacyQuery {
13932
14499
  // NG0205 ("Injector has already been destroyed") from inside the view's cleanup phase.
13933
14500
  this.newQuery.subtle.destroyRef.onDestroy(() => this.teardown());
13934
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
+ }
13935
14506
  get rawState() {
13936
14507
  return this.toQueryState(this.newQuery.executionState());
13937
14508
  }
@@ -14190,9 +14761,10 @@ const createInertQuery = () => {
14190
14761
  ...base,
14191
14762
  execute: noop,
14192
14763
  createSnapshot,
14764
+ url: inertSignal(null),
14193
14765
  reset: noop,
14194
14766
  abort: () => false,
14195
- asReadonly: () => ({ ...base, createSnapshot }),
14767
+ asReadonly: () => ({ ...base, createSnapshot, url: inertSignal(null) }),
14196
14768
  subtle: {
14197
14769
  destroy: noop,
14198
14770
  setResponse: noop,
@@ -15871,7 +16443,8 @@ class QueryDirective {
15871
16443
  this.queryState = queryStateSignal(this.query);
15872
16444
  inject(ViewContainerRef).createEmbeddedView(inject(TemplateRef), this.viewContext);
15873
16445
  effect(() => {
15874
- const query = extractQuery(this.query());
16446
+ const rawQuery = this.query();
16447
+ const query = extractQuery(rawQuery);
15875
16448
  untracked(() => {
15876
16449
  if (!query) {
15877
16450
  this.viewContext.$implicit = null;
@@ -15887,7 +16460,7 @@ class QueryDirective {
15887
16460
  if (isQueryStatePrepared(query.rawState) || isQueryStateCancelled(query.rawState)) {
15888
16461
  query.execute();
15889
16462
  }
15890
- this.viewContext.scope = isQueryCollection(query) ? query.type : null;
16463
+ this.viewContext.scope = isQueryCollection(rawQuery) ? rawQuery.type : null;
15891
16464
  this.viewContext.query = query;
15892
16465
  });
15893
16466
  });
@@ -16957,13 +17530,13 @@ const transformToStringArray = (value) => {
16957
17530
  };
16958
17531
  const transformToNumber = (value) => {
16959
17532
  if (typeof value === 'number') {
16960
- return value;
17533
+ return Number.isFinite(value) ? value : null;
16961
17534
  }
16962
17535
  if (typeof value === 'string') {
16963
17536
  if (!value.trim())
16964
17537
  return null;
16965
17538
  const number = Number(value);
16966
- return Number.isNaN(number) ? null : number;
17539
+ return Number.isFinite(number) ? number : null;
16967
17540
  }
16968
17541
  return null;
16969
17542
  };
@@ -16999,7 +17572,7 @@ const transformToBooleanArray = (value) => {
16999
17572
  const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
17000
17573
  const transformToDate = (value) => {
17001
17574
  if (value instanceof Date) {
17002
- return value;
17575
+ return Number.isNaN(value.getTime()) ? null : value;
17003
17576
  }
17004
17577
  if (typeof value === 'string') {
17005
17578
  const date = new Date(value);
@@ -17026,7 +17599,9 @@ const transformToDateArray = (value) => {
17026
17599
  };
17027
17600
  const transformToSort = (value) => {
17028
17601
  if (typeof value === 'string') {
17029
- 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);
17030
17605
  if (!active) {
17031
17606
  return null;
17032
17607
  }
@@ -17729,6 +18304,8 @@ const MAX_RESET_PASSES = 10;
17729
18304
  const changedKeysBetween = (previous, current) => Object.keys(current).filter((key) => !equal(previous?.[key], current[key]));
17730
18305
  /** Reads a URL string into the type of the field's default; `null` means the default's type cannot hold it. */
17731
18306
  const autoCoerce = (raw, defaultValue) => {
18307
+ if (Array.isArray(raw))
18308
+ return Array.isArray(defaultValue) ? raw : null;
17732
18309
  if (typeof raw !== 'string')
17733
18310
  return raw;
17734
18311
  if (Array.isArray(defaultValue))
@@ -17756,10 +18333,13 @@ const resolveDebounce = (fieldDefs, changedKeys, live) => {
17756
18333
  }
17757
18334
  return Math.min(...times);
17758
18335
  };
18336
+ const isUnserializable = (value) => value === undefined ||
18337
+ (typeof value === 'number' && Number.isNaN(value)) ||
18338
+ (value instanceof Date && Number.isNaN(value.getTime()));
17759
18339
  /**
17760
18340
  * A value that cannot be told apart from the default once serialized - a cleared text (`''`), an emptied list
17761
- * (`[]`) or one whose `valueToQueryParam` yields nothing - commits as the default, so the URL reproduces the
17762
- * 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.
17763
18343
  */
17764
18344
  const normalizeLive = (fieldDefs, rawLive, defaults) => {
17765
18345
  const live = { ...rawLive };
@@ -17770,7 +18350,7 @@ const normalizeLive = (fieldDefs, rawLive, defaults) => {
17770
18350
  const serialized = def.valueToQueryParam?.(value);
17771
18351
  const serializedToNothing = !!def.valueToQueryParam && (serialized === null || serialized === undefined);
17772
18352
  const clearedNullable = defaults[key] === null && (value === '' || (Array.isArray(value) && value.length === 0));
17773
- if (serializedToNothing || clearedNullable) {
18353
+ if (serializedToNothing || clearedNullable || isUnserializable(value)) {
17774
18354
  live[key] = defaults[key];
17775
18355
  }
17776
18356
  }
@@ -18261,6 +18841,13 @@ const defineQueryForm = (config) => {
18261
18841
  }
18262
18842
  observeOptions = options;
18263
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
+ }
18264
18851
  changesSeenAtObserve = untracked(queryParamChanges);
18265
18852
  if (options?.persistence) {
18266
18853
  restoreFromStorage(options.persistence, route.snapshot.queryParams);
@@ -18387,6 +18974,11 @@ const normalizeConfig = (config) => {
18387
18974
  isResetBy: isResetBy === undefined ? undefined : Array.isArray(isResetBy) ? isResetBy : [isResetBy],
18388
18975
  };
18389
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
+ };
18390
18982
  function queryField(config) {
18391
18983
  return {
18392
18984
  defaultValue: null,
@@ -18463,6 +19055,7 @@ function dateQueryField(config) {
18463
19055
  return {
18464
19056
  defaultValue: null,
18465
19057
  queryParamToValue: transformToDate,
19058
+ valueToQueryParam: dateToQueryParam,
18466
19059
  ...normalizeConfig(rest),
18467
19060
  };
18468
19061
  }
@@ -18470,6 +19063,11 @@ function dateQueryField(config) {
18470
19063
  const dateArrayQueryField = (config) => ({
18471
19064
  defaultValue: null,
18472
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
+ },
18473
19071
  ...normalizeConfig(config),
18474
19072
  });
18475
19073
 
@@ -18478,6 +19076,7 @@ const WebSocketRuntimeErrorCode = {
18478
19076
  // Web Socket Client
18479
19077
  ROOM_NOT_JOINED: 1000,
18480
19078
  MESSAGE_MALFORMED: 1001,
19079
+ AUTH_AND_AUTH_PROVIDER: 1002,
18481
19080
  };
18482
19081
  const roomNotJoined = (room) => {
18483
19082
  return new RuntimeError(WebSocketRuntimeErrorCode.ROOM_NOT_JOINED, `Tried leaving the room "${room}" but it has not been joined.`);
@@ -18485,6 +19084,9 @@ const roomNotJoined = (room) => {
18485
19084
  const messageMalformed = () => {
18486
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 }.');
18487
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
+ };
18488
19090
 
18489
19091
  const MAX_DEVTOOLS_MESSAGES = 100;
18490
19092
  const RECONNECT_BASE_DELAY = 1000;
@@ -18494,7 +19096,10 @@ const createWebSocketClient = (options) => {
18494
19096
  return defineRootProvider(() => {
18495
19097
  if (isPlatformServer(inject(PLATFORM_ID)))
18496
19098
  return createServerWebSocketClient();
18497
- 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;
18498
19103
  const socket = options.io(options.url, {
18499
19104
  withCredentials: options.withCredentials ?? true,
18500
19105
  autoConnect: false,
@@ -18502,6 +19107,7 @@ const createWebSocketClient = (options) => {
18502
19107
  auth: auth ? (cb) => cb(typeof auth === 'function' ? auth() : auth) : undefined,
18503
19108
  });
18504
19109
  const rooms = new Map();
19110
+ const endedRoomHolders = new Map();
18505
19111
  const joinsDeliveredThisConnection = new Set();
18506
19112
  const joinsDeliveredToClosedConnection = new Set();
18507
19113
  const isConnected = signal(false, /* @ts-ignore */
@@ -18532,50 +19138,54 @@ const createWebSocketClient = (options) => {
18532
19138
  direction: 'out',
18533
19139
  });
18534
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
+ };
18535
19170
  const joinRoom = (room) => {
18536
- const roomFn = typeof room === 'function' ? room : () => room;
18537
- // Must stay above the effect below: `previousSignalValue` registers a `toObservable` effect that has to run
18538
- // first in the same flush. Registered later, it leaves `pre()` on the room from the flush before, and the
18539
- // effect joins the new room without ever leaving the old one.
18540
- const pre = previousSignalValue(computed(() => roomFn()));
19171
+ if (typeof room === 'string')
19172
+ return joinStaticRoom(room);
19173
+ const roomFn = room;
18541
19174
  const roomData = signal(null, /* @ts-ignore */
18542
19175
  ...(ngDevMode ? [{ debugName: "roomData" }] : /* istanbul ignore next */ []));
18543
19176
  let joinedRoomName = null;
18544
- const join = (name) => {
18545
- const existingRoom = rooms.get(name);
18546
- if (existingRoom) {
18547
- existingRoom.joinCount++;
18548
- return existingRoom;
18549
- }
18550
- emit({ event: 'join-room', data: name, room: name });
18551
- joinsDeliveredToClosedConnection.delete(name);
18552
- const messages = new Subject();
18553
- const newRoom = {
18554
- latestMessage: signal(null),
18555
- messages,
18556
- messages$: messages.asObservable(),
18557
- joinCount: 1,
18558
- };
18559
- rooms.set(name, newRoom);
18560
- syncDevtoolsRooms();
18561
- return newRoom;
18562
- };
18563
19177
  effect(() => {
18564
- const current = roomFn();
19178
+ const current = roomFn() || null;
18565
19179
  untracked(() => {
18566
- const previous = pre();
18567
- if (previous === current)
19180
+ if (current === joinedRoomName)
18568
19181
  return;
18569
- if (previous) {
18570
- leaveRoom(previous);
19182
+ if (joinedRoomName) {
19183
+ leaveRoom(joinedRoomName);
18571
19184
  joinedRoomName = null;
18572
19185
  }
18573
19186
  if (current) {
18574
- const joinedRoom = join(current);
18575
- if (joinedRoom) {
18576
- joinedRoomName = current;
18577
- roomData.set(joinedRoom);
18578
- }
19187
+ joinedRoomName = current;
19188
+ roomData.set(join(current));
18579
19189
  }
18580
19190
  else {
18581
19191
  roomData.set(null);
@@ -18592,6 +19202,14 @@ const createWebSocketClient = (options) => {
18592
19202
  return roomData.asReadonly();
18593
19203
  };
18594
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
+ }
18595
19213
  const joinedRoom = rooms.get(room);
18596
19214
  if (!joinedRoom) {
18597
19215
  if (isDevMode())
@@ -18612,9 +19230,10 @@ const createWebSocketClient = (options) => {
18612
19230
  let reconnectTimer = null;
18613
19231
  let connectedAt = null;
18614
19232
  let destroyed = false;
19233
+ let suspended = false;
18615
19234
  /** socket.io stops reconnecting after a server disconnect or a rejected handshake, so the client takes over. */
18616
19235
  const scheduleReconnect = () => {
18617
- if (reconnectTimer !== null || destroyed)
19236
+ if (reconnectTimer !== null || destroyed || suspended)
18618
19237
  return;
18619
19238
  const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** reconnectAttempt, RECONNECT_MAX_DELAY);
18620
19239
  reconnectAttempt++;
@@ -18704,7 +19323,62 @@ const createWebSocketClient = (options) => {
18704
19323
  });
18705
19324
  setupWebSocketConnectionListener();
18706
19325
  setupWebSocketListener();
18707
- 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
+ }
18708
19382
  const client = {
18709
19383
  joinRoom,
18710
19384
  isConnected: isConnected.asReadonly(),
@@ -18751,5 +19425,5 @@ const createServerWebSocketClient = () => {
18751
19425
  * Generated bundle index. Do not edit.
18752
19426
  */
18753
19427
 
18754
- 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 };
18755
19429
  //# sourceMappingURL=ethlete-query.mjs.map