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