@ethlete/query 6.0.0-next.34 → 6.0.0-next.36
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 +28 -0
- package/fesm2022/ethlete-query.mjs +463 -106
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +199 -20
|
@@ -380,6 +380,8 @@ const QueryRuntimeErrorCode = {
|
|
|
380
380
|
WITH_AUTO_REFRESH_USED_ON_UNSUPPORTED_HTTP_METHOD: 102,
|
|
381
381
|
WITH_AUTO_REFRESH_USED_IN_MANUAL_QUERY: 103,
|
|
382
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,
|
|
383
385
|
// Auth provider
|
|
384
386
|
AUTH_EXTRACT_TOKENS_RESPONSE_NOT_OBJECT: 200,
|
|
385
387
|
AUTH_EXTRACT_TOKENS_RESPONSE_MISSING_ACCESS_TOKEN: 201,
|
|
@@ -430,6 +432,13 @@ const withAutoRefreshUsedInManualQuery = () => {
|
|
|
430
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.` +
|
|
431
433
|
` If this is intentional, set "ignoreOnlyManualExecution" to true inside the auto refresh config.`);
|
|
432
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
|
+
};
|
|
433
442
|
const silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent = () => {
|
|
434
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.`);
|
|
435
444
|
};
|
|
@@ -534,11 +543,6 @@ const isSymfonyListError = (error) => {
|
|
|
534
543
|
'invalidValue' in error[0]);
|
|
535
544
|
};
|
|
536
545
|
|
|
537
|
-
/**
|
|
538
|
-
* Returning this inside a withArgs feature will reset the query args to null.
|
|
539
|
-
* This will also pause polling and auto refresh until new args are set.
|
|
540
|
-
*/
|
|
541
|
-
const CLEAR_QUERY_ARGS = /* @__PURE__ */ Symbol('CLEAR_QUERY_ARGS');
|
|
542
546
|
/** A angular effect that can be nested inside another effect */
|
|
543
547
|
const nestedEffect = (fn, options) => {
|
|
544
548
|
const activeConsumer = getActiveConsumer();
|
|
@@ -555,6 +559,7 @@ const QueryFeatureType = {
|
|
|
555
559
|
WITH_ERROR_HANDLING: 'WITH_ERROR_HANDLING',
|
|
556
560
|
WITH_SUCCESS_HANDLING: 'WITH_SUCCESS_HANDLING',
|
|
557
561
|
WITH_POLLING: 'WITH_POLLING',
|
|
562
|
+
WITH_LONG_POLLING: 'WITH_LONG_POLLING',
|
|
558
563
|
WITH_AUTO_REFRESH: 'WITH_AUTO_REFRESH',
|
|
559
564
|
WITH_RESPONSE_UPDATE: 'WITH_RESPONSE_UPDATE',
|
|
560
565
|
WITH_PAGE_RESET_ON_ERROR: 'WITH_PAGE_RESET_ON_ERROR',
|
|
@@ -566,8 +571,8 @@ const createQueryFeature = (config) => {
|
|
|
566
571
|
* A query feature that allows you to set the arguments of the query.
|
|
567
572
|
* The arguments are read within a computed function, so you can use reactive values.
|
|
568
573
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
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.
|
|
571
576
|
*
|
|
572
577
|
* Changing arguments will automatically trigger a new execution of the query if it is eligible for auto execution (e.g. a GET request).
|
|
573
578
|
*/
|
|
@@ -575,26 +580,11 @@ const withArgs = (args) => {
|
|
|
575
580
|
return createQueryFeature({
|
|
576
581
|
type: QueryFeatureType.WITH_ARGS,
|
|
577
582
|
fn: (context) => {
|
|
578
|
-
// `state.args` is reactively backed by this source, so every reader (exec, polling,
|
|
579
|
-
// auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
|
|
580
|
-
// resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
|
|
581
|
-
let previous = null;
|
|
582
|
-
const resolve = () => {
|
|
583
|
-
const value = args();
|
|
584
|
-
if (value === CLEAR_QUERY_ARGS) {
|
|
585
|
-
previous = null;
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
if (value === null)
|
|
589
|
-
return previous;
|
|
590
|
-
previous = value;
|
|
591
|
-
return value;
|
|
592
|
-
};
|
|
593
583
|
// Only ever non-null while the devtools are installed. Wrapping the source (rather than reading
|
|
594
584
|
// `state.args` from the outside) is what puts the tracking window around the *evaluation* of the
|
|
595
585
|
// args - a cached read of the computed runs nothing and would record no form at all.
|
|
596
586
|
const formLinks = context.state.subtle.devtoolsFormLinks;
|
|
597
|
-
context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(
|
|
587
|
+
context.state.subtle.setArgsSource(formLinks ? () => formLinks.track(args) : args);
|
|
598
588
|
// Trigger (auto-)execution whenever the resolved args change.
|
|
599
589
|
nestedEffect(() => {
|
|
600
590
|
const currArgsNow = context.state.args();
|
|
@@ -690,6 +680,107 @@ const withPolling = (options) => {
|
|
|
690
680
|
},
|
|
691
681
|
});
|
|
692
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
|
+
};
|
|
693
784
|
/** A query feature that can be used to log the latest http event */
|
|
694
785
|
const withLogging = (options) => {
|
|
695
786
|
return createQueryFeature({
|
|
@@ -1004,6 +1095,7 @@ const getQueryFeatureUsage = (
|
|
|
1004
1095
|
options) => {
|
|
1005
1096
|
const { creator, features, queryConfig, creatorInternals } = options;
|
|
1006
1097
|
const hasWithArgsFeature = features.some((f) => f.type === QueryFeatureType.WITH_ARGS);
|
|
1098
|
+
const hasPollingFeature = features.some((f) => f.type === QueryFeatureType.WITH_POLLING);
|
|
1007
1099
|
const shouldAutoExecuteMethod = isCreateGqlQueryOptions(options)
|
|
1008
1100
|
? shouldAutoExecuteGqlQuery(options.creatorInternals.method)
|
|
1009
1101
|
: shouldAutoExecuteQuery(options.creatorInternals.method);
|
|
@@ -1018,6 +1110,7 @@ options) => {
|
|
|
1018
1110
|
}
|
|
1019
1111
|
const featureFnContext = {
|
|
1020
1112
|
hasWithArgsFeature,
|
|
1113
|
+
hasPollingFeature,
|
|
1021
1114
|
shouldAutoExecuteMethod,
|
|
1022
1115
|
shouldAutoExecute,
|
|
1023
1116
|
hasRouteFunction,
|
|
@@ -1547,16 +1640,16 @@ const runQueryErrorParsers = (detail, response) => {
|
|
|
1547
1640
|
return null;
|
|
1548
1641
|
};
|
|
1549
1642
|
const NO_RETRY = { retry: false };
|
|
1550
|
-
let defaultRetryFn = null;
|
|
1643
|
+
let defaultRetryFn$1 = null;
|
|
1551
1644
|
/**
|
|
1552
1645
|
* Installs the retry policy used by every request that does not bring its own `retryFn`. Installed
|
|
1553
1646
|
* by the {@link withDefaultRetry} query client feature; without it nothing is retried automatically.
|
|
1554
1647
|
*/
|
|
1555
1648
|
const setDefaultQueryRetryFn = (fn) => {
|
|
1556
|
-
defaultRetryFn = fn;
|
|
1649
|
+
defaultRetryFn$1 = fn;
|
|
1557
1650
|
};
|
|
1558
1651
|
/** @internal */
|
|
1559
|
-
const runDefaultQueryRetry = (options) => defaultRetryFn?.(options) ?? NO_RETRY;
|
|
1652
|
+
const runDefaultQueryRetry = (options) => defaultRetryFn$1?.(options) ?? NO_RETRY;
|
|
1560
1653
|
|
|
1561
1654
|
const isQueryErrorResponse = (error) => {
|
|
1562
1655
|
return typeof error === 'object' && error !== null && 'raw' in error && 'retryState' in error && 'isList' in error;
|
|
@@ -1823,12 +1916,19 @@ const createHttpRequest = (options) => {
|
|
|
1823
1916
|
});
|
|
1824
1917
|
return true;
|
|
1825
1918
|
};
|
|
1826
|
-
const
|
|
1919
|
+
const abort = () => {
|
|
1827
1920
|
const wasActive = !currentStreamSubscription.closed;
|
|
1828
1921
|
currentStreamSubscription.unsubscribe();
|
|
1829
|
-
|
|
1830
|
-
//
|
|
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.
|
|
1831
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();
|
|
1832
1932
|
event$.complete();
|
|
1833
1933
|
return wasActive;
|
|
1834
1934
|
};
|
|
@@ -1947,6 +2047,7 @@ const createHttpRequest = (options) => {
|
|
|
1947
2047
|
attempts: attempts.asReadonly(),
|
|
1948
2048
|
retryState: retryState.asReadonly(),
|
|
1949
2049
|
lastDurationMs: lastDurationMs.asReadonly(),
|
|
2050
|
+
abort,
|
|
1950
2051
|
},
|
|
1951
2052
|
};
|
|
1952
2053
|
return httpRequest;
|
|
@@ -3030,48 +3131,68 @@ const symfonyQueryErrorParser = (detail) => {
|
|
|
3030
3131
|
return null;
|
|
3031
3132
|
};
|
|
3032
3133
|
|
|
3033
|
-
const
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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))
|
|
3056
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) };
|
|
3057
3183
|
}
|
|
3058
|
-
return { retry: true, delay:
|
|
3059
|
-
}
|
|
3060
|
-
// Retry on 408 or 425 errors
|
|
3061
|
-
if (status === 408 || status === 425) {
|
|
3062
|
-
return { retry: true, delay: defaultRetryDelay };
|
|
3063
|
-
}
|
|
3064
|
-
// Retry on 429 errors
|
|
3065
|
-
if (status === 429) {
|
|
3066
|
-
const retryAfter = headers.get('retry-after') || headers.get('x-retry-after');
|
|
3067
|
-
if (retryAfter) {
|
|
3068
|
-
const delay = parseInt(retryAfter) * 1000;
|
|
3069
|
-
return { retry: true, delay: Number.isNaN(delay) ? defaultRetryDelay : delay };
|
|
3070
|
-
}
|
|
3071
|
-
return { retry: true, delay: defaultRetryDelay };
|
|
3072
|
-
}
|
|
3073
|
-
return { retry: false };
|
|
3184
|
+
return { retry: true, delay: backoff(retryCount) };
|
|
3185
|
+
};
|
|
3074
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);
|
|
3075
3196
|
|
|
3076
3197
|
const noop$2 = () => undefined;
|
|
3077
3198
|
/** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
|
|
@@ -3475,10 +3596,9 @@ const withSymfonyErrors = () => () => {
|
|
|
3475
3596
|
return { type: QueryClientFeatureType.SYMFONY_ERRORS, instance: null };
|
|
3476
3597
|
};
|
|
3477
3598
|
/**
|
|
3478
|
-
* Installs the SDK's default retry policy for every request that does not bring its own `retryFn`:
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3481
|
-
* 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.
|
|
3482
3602
|
*
|
|
3483
3603
|
* Without it nothing is retried automatically and `error.retryState` always reads `{ retry: false }`.
|
|
3484
3604
|
* A per-client or per-creator `retryFn` keeps working either way.
|
|
@@ -3487,11 +3607,11 @@ const withSymfonyErrors = () => () => {
|
|
|
3487
3607
|
* const MY_CLIENT = createQueryClient({
|
|
3488
3608
|
* name: 'my-api',
|
|
3489
3609
|
* baseUrl: 'https://api.example.com',
|
|
3490
|
-
* features: [withDefaultRetry()],
|
|
3610
|
+
* features: [withDefaultRetry({ maxAttempts: 5 })],
|
|
3491
3611
|
* });
|
|
3492
3612
|
*/
|
|
3493
|
-
const withDefaultRetry = () => () => {
|
|
3494
|
-
setDefaultQueryRetryFn(shouldRetryRequest);
|
|
3613
|
+
const withDefaultRetry = (options) => () => {
|
|
3614
|
+
setDefaultQueryRetryFn(options ? createDefaultRetryFn(options) : shouldRetryRequest);
|
|
3495
3615
|
return { type: QueryClientFeatureType.DEFAULT_RETRY, instance: null };
|
|
3496
3616
|
};
|
|
3497
3617
|
/**
|
|
@@ -3509,10 +3629,10 @@ const withDefaultRetry = () => () => {
|
|
|
3509
3629
|
* features: [withEthleteApiErrors()],
|
|
3510
3630
|
* });
|
|
3511
3631
|
*/
|
|
3512
|
-
const withEthleteApiErrors = () => () => {
|
|
3632
|
+
const withEthleteApiErrors = (options) => () => {
|
|
3513
3633
|
registerQueryErrorParser(htmlQueryErrorParser);
|
|
3514
3634
|
registerQueryErrorParser(symfonyQueryErrorParser);
|
|
3515
|
-
setDefaultQueryRetryFn(shouldRetryRequest);
|
|
3635
|
+
setDefaultQueryRetryFn(options?.retry ? createDefaultRetryFn(options.retry) : shouldRetryRequest);
|
|
3516
3636
|
return { type: QueryClientFeatureType.ETHLETE_API_ERRORS, instance: null };
|
|
3517
3637
|
};
|
|
3518
3638
|
|
|
@@ -3533,10 +3653,10 @@ const createQueryRepository = (config) => {
|
|
|
3533
3653
|
const cacheVersion = signal(0, /* @ts-ignore */
|
|
3534
3654
|
...(ngDevMode ? [{ debugName: "cacheVersion" }] : /* istanbul ignore next */ []));
|
|
3535
3655
|
const bumpCacheVersion = () => cacheVersion.update((v) => v + 1);
|
|
3536
|
-
const resolveKeepUnusedFor = (creatorOptions, shouldCache) => {
|
|
3656
|
+
const resolveKeepUnusedFor = (creatorOptions, shouldCache, override) => {
|
|
3537
3657
|
if (config.retentionEnabled === false || !shouldCache)
|
|
3538
3658
|
return 0;
|
|
3539
|
-
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);
|
|
3540
3660
|
};
|
|
3541
3661
|
const request = (options) => {
|
|
3542
3662
|
const { args, creatorOptions, runQueryOptions } = options;
|
|
@@ -3567,7 +3687,7 @@ const createQueryRepository = (config) => {
|
|
|
3567
3687
|
})
|
|
3568
3688
|
: false;
|
|
3569
3689
|
const trackingKey = cacheKey || generateUuid();
|
|
3570
|
-
const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache);
|
|
3690
|
+
const keepUnusedFor = resolveKeepUnusedFor(creatorOptions, shouldCache, runQueryOptions?.keepUnusedFor);
|
|
3571
3691
|
const isMultiTabSyncEnabled = creatorOptions?.multiTabSync !== false;
|
|
3572
3692
|
const isSecure = options.isSecure ?? false;
|
|
3573
3693
|
// A secure response needs an explicit `persistence: true` to reach the disk: leaving an
|
|
@@ -3672,6 +3792,10 @@ const createQueryRepository = (config) => {
|
|
|
3672
3792
|
// Only data is worth keeping around: an entry that never produced a response has nothing to
|
|
3673
3793
|
// hand back to a returning consumer, so it is aborted immediately as it always was.
|
|
3674
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();
|
|
3675
3799
|
retain(key, cacheEntry);
|
|
3676
3800
|
}
|
|
3677
3801
|
else {
|
|
@@ -4657,6 +4781,8 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
|
4657
4781
|
let isLeaderFn = alwaysLeader;
|
|
4658
4782
|
let leaderElectionContext;
|
|
4659
4783
|
let refreshCoordination;
|
|
4784
|
+
let sessionAdoption;
|
|
4785
|
+
let activityCoordination;
|
|
4660
4786
|
for (const featureBuilder of featureBuilders ?? []) {
|
|
4661
4787
|
const earlySetup = featureBuilder.earlySetup;
|
|
4662
4788
|
if (!earlySetup)
|
|
@@ -4665,8 +4791,10 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
|
4665
4791
|
isLeaderFn = result.isLeader ?? isLeaderFn;
|
|
4666
4792
|
leaderElectionContext = result.leaderElection ?? leaderElectionContext;
|
|
4667
4793
|
refreshCoordination = result.refreshCoordination ?? refreshCoordination;
|
|
4794
|
+
sessionAdoption = result.sessionAdoption ?? sessionAdoption;
|
|
4795
|
+
activityCoordination = result.activityCoordination ?? activityCoordination;
|
|
4668
4796
|
}
|
|
4669
|
-
return { isLeaderFn, leaderElectionContext, refreshCoordination };
|
|
4797
|
+
return { isLeaderFn, leaderElectionContext, refreshCoordination, sessionAdoption, activityCoordination };
|
|
4670
4798
|
};
|
|
4671
4799
|
const createBearerAuthProviderImpl = (config) => {
|
|
4672
4800
|
const injector = inject(Injector);
|
|
@@ -4757,6 +4885,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4757
4885
|
queryClient,
|
|
4758
4886
|
name: config.name,
|
|
4759
4887
|
applyTokens,
|
|
4888
|
+
setTokens,
|
|
4760
4889
|
logout,
|
|
4761
4890
|
sessionEndCause: sessionEndCause.asReadonly(),
|
|
4762
4891
|
});
|
|
@@ -4787,6 +4916,8 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4787
4916
|
setTokens,
|
|
4788
4917
|
isLeader: isLeader.isLeaderFn,
|
|
4789
4918
|
leaderElection: isLeader.leaderElectionContext,
|
|
4919
|
+
sessionAdoption: isLeader.sessionAdoption,
|
|
4920
|
+
activityCoordination: isLeader.activityCoordination,
|
|
4790
4921
|
afterTokenRefresh$,
|
|
4791
4922
|
queries: queries,
|
|
4792
4923
|
executionState,
|
|
@@ -4795,9 +4926,21 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4795
4926
|
};
|
|
4796
4927
|
const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
|
|
4797
4928
|
// Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
|
|
4798
|
-
// `unknown` here means nothing is trying to restore a session, so there is nothing to wait for
|
|
4929
|
+
// `unknown` here means nothing is trying to restore a session, so there is nothing to wait for -
|
|
4930
|
+
// unless the join handshake is still out, in which case another tab may be about to hand this one a
|
|
4931
|
+
// live session. Saying `anonymous` in that window would send the auth guard to the login page for a
|
|
4932
|
+
// session that exists, so the fallback waits for the handshake instead.
|
|
4799
4933
|
if (sessionStatus() === 'unknown') {
|
|
4800
|
-
|
|
4934
|
+
const adoption = isLeader.sessionAdoption;
|
|
4935
|
+
if (adoption?.isPending()) {
|
|
4936
|
+
void adoption.settled.then(() => {
|
|
4937
|
+
if (sessionStatus() === 'unknown')
|
|
4938
|
+
sessionStatus.set('anonymous');
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
else {
|
|
4942
|
+
sessionStatus.set('anonymous');
|
|
4943
|
+
}
|
|
4801
4944
|
}
|
|
4802
4945
|
const provider = {
|
|
4803
4946
|
queries,
|
|
@@ -4837,6 +4980,14 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
|
|
|
4837
4980
|
|
|
4838
4981
|
/** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
|
|
4839
4982
|
const rescheduleDelayMs = 5000;
|
|
4983
|
+
/**
|
|
4984
|
+
* How long a follower waits for the leader to answer a delegated refresh before asking again. The
|
|
4985
|
+
* request is a `BroadcastChannel` message with no ack, so a leader that was frozen or handed the
|
|
4986
|
+
* leadership over mid-flight simply never acts on it.
|
|
4987
|
+
*/
|
|
4988
|
+
const delegatedRefreshRetryMs = 3000;
|
|
4989
|
+
/** How often a delegated refresh is re-asked for before the tab leaves it to the next 401. */
|
|
4990
|
+
const maxDelegatedRefreshAttempts = 3;
|
|
4840
4991
|
/**
|
|
4841
4992
|
* How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
|
|
4842
4993
|
* declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
|
|
@@ -4889,9 +5040,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4889
5040
|
const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
|
|
4890
5041
|
const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
|
|
4891
5042
|
const refreshIfExpired = config.refreshIfExpired ?? true;
|
|
5043
|
+
const destroyRef = inject(DestroyRef);
|
|
4892
5044
|
let lastRefreshTime = 0;
|
|
4893
5045
|
let lastUnauthorizedRefreshTime = 0;
|
|
4894
5046
|
let unauthorizedRefreshStreak = 0;
|
|
5047
|
+
let delegatedRefreshAttempts = 0;
|
|
5048
|
+
let isWatchingDelegatedRefresh = false;
|
|
4895
5049
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4896
5050
|
const refreshQuery = () => context.queries[key];
|
|
4897
5051
|
const executeRefresh = (reason) => {
|
|
@@ -4899,9 +5053,14 @@ const withRefreshQuery = (key, config) => {
|
|
|
4899
5053
|
if (!currentRefreshToken)
|
|
4900
5054
|
return 'noToken';
|
|
4901
5055
|
if (!context.isLeader()) {
|
|
4902
|
-
//
|
|
4903
|
-
//
|
|
5056
|
+
// A scheduled tick on a follower is pure duplicate: multi-tab sync keeps the access token
|
|
5057
|
+
// identical across tabs, so the leader's own timer is due at the same instant and it is the
|
|
5058
|
+
// only tab allowed to act. Delegation is for the 401 path, where this tab has a request that
|
|
5059
|
+
// really failed and the leader has no way of knowing.
|
|
5060
|
+
if (reason === 'scheduled')
|
|
5061
|
+
return 'notLeader';
|
|
4904
5062
|
context.refreshCoordination?.request();
|
|
5063
|
+
watchDelegatedRefresh(context.accessToken());
|
|
4905
5064
|
return 'delegated';
|
|
4906
5065
|
}
|
|
4907
5066
|
// Any token-issuing execution, not just another refresh: a login already in flight is about to
|
|
@@ -4909,14 +5068,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4909
5068
|
// that login is replacing.
|
|
4910
5069
|
if (context.hasTokenIssuingExecutionInFlight())
|
|
4911
5070
|
return 'busy';
|
|
5071
|
+
const now = Date.now();
|
|
4912
5072
|
if (reason === 'scheduled') {
|
|
4913
|
-
const now = Date.now();
|
|
4914
5073
|
if (now - lastRefreshTime < minRefreshInterval)
|
|
4915
5074
|
return 'throttled';
|
|
4916
|
-
lastRefreshTime = now;
|
|
4917
5075
|
}
|
|
4918
5076
|
else {
|
|
4919
|
-
const now = Date.now();
|
|
4920
5077
|
if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
|
|
4921
5078
|
now - lastUnauthorizedRefreshTime < minRefreshInterval) {
|
|
4922
5079
|
return 'throttled';
|
|
@@ -4924,9 +5081,37 @@ const withRefreshQuery = (key, config) => {
|
|
|
4924
5081
|
unauthorizedRefreshStreak++;
|
|
4925
5082
|
lastUnauthorizedRefreshTime = now;
|
|
4926
5083
|
}
|
|
5084
|
+
// Stamped whatever the reason, so the scheduled floor covers a 401-driven rotation too - a
|
|
5085
|
+
// scheduled tick right behind one would otherwise spend a second refresh token. The 401 path
|
|
5086
|
+
// deliberately does not *gate* on it: a token revoked seconds after a refresh is exactly when
|
|
5087
|
+
// the request has to go out.
|
|
5088
|
+
lastRefreshTime = now;
|
|
4927
5089
|
refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
|
|
4928
5090
|
return 'executed';
|
|
4929
5091
|
};
|
|
5092
|
+
/**
|
|
5093
|
+
* Asks the leader again if a delegated refresh went unanswered - the request is a channel message
|
|
5094
|
+
* with no ack, so a frozen leader, or one that handed the leadership over mid-flight, drops it
|
|
5095
|
+
* silently. One watch at a time, and by the time it re-asks this tab may be the leader itself;
|
|
5096
|
+
* `executeRefresh` re-decides that on every attempt.
|
|
5097
|
+
*/
|
|
5098
|
+
function watchDelegatedRefresh(tokenAtRequest) {
|
|
5099
|
+
if (isWatchingDelegatedRefresh || delegatedRefreshAttempts >= maxDelegatedRefreshAttempts)
|
|
5100
|
+
return;
|
|
5101
|
+
delegatedRefreshAttempts++;
|
|
5102
|
+
isWatchingDelegatedRefresh = true;
|
|
5103
|
+
timer(delegatedRefreshRetryMs)
|
|
5104
|
+
.pipe(takeUntilDestroyed(destroyRef))
|
|
5105
|
+
.subscribe(() => {
|
|
5106
|
+
isWatchingDelegatedRefresh = false;
|
|
5107
|
+
// A new access token is the leader's answer - nothing to re-ask for.
|
|
5108
|
+
if (context.accessToken() !== tokenAtRequest) {
|
|
5109
|
+
delegatedRefreshAttempts = 0;
|
|
5110
|
+
return;
|
|
5111
|
+
}
|
|
5112
|
+
executeRefresh('unauthorized');
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
4930
5115
|
/**
|
|
4931
5116
|
* How long a scheduled refresh that did not spend the refresh token waits before trying again.
|
|
4932
5117
|
* `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
|
|
@@ -4941,7 +5126,10 @@ const withRefreshQuery = (key, config) => {
|
|
|
4941
5126
|
return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
|
|
4942
5127
|
case 'busy':
|
|
4943
5128
|
return rescheduleDelayMs;
|
|
5129
|
+
// Both wait for the leader: `delegated` asked it to act, `notLeader` left it to its own timer.
|
|
5130
|
+
// Coming back keeps this tab covered if the leadership moves here in the meantime.
|
|
4944
5131
|
case 'delegated':
|
|
5132
|
+
case 'notLeader':
|
|
4945
5133
|
return minRefreshInterval;
|
|
4946
5134
|
}
|
|
4947
5135
|
};
|
|
@@ -4995,12 +5183,28 @@ const withRefreshQuery = (key, config) => {
|
|
|
4995
5183
|
const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
|
|
4996
5184
|
return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
|
|
4997
5185
|
}));
|
|
5186
|
+
// A backgrounded tab's `timer()` does not fire on time - the browser throttles it, and a frozen
|
|
5187
|
+
// page stops running it altogether while keeping the Web Lock that makes it the leader. So the
|
|
5188
|
+
// schedule is recomputed whenever the page becomes visible again: an overdue token resolves to a
|
|
5189
|
+
// delay of 0 and refreshes immediately, instead of waiting for a secure query to 401.
|
|
5190
|
+
const foregroundEpoch = signal(0, /* @ts-ignore */
|
|
5191
|
+
...(ngDevMode ? [{ debugName: "foregroundEpoch" }] : /* istanbul ignore next */ []));
|
|
5192
|
+
if (typeof document !== 'undefined') {
|
|
5193
|
+
fromEvent(document, 'visibilitychange')
|
|
5194
|
+
.pipe(takeUntilDestroyed(destroyRef))
|
|
5195
|
+
.subscribe(() => {
|
|
5196
|
+
if (document.visibilityState !== 'visible')
|
|
5197
|
+
return;
|
|
5198
|
+
foregroundEpoch.update((epoch) => epoch + 1);
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
4998
5201
|
// The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
|
|
4999
5202
|
// lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
|
|
5000
5203
|
// timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
|
|
5001
5204
|
// two tokens that happen to be due at the same moment still restart the timer.
|
|
5002
5205
|
const nextScheduledRefresh = computed(() => {
|
|
5003
5206
|
const token = context.accessToken();
|
|
5207
|
+
foregroundEpoch();
|
|
5004
5208
|
return { dueInMs: token ? scheduledRefreshDelay(token) : null };
|
|
5005
5209
|
}, /* @ts-ignore */
|
|
5006
5210
|
...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
|
|
@@ -5028,6 +5232,7 @@ const withRefreshQuery = (key, config) => {
|
|
|
5028
5232
|
context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
|
|
5029
5233
|
if (event.type === 'request-success' && event.isSecure) {
|
|
5030
5234
|
unauthorizedRefreshStreak = 0;
|
|
5235
|
+
delegatedRefreshAttempts = 0;
|
|
5031
5236
|
return;
|
|
5032
5237
|
}
|
|
5033
5238
|
if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
|
|
@@ -5061,6 +5266,23 @@ const withRefreshQuery = (key, config) => {
|
|
|
5061
5266
|
};
|
|
5062
5267
|
};
|
|
5063
5268
|
|
|
5269
|
+
/**
|
|
5270
|
+
* Ends the session once the user has been inactive for {@link InactivityLogoutConfig.inactivityTimeout},
|
|
5271
|
+
* with `sessionEndCause` reporting `'inactivity'`.
|
|
5272
|
+
*
|
|
5273
|
+
* Idleness is a property of the session, not of a tab: with `withBearerAuthMultiTabSync` on, tabs tell
|
|
5274
|
+
* each other about activity, so the tab the user is typing in keeps the forgotten one from logging
|
|
5275
|
+
* everybody out. Without it - or with `syncLogout: false`, where a logout stays in the tab that
|
|
5276
|
+
* decided it - each tab times out on its own.
|
|
5277
|
+
*
|
|
5278
|
+
* @example
|
|
5279
|
+
* export const AUTH_PROVIDER = createBearerAuthProvider({
|
|
5280
|
+
* name: 'my-auth',
|
|
5281
|
+
* queryClientRef: MY_CLIENT,
|
|
5282
|
+
* queries: [loginQuery, refreshQuery],
|
|
5283
|
+
* features: [withBearerAuthMultiTabSync(), withInactivityLogout({ inactivityTimeout: 5 * 60 * 1000 })],
|
|
5284
|
+
* });
|
|
5285
|
+
*/
|
|
5064
5286
|
const withInactivityLogout = (config = {}) => {
|
|
5065
5287
|
return (context) => {
|
|
5066
5288
|
const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
|
|
@@ -5071,8 +5293,25 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5071
5293
|
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
5072
5294
|
const lastActivityTime = signal(Date.now(), /* @ts-ignore */
|
|
5073
5295
|
...(ngDevMode ? [{ debugName: "lastActivityTime" }] : /* istanbul ignore next */ []));
|
|
5296
|
+
const activityCoordination = context.activityCoordination;
|
|
5297
|
+
// Announcing every throttled event would wake every other tab once a second for as long as the
|
|
5298
|
+
// user scrolls. A quarter of the timeout is far more than the other tabs need to never expire,
|
|
5299
|
+
// and it is one message per several minutes at the default.
|
|
5300
|
+
const announceInterval = inactivityTimeout / 4;
|
|
5301
|
+
let lastAnnouncedAt = 0;
|
|
5302
|
+
const markActive = () => lastActivityTime.set(Date.now());
|
|
5303
|
+
const announceActivity = () => {
|
|
5304
|
+
if (!activityCoordination)
|
|
5305
|
+
return;
|
|
5306
|
+
const now = Date.now();
|
|
5307
|
+
if (now - lastAnnouncedAt < announceInterval)
|
|
5308
|
+
return;
|
|
5309
|
+
lastAnnouncedAt = now;
|
|
5310
|
+
activityCoordination.announce();
|
|
5311
|
+
};
|
|
5074
5312
|
const resetTimer = () => {
|
|
5075
|
-
|
|
5313
|
+
markActive();
|
|
5314
|
+
announceActivity();
|
|
5076
5315
|
};
|
|
5077
5316
|
const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
|
|
5078
5317
|
const activityFromCustomCheck$ = config.customActivityCheck
|
|
@@ -5084,15 +5323,35 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5084
5323
|
resetTimer();
|
|
5085
5324
|
}
|
|
5086
5325
|
});
|
|
5087
|
-
|
|
5326
|
+
// Activity from another tab is never announced onwards - two tabs would echo one keystroke back
|
|
5327
|
+
// and forth for as long as they are both open.
|
|
5328
|
+
const remoteActivitySubscription = activityCoordination?.activity$.subscribe(() => {
|
|
5329
|
+
if (enabled() && context.accessToken()) {
|
|
5330
|
+
markActive();
|
|
5331
|
+
}
|
|
5332
|
+
});
|
|
5333
|
+
const isIdle = () => Date.now() - lastActivityTime() >= inactivityTimeout;
|
|
5334
|
+
// Driven off `lastActivityTime` rather than off the activity stream, so the countdown this feature
|
|
5335
|
+
// reports and the logout it performs cannot disagree: a tab that has seen no event still has a
|
|
5336
|
+
// deadline, and `resetTimer()` postpones the logout it says it resets.
|
|
5337
|
+
//
|
|
5338
|
+
// `isIdle` re-checks at the moment the deadline fires, because the re-arm goes through an effect:
|
|
5339
|
+
// activity recorded in the same tick as the old deadline has not moved it yet. The write that was
|
|
5340
|
+
// missed is what schedules that re-arm, so nothing is dropped by waiting for it.
|
|
5341
|
+
const inactivityLogout$ = toObservable(lastActivityTime).pipe(switchMap((last) => timer(last + inactivityTimeout - Date.now())), filter(() => enabled() && !!context.accessToken() && isIdle()));
|
|
5088
5342
|
const logoutSubscription = inactivityLogout$.subscribe(() => {
|
|
5089
5343
|
context.logout('inactivity');
|
|
5090
5344
|
});
|
|
5345
|
+
let hadToken = false;
|
|
5346
|
+
// Only the start of a session counts as activity. A token _refresh_ is the app working, not the
|
|
5347
|
+
// user, and resetting on one would mean an app that refreshes faster than this timeout never logs
|
|
5348
|
+
// an idle user out at all.
|
|
5091
5349
|
effect(() => {
|
|
5092
|
-
const
|
|
5093
|
-
if (
|
|
5094
|
-
|
|
5350
|
+
const hasToken = !!context.accessToken();
|
|
5351
|
+
if (hasToken && !hadToken) {
|
|
5352
|
+
markActive();
|
|
5095
5353
|
}
|
|
5354
|
+
hadToken = hasToken;
|
|
5096
5355
|
});
|
|
5097
5356
|
const enable = () => {
|
|
5098
5357
|
enabled.set(true);
|
|
@@ -5109,9 +5368,9 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5109
5368
|
const remaining = inactivityTimeout - elapsed;
|
|
5110
5369
|
return remaining > 0 ? remaining : 0;
|
|
5111
5370
|
};
|
|
5112
|
-
resetTimer();
|
|
5113
5371
|
destroyRef.onDestroy(() => {
|
|
5114
5372
|
activitySubscription.unsubscribe();
|
|
5373
|
+
remoteActivitySubscription?.unsubscribe();
|
|
5115
5374
|
logoutSubscription.unsubscribe();
|
|
5116
5375
|
});
|
|
5117
5376
|
const instance = {
|
|
@@ -5127,6 +5386,7 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5127
5386
|
devtools: () => [
|
|
5128
5387
|
{ label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
|
|
5129
5388
|
{ label: 'activity events', value: activityEvents.join(', ') },
|
|
5389
|
+
{ label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
|
|
5130
5390
|
...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
|
|
5131
5391
|
],
|
|
5132
5392
|
};
|
|
@@ -5331,6 +5591,12 @@ const resetEncryptionKey = () => {
|
|
|
5331
5591
|
* through. A message without one comes from a tab running an older version.
|
|
5332
5592
|
*/
|
|
5333
5593
|
const incomingCause = (cause) => !cause || cause === 'user' ? 'otherTab' : cause;
|
|
5594
|
+
/**
|
|
5595
|
+
* The bounded wait a joining tab gives the leader to send the live session over. Short enough to be
|
|
5596
|
+
* invisible in front of the network request it defers, long enough for a same-origin channel message
|
|
5597
|
+
* plus the answer to come back.
|
|
5598
|
+
*/
|
|
5599
|
+
const sessionAdoptionTimeoutMs = 250;
|
|
5334
5600
|
/**
|
|
5335
5601
|
* The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
|
|
5336
5602
|
* providers reachable from the same origin do not push each other's tokens around.
|
|
@@ -5360,8 +5626,32 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5360
5626
|
// broadcasting effects below run after it, so they would always see it cleared.
|
|
5361
5627
|
let lastSyncedState = null;
|
|
5362
5628
|
let hadTokens = false;
|
|
5629
|
+
let isAdoptionPending = false;
|
|
5630
|
+
let settleAdoption = () => {
|
|
5631
|
+
/* replaced below when the handshake actually runs */
|
|
5632
|
+
};
|
|
5633
|
+
const remoteActivity = new Subject();
|
|
5363
5634
|
channel.onmessage = (event) => {
|
|
5364
5635
|
const message = event.data;
|
|
5636
|
+
if (message.type === 'activity') {
|
|
5637
|
+
if (syncLogout)
|
|
5638
|
+
remoteActivity.next();
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
5641
|
+
if (message.type === 'state-request') {
|
|
5642
|
+
if (!syncTokens || !context.isLeader())
|
|
5643
|
+
return;
|
|
5644
|
+
const access = context.accessToken();
|
|
5645
|
+
const refresh = context.refreshToken();
|
|
5646
|
+
if (!access || !refresh)
|
|
5647
|
+
return;
|
|
5648
|
+
channel.postMessage({
|
|
5649
|
+
type: 'tokens-updated',
|
|
5650
|
+
accessToken: encryptToken(access),
|
|
5651
|
+
refreshToken: encryptToken(refresh),
|
|
5652
|
+
});
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5365
5655
|
if (message.type === 'logout' && syncLogout) {
|
|
5366
5656
|
lastSyncedState = LOGGED_OUT;
|
|
5367
5657
|
hadTokens = false;
|
|
@@ -5374,14 +5664,43 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5374
5664
|
const refresh = decryptToken(message.refreshToken);
|
|
5375
5665
|
if (!access || !refresh)
|
|
5376
5666
|
return;
|
|
5667
|
+
const isNewSession = !context.accessToken();
|
|
5377
5668
|
lastSyncedState = tokenState(access, refresh);
|
|
5378
5669
|
hadTokens = true;
|
|
5379
|
-
|
|
5670
|
+
if (isNewSession) {
|
|
5671
|
+
context.setTokens(access, refresh);
|
|
5672
|
+
}
|
|
5673
|
+
else {
|
|
5674
|
+
context.applyTokens(access, refresh);
|
|
5675
|
+
}
|
|
5676
|
+
settleAdoption();
|
|
5380
5677
|
};
|
|
5678
|
+
let adoptionTimeout;
|
|
5381
5679
|
const cleanup = () => {
|
|
5680
|
+
clearTimeout(adoptionTimeout);
|
|
5681
|
+
settleAdoption();
|
|
5682
|
+
remoteActivity.complete();
|
|
5382
5683
|
channel.close();
|
|
5383
5684
|
};
|
|
5384
5685
|
destroyRef.onDestroy(cleanup);
|
|
5686
|
+
// Sync is push-only otherwise: a tab only broadcasts tokens that just changed, so a tab joining a
|
|
5687
|
+
// live session would hear nothing and start its own login. Asking is the missing half.
|
|
5688
|
+
const sessionAdoption = syncTokens
|
|
5689
|
+
? (() => {
|
|
5690
|
+
isAdoptionPending = true;
|
|
5691
|
+
const settled = new Promise((resolve) => {
|
|
5692
|
+
settleAdoption = () => {
|
|
5693
|
+
if (!isAdoptionPending)
|
|
5694
|
+
return;
|
|
5695
|
+
isAdoptionPending = false;
|
|
5696
|
+
resolve();
|
|
5697
|
+
};
|
|
5698
|
+
});
|
|
5699
|
+
adoptionTimeout = setTimeout(settleAdoption, sessionAdoptionTimeoutMs);
|
|
5700
|
+
channel.postMessage({ type: 'state-request' });
|
|
5701
|
+
return { isPending: () => isAdoptionPending, settled };
|
|
5702
|
+
})()
|
|
5703
|
+
: undefined;
|
|
5385
5704
|
if (syncTokens) {
|
|
5386
5705
|
effect(() => {
|
|
5387
5706
|
const access = context.accessToken();
|
|
@@ -5417,7 +5736,15 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5417
5736
|
channel.postMessage(message);
|
|
5418
5737
|
});
|
|
5419
5738
|
}
|
|
5420
|
-
|
|
5739
|
+
// Only where a logout travels: with `syncLogout: false` each tab already ends its own session and
|
|
5740
|
+
// nothing else's, so its idleness is genuinely its own.
|
|
5741
|
+
const activityCoordination = syncLogout
|
|
5742
|
+
? {
|
|
5743
|
+
announce: () => channel.postMessage({ type: 'activity' }),
|
|
5744
|
+
activity$: remoteActivity.asObservable(),
|
|
5745
|
+
}
|
|
5746
|
+
: undefined;
|
|
5747
|
+
return { cleanup, sessionAdoption, activityCoordination };
|
|
5421
5748
|
};
|
|
5422
5749
|
|
|
5423
5750
|
const SINGLE_TAB = /* @__PURE__ */ (() => {
|
|
@@ -5466,17 +5793,25 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5466
5793
|
// regular feature setup runs - so this half has to happen earlier than `setup`.
|
|
5467
5794
|
const earlySetup = (context) => {
|
|
5468
5795
|
channelName = config.channelName ?? defaultSyncChannelName(context.name);
|
|
5469
|
-
|
|
5796
|
+
// Filled in below once the election exists. Until then this tab answers as its own leader, which
|
|
5797
|
+
// is what it is: the sync is set up first because the auth queries read `isLeader` while being
|
|
5798
|
+
// wired, and a `state-request` cannot arrive before this function returns anyway.
|
|
5799
|
+
let isLeaderRef = () => true;
|
|
5800
|
+
const sync = setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
|
|
5470
5801
|
accessToken: context.accessToken,
|
|
5471
5802
|
refreshToken: context.refreshToken,
|
|
5472
5803
|
sessionEndCause: context.sessionEndCause,
|
|
5473
5804
|
name: context.name,
|
|
5805
|
+
isLeader: () => isLeaderRef(),
|
|
5474
5806
|
applyTokens: context.applyTokens,
|
|
5807
|
+
setTokens: context.setTokens,
|
|
5475
5808
|
logout: context.logout,
|
|
5476
5809
|
});
|
|
5477
|
-
if (config.leaderElection === false)
|
|
5478
|
-
return {};
|
|
5810
|
+
if (config.leaderElection === false) {
|
|
5811
|
+
return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
|
|
5812
|
+
}
|
|
5479
5813
|
const election = setupLeaderElection({ name: context.name });
|
|
5814
|
+
isLeaderRef = () => election.isLeader();
|
|
5480
5815
|
instance = {
|
|
5481
5816
|
isLeader: election.isLeader,
|
|
5482
5817
|
instanceCount: election.instanceCount,
|
|
@@ -5486,6 +5821,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5486
5821
|
isLeader: () => election.isLeader(),
|
|
5487
5822
|
leaderElection: instance,
|
|
5488
5823
|
refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
|
|
5824
|
+
sessionAdoption: sync.sessionAdoption,
|
|
5825
|
+
activityCoordination: sync.activityCoordination,
|
|
5489
5826
|
};
|
|
5490
5827
|
};
|
|
5491
5828
|
return Object.assign(setup, { earlySetup });
|
|
@@ -5560,6 +5897,14 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5560
5897
|
if (wasRejected)
|
|
5561
5898
|
removeCookie();
|
|
5562
5899
|
});
|
|
5900
|
+
const exchangeCookie = () => {
|
|
5901
|
+
const storedToken = getCookie(cookieName);
|
|
5902
|
+
if (!storedToken)
|
|
5903
|
+
return;
|
|
5904
|
+
const decryptedToken = decryptToken(storedToken);
|
|
5905
|
+
const args = config.autoLogin.buildArgs(decryptedToken);
|
|
5906
|
+
context.queries[config.autoLogin.queryKey].execute(args, { triggeredBy: 'persistent-auth' });
|
|
5907
|
+
};
|
|
5563
5908
|
const tryLogin = () => {
|
|
5564
5909
|
const currentRoute = route();
|
|
5565
5910
|
const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
|
|
@@ -5570,12 +5915,24 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5570
5915
|
if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
|
|
5571
5916
|
return;
|
|
5572
5917
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5918
|
+
// A session is already here - synced in from another tab, or restored by a previous call. Spending
|
|
5919
|
+
// the cookie now would rotate a refresh token every other tab is holding, for nothing.
|
|
5920
|
+
if (refreshToken())
|
|
5921
|
+
return;
|
|
5922
|
+
const adoption = context.sessionAdoption;
|
|
5923
|
+
// With multi-tab sync on, another tab may be about to hand this one its live session. Wait for
|
|
5924
|
+
// that answer before exchanging the cookie - bounded, so a lone tab is not held up by a reply
|
|
5925
|
+
// that is never coming - and only when there is a cookie to spend, so a tab with nothing to
|
|
5926
|
+
// restore still starts up synchronously.
|
|
5927
|
+
if (adoption?.isPending() && getCookie(cookieName)) {
|
|
5928
|
+
void adoption.settled.then(() => {
|
|
5929
|
+
if (refreshToken())
|
|
5930
|
+
return;
|
|
5931
|
+
exchangeCookie();
|
|
5932
|
+
});
|
|
5933
|
+
return;
|
|
5578
5934
|
}
|
|
5935
|
+
exchangeCookie();
|
|
5579
5936
|
};
|
|
5580
5937
|
const setRememberMe = (enabled) => {
|
|
5581
5938
|
if (typeof localStorage !== 'undefined') {
|
|
@@ -7097,7 +7454,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
|
|
|
7097
7454
|
|
|
7098
7455
|
// Generated from package.json by tools/scripts/generate-version.js - do not edit.
|
|
7099
7456
|
/** The version of `@ethlete/query` this build was cut from. */
|
|
7100
|
-
const QUERY_VERSION = '6.0.0-next.
|
|
7457
|
+
const QUERY_VERSION = '6.0.0-next.36';
|
|
7101
7458
|
|
|
7102
7459
|
/** How deep a schema is expanded before a nested branch is given up on. */
|
|
7103
7460
|
const MAX_DEPTH = 12;
|
|
@@ -13839,5 +14196,5 @@ const createWebSocketClient = (options) => {
|
|
|
13839
14196
|
* Generated bundle index. Do not edit.
|
|
13840
14197
|
*/
|
|
13841
14198
|
|
|
13842
|
-
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_DEVTOOLS_TOKEN_TTL_LIMIT, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QUERY_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryDevtoolsTokenTtl, applyQueryFeatures, armAllQueryDevtoolsMocks, armQueryDevtoolsMock, armQueryDevtoolsOverrideTransfer, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, canOverrideQueryDevtoolsTokenTtl, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsArmedMocks, clearQueryDevtoolsFaults, clearQueryDevtoolsMockStore, clearQueryDevtoolsOverrideStore, clearQueryDevtoolsStore, clearQueryDevtoolsTokenTtl, clearQueryDevtoolsTombstones, clearRestoredQueryDevtoolsOverrides, collectLeafPaths, collectQueryDevtoolsSchemaComponents, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, countUnresolvedQueryDevtoolsOverrides, createAuthGuard, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, deleteQueryDevtoolsMock, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, initQueryDevtoolsFaults, initQueryDevtoolsMocks, initQueryDevtoolsOverridePersistence, initQueryDevtoolsSettings, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsRequestInterceptionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, loadQueryDevtoolsSchema, mapToPaginated, mapViolationsToFormErrors, matchesQueryDevtoolsMockPattern, matchesQueryDevtoolsMockQuery, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryDevtoolsOverrideTransfer, parseQueryRoute, patchQueryDevtoolsTokenPayload, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsAbout, queryDevtoolsArmedMocks, queryDevtoolsArmedMocksRestored, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFaultsRestored, queryDevtoolsFnDetail, queryDevtoolsMockId, queryDevtoolsMocks, queryDevtoolsOverridePersistence, queryDevtoolsRequestPath, queryDevtoolsResponseHistory, queryDevtoolsRestoredOverridesScope, queryDevtoolsSchemaNames, queryDevtoolsSchemaRoutes, queryDevtoolsSchemaState, queryDevtoolsSettings, queryDevtoolsStorage, queryDevtoolsTokenTtls, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, readQueryDevtoolsStore, registerEthleteVersion, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, resolveQueryDevtoolsMock, resolveQueryDevtoolsMockForAttempt, restoredQueryDevtoolsOverrides, runDefaultQueryRetry, runQueryErrorParsers, saveQueryDevtoolsMock, searchQueryField, seedQueryDevtoolsSchemaBody, seedQueryDevtoolsSchemaRoute, serializeBody, serializeQueryDevtoolsOverrideTransfer, setDefaultQueryRetryFn, setQueryDevtoolsAppInfo, setQueryDevtoolsArmedMocksScope, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFaultsScope, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsMockResolver, setQueryDevtoolsOverridePersistence, setQueryDevtoolsOverridesFactory, setQueryDevtoolsOverridesScope, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsSchemaLoader, setQueryDevtoolsSettings, setQueryDevtoolsStatsFactory, setQueryDevtoolsTokenPayloadPatcher, setQueryDevtoolsTokenTtl, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryDevtoolsOverridePersistence, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage, writeQueryDevtoolsStore };
|
|
14199
|
+
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 };
|
|
13843
14200
|
//# sourceMappingURL=ethlete-query.mjs.map
|