@ethlete/query 6.0.0-next.34 → 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 +22 -0
- package/fesm2022/ethlete-query.mjs +454 -105
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +193 -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);
|
|
@@ -4787,6 +4915,8 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4787
4915
|
setTokens,
|
|
4788
4916
|
isLeader: isLeader.isLeaderFn,
|
|
4789
4917
|
leaderElection: isLeader.leaderElectionContext,
|
|
4918
|
+
sessionAdoption: isLeader.sessionAdoption,
|
|
4919
|
+
activityCoordination: isLeader.activityCoordination,
|
|
4790
4920
|
afterTokenRefresh$,
|
|
4791
4921
|
queries: queries,
|
|
4792
4922
|
executionState,
|
|
@@ -4795,9 +4925,21 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4795
4925
|
};
|
|
4796
4926
|
const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
|
|
4797
4927
|
// 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
|
|
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.
|
|
4799
4932
|
if (sessionStatus() === 'unknown') {
|
|
4800
|
-
|
|
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
|
+
}
|
|
4801
4943
|
}
|
|
4802
4944
|
const provider = {
|
|
4803
4945
|
queries,
|
|
@@ -4837,6 +4979,14 @@ const createBearerAuthProvider = (config) => defineRootProvider(() => createBear
|
|
|
4837
4979
|
|
|
4838
4980
|
/** How long a scheduled refresh waits before retrying an attempt that could not run yet. */
|
|
4839
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;
|
|
4840
4990
|
/**
|
|
4841
4991
|
* How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
|
|
4842
4992
|
* declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
|
|
@@ -4889,9 +5039,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4889
5039
|
const expiresInPropertyName = config.expiresInPropertyName ?? 'exp';
|
|
4890
5040
|
const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
|
|
4891
5041
|
const refreshIfExpired = config.refreshIfExpired ?? true;
|
|
5042
|
+
const destroyRef = inject(DestroyRef);
|
|
4892
5043
|
let lastRefreshTime = 0;
|
|
4893
5044
|
let lastUnauthorizedRefreshTime = 0;
|
|
4894
5045
|
let unauthorizedRefreshStreak = 0;
|
|
5046
|
+
let delegatedRefreshAttempts = 0;
|
|
5047
|
+
let isWatchingDelegatedRefresh = false;
|
|
4895
5048
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4896
5049
|
const refreshQuery = () => context.queries[key];
|
|
4897
5050
|
const executeRefresh = (reason) => {
|
|
@@ -4899,9 +5052,14 @@ const withRefreshQuery = (key, config) => {
|
|
|
4899
5052
|
if (!currentRefreshToken)
|
|
4900
5053
|
return 'noToken';
|
|
4901
5054
|
if (!context.isLeader()) {
|
|
4902
|
-
//
|
|
4903
|
-
//
|
|
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';
|
|
4904
5061
|
context.refreshCoordination?.request();
|
|
5062
|
+
watchDelegatedRefresh(context.accessToken());
|
|
4905
5063
|
return 'delegated';
|
|
4906
5064
|
}
|
|
4907
5065
|
// Any token-issuing execution, not just another refresh: a login already in flight is about to
|
|
@@ -4909,14 +5067,12 @@ const withRefreshQuery = (key, config) => {
|
|
|
4909
5067
|
// that login is replacing.
|
|
4910
5068
|
if (context.hasTokenIssuingExecutionInFlight())
|
|
4911
5069
|
return 'busy';
|
|
5070
|
+
const now = Date.now();
|
|
4912
5071
|
if (reason === 'scheduled') {
|
|
4913
|
-
const now = Date.now();
|
|
4914
5072
|
if (now - lastRefreshTime < minRefreshInterval)
|
|
4915
5073
|
return 'throttled';
|
|
4916
|
-
lastRefreshTime = now;
|
|
4917
5074
|
}
|
|
4918
5075
|
else {
|
|
4919
|
-
const now = Date.now();
|
|
4920
5076
|
if (unauthorizedRefreshStreak >= maxUnauthorizedRefreshStreak &&
|
|
4921
5077
|
now - lastUnauthorizedRefreshTime < minRefreshInterval) {
|
|
4922
5078
|
return 'throttled';
|
|
@@ -4924,9 +5080,37 @@ const withRefreshQuery = (key, config) => {
|
|
|
4924
5080
|
unauthorizedRefreshStreak++;
|
|
4925
5081
|
lastUnauthorizedRefreshTime = now;
|
|
4926
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;
|
|
4927
5088
|
refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
|
|
4928
5089
|
return 'executed';
|
|
4929
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
|
+
}
|
|
4930
5114
|
/**
|
|
4931
5115
|
* How long a scheduled refresh that did not spend the refresh token waits before trying again.
|
|
4932
5116
|
* `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
|
|
@@ -4941,7 +5125,10 @@ const withRefreshQuery = (key, config) => {
|
|
|
4941
5125
|
return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
|
|
4942
5126
|
case 'busy':
|
|
4943
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.
|
|
4944
5130
|
case 'delegated':
|
|
5131
|
+
case 'notLeader':
|
|
4945
5132
|
return minRefreshInterval;
|
|
4946
5133
|
}
|
|
4947
5134
|
};
|
|
@@ -4995,12 +5182,28 @@ const withRefreshQuery = (key, config) => {
|
|
|
4995
5182
|
const retryInMs = retryScheduledIn(executeRefresh('scheduled'));
|
|
4996
5183
|
return retryInMs === null || attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
|
|
4997
5184
|
}));
|
|
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
|
+
}
|
|
4998
5200
|
// The delay is read in a computed rather than in the `switchMap` so that a devtools-armed token
|
|
4999
5201
|
// lifetime re-arms the schedule: the override is a signal, and only a reactive read of it moves the
|
|
5000
5202
|
// timer before the next token arrives. The wrapper object keeps every recomputation an emission, so
|
|
5001
5203
|
// two tokens that happen to be due at the same moment still restart the timer.
|
|
5002
5204
|
const nextScheduledRefresh = computed(() => {
|
|
5003
5205
|
const token = context.accessToken();
|
|
5206
|
+
foregroundEpoch();
|
|
5004
5207
|
return { dueInMs: token ? scheduledRefreshDelay(token) : null };
|
|
5005
5208
|
}, /* @ts-ignore */
|
|
5006
5209
|
...(ngDevMode ? [{ debugName: "nextScheduledRefresh" }] : /* istanbul ignore next */ []));
|
|
@@ -5028,6 +5231,7 @@ const withRefreshQuery = (key, config) => {
|
|
|
5028
5231
|
context.repository.events$.pipe(takeUntilDestroyed()).subscribe((event) => {
|
|
5029
5232
|
if (event.type === 'request-success' && event.isSecure) {
|
|
5030
5233
|
unauthorizedRefreshStreak = 0;
|
|
5234
|
+
delegatedRefreshAttempts = 0;
|
|
5031
5235
|
return;
|
|
5032
5236
|
}
|
|
5033
5237
|
if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
|
|
@@ -5061,6 +5265,23 @@ const withRefreshQuery = (key, config) => {
|
|
|
5061
5265
|
};
|
|
5062
5266
|
};
|
|
5063
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
|
+
*/
|
|
5064
5285
|
const withInactivityLogout = (config = {}) => {
|
|
5065
5286
|
return (context) => {
|
|
5066
5287
|
const inactivityTimeout = config.inactivityTimeout ?? 15 * 60 * 1000;
|
|
@@ -5071,8 +5292,25 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5071
5292
|
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
5072
5293
|
const lastActivityTime = signal(Date.now(), /* @ts-ignore */
|
|
5073
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
|
+
};
|
|
5074
5311
|
const resetTimer = () => {
|
|
5075
|
-
|
|
5312
|
+
markActive();
|
|
5313
|
+
announceActivity();
|
|
5076
5314
|
};
|
|
5077
5315
|
const activityFromEvents$ = merge(...activityEvents.map((event) => fromEvent(document, event))).pipe(throttleTime(1000));
|
|
5078
5316
|
const activityFromCustomCheck$ = config.customActivityCheck
|
|
@@ -5084,15 +5322,35 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5084
5322
|
resetTimer();
|
|
5085
5323
|
}
|
|
5086
5324
|
});
|
|
5087
|
-
|
|
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()));
|
|
5088
5341
|
const logoutSubscription = inactivityLogout$.subscribe(() => {
|
|
5089
5342
|
context.logout('inactivity');
|
|
5090
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.
|
|
5091
5348
|
effect(() => {
|
|
5092
|
-
const
|
|
5093
|
-
if (
|
|
5094
|
-
|
|
5349
|
+
const hasToken = !!context.accessToken();
|
|
5350
|
+
if (hasToken && !hadToken) {
|
|
5351
|
+
markActive();
|
|
5095
5352
|
}
|
|
5353
|
+
hadToken = hasToken;
|
|
5096
5354
|
});
|
|
5097
5355
|
const enable = () => {
|
|
5098
5356
|
enabled.set(true);
|
|
@@ -5109,9 +5367,9 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5109
5367
|
const remaining = inactivityTimeout - elapsed;
|
|
5110
5368
|
return remaining > 0 ? remaining : 0;
|
|
5111
5369
|
};
|
|
5112
|
-
resetTimer();
|
|
5113
5370
|
destroyRef.onDestroy(() => {
|
|
5114
5371
|
activitySubscription.unsubscribe();
|
|
5372
|
+
remoteActivitySubscription?.unsubscribe();
|
|
5115
5373
|
logoutSubscription.unsubscribe();
|
|
5116
5374
|
});
|
|
5117
5375
|
const instance = {
|
|
@@ -5127,6 +5385,7 @@ const withInactivityLogout = (config = {}) => {
|
|
|
5127
5385
|
devtools: () => [
|
|
5128
5386
|
{ label: 'timeout', value: formatQueryDevtoolsDuration(inactivityTimeout) },
|
|
5129
5387
|
{ label: 'activity events', value: activityEvents.join(', ') },
|
|
5388
|
+
{ label: 'idleness', value: activityCoordination ? 'shared across tabs' : 'this tab only' },
|
|
5130
5389
|
...(config.customActivityCheck ? [{ label: 'custom activity check', value: 'yes' }] : []),
|
|
5131
5390
|
],
|
|
5132
5391
|
};
|
|
@@ -5331,6 +5590,12 @@ const resetEncryptionKey = () => {
|
|
|
5331
5590
|
* through. A message without one comes from a tab running an older version.
|
|
5332
5591
|
*/
|
|
5333
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;
|
|
5334
5599
|
/**
|
|
5335
5600
|
* The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
|
|
5336
5601
|
* providers reachable from the same origin do not push each other's tokens around.
|
|
@@ -5360,8 +5625,32 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5360
5625
|
// broadcasting effects below run after it, so they would always see it cleared.
|
|
5361
5626
|
let lastSyncedState = null;
|
|
5362
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();
|
|
5363
5633
|
channel.onmessage = (event) => {
|
|
5364
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
|
+
}
|
|
5365
5654
|
if (message.type === 'logout' && syncLogout) {
|
|
5366
5655
|
lastSyncedState = LOGGED_OUT;
|
|
5367
5656
|
hadTokens = false;
|
|
@@ -5377,11 +5666,34 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5377
5666
|
lastSyncedState = tokenState(access, refresh);
|
|
5378
5667
|
hadTokens = true;
|
|
5379
5668
|
context.applyTokens(access, refresh);
|
|
5669
|
+
settleAdoption();
|
|
5380
5670
|
};
|
|
5671
|
+
let adoptionTimeout;
|
|
5381
5672
|
const cleanup = () => {
|
|
5673
|
+
clearTimeout(adoptionTimeout);
|
|
5674
|
+
settleAdoption();
|
|
5675
|
+
remoteActivity.complete();
|
|
5382
5676
|
channel.close();
|
|
5383
5677
|
};
|
|
5384
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;
|
|
5385
5697
|
if (syncTokens) {
|
|
5386
5698
|
effect(() => {
|
|
5387
5699
|
const access = context.accessToken();
|
|
@@ -5417,7 +5729,15 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5417
5729
|
channel.postMessage(message);
|
|
5418
5730
|
});
|
|
5419
5731
|
}
|
|
5420
|
-
|
|
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 };
|
|
5421
5741
|
};
|
|
5422
5742
|
|
|
5423
5743
|
const SINGLE_TAB = /* @__PURE__ */ (() => {
|
|
@@ -5466,17 +5786,24 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5466
5786
|
// regular feature setup runs - so this half has to happen earlier than `setup`.
|
|
5467
5787
|
const earlySetup = (context) => {
|
|
5468
5788
|
channelName = config.channelName ?? defaultSyncChannelName(context.name);
|
|
5469
|
-
|
|
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 }, {
|
|
5470
5794
|
accessToken: context.accessToken,
|
|
5471
5795
|
refreshToken: context.refreshToken,
|
|
5472
5796
|
sessionEndCause: context.sessionEndCause,
|
|
5473
5797
|
name: context.name,
|
|
5798
|
+
isLeader: () => isLeaderRef(),
|
|
5474
5799
|
applyTokens: context.applyTokens,
|
|
5475
5800
|
logout: context.logout,
|
|
5476
5801
|
});
|
|
5477
|
-
if (config.leaderElection === false)
|
|
5478
|
-
return {};
|
|
5802
|
+
if (config.leaderElection === false) {
|
|
5803
|
+
return { sessionAdoption: sync.sessionAdoption, activityCoordination: sync.activityCoordination };
|
|
5804
|
+
}
|
|
5479
5805
|
const election = setupLeaderElection({ name: context.name });
|
|
5806
|
+
isLeaderRef = () => election.isLeader();
|
|
5480
5807
|
instance = {
|
|
5481
5808
|
isLeader: election.isLeader,
|
|
5482
5809
|
instanceCount: election.instanceCount,
|
|
@@ -5486,6 +5813,8 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5486
5813
|
isLeader: () => election.isLeader(),
|
|
5487
5814
|
leaderElection: instance,
|
|
5488
5815
|
refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
|
|
5816
|
+
sessionAdoption: sync.sessionAdoption,
|
|
5817
|
+
activityCoordination: sync.activityCoordination,
|
|
5489
5818
|
};
|
|
5490
5819
|
};
|
|
5491
5820
|
return Object.assign(setup, { earlySetup });
|
|
@@ -5560,6 +5889,14 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5560
5889
|
if (wasRejected)
|
|
5561
5890
|
removeCookie();
|
|
5562
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
|
+
};
|
|
5563
5900
|
const tryLogin = () => {
|
|
5564
5901
|
const currentRoute = route();
|
|
5565
5902
|
const excludeRoutes = config.autoLogin.excludeRoutes ?? [];
|
|
@@ -5570,12 +5907,24 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5570
5907
|
if (config.autoLogin.shouldAutoLogin && !config.autoLogin.shouldAutoLogin(currentRoute)) {
|
|
5571
5908
|
return;
|
|
5572
5909
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
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;
|
|
5578
5926
|
}
|
|
5927
|
+
exchangeCookie();
|
|
5579
5928
|
};
|
|
5580
5929
|
const setRememberMe = (enabled) => {
|
|
5581
5930
|
if (typeof localStorage !== 'undefined') {
|
|
@@ -7097,7 +7446,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
|
|
|
7097
7446
|
|
|
7098
7447
|
// Generated from package.json by tools/scripts/generate-version.js - do not edit.
|
|
7099
7448
|
/** The version of `@ethlete/query` this build was cut from. */
|
|
7100
|
-
const QUERY_VERSION = '6.0.0-next.
|
|
7449
|
+
const QUERY_VERSION = '6.0.0-next.35';
|
|
7101
7450
|
|
|
7102
7451
|
/** How deep a schema is expanded before a nested branch is given up on. */
|
|
7103
7452
|
const MAX_DEPTH = 12;
|
|
@@ -13839,5 +14188,5 @@ const createWebSocketClient = (options) => {
|
|
|
13839
14188
|
* Generated bundle index. Do not edit.
|
|
13840
14189
|
*/
|
|
13841
14190
|
|
|
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 };
|
|
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 };
|
|
13843
14192
|
//# sourceMappingURL=ethlete-query.mjs.map
|