@ethlete/query 6.0.0-next.33 → 6.0.0-next.35
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 +38 -0
- package/fesm2022/ethlete-query.mjs +955 -256
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +393 -104
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
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, of, timer, switchMap, tap, retry, catchError, EMPTY, takeWhile,
|
|
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
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
7
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
8
8
|
import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
|
|
@@ -265,6 +265,24 @@ const resolveQueryDevtoolsMock = (target) => mockResolver?.(target) ?? null;
|
|
|
265
265
|
* @internal
|
|
266
266
|
*/
|
|
267
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);
|
|
268
286
|
let suppressStackRegistration = false;
|
|
269
287
|
/**
|
|
270
288
|
* Suppresses devtools registration for the next {@link createQueryStack} call. Used by
|
|
@@ -362,6 +380,8 @@ const QueryRuntimeErrorCode = {
|
|
|
362
380
|
WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
|
|
363
381
|
WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
|
|
364
382
|
SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT: 104,
|
|
383
|
+
WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD: 105,
|
|
384
|
+
WITH_LONG_POLLING_USED_WITH_POLLING: 106,
|
|
365
385
|
// Auth provider
|
|
366
386
|
AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
|
|
367
387
|
AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
|
|
@@ -412,6 +432,13 @@ const withAutoRefreshUsedInManualQuery = () => {
|
|
|
412
432
|
return new RuntimeError(QueryRuntimeErrorCode.WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY, `"withAutoRefresh()" has been used inside a query that has "onlyManualExecution" set to true.` +
|
|
413
433
|
` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
|
|
414
434
|
};
|
|
435
|
+
const withLongPollingUsedOnUnsupportedHttpMethod = (method) => {
|
|
436
|
+
return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_ON_UNSUPPORTED_HTTP_METHOD, `This is a "${method}" request, "withLongPolling()" is only supported for GET, HEAD, OPTIONS requests and GQL queries.`);
|
|
437
|
+
};
|
|
438
|
+
const withLongPollingUsedWithPolling = () => {
|
|
439
|
+
return new RuntimeError(QueryRuntimeErrorCode.WITH_LONG_POLLING_USED_WITH_POLLING, `"withLongPolling()" and "withPolling()" cannot be used on the same query - both would drive its re-execution.` +
|
|
440
|
+
` Keep the long polling feature and drop the interval, or the other way around.`);
|
|
441
|
+
};
|
|
415
442
|
const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
|
|
416
443
|
return new RuntimeError(QueryRuntimeErrorCode.SILENCE_MISSING_WITH_ARGS_FEATURE_ERROR_USED_BUT_WITH_ARGS_PRESENT, `The "silenceMissingWithArgsFeatureError" config is set to true, but a "withArgs()" feature is present.`);
|
|
417
444
|
};
|
|
@@ -516,11 +543,6 @@ const isSymfonyListError = (error) => {
|
|
|
516
543
|
'invalidValue' in error[0]);
|
|
517
544
|
};
|
|
518
545
|
|
|
519
|
-
/**
|
|
520
|
-
* Returning this inside a withArgs feature will reset the query args to null.
|
|
521
|
-
* This will also pause polling and auto refresh until new args are set.
|
|
522
|
-
*/
|
|
523
|
-
const CLEAR_QUERY_ARGS = /* @__PURE__ */ Symbol('CLEAR_QUERY_ARGS');
|
|
524
546
|
/** A angular effect that can be nested inside another effect */
|
|
525
547
|
const nestedEffect = (fn, options) => {
|
|
526
548
|
const activeConsumer = getActiveConsumer();
|
|
@@ -537,6 +559,7 @@ const QueryFeatureType = {
|
|
|
537
559
|
WITH_ERROR_HANDLING: 'WITH_ERROR_HANDLING',
|
|
538
560
|
WITH_SUCCESS_HANDLING: 'WITH_SUCCESS_HANDLING',
|
|
539
561
|
WITH_POLLING: 'WITH_POLLING',
|
|
562
|
+
WITH_LONG_POLLING: 'WITH_LONG_POLLING',
|
|
540
563
|
WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
|
|
541
564
|
WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
|
|
542
565
|
WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
|
|
@@ -548,8 +571,8 @@ const createQueryFeature = (config) => {
|
|
|
548
571
|
* A query feature that allows you to set the arguments of the query.
|
|
549
572
|
* The arguments are read within a computed function, so you can use reactive values.
|
|
550
573
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
574
|
+
* Return `null` to park the query: its args are reset to `null`, and polling and auto refresh
|
|
575
|
+
* pause until args are set again.
|
|
553
576
|
*
|
|
554
577
|
* Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
|
|
555
578
|
*/
|
|
@@ -557,26 +580,11 @@ const withArgs = (args) => {
|
|
|
557
580
|
return createQueryFeature({
|
|
558
581
|
type: QueryFeatureType.WITH_ARGS,
|
|
559
582
|
fn: (context) => {
|
|
560
|
-
// `state.args` is reactively backed by this source, so every reader (exec, polling,
|
|
561
|
-
// auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
|
|
562
|
-
// resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
|
|
563
|
-
let previous = null;
|
|
564
|
-
const resolve = () => {
|
|
565
|
-
const value = args();
|
|
566
|
-
if (value === CLEAR_QUERY_ARGS) {
|
|
567
|
-
previous = null;
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
if (value === null)
|
|
571
|
-
return previous;
|
|
572
|
-
previous = value;
|
|
573
|
-
return value;
|
|
574
|
-
};
|
|
575
583
|
// Only ever non-null while the devtools are installed. Wrapping the source (rather than reading
|
|
576
584
|
// `state.args` from the outside) is what puts the tracking window around the *evaluation* of the
|
|
577
585
|
// args - a cached read of the computed runs nothing and would record no form at all.
|
|
578
586
|
const formLinks = context.state.subtle.devtoolsFormLinks;
|
|
579
|
-
context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(
|
|
587
|
+
context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(args) : args);
|
|
580
588
|
// Trigger (auto-)execution whenever the resolved args change.
|
|
581
589
|
nestedEffect(() => {
|
|
582
590
|
const currArgsNow = context.state.args();
|
|
@@ -672,6 +680,107 @@ const withPolling = (options) => {
|
|
|
672
680
|
},
|
|
673
681
|
});
|
|
674
682
|
};
|
|
683
|
+
const DEFAULT_LONG_POLLING_DELAY = 250;
|
|
684
|
+
const DEFAULT_LONG_POLLING_ERROR_DELAY = 1_000;
|
|
685
|
+
const DEFAULT_LONG_POLLING_MAX_ERROR_DELAY = 30_000;
|
|
686
|
+
const DEFAULT_LONG_POLLING_STOP_AFTER_ERRORS = 10;
|
|
687
|
+
/**
|
|
688
|
+
* A query feature for long polling: a chain of requests where each round starts once the previous one
|
|
689
|
+
* settled, with args derived from what it returned. Unlike {@link withPolling} there is no interval -
|
|
690
|
+
* the server decides the cadence by holding each request open until it has something to say.
|
|
691
|
+
*
|
|
692
|
+
* A failed round is repeated after a growing delay, so a transient failure does not silently end the
|
|
693
|
+
* feed. A new value from the `withArgs` source cancels the pending round and starts over from it.
|
|
694
|
+
*
|
|
695
|
+
* Rounds do not share the cache: each one is requested with `keepUnusedFor: 0`, because a cursor is
|
|
696
|
+
* asked for once and never again. They are also not deduped across tabs - `withMultiTabSync` elects a
|
|
697
|
+
* poller per cache key, and a chain's key moves with every round.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* const events = getEvents(
|
|
701
|
+
* withArgs(() => ({ queryParams: { cursor: null } })),
|
|
702
|
+
* withLongPolling({
|
|
703
|
+
* nextArgs: (response) => (response ? { queryParams: { cursor: response.cursor } } : null),
|
|
704
|
+
* }),
|
|
705
|
+
* );
|
|
706
|
+
*
|
|
707
|
+
* @throws If the query is not eligible for auto execution (e.g. a POST request)
|
|
708
|
+
* @throws If the query also has a `withPolling` feature
|
|
709
|
+
*/
|
|
710
|
+
const withLongPolling = (options) => {
|
|
711
|
+
return createQueryFeature({
|
|
712
|
+
type: QueryFeatureType.WITH_LONG_POLLING,
|
|
713
|
+
devtools: () => [
|
|
714
|
+
{ label: 'delay', value: formatQueryDevtoolsDuration(options.delay ?? DEFAULT_LONG_POLLING_DELAY) },
|
|
715
|
+
...queryDevtoolsFnDetail(options.nextArgs, 'nextArgs'),
|
|
716
|
+
],
|
|
717
|
+
fn: (context) => {
|
|
718
|
+
if (!context.flags.shouldAutoExecuteMethod) {
|
|
719
|
+
throw withLongPollingUsedOnUnsupportedHttpMethod(context.flags.method);
|
|
720
|
+
}
|
|
721
|
+
if (context.flags.hasPollingFeature) {
|
|
722
|
+
throw withLongPollingUsedWithPolling();
|
|
723
|
+
}
|
|
724
|
+
const delay = options.delay ?? DEFAULT_LONG_POLLING_DELAY;
|
|
725
|
+
const errorDelay = options.errorDelay ?? DEFAULT_LONG_POLLING_ERROR_DELAY;
|
|
726
|
+
const maxErrorDelay = options.maxErrorDelay ?? DEFAULT_LONG_POLLING_MAX_ERROR_DELAY;
|
|
727
|
+
const stopAfterErrors = options.stopAfterErrors ?? DEFAULT_LONG_POLLING_STOP_AFTER_ERRORS;
|
|
728
|
+
let timeoutId = null;
|
|
729
|
+
let consecutiveErrors = 0;
|
|
730
|
+
const stop = () => {
|
|
731
|
+
if (timeoutId !== null)
|
|
732
|
+
clearTimeout(timeoutId);
|
|
733
|
+
timeoutId = null;
|
|
734
|
+
};
|
|
735
|
+
const schedule = (args, wait) => {
|
|
736
|
+
stop();
|
|
737
|
+
timeoutId = setTimeout(() => {
|
|
738
|
+
timeoutId = null;
|
|
739
|
+
context.execute({ args, options: { keepUnusedFor: 0, triggeredBy: 'long-polling' } });
|
|
740
|
+
}, wait);
|
|
741
|
+
};
|
|
742
|
+
// The chain never writes `state.args`, so what a round was sent with lives on the request itself -
|
|
743
|
+
// which is also the only place a manual `execute({ args })` records them.
|
|
744
|
+
const settledArgs = () => context.state.subtle.request()?.args ?? null;
|
|
745
|
+
context.state.events$.pipe(takeUntilDestroyed(context.deps.destroyRef)).subscribe((event) => {
|
|
746
|
+
if (event.type === HttpEventType.Response) {
|
|
747
|
+
consecutiveErrors = 0;
|
|
748
|
+
const next = options.nextArgs(context.state.response(), settledArgs());
|
|
749
|
+
if (next === null)
|
|
750
|
+
return stop();
|
|
751
|
+
schedule(next, delay);
|
|
752
|
+
}
|
|
753
|
+
else if (event.type === 'error') {
|
|
754
|
+
consecutiveErrors++;
|
|
755
|
+
if (consecutiveErrors >= stopAfterErrors)
|
|
756
|
+
return stop();
|
|
757
|
+
schedule(settledArgs(), Math.min(errorDelay * 2 ** (consecutiveErrors - 1), maxErrorDelay));
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
// A pending round belongs to the args it was derived from, so a fresh value from the `withArgs`
|
|
761
|
+
// source must cancel it - the source's own re-execution is what starts the new chain.
|
|
762
|
+
//
|
|
763
|
+
// Only an actual change may cancel: the effect's first run can land after the chain already
|
|
764
|
+
// scheduled a round (effects flush later than the response that scheduled it), and cancelling
|
|
765
|
+
// there would end the chain before its second round.
|
|
766
|
+
let hasSeenArgs = false;
|
|
767
|
+
let seenArgs = null;
|
|
768
|
+
nestedEffect(() => {
|
|
769
|
+
const args = context.state.args();
|
|
770
|
+
untracked(() => {
|
|
771
|
+
const hasChanged = hasSeenArgs && args !== seenArgs;
|
|
772
|
+
hasSeenArgs = true;
|
|
773
|
+
seenArgs = args;
|
|
774
|
+
if (!hasChanged)
|
|
775
|
+
return;
|
|
776
|
+
consecutiveErrors = 0;
|
|
777
|
+
stop();
|
|
778
|
+
});
|
|
779
|
+
}, { injector: context.deps.injector });
|
|
780
|
+
context.deps.destroyRef.onDestroy(stop);
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
};
|
|
675
784
|
/** A query feature that can be used to log the latest http event */
|
|
676
785
|
const withLogging = (options) => {
|
|
677
786
|
return createQueryFeature({
|
|
@@ -986,6 +1095,7 @@ const getQueryFeatureUsage = (
|
|
|
986
1095
|
options) => {
|
|
987
1096
|
const { creator, features, queryConfig, creatorInternals } = options;
|
|
988
1097
|
const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
|
|
1098
|
+
const hasPollingFeature = features.some((f) => f.type === QueryFeatureType.WITH_POLLING);
|
|
989
1099
|
const shouldAutoExecuteMethod = isCreateGqlQueryOptions(options)
|
|
990
1100
|
? shouldAutoExecuteGqlQuery(options.creatorInternals.method)
|
|
991
1101
|
: shouldAutoExecuteQuery(options.creatorInternals.method);
|
|
@@ -1000,6 +1110,7 @@ options) => {
|
|
|
1000
1110
|
}
|
|
1001
1111
|
const featureFnContext = {
|
|
1002
1112
|
hasWithArgsFeature,
|
|
1113
|
+
hasPollingFeature,
|
|
1003
1114
|
shouldAutoExecuteMethod,
|
|
1004
1115
|
shouldAutoExecute,
|
|
1005
1116
|
hasRouteFunction,
|
|
@@ -1529,16 +1640,16 @@ const runQueryErrorParsers = (detail, response) => {
|
|
|
1529
1640
|
return null;
|
|
1530
1641
|
};
|
|
1531
1642
|
const NO_RETRY = { retry: false };
|
|
1532
|
-
let defaultRetryFn = null;
|
|
1643
|
+
let defaultRetryFn$1 = null;
|
|
1533
1644
|
/**
|
|
1534
1645
|
* Installs the retry policy used by every request that does not bring its own `retryFn`. Installed
|
|
1535
1646
|
* by the {@link withDefaultRetry} query client feature; without it nothing is retried automatically.
|
|
1536
1647
|
*/
|
|
1537
1648
|
const setDefaultQueryRetryFn = (fn) => {
|
|
1538
|
-
defaultRetryFn = fn;
|
|
1649
|
+
defaultRetryFn$1 = fn;
|
|
1539
1650
|
};
|
|
1540
1651
|
/** @internal */
|
|
1541
|
-
const runDefaultQueryRetry = (options) => defaultRetryFn?.(options) ?? NO_RETRY;
|
|
1652
|
+
const runDefaultQueryRetry = (options) => defaultRetryFn$1?.(options) ?? NO_RETRY;
|
|
1542
1653
|
|
|
1543
1654
|
const isQueryErrorResponse = (error) => {
|
|
1544
1655
|
return typeof error === 'object' && error !== null && 'raw' in error && 'retryState' in error && 'isList' in error;
|
|
@@ -1630,6 +1741,7 @@ const SPEED_BUFFER_TIME_IN_MS = 2000;
|
|
|
1630
1741
|
const MOCKED_STATUS_TEXT = 'Served by the query devtools';
|
|
1631
1742
|
const createHttpRequest = (options) => {
|
|
1632
1743
|
let currentStreamSubscription = Subscription.EMPTY;
|
|
1744
|
+
let lastSentHeaders;
|
|
1633
1745
|
const { args, clientOptions, dependencies } = options;
|
|
1634
1746
|
const currentEvent = signal(null, /* @ts-ignore */
|
|
1635
1747
|
...(ngDevMode ? [{ debugName: "currentEvent" }] : /* istanbul ignore next */ []));
|
|
@@ -1751,6 +1863,7 @@ const createHttpRequest = (options) => {
|
|
|
1751
1863
|
});
|
|
1752
1864
|
const createStream = () => {
|
|
1753
1865
|
const headers = resolveHeaders();
|
|
1866
|
+
lastSentHeaders = headers;
|
|
1754
1867
|
const source$ = isQueryDevtoolsRequestInterceptionEnabled() ? sendWithFaults(headers) : send(headers);
|
|
1755
1868
|
return source$.pipe(tap((event) => updateState(event)), retry({
|
|
1756
1869
|
delay: (error, retryCount) => {
|
|
@@ -1803,12 +1916,19 @@ const createHttpRequest = (options) => {
|
|
|
1803
1916
|
});
|
|
1804
1917
|
return true;
|
|
1805
1918
|
};
|
|
1806
|
-
const
|
|
1919
|
+
const abort = () => {
|
|
1807
1920
|
const wasActive = !currentStreamSubscription.closed;
|
|
1808
1921
|
currentStreamSubscription.unsubscribe();
|
|
1809
|
-
|
|
1810
|
-
//
|
|
1922
|
+
currentStreamSubscription = Subscription.EMPTY;
|
|
1923
|
+
// A request stopped mid-backoff never runs the attempt it was waiting for, so leaving the state
|
|
1924
|
+
// set would keep a "retrying in …" readout counting down for a request that is not coming.
|
|
1811
1925
|
retryState.set(null);
|
|
1926
|
+
// Nothing is on its way any more, and `execute()` refuses to run while this is set.
|
|
1927
|
+
loading.set(null);
|
|
1928
|
+
return wasActive;
|
|
1929
|
+
};
|
|
1930
|
+
const destroy = () => {
|
|
1931
|
+
const wasActive = abort();
|
|
1812
1932
|
event$.complete();
|
|
1813
1933
|
return wasActive;
|
|
1814
1934
|
};
|
|
@@ -1923,9 +2043,11 @@ const createHttpRequest = (options) => {
|
|
|
1923
2043
|
applyPersistedResponse,
|
|
1924
2044
|
lastPersistedResponseAt: lastPersistedResponseAt.asReadonly(),
|
|
1925
2045
|
resolveHeaders,
|
|
2046
|
+
lastSentHeaders: () => lastSentHeaders,
|
|
1926
2047
|
attempts: attempts.asReadonly(),
|
|
1927
2048
|
retryState: retryState.asReadonly(),
|
|
1928
2049
|
lastDurationMs: lastDurationMs.asReadonly(),
|
|
2050
|
+
abort,
|
|
1929
2051
|
},
|
|
1930
2052
|
};
|
|
1931
2053
|
return httpRequest;
|
|
@@ -3009,48 +3131,68 @@ const symfonyQueryErrorParser = (detail) => {
|
|
|
3009
3131
|
return null;
|
|
3010
3132
|
};
|
|
3011
3133
|
|
|
3012
|
-
const
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3134
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
3135
|
+
const DEFAULT_BASE_DELAY_MS = 1000;
|
|
3136
|
+
const DEFAULT_MAX_DELAY_MS = 30_000;
|
|
3137
|
+
const DEFAULT_JITTER = 0.25;
|
|
3138
|
+
// 500 is left out on purpose: an internal server error is a bug in the backend far more often than a
|
|
3139
|
+
// blip, and repeating the request that triggered it does not make it go away.
|
|
3140
|
+
const isRetryableByDefault = (status) => status === 0 || status === 408 || status === 425 || status === 429 || status >= 501;
|
|
3141
|
+
/**
|
|
3142
|
+
* Builds a retry policy: an exponentially backing off, jittered {@link ShouldRetryRequestFn} bounded by
|
|
3143
|
+
* an attempt ceiling. What {@link withDefaultRetry} installs - reach for this directly to hand a single
|
|
3144
|
+
* client or creator its own `retryFn`.
|
|
3145
|
+
*
|
|
3146
|
+
* @example
|
|
3147
|
+
* const flakyEndpointRetry = createDefaultRetryFn({ maxAttempts: 8, maxDelayMs: 10_000 });
|
|
3148
|
+
*
|
|
3149
|
+
* const getReport = myApiGet<GetReportArgs>('/report').clone({ retryFn: flakyEndpointRetry });
|
|
3150
|
+
*/
|
|
3151
|
+
const createDefaultRetryFn = (options = {}) => {
|
|
3152
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
3153
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
3154
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
3155
|
+
const jitter = clamp(options.jitter ?? DEFAULT_JITTER, 0, 1);
|
|
3156
|
+
const retryableStatusCodes = options.retryableStatusCodes;
|
|
3157
|
+
const isRetryableStatus = (status) => retryableStatusCodes ? retryableStatusCodes.includes(status) : isRetryableByDefault(status);
|
|
3158
|
+
const backoff = (retryCount) => {
|
|
3159
|
+
const exponential = baseDelayMs * Math.pow(2, retryCount);
|
|
3160
|
+
const spread = jitter === 0 ? 1 : 1 + (Math.random() * 2 - 1) * jitter;
|
|
3161
|
+
return Math.round(clamp(exponential * spread, 0, maxDelayMs));
|
|
3162
|
+
};
|
|
3163
|
+
return ({ retryCount, error }) => {
|
|
3164
|
+
// Not an HTTP failure at all, so nothing about it says a second attempt would go any better.
|
|
3165
|
+
if (!(error instanceof HttpErrorResponse))
|
|
3035
3166
|
return { retry: false };
|
|
3167
|
+
if (maxAttempts > 0 && retryCount > maxAttempts)
|
|
3168
|
+
return { retry: false };
|
|
3169
|
+
const { status, error: detail, headers } = error;
|
|
3170
|
+
if (!isRetryableStatus(status))
|
|
3171
|
+
return { retry: false };
|
|
3172
|
+
// A page past the end of a Pagerfanta collection answers 5xx, and asking for it again keeps
|
|
3173
|
+
// answering 5xx - the request is wrong, not the server.
|
|
3174
|
+
if (status >= 501 && isSymfonyPagerfantaOutOfRangeError(detail))
|
|
3175
|
+
return { retry: false };
|
|
3176
|
+
if (status === 429) {
|
|
3177
|
+
const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
|
|
3178
|
+
const delay = retryAfter ? parseInt(retryAfter) * 1000 : NaN;
|
|
3179
|
+
// The server named a time, so it is used as named rather than jittered - only capped, since a
|
|
3180
|
+
// `retry-after` of an hour would otherwise arm a timer nobody is still around for.
|
|
3181
|
+
if (!Number.isNaN(delay))
|
|
3182
|
+
return { retry: true, delay: Math.min(delay, maxDelayMs) };
|
|
3036
3183
|
}
|
|
3037
|
-
return { retry: true, delay:
|
|
3038
|
-
}
|
|
3039
|
-
// Retry on 408 or 425 errors
|
|
3040
|
-
if (status === 408 || status === 425) {
|
|
3041
|
-
return { retry: true, delay: defaultRetryDelay };
|
|
3042
|
-
}
|
|
3043
|
-
// Retry on 429 errors
|
|
3044
|
-
if (status === 429) {
|
|
3045
|
-
const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
|
|
3046
|
-
if (retryAfter) {
|
|
3047
|
-
const delay = parseInt(retryAfter) * 1000;
|
|
3048
|
-
return { retry: true, delay: Number.isNaN(delay) ? defaultRetryDelay : delay };
|
|
3049
|
-
}
|
|
3050
|
-
return { retry: true, delay: defaultRetryDelay };
|
|
3051
|
-
}
|
|
3052
|
-
return { retry: false };
|
|
3184
|
+
return { retry: true, delay: backoff(retryCount) };
|
|
3185
|
+
};
|
|
3053
3186
|
};
|
|
3187
|
+
const defaultRetryFn = /* @__PURE__ */ createDefaultRetryFn();
|
|
3188
|
+
/**
|
|
3189
|
+
* The SDK's default retry policy, installed by {@link withDefaultRetry}: three retries with an
|
|
3190
|
+
* exponentially backing off, jittered delay, for a connection failure, a `408`, a `425`, a `429`
|
|
3191
|
+
* (honouring `retry-after`) and a 5xx above `500`.
|
|
3192
|
+
*
|
|
3193
|
+
* Pass {@link createDefaultRetryFn} options to {@link withDefaultRetry} to configure it.
|
|
3194
|
+
*/
|
|
3195
|
+
const shouldRetryRequest = (options) => defaultRetryFn(options instanceof HttpErrorResponse ? { retryCount: 0, error: options } : options);
|
|
3054
3196
|
|
|
3055
3197
|
const noop$2 = () => undefined;
|
|
3056
3198
|
/** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
|
|
@@ -3454,10 +3596,9 @@ const withSymfonyErrors = () => () => {
|
|
|
3454
3596
|
return { type: QueryClientFeatureType.SYMFONY_ERRORS, instance: null };
|
|
3455
3597
|
};
|
|
3456
3598
|
/**
|
|
3457
|
-
* Installs the SDK's default retry policy for every request that does not bring its own `retryFn`:
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
3460
|
-
* page.
|
|
3599
|
+
* Installs the SDK's default retry policy for every request that does not bring its own `retryFn`: a
|
|
3600
|
+
* connection failure, a 5xx above 500, a 408/425 and a 429 (honouring `retry-after`) are retried three
|
|
3601
|
+
* times with an exponentially backing off, jittered delay - and a Pagerfanta out-of-range page never is.
|
|
3461
3602
|
*
|
|
3462
3603
|
* Without it nothing is retried automatically and `error.retryState` always reads `{ retry: false }`.
|
|
3463
3604
|
* A per-client or per-creator `retryFn` keeps working either way.
|
|
@@ -3466,11 +3607,11 @@ const withSymfonyErrors = () => () => {
|
|
|
3466
3607
|
* const MY_CLIENT = createQueryClient({
|
|
3467
3608
|
* name: 'my-api',
|
|
3468
3609
|
* baseUrl: 'https://api.example.com',
|
|
3469
|
-
* features: [withDefaultRetry()],
|
|
3610
|
+
* features: [withDefaultRetry({ maxAttempts: 5 })],
|
|
3470
3611
|
* });
|
|
3471
3612
|
*/
|
|
3472
|
-
const withDefaultRetry = () => () => {
|
|
3473
|
-
setDefaultQueryRetryFn(shouldRetryRequest);
|
|
3613
|
+
const withDefaultRetry = (options) => () => {
|
|
3614
|
+
setDefaultQueryRetryFn(options ? createDefaultRetryFn(options) : shouldRetryRequest);
|
|
3474
3615
|
return { type: QueryClientFeatureType.DEFAULT_RETRY, instance: null };
|
|
3475
3616
|
};
|
|
3476
3617
|
/**
|
|
@@ -3488,10 +3629,10 @@ const withDefaultRetry = () => () => {
|
|
|
3488
3629
|
* features: [withEthleteApiErrors()],
|
|
3489
3630
|
* });
|
|
3490
3631
|
*/
|
|
3491
|
-
const withEthleteApiErrors = () => () => {
|
|
3632
|
+
const withEthleteApiErrors = (options) => () => {
|
|
3492
3633
|
registerQueryErrorParser(htmlQueryErrorParser);
|
|
3493
3634
|
registerQueryErrorParser(symfonyQueryErrorParser);
|
|
3494
|
-
setDefaultQueryRetryFn(shouldRetryRequest);
|
|
3635
|
+
setDefaultQueryRetryFn(options?.retry ? createDefaultRetryFn(options.retry) : shouldRetryRequest);
|
|
3495
3636
|
return { type: QueryClientFeatureType.ETHLETE_API_ERRORS, instance: null };
|
|
3496
3637
|
};
|
|
3497
3638
|
|
|
@@ -3512,10 +3653,10 @@ const createQueryRepository = (config) => {
|
|
|
3512
3653
|
const cacheVersion = signal(0, /* @ts-ignore */
|
|
3513
3654
|
...(ngDevMode ? [{ debugName: "cacheVersion" }] : /* istanbul ignore next */ []));
|
|
3514
3655
|
const bumpCacheVersion = () => cacheVersion.update((v) => v + 1);
|
|
3515
|
-
const resolveKeepUnusedFor = (creatorOptions, shouldCache) => {
|
|
3656
|
+
const resolveKeepUnusedFor = (creatorOptions, shouldCache, override) => {
|
|
3516
3657
|
if (config.retentionEnabled === false || !shouldCache)
|
|
3517
3658
|
return 0;
|
|
3518
|
-
return Math.max(0, creatorOptions?.keepUnusedFor ?? config.keepUnusedFor ?? DEFAULT_KEEP_UNUSED_FOR);
|
|
3659
|
+
return Math.max(0, override ?? creatorOptions?.keepUnusedFor ?? config.keepUnusedFor ?? DEFAULT_KEEP_UNUSED_FOR);
|
|
3519
3660
|
};
|
|
3520
3661
|
const request = (options) => {
|
|
3521
3662
|
const { args, creatorOptions, runQueryOptions } = options;
|
|
@@ -3546,7 +3687,7 @@ const createQueryRepository = (config) => {
|
|
|
3546
3687
|
})
|
|
3547
3688
|
: false;
|
|
3548
3689
|
const trackingKey = cacheKey || generateUuid();
|
|
3549
|
-
const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache);
|
|
3690
|
+
const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
|
|
3550
3691
|
const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
|
|
3551
3692
|
const isSecure = options.isSecure ?? false;
|
|
3552
3693
|
// A secure response needs an explicit `persistence: true` to reach the disk: leaving an
|
|
@@ -3651,6 +3792,10 @@ const createQueryRepository = (config) => {
|
|
|
3651
3792
|
// Only data is worth keeping around: an entry that never produced a response has nothing to
|
|
3652
3793
|
// hand back to a returning consumer, so it is aborted immediately as it always was.
|
|
3653
3794
|
if (cacheEntry.keepUnusedFor > 0 && cacheEntry.request.response() !== null) {
|
|
3795
|
+
// Retention keeps the response, not the work: a revalidation nobody is waiting for is stopped
|
|
3796
|
+
// rather than left running - and a failing one left running would go on retrying into an empty
|
|
3797
|
+
// room. A consumer that binds again re-executes anyway.
|
|
3798
|
+
cacheEntry.request.subtle.abort();
|
|
3654
3799
|
retain(key, cacheEntry);
|
|
3655
3800
|
}
|
|
3656
3801
|
else {
|
|
@@ -3863,6 +4008,7 @@ const createSecureExecuteFactory = (options) => {
|
|
|
3863
4008
|
let lastExecuteArgs;
|
|
3864
4009
|
/** The access token the last request went out with - what the 401 retry below compares against. */
|
|
3865
4010
|
let lastRequestedWithToken = null;
|
|
4011
|
+
const error$ = toObservable(options.state.error, { injector: options.deps.injector });
|
|
3866
4012
|
const reset = () => {
|
|
3867
4013
|
authQuerySubscription.unsubscribe();
|
|
3868
4014
|
authQuerySubscription = Subscription.EMPTY;
|
|
@@ -3973,7 +4119,13 @@ const createSecureExecuteFactory = (options) => {
|
|
|
3973
4119
|
// another refresh - an endless refresh/retry loop for as long as the server keeps handing out
|
|
3974
4120
|
// tokens it rejects. A refresh that did not change the access token is therefore not a reason to
|
|
3975
4121
|
// retry; the subscription stays open, so a later one that does change it still is.
|
|
3976
|
-
|
|
4122
|
+
//
|
|
4123
|
+
// The error landing is a trigger of its own: a refresh can complete before this query's 401 has
|
|
4124
|
+
// even come back (which is also why that 401 must not start another refresh - see
|
|
4125
|
+
// withRefreshQuery's 401 handling), and the refresh emission alone would find no error to act
|
|
4126
|
+
// on. Emissions of the error this execution armed with are not landings, only replays.
|
|
4127
|
+
const errorAtArm = options.state.error();
|
|
4128
|
+
tokenRefreshSubscription = merge(options.authProvider.afterTokenRefresh$, error$.pipe(filter((landedError) => landedError !== errorAtArm)))
|
|
3977
4129
|
.pipe(filter(() => {
|
|
3978
4130
|
const currentError = options.state.error();
|
|
3979
4131
|
return currentError?.code === 401 && options.authProvider.accessToken() !== lastRequestedWithToken;
|
|
@@ -4629,6 +4781,8 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
|
4629
4781
|
let isLeaderFn = alwaysLeader;
|
|
4630
4782
|
let leaderElectionContext;
|
|
4631
4783
|
let refreshCoordination;
|
|
4784
|
+
let sessionAdoption;
|
|
4785
|
+
let activityCoordination;
|
|
4632
4786
|
for (const featureBuilder of featureBuilders ?? []) {
|
|
4633
4787
|
const earlySetup = featureBuilder.earlySetup;
|
|
4634
4788
|
if (!earlySetup)
|
|
@@ -4637,8 +4791,10 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
|
4637
4791
|
isLeaderFn = result.isLeader ?? isLeaderFn;
|
|
4638
4792
|
leaderElectionContext = result.leaderElection ?? leaderElectionContext;
|
|
4639
4793
|
refreshCoordination = result.refreshCoordination ?? refreshCoordination;
|
|
4794
|
+
sessionAdoption = result.sessionAdoption ?? sessionAdoption;
|
|
4795
|
+
activityCoordination = result.activityCoordination ?? activityCoordination;
|
|
4640
4796
|
}
|
|
4641
|
-
return { isLeaderFn, leaderElectionContext, refreshCoordination };
|
|
4797
|
+
return { isLeaderFn, leaderElectionContext, refreshCoordination, sessionAdoption, activityCoordination };
|
|
4642
4798
|
};
|
|
4643
4799
|
const createBearerAuthProviderImpl = (config) => {
|
|
4644
4800
|
const injector = inject(Injector);
|
|
@@ -4655,7 +4811,12 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4655
4811
|
if (!token)
|
|
4656
4812
|
return null;
|
|
4657
4813
|
try {
|
|
4658
|
-
|
|
4814
|
+
const decoded = config.bearerDecryptFn?.(token) ?? decryptBearer(token);
|
|
4815
|
+
return patchQueryDevtoolsTokenPayload({
|
|
4816
|
+
payload: decoded,
|
|
4817
|
+
providerName: config.name,
|
|
4818
|
+
expiresInPropertyName: 'exp',
|
|
4819
|
+
});
|
|
4659
4820
|
}
|
|
4660
4821
|
catch (error) {
|
|
4661
4822
|
if (isDevMode()) {
|
|
@@ -4728,6 +4889,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4728
4889
|
sessionEndCause: sessionEndCause.asReadonly(),
|
|
4729
4890
|
});
|
|
4730
4891
|
const querySetupContext = {
|
|
4892
|
+
name: config.name,
|
|
4731
4893
|
accessToken,
|
|
4732
4894
|
refreshToken,
|
|
4733
4895
|
bearerDecryptFn: config.bearerDecryptFn,
|
|
@@ -4753,6 +4915,8 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4753
4915
|
setTokens,
|
|
4754
4916
|
isLeader: isLeader.isLeaderFn,
|
|
4755
4917
|
leaderElection: isLeader.leaderElectionContext,
|
|
4918
|
+
sessionAdoption: isLeader.sessionAdoption,
|
|
4919
|
+
activityCoordination: isLeader.activityCoordination,
|
|
4756
4920
|
afterTokenRefresh$,
|
|
4757
4921
|
queries: queries,
|
|
4758
4922
|
executionState,
|
|
@@ -4761,9 +4925,21 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4761
4925
|
};
|
|
4762
4926
|
const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
|
|
4763
4927
|
// Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
|
|
4764
|
-
// `unknown` here means nothing is trying to restore a session, so there is nothing to wait for
|
|
4928
|
+
// `unknown` here means nothing is trying to restore a session, so there is nothing to wait for -
|
|
4929
|
+
// unless the join handshake is still out, in which case another tab may be about to hand this one a
|
|
4930
|
+
// live session. Saying `anonymous` in that window would send the auth guard to the login page for a
|
|
4931
|
+
// session that exists, so the fallback waits for the handshake instead.
|
|
4765
4932
|
if (sessionStatus() === 'unknown') {
|
|
4766
|
-
|
|
4933
|
+
const adoption = isLeader.sessionAdoption;
|
|
4934
|
+
if (adoption?.isPending()) {
|
|
4935
|
+
void adoption.settled.then(() => {
|
|
4936
|
+
if (sessionStatus() === 'unknown')
|
|
4937
|
+
sessionStatus.set('anonymous');
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
else {
|
|
4941
|
+
sessionStatus.set('anonymous');
|
|
4942
|
+
}
|
|
4767
4943
|
}
|
|
4768
4944
|
const provider = {
|
|
4769
4945
|
queries,
|
|
@@ -4803,11 +4979,25 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
|
|
|
4803
4979
|
|
|
4804
4980
|
/** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
|
|
4805
4981
|
const rescheduleDelayMs = 5000;
|
|
4982
|
+
/**
|
|
4983
|
+
* How long a follower waits for the leader to answer a delegated refresh before asking again. The
|
|
4984
|
+
* request is a `BroadcastChannel` message with no ack, so a leader that was frozen or handed the
|
|
4985
|
+
* leadership over mid-flight simply never acts on it.
|
|
4986
|
+
*/
|
|
4987
|
+
const delegatedRefreshRetryMs = 3000;
|
|
4988
|
+
/** How often a delegated refresh is re-asked for before the tab leaves it to the next 401. */
|
|
4989
|
+
const maxDelegatedRefreshAttempts = 3;
|
|
4806
4990
|
/**
|
|
4807
4991
|
* How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
|
|
4808
4992
|
* declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
|
|
4809
4993
|
*/
|
|
4810
4994
|
const maxRescheduleAttempts = 5;
|
|
4995
|
+
/**
|
|
4996
|
+
* How many 401-driven refreshes may run back to back before they fall back to `minRefreshInterval`.
|
|
4997
|
+
* Every refresh in such a streak means the token the previous one issued was rejected too, so an
|
|
4998
|
+
* unbounded streak is a refresh loop, not recovery. A secure request succeeding ends the streak.
|
|
4999
|
+
*/
|
|
5000
|
+
const maxUnauthorizedRefreshStreak = 3;
|
|
4811
5001
|
const withAuthenticationQuery = (key, config, setup) => ({
|
|
4812
5002
|
_type: 'authQuery',
|
|
4813
5003
|
key,
|
|
@@ -4849,7 +5039,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4849
5039
|
const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
|
|
4850
5040
|
const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
|
|
4851
5041
|
const refreshIfExpired = config.refreshIfExpired ?? true;
|
|
5042
|
+
const destroyRef = inject(DestroyRef);
|
|
4852
5043
|
let lastRefreshTime = 0;
|
|
5044
|
+
let lastUnauthorizedRefreshTime = 0;
|
|
5045
|
+
let unauthorizedRefreshStreak = 0;
|
|
5046
|
+
let delegatedRefreshAttempts = 0;
|
|
5047
|
+
let isWatchingDelegatedRefresh = false;
|
|
4853
5048
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4854
5049
|
const refreshQuery = () => context.queries[key];
|
|
4855
5050
|
const executeRefresh = (reason) => {
|
|
@@ -4857,9 +5052,14 @@ const withRefreshQuery = (key, config) => {
|
|
|
4857
5052
|
if (!currentRefreshToken)
|
|
4858
5053
|
return 'noToken';
|
|
4859
5054
|
if (!context.isLeader()) {
|
|
4860
|
-
//
|
|
4861
|
-
//
|
|
5055
|
+
// A scheduled tick on a follower is pure duplicate: multi-tab sync keeps the access token
|
|
5056
|
+
// identical across tabs, so the leader's own timer is due at the same instant and it is the
|
|
5057
|
+
// only tab allowed to act. Delegation is for the 401 path, where this tab has a request that
|
|
5058
|
+
// really failed and the leader has no way of knowing.
|
|
5059
|
+
if (reason === 'scheduled')
|
|
5060
|
+
return 'notLeader';
|
|
4862
5061
|
context.refreshCoordination?.request();
|
|
5062
|
+
watchDelegatedRefresh(context.accessToken());
|
|
4863
5063
|
return 'delegated';
|
|
4864
5064
|
}
|
|
4865
5065
|
// Any token-issuing execution, not just another refresh: a login already in flight is about to
|
|
@@ -4867,15 +5067,50 @@ const withRefreshQuery = (key, config) => {
|
|
|
4867
5067
|
// that login is replacing.
|
|
4868
5068
|
if (context.hasTokenIssuingExecutionInFlight())
|
|
4869
5069
|
return 'busy';
|
|
5070
|
+
const now = Date.now();
|
|
4870
5071
|
if (reason === 'scheduled') {
|
|
4871
|
-
const now = Date.now();
|
|
4872
5072
|
if (now - lastRefreshTime < minRefreshInterval)
|
|
4873
5073
|
return 'throttled';
|
|
4874
|
-
lastRefreshTime = now;
|
|
4875
5074
|
}
|
|
5075
|
+
else {
|
|
5076
|
+
if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
|
|
5077
|
+
now - lastUnauthorizedRefreshTime < minRefreshInterval) {
|
|
5078
|
+
return 'throttled';
|
|
5079
|
+
}
|
|
5080
|
+
unauthorizedRefreshStreak++;
|
|
5081
|
+
lastUnauthorizedRefreshTime = now;
|
|
5082
|
+
}
|
|
5083
|
+
// Stamped whatever the reason, so the scheduled floor covers a 401-driven rotation too - a
|
|
5084
|
+
// scheduled tick right behind one would otherwise spend a second refresh token. The 401 path
|
|
5085
|
+
// deliberately does not *gate* on it: a token revoked seconds after a refresh is exactly when
|
|
5086
|
+
// the request has to go out.
|
|
5087
|
+
lastRefreshTime = now;
|
|
4876
5088
|
refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
|
|
4877
5089
|
return 'executed';
|
|
4878
5090
|
};
|
|
5091
|
+
/**
|
|
5092
|
+
* Asks the leader again if a delegated refresh went unanswered - the request is a channel message
|
|
5093
|
+
* with no ack, so a frozen leader, or one that handed the leadership over mid-flight, drops it
|
|
5094
|
+
* silently. One watch at a time, and by the time it re-asks this tab may be the leader itself;
|
|
5095
|
+
* `executeRefresh` re-decides that on every attempt.
|
|
5096
|
+
*/
|
|
5097
|
+
function watchDelegatedRefresh(tokenAtRequest) {
|
|
5098
|
+
if (isWatchingDelegatedRefresh || delegatedRefreshAttempts >= maxDelegatedRefreshAttempts)
|
|
5099
|
+
return;
|
|
5100
|
+
delegatedRefreshAttempts++;
|
|
5101
|
+
isWatchingDelegatedRefresh = true;
|
|
5102
|
+
timer(delegatedRefreshRetryMs)
|
|
5103
|
+
.pipe(takeUntilDestroyed(destroyRef))
|
|
5104
|
+
.subscribe(() => {
|
|
5105
|
+
isWatchingDelegatedRefresh = false;
|
|
5106
|
+
// A new access token is the leader's answer - nothing to re-ask for.
|
|
5107
|
+
if (context.accessToken() !== tokenAtRequest) {
|
|
5108
|
+
delegatedRefreshAttempts = 0;
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
executeRefresh('unauthorized');
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
4879
5114
|
/**
|
|
4880
5115
|
* How long a scheduled refresh that did not spend the refresh token waits before trying again.
|
|
4881
5116
|
* `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
|
|
@@ -4890,7 +5125,10 @@ const withRefreshQuery = (key, config) => {
|
|
|
4890
5125
|
return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
|
|
4891
5126
|
case 'busy':
|
|
4892
5127
|
return rescheduleDelayMs;
|
|
5128
|
+
// Both wait for the leader: `delegated` asked it to act, `notLeader` left it to its own timer.
|
|
5129
|
+
// Coming back keeps this tab covered if the leadership moves here in the meantime.
|
|
4893
5130
|
case 'delegated':
|
|
5131
|
+
case 'notLeader':
|
|
4894
5132
|
return minRefreshInterval;
|
|
4895
5133
|
}
|
|
4896
5134
|
};
|
|
@@ -4907,7 +5145,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4907
5145
|
/** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
|
|
4908
5146
|
const scheduledRefreshDelay = (token) => {
|
|
4909
5147
|
try {
|
|
4910
|
-
const
|
|
5148
|
+
const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
|
|
5149
|
+
const bearerDataValue = patchQueryDevtoolsTokenPayload({
|
|
5150
|
+
payload: decoded,
|
|
5151
|
+
providerName: context.name,
|
|
5152
|
+
expiresInPropertyName,
|
|
5153
|
+
});
|
|
4911
5154
|
const expiresIn = bearerDataValue?.[expiresInPropertyName];
|
|
4912
5155
|
if (typeof expiresIn !== 'number') {
|
|
4913
5156
|
if (isDevMode()) {
|
|
@@ -4939,13 +5182,33 @@ const withRefreshQuery = (key, config) => {
|
|
|
4939
5182
|
const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
|
|
4940
5183
|
return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
|
|
4941
5184
|
}));
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
5185
|
+
// A backgrounded tab's `timer()` does not fire on time - the browser throttles it, and a frozen
|
|
5186
|
+
// page stops running it altogether while keeping the Web Lock that makes it the leader. So the
|
|
5187
|
+
// schedule is recomputed whenever the page becomes visible again: an overdue token resolves to a
|
|
5188
|
+
// delay of 0 and refreshes immediately, instead of waiting for a secure query to 401.
|
|
5189
|
+
const foregroundEpoch = signal(0, /* @ts-ignore */
|
|
5190
|
+
...(ngDevMode ? [{ debugName: "foregroundEpoch" }] : /* istanbul ignore next */ []));
|
|
5191
|
+
if (typeof document !== 'undefined') {
|
|
5192
|
+
fromEvent(document, 'visibilitychange')
|
|
5193
|
+
.pipe(takeUntilDestroyed(destroyRef))
|
|
5194
|
+
.subscribe(() => {
|
|
5195
|
+
if (document.visibilityState !== 'visible')
|
|
5196
|
+
return;
|
|
5197
|
+
foregroundEpoch.update((epoch) => epoch + 1);
|
|
5198
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
// The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
|
|
5201
|
+
// lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
|
|
5202
|
+
// timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
|
|
5203
|
+
// two tokens that happen to be due at the same moment still restart the timer.
|
|
5204
|
+
const nextScheduledRefresh = computed(() => {
|
|
5205
|
+
const token = context.accessToken();
|
|
5206
|
+
foregroundEpoch();
|
|
5207
|
+
return { dueInMs: token ? scheduledRefreshDelay(token) : null };
|
|
5208
|
+
}, /* @ts-ignore */
|
|
5209
|
+
...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
|
|
5210
|
+
toObservable(nextScheduledRefresh)
|
|
5211
|
+
.pipe(switchMap(({ dueInMs }) => (dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts))), takeUntilDestroyed())
|
|
4949
5212
|
.subscribe();
|
|
4950
5213
|
context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
|
|
4951
5214
|
const onRefreshFailure = config.onRefreshFailure ??
|
|
@@ -4965,18 +5228,23 @@ const withRefreshQuery = (key, config) => {
|
|
|
4965
5228
|
// Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
|
|
4966
5229
|
const autoRetryOn401 = config.autoRetryOn401 ?? true;
|
|
4967
5230
|
if (autoRetryOn401) {
|
|
4968
|
-
context.repository.events
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
return
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5231
|
+
context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
|
|
5232
|
+
if (event.type === 'request-success' && event.isSecure) {
|
|
5233
|
+
unauthorizedRefreshStreak = 0;
|
|
5234
|
+
delegatedRefreshAttempts = 0;
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
|
|
5238
|
+
return;
|
|
5239
|
+
// A 401 from a request that went out with an older access token asks for a refresh that
|
|
5240
|
+
// already happened - the query re-runs itself with the current token (see
|
|
5241
|
+
// createSecureExecuteFactory). Refreshing again here would spend the refresh token that
|
|
5242
|
+
// pair came with, invalidating the tokens every other in-flight request is using; their
|
|
5243
|
+
// 401s would each do the same, keeping the loop alive for as long as queries are in flight.
|
|
5244
|
+
const sentAuthorization = event.request.subtle.lastSentHeaders()?.get('Authorization');
|
|
5245
|
+
const currentToken = context.accessToken();
|
|
5246
|
+
if (sentAuthorization && currentToken && sentAuthorization !== `Bearer ${currentToken}`)
|
|
5247
|
+
return;
|
|
4980
5248
|
executeRefresh('unauthorized');
|
|
4981
5249
|
});
|
|
4982
5250
|
}
|
|
@@ -4997,6 +5265,23 @@ const withRefreshQuery = (key, config) => {
|
|
|
4997
5265
|
};
|
|
4998
5266
|
};
|
|
4999
5267
|
|
|
5268
|
+
/**
|
|
5269
|
+
* Ends the session once the user has been inactive for {@link InactivityLogoutConfig.inactivityTimeout},
|
|
5270
|
+
* with `sessionEndCause` reporting `'inactivity'`.
|
|
5271
|
+
*
|
|
5272
|
+
* Idleness is a property of the session, not of a tab: with `withBearerAuthMultiTabSync` on, tabs tell
|
|
5273
|
+
* each other about activity, so the tab the user is typing in keeps the forgotten one from logging
|
|
5274
|
+
* everybody out. Without it - or with `syncLogout: false`, where a logout stays in the tab that
|
|
5275
|
+
* decided it - each tab times out on its own.
|
|
5276
|
+
*
|
|
5277
|
+
* @example
|
|
5278
|
+
* export const AUTH_PROVIDER = createBearerAuthProvider({
|
|
5279
|
+
* name: 'my-auth',
|
|
5280
|
+
* queryClientRef: MY_CLIENT,
|
|
5281
|
+
* queries: [loginQuery, refreshQuery],
|
|
5282
|
+
* features: [withBearerAuthMultiTabSync(), withInactivityLogout({ inactivityTimeout: 5 * 60 * 1000 })],
|
|
5283
|
+
* });
|
|
5284
|
+
*/
|
|
5000
5285
|
const withInactivityLogout = (config = {}) => {
|
|
5001
5286
|
return (context) => {
|
|
5002
5287
|
const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
|
|
@@ -5007,8 +5292,25 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5007
5292
|
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
5008
5293
|
const lastActivityTime = signal(Date.now(), /* @ts-ignore */
|
|
5009
5294
|
...(ngDevMode ? [{ debugName: "lastActivityTime" }] : /* istanbul ignore next */ []));
|
|
5295
|
+
const activityCoordination = context.activityCoordination;
|
|
5296
|
+
// Announcing every throttled event would wake every other tab once a second for as long as the
|
|
5297
|
+
// user scrolls. A quarter of the timeout is far more than the other tabs need to never expire,
|
|
5298
|
+
// and it is one message per several minutes at the default.
|
|
5299
|
+
const announceInterval = inactivityTimeout / 4;
|
|
5300
|
+
let lastAnnouncedAt = 0;
|
|
5301
|
+
const markActive = () => lastActivityTime.set(Date.now());
|
|
5302
|
+
const announceActivity = () => {
|
|
5303
|
+
if (!activityCoordination)
|
|
5304
|
+
return;
|
|
5305
|
+
const now = Date.now();
|
|
5306
|
+
if (now - lastAnnouncedAt < announceInterval)
|
|
5307
|
+
return;
|
|
5308
|
+
lastAnnouncedAt = now;
|
|
5309
|
+
activityCoordination.announce();
|
|
5310
|
+
};
|
|
5010
5311
|
const resetTimer = () => {
|
|
5011
|
-
|
|
5312
|
+
markActive();
|
|
5313
|
+
announceActivity();
|
|
5012
5314
|
};
|
|
5013
5315
|
const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
|
|
5014
5316
|
const activityFromCustomCheck$ = config.customActivityCheck
|
|
@@ -5020,15 +5322,35 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5020
5322
|
resetTimer();
|
|
5021
5323
|
}
|
|
5022
5324
|
});
|
|
5023
|
-
|
|
5325
|
+
// Activity from another tab is never announced onwards - two tabs would echo one keystroke back
|
|
5326
|
+
// and forth for as long as they are both open.
|
|
5327
|
+
const remoteActivitySubscription = activityCoordination?.activity$.subscribe(() => {
|
|
5328
|
+
if (enabled() && context.accessToken()) {
|
|
5329
|
+
markActive();
|
|
5330
|
+
}
|
|
5331
|
+
});
|
|
5332
|
+
const isIdle = () => Date.now() - lastActivityTime() >= inactivityTimeout;
|
|
5333
|
+
// Driven off `lastActivityTime` rather than off the activity stream, so the countdown this feature
|
|
5334
|
+
// reports and the logout it performs cannot disagree: a tab that has seen no event still has a
|
|
5335
|
+
// deadline, and `resetTimer()` postpones the logout it says it resets.
|
|
5336
|
+
//
|
|
5337
|
+
// `isIdle` re-checks at the moment the deadline fires, because the re-arm goes through an effect:
|
|
5338
|
+
// activity recorded in the same tick as the old deadline has not moved it yet. The write that was
|
|
5339
|
+
// missed is what schedules that re-arm, so nothing is dropped by waiting for it.
|
|
5340
|
+
const inactivityLogout$ = toObservable(lastActivityTime).pipe(switchMap((last) => timer(last + inactivityTimeout - Date.now())), filter(() => enabled() && !!context.accessToken() && isIdle()));
|
|
5024
5341
|
const logoutSubscription = inactivityLogout$.subscribe(() => {
|
|
5025
5342
|
context.logout('inactivity');
|
|
5026
5343
|
});
|
|
5344
|
+
let hadToken = false;
|
|
5345
|
+
// Only the start of a session counts as activity. A token _refresh_ is the app working, not the
|
|
5346
|
+
// user, and resetting on one would mean an app that refreshes faster than this timeout never logs
|
|
5347
|
+
// an idle user out at all.
|
|
5027
5348
|
effect(() => {
|
|
5028
|
-
const
|
|
5029
|
-
if (
|
|
5030
|
-
|
|
5349
|
+
const hasToken = !!context.accessToken();
|
|
5350
|
+
if (hasToken && !hadToken) {
|
|
5351
|
+
markActive();
|
|
5031
5352
|
}
|
|
5353
|
+
hadToken = hasToken;
|
|
5032
5354
|
});
|
|
5033
5355
|
const enable = () => {
|
|
5034
5356
|
enabled.set(true);
|
|
@@ -5045,9 +5367,9 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5045
5367
|
const remaining = inactivityTimeout - elapsed;
|
|
5046
5368
|
return remaining > 0 ? remaining : 0;
|
|
5047
5369
|
};
|
|
5048
|
-
resetTimer();
|
|
5049
5370
|
destroyRef.onDestroy(() => {
|
|
5050
5371
|
activitySubscription.unsubscribe();
|
|
5372
|
+
remoteActivitySubscription?.unsubscribe();
|
|
5051
5373
|
logoutSubscription.unsubscribe();
|
|
5052
5374
|
});
|
|
5053
5375
|
const instance = {
|
|
@@ -5063,6 +5385,7 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5063
5385
|
devtools: () => [
|
|
5064
5386
|
{ label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
|
|
5065
5387
|
{ label: 'activity events', value: activityEvents.join(', ') },
|
|
5388
|
+
{ label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
|
|
5066
5389
|
...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
|
|
5067
5390
|
],
|
|
5068
5391
|
};
|
|
@@ -5267,6 +5590,12 @@ const resetEncryptionKey = () => {
|
|
|
5267
5590
|
* through. A message without one comes from a tab running an older version.
|
|
5268
5591
|
*/
|
|
5269
5592
|
const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
|
|
5593
|
+
/**
|
|
5594
|
+
* The bounded wait a joining tab gives the leader to send the live session over. Short enough to be
|
|
5595
|
+
* invisible in front of the network request it defers, long enough for a same-origin channel message
|
|
5596
|
+
* plus the answer to come back.
|
|
5597
|
+
*/
|
|
5598
|
+
const sessionAdoptionTimeoutMs = 250;
|
|
5270
5599
|
/**
|
|
5271
5600
|
* The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
|
|
5272
5601
|
* providers reachable from the same origin do not push each other's tokens around.
|
|
@@ -5296,8 +5625,32 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5296
5625
|
// broadcasting effects below run after it, so they would always see it cleared.
|
|
5297
5626
|
let lastSyncedState = null;
|
|
5298
5627
|
let hadTokens = false;
|
|
5628
|
+
let isAdoptionPending = false;
|
|
5629
|
+
let settleAdoption = () => {
|
|
5630
|
+
/* replaced below when the handshake actually runs */
|
|
5631
|
+
};
|
|
5632
|
+
const remoteActivity = new Subject();
|
|
5299
5633
|
channel.onmessage = (event) => {
|
|
5300
5634
|
const message = event.data;
|
|
5635
|
+
if (message.type === 'activity') {
|
|
5636
|
+
if (syncLogout)
|
|
5637
|
+
remoteActivity.next();
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
if (message.type === 'state-request') {
|
|
5641
|
+
if (!syncTokens || !context.isLeader())
|
|
5642
|
+
return;
|
|
5643
|
+
const access = context.accessToken();
|
|
5644
|
+
const refresh = context.refreshToken();
|
|
5645
|
+
if (!access || !refresh)
|
|
5646
|
+
return;
|
|
5647
|
+
channel.postMessage({
|
|
5648
|
+
type: 'tokens-updated',
|
|
5649
|
+
accessToken: encryptToken(access),
|
|
5650
|
+
refreshToken: encryptToken(refresh),
|
|
5651
|
+
});
|
|
5652
|
+
return;
|
|
5653
|
+
}
|
|
5301
5654
|
if (message.type === 'logout' && syncLogout) {
|
|
5302
5655
|
lastSyncedState = LOGGED_OUT;
|
|
5303
5656
|
hadTokens = false;
|
|
@@ -5313,11 +5666,34 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5313
5666
|
lastSyncedState = tokenState(access, refresh);
|
|
5314
5667
|
hadTokens = true;
|
|
5315
5668
|
context.applyTokens(access, refresh);
|
|
5669
|
+
settleAdoption();
|
|
5316
5670
|
};
|
|
5671
|
+
let adoptionTimeout;
|
|
5317
5672
|
const cleanup = () => {
|
|
5673
|
+
clearTimeout(adoptionTimeout);
|
|
5674
|
+
settleAdoption();
|
|
5675
|
+
remoteActivity.complete();
|
|
5318
5676
|
channel.close();
|
|
5319
5677
|
};
|
|
5320
5678
|
destroyRef.onDestroy(cleanup);
|
|
5679
|
+
// Sync is push-only otherwise: a tab only broadcasts tokens that just changed, so a tab joining a
|
|
5680
|
+
// live session would hear nothing and start its own login. Asking is the missing half.
|
|
5681
|
+
const sessionAdoption = syncTokens
|
|
5682
|
+
? (() => {
|
|
5683
|
+
isAdoptionPending = true;
|
|
5684
|
+
const settled = new Promise((resolve) => {
|
|
5685
|
+
settleAdoption = () => {
|
|
5686
|
+
if (!isAdoptionPending)
|
|
5687
|
+
return;
|
|
5688
|
+
isAdoptionPending = false;
|
|
5689
|
+
resolve();
|
|
5690
|
+
};
|
|
5691
|
+
});
|
|
5692
|
+
adoptionTimeout = setTimeout(settleAdoption, sessionAdoptionTimeoutMs);
|
|
5693
|
+
channel.postMessage({ type: 'state-request' });
|
|
5694
|
+
return { isPending: () => isAdoptionPending, settled };
|
|
5695
|
+
})()
|
|
5696
|
+
: undefined;
|
|
5321
5697
|
if (syncTokens) {
|
|
5322
5698
|
effect(() => {
|
|
5323
5699
|
const access = context.accessToken();
|
|
@@ -5353,7 +5729,15 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5353
5729
|
channel.postMessage(message);
|
|
5354
5730
|
});
|
|
5355
5731
|
}
|
|
5356
|
-
|
|
5732
|
+
// Only where a logout travels: with `syncLogout: false` each tab already ends its own session and
|
|
5733
|
+
// nothing else's, so its idleness is genuinely its own.
|
|
5734
|
+
const activityCoordination = syncLogout
|
|
5735
|
+
? {
|
|
5736
|
+
announce: () => channel.postMessage({ type: 'activity' }),
|
|
5737
|
+
activity$: remoteActivity.asObservable(),
|
|
5738
|
+
}
|
|
5739
|
+
: undefined;
|
|
5740
|
+
return { cleanup, sessionAdoption, activityCoordination };
|
|
5357
5741
|
};
|
|
5358
5742
|
|
|
5359
5743
|
const SINGLE_TAB = /* @__PURE__ */ (() => {
|
|
@@ -5402,17 +5786,24 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5402
5786
|
// regular feature setup runs - so this half has to happen earlier than `setup`.
|
|
5403
5787
|
const earlySetup = (context) => {
|
|
5404
5788
|
channelName = config.channelName ?? defaultSyncChannelName(context.name);
|
|
5405
|
-
|
|
5789
|
+
// Filled in below once the election exists. Until then this tab answers as its own leader, which
|
|
5790
|
+
// is what it is: the sync is set up first because the auth queries read `isLeader` while being
|
|
5791
|
+
// wired, and a `state-request` cannot arrive before this function returns anyway.
|
|
5792
|
+
let isLeaderRef = () => true;
|
|
5793
|
+
const sync = setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
|
|
5406
5794
|
accessToken: context.accessToken,
|
|
5407
5795
|
refreshToken: context.refreshToken,
|
|
5408
5796
|
sessionEndCause: context.sessionEndCause,
|
|
5409
5797
|
name: context.name,
|
|
5798
|
+
isLeader: () => isLeaderRef(),
|
|
5410
5799
|
applyTokens: context.applyTokens,
|
|
5411
5800
|
logout: context.logout,
|
|
5412
5801
|
});
|
|
5413
|
-
if (config.leaderElection === false)
|
|
5414
|
-
return {};
|
|
5802
|
+
if (config.leaderElection === false) {
|
|
5803
|
+
return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
|
|
5804
|
+
}
|
|
5415
5805
|
const election = setupLeaderElection({ name: context.name });
|
|
5806
|
+
isLeaderRef = () => election.isLeader();
|
|
5416
5807
|
instance = {
|
|
5417
5808
|
isLeader: election.isLeader,
|
|
5418
5809
|
instanceCount: election.instanceCount,
|
|
@@ -5422,6 +5813,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5422
5813
|
isLeader: () => election.isLeader(),
|
|
5423
5814
|
leaderElection: instance,
|
|
5424
5815
|
refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
|
|
5816
|
+
sessionAdoption: sync.sessionAdoption,
|
|
5817
|
+
activityCoordination: sync.activityCoordination,
|
|
5425
5818
|
};
|
|
5426
5819
|
};
|
|
5427
5820
|
return Object.assign(setup, { earlySetup });
|
|
@@ -5496,6 +5889,14 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5496
5889
|
if (wasRejected)
|
|
5497
5890
|
removeCookie();
|
|
5498
5891
|
});
|
|
5892
|
+
const exchangeCookie = () => {
|
|
5893
|
+
const storedToken = getCookie(cookieName);
|
|
5894
|
+
if (!storedToken)
|
|
5895
|
+
return;
|
|
5896
|
+
const decryptedToken = decryptToken(storedToken);
|
|
5897
|
+
const args = config.autoLogin.buildArgs(decryptedToken);
|
|
5898
|
+
context.queries[config.autoLogin.queryKey].execute(args, { triggeredBy: 'persistent-auth' });
|
|
5899
|
+
};
|
|
5499
5900
|
const tryLogin = () => {
|
|
5500
5901
|
const currentRoute = route();
|
|
5501
5902
|
const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
|
|
@@ -5506,12 +5907,24 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5506
5907
|
if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
|
|
5507
5908
|
return;
|
|
5508
5909
|
}
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5910
|
+
// A session is already here - synced in from another tab, or restored by a previous call. Spending
|
|
5911
|
+
// the cookie now would rotate a refresh token every other tab is holding, for nothing.
|
|
5912
|
+
if (refreshToken())
|
|
5913
|
+
return;
|
|
5914
|
+
const adoption = context.sessionAdoption;
|
|
5915
|
+
// With multi-tab sync on, another tab may be about to hand this one its live session. Wait for
|
|
5916
|
+
// that answer before exchanging the cookie - bounded, so a lone tab is not held up by a reply
|
|
5917
|
+
// that is never coming - and only when there is a cookie to spend, so a tab with nothing to
|
|
5918
|
+
// restore still starts up synchronously.
|
|
5919
|
+
if (adoption?.isPending() && getCookie(cookieName)) {
|
|
5920
|
+
void adoption.settled.then(() => {
|
|
5921
|
+
if (refreshToken())
|
|
5922
|
+
return;
|
|
5923
|
+
exchangeCookie();
|
|
5924
|
+
});
|
|
5925
|
+
return;
|
|
5514
5926
|
}
|
|
5927
|
+
exchangeCookie();
|
|
5515
5928
|
};
|
|
5516
5929
|
const setRememberMe = (enabled) => {
|
|
5517
5930
|
if (typeof localStorage !== 'undefined') {
|
|
@@ -5811,124 +6224,13 @@ const queryDevtoolsAbout = () => ({
|
|
|
5811
6224
|
app,
|
|
5812
6225
|
});
|
|
5813
6226
|
|
|
5814
|
-
/** The neutral fault: nothing armed. */
|
|
5815
|
-
const EMPTY_QUERY_DEVTOOLS_FAULT = {
|
|
5816
|
-
latencyMs: 0,
|
|
5817
|
-
failNext: 0,
|
|
5818
|
-
failRate: 0,
|
|
5819
|
-
status: 503,
|
|
5820
|
-
};
|
|
5821
|
-
/**
|
|
5822
|
-
* Statuses worth injecting, and whether the SDK's default retry policy retries them. A picker built from
|
|
5823
|
-
* this cannot arm a 500 and then leave you wondering why no retry fired.
|
|
5824
|
-
* @see runDefaultQueryRetry
|
|
5825
|
-
*/
|
|
5826
|
-
const QUERY_DEVTOOLS_FAULT_STATUSES = [
|
|
5827
|
-
{ status: 0, label: 'Network error', retryable: true },
|
|
5828
|
-
{ status: 400, label: 'Bad Request', retryable: false },
|
|
5829
|
-
{ status: 401, label: 'Unauthorized', retryable: false },
|
|
5830
|
-
{ status: 403, label: 'Forbidden', retryable: false },
|
|
5831
|
-
{ status: 404, label: 'Not Found', retryable: false },
|
|
5832
|
-
{ status: 408, label: 'Request Timeout', retryable: true },
|
|
5833
|
-
{ status: 429, label: 'Too Many Requests', retryable: true },
|
|
5834
|
-
{ status: 500, label: 'Internal Server Error', retryable: false },
|
|
5835
|
-
{ status: 502, label: 'Bad Gateway', retryable: true },
|
|
5836
|
-
{ status: 503, label: 'Service Unavailable', retryable: true },
|
|
5837
|
-
];
|
|
5838
|
-
const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
|
|
5839
|
-
...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
|
|
5840
|
-
/**
|
|
5841
|
-
* The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
|
|
5842
|
-
* are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
|
|
5843
|
-
*/
|
|
5844
|
-
const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
|
|
5845
|
-
/** Whether anything at all is armed on a fault. */
|
|
5846
|
-
const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
|
|
5847
|
-
/**
|
|
5848
|
-
* Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
|
|
5849
|
-
* value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
|
|
5850
|
-
*
|
|
5851
|
-
* Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
|
|
5852
|
-
* something an application should arm on itself.
|
|
5853
|
-
*/
|
|
5854
|
-
const setQueryDevtoolsFault = (options) => {
|
|
5855
|
-
const { clientName, patch } = options;
|
|
5856
|
-
faults.update((current) => {
|
|
5857
|
-
const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
|
|
5858
|
-
if (!isQueryDevtoolsFaultArmed(next)) {
|
|
5859
|
-
const { [clientName]: _removed, ...rest } = current;
|
|
5860
|
-
return rest;
|
|
5861
|
-
}
|
|
5862
|
-
return { ...current, [clientName]: next };
|
|
5863
|
-
});
|
|
5864
|
-
};
|
|
5865
|
-
/**
|
|
5866
|
-
* Disarms every fault of one client, or of all clients when no name is given.
|
|
5867
|
-
*
|
|
5868
|
-
* @see setQueryDevtoolsFault
|
|
5869
|
-
*/
|
|
5870
|
-
const clearQueryDevtoolsFaults = (clientName) => faults.update((current) => {
|
|
5871
|
-
if (clientName === undefined)
|
|
5872
|
-
return {};
|
|
5873
|
-
const { [clientName]: _removed, ...rest } = current;
|
|
5874
|
-
return rest;
|
|
5875
|
-
});
|
|
5876
|
-
/**
|
|
5877
|
-
* Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
|
|
5878
|
-
* must be called once per attempt and never speculatively.
|
|
5879
|
-
*
|
|
5880
|
-
* Installed as the fault resolver by `provideQueryDevtools()`; nothing else may call it.
|
|
5881
|
-
* @internal
|
|
5882
|
-
*/
|
|
5883
|
-
const resolveQueryDevtoolsFaultForAttempt = (target) => {
|
|
5884
|
-
const fault = faults()[target.clientName];
|
|
5885
|
-
if (!fault || !isQueryDevtoolsFaultArmed(fault))
|
|
5886
|
-
return null;
|
|
5887
|
-
let shouldFail = false;
|
|
5888
|
-
if (fault.failNext > 0) {
|
|
5889
|
-
shouldFail = true;
|
|
5890
|
-
setQueryDevtoolsFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
|
|
5891
|
-
}
|
|
5892
|
-
else if (fault.failRate > 0) {
|
|
5893
|
-
shouldFail = Math.random() * 100 < fault.failRate;
|
|
5894
|
-
}
|
|
5895
|
-
return {
|
|
5896
|
-
latencyMs: fault.latencyMs,
|
|
5897
|
-
status: shouldFail ? fault.status : null,
|
|
5898
|
-
};
|
|
5899
|
-
};
|
|
5900
|
-
|
|
5901
|
-
/**
|
|
5902
|
-
* Records which query forms feed one query's args, so the devtools can answer "which query does this
|
|
5903
|
-
* form drive?" from the reads themselves rather than from a naming convention.
|
|
5904
|
-
* @internal
|
|
5905
|
-
*/
|
|
5906
|
-
const createQueryDevtoolsFormLinks = () => {
|
|
5907
|
-
const ids = signal([], /* @ts-ignore */
|
|
5908
|
-
...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
|
|
5909
|
-
return {
|
|
5910
|
-
ids: ids.asReadonly(),
|
|
5911
|
-
track: (build) => {
|
|
5912
|
-
const collected = collectQueryFormLinks(build);
|
|
5913
|
-
// Args are built inside a reactive computation, which may not write to a signal - `untracked`
|
|
5914
|
-
// lifts that restriction, and skipping the write unless something is new keeps the panel from
|
|
5915
|
-
// being notified on every evaluation.
|
|
5916
|
-
untracked(() => {
|
|
5917
|
-
const known = ids();
|
|
5918
|
-
const added = collected.ids.filter((id) => !known.includes(id));
|
|
5919
|
-
if (added.length)
|
|
5920
|
-
ids.set([...known, ...added]);
|
|
5921
|
-
});
|
|
5922
|
-
return collected.value;
|
|
5923
|
-
},
|
|
5924
|
-
};
|
|
5925
|
-
};
|
|
5926
|
-
|
|
5927
6227
|
const DEFAULTS = {
|
|
5928
6228
|
viewState: 'session',
|
|
5929
6229
|
pins: 'local',
|
|
5930
6230
|
overrides: 'none',
|
|
5931
6231
|
mocks: 'local',
|
|
6232
|
+
armedMocks: 'none',
|
|
6233
|
+
armedFaults: 'none',
|
|
5932
6234
|
maxEvents: 100,
|
|
5933
6235
|
maxDroppedCacheEntries: 20,
|
|
5934
6236
|
responseHistory: null,
|
|
@@ -6031,6 +6333,8 @@ const sanitize = (value) => ({
|
|
|
6031
6333
|
pins: asScope(value?.pins, DEFAULTS.pins),
|
|
6032
6334
|
overrides: asScope(value?.overrides, DEFAULTS.overrides),
|
|
6033
6335
|
mocks: asScope(value?.mocks, DEFAULTS.mocks),
|
|
6336
|
+
armedMocks: asScope(value?.armedMocks, DEFAULTS.armedMocks),
|
|
6337
|
+
armedFaults: asScope(value?.armedFaults, DEFAULTS.armedFaults),
|
|
6034
6338
|
maxEvents: asCount(value?.maxEvents, LIMITS.maxEvents, DEFAULTS.maxEvents),
|
|
6035
6339
|
maxDroppedCacheEntries: asCount(value?.maxDroppedCacheEntries, LIMITS.maxDroppedCacheEntries, DEFAULTS.maxDroppedCacheEntries),
|
|
6036
6340
|
responseHistory: asOptionalCount(value?.responseHistory, LIMITS.responseHistory),
|
|
@@ -6059,23 +6363,213 @@ const setQueryDevtoolsSettings = (patch) => {
|
|
|
6059
6363
|
writeQueryDevtoolsStore('local', SETTINGS_KEY, next);
|
|
6060
6364
|
};
|
|
6061
6365
|
|
|
6366
|
+
/** The neutral fault: nothing armed. */
|
|
6367
|
+
const EMPTY_QUERY_DEVTOOLS_FAULT = {
|
|
6368
|
+
latencyMs: 0,
|
|
6369
|
+
failNext: 0,
|
|
6370
|
+
failRate: 0,
|
|
6371
|
+
status: 503,
|
|
6372
|
+
};
|
|
6373
|
+
/**
|
|
6374
|
+
* Statuses worth injecting, and whether the SDK's default retry policy retries them. A picker built from
|
|
6375
|
+
* this cannot arm a 500 and then leave you wondering why no retry fired.
|
|
6376
|
+
* @see runDefaultQueryRetry
|
|
6377
|
+
*/
|
|
6378
|
+
const QUERY_DEVTOOLS_FAULT_STATUSES = [
|
|
6379
|
+
{ status: 0, label: 'Network error', retryable: true },
|
|
6380
|
+
{ status: 400, label: 'Bad Request', retryable: false },
|
|
6381
|
+
{ status: 401, label: 'Unauthorized', retryable: false },
|
|
6382
|
+
{ status: 403, label: 'Forbidden', retryable: false },
|
|
6383
|
+
{ status: 404, label: 'Not Found', retryable: false },
|
|
6384
|
+
{ status: 408, label: 'Request Timeout', retryable: true },
|
|
6385
|
+
{ status: 429, label: 'Too Many Requests', retryable: true },
|
|
6386
|
+
{ status: 500, label: 'Internal Server Error', retryable: false },
|
|
6387
|
+
{ status: 502, label: 'Bad Gateway', retryable: true },
|
|
6388
|
+
{ status: 503, label: 'Service Unavailable', retryable: true },
|
|
6389
|
+
];
|
|
6390
|
+
const STORAGE_KEY$2 = 'ethlete:query:devtools:faults:v1';
|
|
6391
|
+
const faults = /* @__PURE__ */ signal({}, /* @ts-ignore */
|
|
6392
|
+
...(ngDevMode ? [{ debugName: "faults" }] : /* istanbul ignore next */ []));
|
|
6393
|
+
const restored$1 = /* @__PURE__ */ signal(false, /* @ts-ignore */
|
|
6394
|
+
...(ngDevMode ? [{ debugName: "restored" }] : /* istanbul ignore next */ []));
|
|
6395
|
+
const scope$2 = () => queryDevtoolsSettings().armedFaults;
|
|
6396
|
+
/**
|
|
6397
|
+
* The faults armed per client name, consumed by the `<et-query-devtools>` UI. Clients with nothing armed
|
|
6398
|
+
* are absent rather than present with an {@link EMPTY_QUERY_DEVTOOLS_FAULT}.
|
|
6399
|
+
*/
|
|
6400
|
+
const queryDevtoolsFaults = /* @__PURE__ */ faults.asReadonly();
|
|
6401
|
+
/**
|
|
6402
|
+
* Whether what is armed right now came back from a previous page load rather than being armed by hand.
|
|
6403
|
+
* The panel's armed bar says so, because injected failures nobody remembers arming look exactly like a
|
|
6404
|
+
* broken API.
|
|
6405
|
+
*/
|
|
6406
|
+
const queryDevtoolsFaultsRestored = /* @__PURE__ */ restored$1.asReadonly();
|
|
6407
|
+
const write$1 = () => {
|
|
6408
|
+
if (scope$2() === 'none') {
|
|
6409
|
+
clearQueryDevtoolsStore(STORAGE_KEY$2);
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
writeQueryDevtoolsStore(scope$2(), STORAGE_KEY$2, faults());
|
|
6413
|
+
};
|
|
6414
|
+
const asFault = (value) => {
|
|
6415
|
+
const fault = value;
|
|
6416
|
+
if (!fault || typeof fault !== 'object')
|
|
6417
|
+
return null;
|
|
6418
|
+
const next = {
|
|
6419
|
+
latencyMs: Number(fault.latencyMs) || 0,
|
|
6420
|
+
failNext: Number(fault.failNext) || 0,
|
|
6421
|
+
failRate: Number(fault.failRate) || 0,
|
|
6422
|
+
status: Number.isFinite(Number(fault.status)) ? Number(fault.status) : EMPTY_QUERY_DEVTOOLS_FAULT.status,
|
|
6423
|
+
};
|
|
6424
|
+
return isQueryDevtoolsFaultArmed(next) ? next : null;
|
|
6425
|
+
};
|
|
6426
|
+
/**
|
|
6427
|
+
* Reads the armed faults back, when the `armedFaults` setting asks for them. Called by
|
|
6428
|
+
* `provideQueryDevtools()` after the settings it takes its scope from; nothing else may call it.
|
|
6429
|
+
* @internal
|
|
6430
|
+
*/
|
|
6431
|
+
const initQueryDevtoolsFaults = () => {
|
|
6432
|
+
const stored = readQueryDevtoolsStore(scope$2(), STORAGE_KEY$2);
|
|
6433
|
+
const inherited = {};
|
|
6434
|
+
for (const [clientName, value] of Object.entries(stored ?? {})) {
|
|
6435
|
+
const fault = asFault(value);
|
|
6436
|
+
if (fault)
|
|
6437
|
+
inherited[clientName] = fault;
|
|
6438
|
+
}
|
|
6439
|
+
faults.set(inherited);
|
|
6440
|
+
restored$1.set(Object.keys(inherited).length > 0);
|
|
6441
|
+
};
|
|
6442
|
+
/**
|
|
6443
|
+
* Where the armed faults are kept. Switching to a scope that keeps them stores whatever is armed right
|
|
6444
|
+
* now; switching to `none` empties the store and leaves this page's faults exactly where they are.
|
|
6445
|
+
*/
|
|
6446
|
+
const setQueryDevtoolsFaultsScope = (next) => {
|
|
6447
|
+
if (next === scope$2())
|
|
6448
|
+
return;
|
|
6449
|
+
// Off both stores first: whichever the previous scope was, its copy must not outlive the change.
|
|
6450
|
+
clearQueryDevtoolsStore(STORAGE_KEY$2);
|
|
6451
|
+
setQueryDevtoolsSettings({ armedFaults: next });
|
|
6452
|
+
write$1();
|
|
6453
|
+
};
|
|
6454
|
+
/** Whether anything at all is armed on a fault. */
|
|
6455
|
+
const isQueryDevtoolsFaultArmed = (fault) => fault.latencyMs > 0 || fault.failNext > 0 || fault.failRate > 0;
|
|
6456
|
+
const patchFault = (options) => {
|
|
6457
|
+
const { clientName, patch } = options;
|
|
6458
|
+
faults.update((current) => {
|
|
6459
|
+
const next = { ...(current[clientName] ?? EMPTY_QUERY_DEVTOOLS_FAULT), ...patch };
|
|
6460
|
+
if (!isQueryDevtoolsFaultArmed(next)) {
|
|
6461
|
+
const { [clientName]: _removed, ...rest } = current;
|
|
6462
|
+
return rest;
|
|
6463
|
+
}
|
|
6464
|
+
return { ...current, [clientName]: next };
|
|
6465
|
+
});
|
|
6466
|
+
write$1();
|
|
6467
|
+
};
|
|
6468
|
+
/**
|
|
6469
|
+
* Arms (or updates) the fault of one client. Only the given fields change; the rest keep their current
|
|
6470
|
+
* value. A client whose resulting fault has nothing armed is dropped from {@link queryDevtoolsFaults}.
|
|
6471
|
+
*
|
|
6472
|
+
* Part of the devtools contract consumed by `<et-query-devtools>`; a fault is a debugging tool, not
|
|
6473
|
+
* something an application should arm on itself.
|
|
6474
|
+
*/
|
|
6475
|
+
const setQueryDevtoolsFault = (options) => {
|
|
6476
|
+
patchFault(options);
|
|
6477
|
+
restored$1.set(false);
|
|
6478
|
+
};
|
|
6479
|
+
/**
|
|
6480
|
+
* Disarms every fault of one client, or of all clients when no name is given.
|
|
6481
|
+
*
|
|
6482
|
+
* @see setQueryDevtoolsFault
|
|
6483
|
+
*/
|
|
6484
|
+
const clearQueryDevtoolsFaults = (clientName) => {
|
|
6485
|
+
faults.update((current) => {
|
|
6486
|
+
if (clientName === undefined)
|
|
6487
|
+
return {};
|
|
6488
|
+
const { [clientName]: _removed, ...rest } = current;
|
|
6489
|
+
return rest;
|
|
6490
|
+
});
|
|
6491
|
+
restored$1.set(false);
|
|
6492
|
+
write$1();
|
|
6493
|
+
};
|
|
6494
|
+
/**
|
|
6495
|
+
* Resolves what to do with one upcoming attempt, consuming a `failNext` budget in the process - so this
|
|
6496
|
+
* must be called once per attempt and never speculatively.
|
|
6497
|
+
*
|
|
6498
|
+
* Installed as the fault resolver by `provideQueryDevtools()`; nothing else may call it.
|
|
6499
|
+
* @internal
|
|
6500
|
+
*/
|
|
6501
|
+
const resolveQueryDevtoolsFaultForAttempt = (target) => {
|
|
6502
|
+
const fault = faults()[target.clientName];
|
|
6503
|
+
if (!fault || !isQueryDevtoolsFaultArmed(fault))
|
|
6504
|
+
return null;
|
|
6505
|
+
let shouldFail = false;
|
|
6506
|
+
if (fault.failNext > 0) {
|
|
6507
|
+
shouldFail = true;
|
|
6508
|
+
// Not the public setter: spending the budget is the armed fault doing its job, not someone re-arming
|
|
6509
|
+
// it, so it must not clear what the panel's bar says about where the fault came from.
|
|
6510
|
+
patchFault({ clientName: target.clientName, patch: { failNext: fault.failNext - 1 } });
|
|
6511
|
+
}
|
|
6512
|
+
else if (fault.failRate > 0) {
|
|
6513
|
+
shouldFail = Math.random() * 100 < fault.failRate;
|
|
6514
|
+
}
|
|
6515
|
+
return {
|
|
6516
|
+
latencyMs: fault.latencyMs,
|
|
6517
|
+
status: shouldFail ? fault.status : null,
|
|
6518
|
+
};
|
|
6519
|
+
};
|
|
6520
|
+
|
|
6521
|
+
/**
|
|
6522
|
+
* Records which query forms feed one query's args, so the devtools can answer "which query does this
|
|
6523
|
+
* form drive?" from the reads themselves rather than from a naming convention.
|
|
6524
|
+
* @internal
|
|
6525
|
+
*/
|
|
6526
|
+
const createQueryDevtoolsFormLinks = () => {
|
|
6527
|
+
const ids = signal([], /* @ts-ignore */
|
|
6528
|
+
...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
|
|
6529
|
+
return {
|
|
6530
|
+
ids: ids.asReadonly(),
|
|
6531
|
+
track: (build) => {
|
|
6532
|
+
const collected = collectQueryFormLinks(build);
|
|
6533
|
+
// Args are built inside a reactive computation, which may not write to a signal - `untracked`
|
|
6534
|
+
// lifts that restriction, and skipping the write unless something is new keeps the panel from
|
|
6535
|
+
// being notified on every evaluation.
|
|
6536
|
+
untracked(() => {
|
|
6537
|
+
const known = ids();
|
|
6538
|
+
const added = collected.ids.filter((id) => !known.includes(id));
|
|
6539
|
+
if (added.length)
|
|
6540
|
+
ids.set([...known, ...added]);
|
|
6541
|
+
});
|
|
6542
|
+
return collected.value;
|
|
6543
|
+
},
|
|
6544
|
+
};
|
|
6545
|
+
};
|
|
6546
|
+
|
|
6062
6547
|
/**
|
|
6063
|
-
* The designed library.
|
|
6548
|
+
* The designed library. Which of them is **armed** is a separate store under a separate scope - see
|
|
6064
6549
|
* {@link queryDevtoolsArmedMocks}.
|
|
6065
6550
|
*/
|
|
6066
6551
|
const STORAGE_KEY$1 = 'ethlete:query:devtools:mocks:v1';
|
|
6552
|
+
const ARMED_STORAGE_KEY = 'ethlete:query:devtools:mocks:armed:v1';
|
|
6067
6553
|
const mocks = /* @__PURE__ */ signal([], /* @ts-ignore */
|
|
6068
6554
|
...(ngDevMode ? [{ debugName: "mocks" }] : /* istanbul ignore next */ []));
|
|
6069
6555
|
const armed = /* @__PURE__ */ signal(/* @__PURE__ */ new Set(), /* @ts-ignore */
|
|
6070
6556
|
...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
|
|
6557
|
+
const armedRestored = /* @__PURE__ */ signal(false, /* @ts-ignore */
|
|
6558
|
+
...(ngDevMode ? [{ debugName: "armedRestored" }] : /* istanbul ignore next */ []));
|
|
6071
6559
|
const scope$1 = () => queryDevtoolsSettings().mocks;
|
|
6560
|
+
const armedScope = () => queryDevtoolsSettings().armedMocks;
|
|
6072
6561
|
/** The designed mocks, armed or not. Persisted, because losing an hour of authoring is unacceptable. */
|
|
6073
6562
|
const queryDevtoolsMocks = /* @__PURE__ */ mocks.asReadonly();
|
|
6074
6563
|
/**
|
|
6075
|
-
* The ids currently being served.
|
|
6076
|
-
* data tomorrow morning is worse than losing which mocks were armed
|
|
6564
|
+
* The ids currently being served. Not kept across page loads unless the `armedMocks` setting asks for it -
|
|
6565
|
+
* an app that silently serves designed data tomorrow morning is worse than losing which mocks were armed.
|
|
6077
6566
|
*/
|
|
6078
6567
|
const queryDevtoolsArmedMocks = /* @__PURE__ */ armed.asReadonly();
|
|
6568
|
+
/**
|
|
6569
|
+
* Whether what is armed right now came back from a previous page load rather than being armed by hand.
|
|
6570
|
+
* The panel's armed bar says so, because mocks nobody remembers arming look exactly like a broken API.
|
|
6571
|
+
*/
|
|
6572
|
+
const queryDevtoolsArmedMocksRestored = /* @__PURE__ */ armedRestored.asReadonly();
|
|
6079
6573
|
/**
|
|
6080
6574
|
* The identity of a designed mock: one per client, method, route **and** declared query. The query is part
|
|
6081
6575
|
* of it so `/posts?page=1` and `/posts?page=2` can hold different bodies.
|
|
@@ -6088,6 +6582,19 @@ const write = () => {
|
|
|
6088
6582
|
}
|
|
6089
6583
|
writeQueryDevtoolsStore(scope$1(), STORAGE_KEY$1, mocks());
|
|
6090
6584
|
};
|
|
6585
|
+
const writeArmed = () => {
|
|
6586
|
+
if (armedScope() === 'none') {
|
|
6587
|
+
clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
|
|
6588
|
+
return;
|
|
6589
|
+
}
|
|
6590
|
+
writeQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY, [...armed()]);
|
|
6591
|
+
};
|
|
6592
|
+
/** Every change made by hand goes through here, so none of them can leave the store or the bar behind. */
|
|
6593
|
+
const setArmed = (next) => {
|
|
6594
|
+
armed.set(next);
|
|
6595
|
+
armedRestored.set(false);
|
|
6596
|
+
writeArmed();
|
|
6597
|
+
};
|
|
6091
6598
|
const isMock = (value) => {
|
|
6092
6599
|
const mock = value;
|
|
6093
6600
|
return (!!mock &&
|
|
@@ -6106,7 +6613,15 @@ const initQueryDevtoolsMocks = () => {
|
|
|
6106
6613
|
const stored = readQueryDevtoolsStore(scope$1(), STORAGE_KEY$1);
|
|
6107
6614
|
// `query` was added after the first version of the store, so an entry written before it still loads.
|
|
6108
6615
|
mocks.set(Array.isArray(stored) ? stored.filter(isMock).map((mock) => ({ ...mock, query: mock.query ?? '' })) : []);
|
|
6109
|
-
|
|
6616
|
+
const kept = readQueryDevtoolsStore(armedScope(), ARMED_STORAGE_KEY);
|
|
6617
|
+
const library = new Set(mocks().map((mock) => mock.id));
|
|
6618
|
+
// An id the library no longer holds would arm nothing, so it is dropped rather than kept waiting for a
|
|
6619
|
+
// mock that may never be designed again.
|
|
6620
|
+
const inherited = Array.isArray(kept)
|
|
6621
|
+
? kept.filter((id) => typeof id === 'string' && library.has(id))
|
|
6622
|
+
: [];
|
|
6623
|
+
armed.set(new Set(inherited));
|
|
6624
|
+
armedRestored.set(inherited.length > 0);
|
|
6110
6625
|
};
|
|
6111
6626
|
/** The path of a request URL, without its origin or query string - what a pattern is matched against. */
|
|
6112
6627
|
const queryDevtoolsRequestPath = (url) => {
|
|
@@ -6163,24 +6678,39 @@ const deleteQueryDevtoolsMock = (id) => {
|
|
|
6163
6678
|
};
|
|
6164
6679
|
/** Starts or stops serving one designed mock. Arming a mock that is not in the library does nothing. */
|
|
6165
6680
|
const armQueryDevtoolsMock = (id, isArmed) => {
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6681
|
+
if (isArmed && !mocks().some((mock) => mock.id === id))
|
|
6682
|
+
return;
|
|
6683
|
+
const next = new Set(armed());
|
|
6684
|
+
if (isArmed)
|
|
6685
|
+
next.add(id);
|
|
6686
|
+
else
|
|
6687
|
+
next.delete(id);
|
|
6688
|
+
setArmed(next);
|
|
6689
|
+
};
|
|
6690
|
+
/** Starts serving every mock in the library - the counterpart of {@link clearQueryDevtoolsArmedMocks}. */
|
|
6691
|
+
const armAllQueryDevtoolsMocks = () => setArmed(new Set(mocks().map((mock) => mock.id)));
|
|
6177
6692
|
/** Stops serving every mock, leaving the library alone - the armed bar's one-click way out. */
|
|
6178
|
-
const clearQueryDevtoolsArmedMocks = () =>
|
|
6693
|
+
const clearQueryDevtoolsArmedMocks = () => setArmed(new Set());
|
|
6694
|
+
/**
|
|
6695
|
+
* Where the armed set is kept. Switching to a scope that keeps it stores whatever is armed right now, so
|
|
6696
|
+
* the choice reads as "keep these"; switching to `none` empties the store and leaves this page serving
|
|
6697
|
+
* exactly what it was already serving.
|
|
6698
|
+
*/
|
|
6699
|
+
const setQueryDevtoolsArmedMocksScope = (next) => {
|
|
6700
|
+
if (next === armedScope())
|
|
6701
|
+
return;
|
|
6702
|
+
// Off both stores first: whichever the previous scope was, its copy must not outlive the change.
|
|
6703
|
+
clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
|
|
6704
|
+
setQueryDevtoolsSettings({ armedMocks: next });
|
|
6705
|
+
writeArmed();
|
|
6706
|
+
};
|
|
6179
6707
|
/** Empties the designed library and disarms everything - the panel's "Reset devtools". */
|
|
6180
6708
|
const clearQueryDevtoolsMockStore = () => {
|
|
6181
6709
|
mocks.set([]);
|
|
6182
6710
|
armed.set(new Set());
|
|
6711
|
+
armedRestored.set(false);
|
|
6183
6712
|
clearQueryDevtoolsStore(STORAGE_KEY$1);
|
|
6713
|
+
clearQueryDevtoolsStore(ARMED_STORAGE_KEY);
|
|
6184
6714
|
};
|
|
6185
6715
|
const declaredParamCount = (mock) => (mock.query ? [...new URLSearchParams(mock.query)].length : 0);
|
|
6186
6716
|
/**
|
|
@@ -6916,7 +7446,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
|
|
|
6916
7446
|
|
|
6917
7447
|
// Generated from package.json by tools/scripts/generate-version.js - do not edit.
|
|
6918
7448
|
/** The version of `@ethlete/query` this build was cut from. */
|
|
6919
|
-
const QUERY_VERSION = '6.0.0-next.
|
|
7449
|
+
const QUERY_VERSION = '6.0.0-next.35';
|
|
6920
7450
|
|
|
6921
7451
|
/** How deep a schema is expanded before a nested branch is given up on. */
|
|
6922
7452
|
const MAX_DEPTH = 12;
|
|
@@ -7248,6 +7778,89 @@ const seedQueryDevtoolsSchemaBody = (name) => {
|
|
|
7248
7778
|
return null;
|
|
7249
7779
|
return seedFrom(schema, name);
|
|
7250
7780
|
};
|
|
7781
|
+
/** Where a schema was collected from, so the same name arriving from two pointers is caught. */
|
|
7782
|
+
const originOf = (label, pointer) => pointer ?? `named:${label}`;
|
|
7783
|
+
/**
|
|
7784
|
+
* Copies a schema, pointing every local `$ref` at `#/components/schemas/<name>` and queueing that name -
|
|
7785
|
+
* so a document assembled from these resolves against its own `components`, whether the description kept
|
|
7786
|
+
* its schemas under `components.schemas`, `definitions` or `$defs`.
|
|
7787
|
+
*/
|
|
7788
|
+
const rewriteRefs = (value, queue) => {
|
|
7789
|
+
if (Array.isArray(value))
|
|
7790
|
+
return value.map((entry) => rewriteRefs(entry, queue));
|
|
7791
|
+
if (!isRecord(value))
|
|
7792
|
+
return value;
|
|
7793
|
+
const result = {};
|
|
7794
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7795
|
+
if (key === '$ref' && typeof entry === 'string') {
|
|
7796
|
+
if (!entry.startsWith('#/')) {
|
|
7797
|
+
result[key] = entry;
|
|
7798
|
+
continue;
|
|
7799
|
+
}
|
|
7800
|
+
const label = refLabel(entry);
|
|
7801
|
+
queue({ label, pointer: entry });
|
|
7802
|
+
result[key] = `#/components/schemas/${label}`;
|
|
7803
|
+
continue;
|
|
7804
|
+
}
|
|
7805
|
+
result[key] = rewriteRefs(entry, queue);
|
|
7806
|
+
}
|
|
7807
|
+
return result;
|
|
7808
|
+
};
|
|
7809
|
+
/**
|
|
7810
|
+
* Collects the named schemas a set of routes was designed against, ready to be handed back as the
|
|
7811
|
+
* `components.schemas` of an exported document - so a route seeded from `MatchView` can reference
|
|
7812
|
+
* `MatchView` instead of an anonymous shape inferred from one body.
|
|
7813
|
+
*
|
|
7814
|
+
* Part of the devtools contract. **Not part of the general public contract.**
|
|
7815
|
+
*/
|
|
7816
|
+
const collectQueryDevtoolsSchemaComponents = (names) => {
|
|
7817
|
+
const declared = namedSchemas();
|
|
7818
|
+
const schemas = {};
|
|
7819
|
+
const notes = new Set();
|
|
7820
|
+
const origins = new Map();
|
|
7821
|
+
const pending = names.map((label) => ({ label, pointer: null }));
|
|
7822
|
+
while (pending.length) {
|
|
7823
|
+
const request = pending.shift();
|
|
7824
|
+
const origin = originOf(request.label, request.pointer);
|
|
7825
|
+
const seen = origins.get(request.label);
|
|
7826
|
+
if (seen !== undefined) {
|
|
7827
|
+
if (seen !== origin) {
|
|
7828
|
+
notes.add(`Two different schemas are both called ${request.label} - only the one from ${seen} was kept.`);
|
|
7829
|
+
}
|
|
7830
|
+
continue;
|
|
7831
|
+
}
|
|
7832
|
+
const schema = request.pointer === null ? declared[request.label] : resolveRef(request.pointer);
|
|
7833
|
+
if (schema === undefined) {
|
|
7834
|
+
if (request.pointer !== null) {
|
|
7835
|
+
notes.add(`${request.pointer} could not be resolved, so ${request.label} is missing from the export.`);
|
|
7836
|
+
}
|
|
7837
|
+
continue;
|
|
7838
|
+
}
|
|
7839
|
+
origins.set(request.label, origin);
|
|
7840
|
+
schemas[request.label] = rewriteRefs(schema, (next) => pending.push(next));
|
|
7841
|
+
}
|
|
7842
|
+
for (const schema of Object.values(schemas)) {
|
|
7843
|
+
collectRemoteRefs(schema, notes);
|
|
7844
|
+
}
|
|
7845
|
+
return { schemas, notes: [...notes] };
|
|
7846
|
+
};
|
|
7847
|
+
/** A `$ref` to another file is left as it was written, so a document says where it stops resolving. */
|
|
7848
|
+
const collectRemoteRefs = (value, notes) => {
|
|
7849
|
+
if (Array.isArray(value)) {
|
|
7850
|
+
for (const entry of value)
|
|
7851
|
+
collectRemoteRefs(entry, notes);
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7854
|
+
if (!isRecord(value))
|
|
7855
|
+
return;
|
|
7856
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7857
|
+
if (key === '$ref' && typeof entry === 'string' && !entry.startsWith('#/')) {
|
|
7858
|
+
notes.add(`${entry} points outside the description - it was exported unchanged and will not resolve.`);
|
|
7859
|
+
continue;
|
|
7860
|
+
}
|
|
7861
|
+
collectRemoteRefs(entry, notes);
|
|
7862
|
+
}
|
|
7863
|
+
};
|
|
7251
7864
|
const segmentsOf = (pattern) => pattern.split('/').filter(Boolean);
|
|
7252
7865
|
/** `/matches/{id}` and `/matches/:id` describe the same route; a mock's pattern is written the second way. */
|
|
7253
7866
|
const toColonPattern = (path) => path.replace(/\{([^}]+)\}/g, ':$1');
|
|
@@ -7662,6 +8275,90 @@ const sumQueryDevtoolsStats = (handles) => {
|
|
|
7662
8275
|
return total;
|
|
7663
8276
|
};
|
|
7664
8277
|
|
|
8278
|
+
/**
|
|
8279
|
+
* The longest lifetime an armed override may claim, in seconds (7 days). Anything beyond it is never
|
|
8280
|
+
* refreshed at all, which is what arming nothing already does.
|
|
8281
|
+
*/
|
|
8282
|
+
const QUERY_DEVTOOLS_TOKEN_TTL_LIMIT = 604800;
|
|
8283
|
+
/** The claims an overridden lifetime is measured from, in the order they are tried. */
|
|
8284
|
+
const ANCHOR_CLAIMS = ['iat', 'nbf'];
|
|
8285
|
+
const ttls = /* @__PURE__ */ signal({}, /* @ts-ignore */
|
|
8286
|
+
...(ngDevMode ? [{ debugName: "ttls" }] : /* istanbul ignore next */ []));
|
|
8287
|
+
/**
|
|
8288
|
+
* The access-token lifetime armed per auth provider name, in seconds, consumed by the
|
|
8289
|
+
* `<et-query-devtools>` panel. A provider on its token's real lifetime is absent rather than present
|
|
8290
|
+
* with it.
|
|
8291
|
+
*/
|
|
8292
|
+
const queryDevtoolsTokenTtls = /* @__PURE__ */ ttls.asReadonly();
|
|
8293
|
+
const tokenTtlAnchor = (options) => {
|
|
8294
|
+
const { payload, expiresInPropertyName } = options;
|
|
8295
|
+
if (!payload || typeof payload[expiresInPropertyName] !== 'number')
|
|
8296
|
+
return null;
|
|
8297
|
+
for (const claim of ANCHOR_CLAIMS) {
|
|
8298
|
+
const value = payload[claim];
|
|
8299
|
+
if (typeof value === 'number')
|
|
8300
|
+
return value;
|
|
8301
|
+
}
|
|
8302
|
+
return null;
|
|
8303
|
+
};
|
|
8304
|
+
/**
|
|
8305
|
+
* Whether an overridden lifetime can be applied to a decoded access token at all. It needs the claim
|
|
8306
|
+
* carrying the expiry - so a lifetime is never invented for a token that declares none - plus `iat` or
|
|
8307
|
+
* `nbf` to measure the new lifetime from.
|
|
8308
|
+
*/
|
|
8309
|
+
const canOverrideQueryDevtoolsTokenTtl = (options) => tokenTtlAnchor({ payload: options.payload, expiresInPropertyName: options.expiresInPropertyName ?? 'exp' }) !== null;
|
|
8310
|
+
/**
|
|
8311
|
+
* Presents one auth provider's access token as living `seconds` from the moment it was issued, rather
|
|
8312
|
+
* than as long as it says it does. Everything that reads the expiry sees the shortened lifetime - the
|
|
8313
|
+
* proactive refresh schedule, `withBearerAuthTokenExpirationWarning`, and the panel's own countdown -
|
|
8314
|
+
* so arming 60 seconds makes a real refresh happen a minute after each token is issued, and keeps
|
|
8315
|
+
* happening for every token that refresh brings back.
|
|
8316
|
+
*
|
|
8317
|
+
* It is a lifetime, not a remaining time: `0` presents the current token as long expired, which is what
|
|
8318
|
+
* makes a refresh happen at once. Nothing is persisted, so a reload leaves every provider on its real
|
|
8319
|
+
* lifetime.
|
|
8320
|
+
*
|
|
8321
|
+
* Part of the devtools contract consumed by `<et-query-devtools>`; a token's lifetime is not something
|
|
8322
|
+
* an application should override on itself.
|
|
8323
|
+
*/
|
|
8324
|
+
const setQueryDevtoolsTokenTtl = (options) => {
|
|
8325
|
+
const { providerName, seconds } = options;
|
|
8326
|
+
const clamped = Math.min(Math.max(Math.trunc(seconds) || 0, 0), QUERY_DEVTOOLS_TOKEN_TTL_LIMIT);
|
|
8327
|
+
ttls.update((current) => ({ ...current, [providerName]: clamped }));
|
|
8328
|
+
};
|
|
8329
|
+
/**
|
|
8330
|
+
* Puts one provider back on its token's real lifetime, or every provider when no name is given.
|
|
8331
|
+
*
|
|
8332
|
+
* @see setQueryDevtoolsTokenTtl
|
|
8333
|
+
*/
|
|
8334
|
+
const clearQueryDevtoolsTokenTtl = (providerName) => {
|
|
8335
|
+
ttls.update((current) => {
|
|
8336
|
+
if (providerName === undefined)
|
|
8337
|
+
return {};
|
|
8338
|
+
const { [providerName]: _removed, ...rest } = current;
|
|
8339
|
+
return rest;
|
|
8340
|
+
});
|
|
8341
|
+
};
|
|
8342
|
+
/**
|
|
8343
|
+
* The decoded access token as the devtools want it seen: the claim carrying the expiry rewritten to the
|
|
8344
|
+
* armed lifetime, measured from `iat`. Returns `payload` untouched when nothing is armed for the
|
|
8345
|
+
* provider, or when the token cannot carry the override - see {@link canOverrideQueryDevtoolsTokenTtl}.
|
|
8346
|
+
*
|
|
8347
|
+
* Installed as the token payload patcher by `provideQueryDevtools()`, and called directly by the panel
|
|
8348
|
+
* for its own countdown.
|
|
8349
|
+
*/
|
|
8350
|
+
const applyQueryDevtoolsTokenTtl = (options) => {
|
|
8351
|
+
const { payload, providerName, expiresInPropertyName = 'exp' } = options;
|
|
8352
|
+
const seconds = ttls()[providerName];
|
|
8353
|
+
if (seconds === undefined || !payload || typeof payload !== 'object')
|
|
8354
|
+
return payload;
|
|
8355
|
+
const record = payload;
|
|
8356
|
+
const anchor = tokenTtlAnchor({ payload: record, expiresInPropertyName });
|
|
8357
|
+
if (anchor === null)
|
|
8358
|
+
return payload;
|
|
8359
|
+
return { ...record, [expiresInPropertyName]: anchor + seconds };
|
|
8360
|
+
};
|
|
8361
|
+
|
|
7665
8362
|
/**
|
|
7666
8363
|
* How many destroyed queries the registry keeps. A tombstone holds the last response body it captured,
|
|
7667
8364
|
* so the list is capped for the same reason the repository caps its unused entries.
|
|
@@ -7942,6 +8639,7 @@ const provideQueryDevtools = (options) => {
|
|
|
7942
8639
|
setQueryDevtoolsAppInfo(options?.about);
|
|
7943
8640
|
initQueryDevtoolsOverridePersistence();
|
|
7944
8641
|
initQueryDevtoolsMocks();
|
|
8642
|
+
initQueryDevtoolsFaults();
|
|
7945
8643
|
setQueryDevtoolsSchemaLoader(options?.schema);
|
|
7946
8644
|
setQueryDevtoolsRegistrar(registerEntry);
|
|
7947
8645
|
setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
|
|
@@ -7949,6 +8647,7 @@ const provideQueryDevtools = (options) => {
|
|
|
7949
8647
|
setQueryDevtoolsOverridesFactory(createQueryDevtoolsOverrides);
|
|
7950
8648
|
setQueryDevtoolsFaultResolver(resolveQueryDevtoolsFaultForAttempt);
|
|
7951
8649
|
setQueryDevtoolsMockResolver(resolveQueryDevtoolsMockForAttempt);
|
|
8650
|
+
setQueryDevtoolsTokenPayloadPatcher(applyQueryDevtoolsTokenTtl);
|
|
7952
8651
|
if (!isDevMode()) {
|
|
7953
8652
|
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.');
|
|
7954
8653
|
}
|
|
@@ -13489,5 +14188,5 @@ const createWebSocketClient = (options) => {
|
|
|
13489
14188
|
* Generated bundle index. Do not edit.
|
|
13490
14189
|
*/
|
|
13491
14190
|
|
|
13492
|
-
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, 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, applyQueryFeatures, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, 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, 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, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, 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, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, 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, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
|
|
14191
|
+
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, 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, createDefaultRetryFn, 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, withLongPolling, withLongPollingUsedOnUnsupportedHttpMethod, withLongPollingUsedWithPolling, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
|
|
13493
14192
|
//# sourceMappingURL=ethlete-query.mjs.map
|