@ethlete/query 6.0.0-next.37 → 6.0.0-next.39
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 +34 -0
- package/fesm2022/ethlete-query-testing.mjs +34 -2
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +809 -66
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query-testing.d.ts +5 -1
- package/types/ethlete-query.d.ts +378 -49
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { inject, computed, DestroyRef, InjectionToken, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, VERSION, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
|
|
3
3
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
4
4
|
import { UrlTree, Router, RedirectCommand, ActivatedRoute } from '@angular/router';
|
|
5
|
-
import { filter, take, map, defer, from, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError,
|
|
5
|
+
import { filter, take, map, defer, from, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, EMPTY, takeUntil, finalize, mergeMap, toArray, merge, takeWhile, firstValueFrom, race, concatMap, fromEvent, throttleTime, interval, combineLatest, Observable, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, withLatestFrom, debounceTime } from 'rxjs';
|
|
6
6
|
import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, deleteCookie, setCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
7
7
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
8
8
|
import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
|
|
@@ -283,6 +283,26 @@ const setQueryDevtoolsTokenPayloadPatcher = (fn) => {
|
|
|
283
283
|
* @internal
|
|
284
284
|
*/
|
|
285
285
|
const patchQueryDevtoolsTokenPayload = (options) => (tokenPayloadPatcher ? tokenPayloadPatcher(options) : options.payload);
|
|
286
|
+
/** The batch item being created right now, or `null` outside one. */
|
|
287
|
+
let batchOwner = null;
|
|
288
|
+
/**
|
|
289
|
+
* Runs `create` with every query it creates attributed to `owner`'s batch and item. The attribution has
|
|
290
|
+
* to be ambient: a batch's item query is built by the query creator the caller handed it, which takes no
|
|
291
|
+
* devtools argument - so there is nothing to thread the owner through. Nested runs restore the outer one.
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
const runInQueryDevtoolsBatch = (owner, create) => {
|
|
295
|
+
const outer = batchOwner;
|
|
296
|
+
batchOwner = owner;
|
|
297
|
+
try {
|
|
298
|
+
return create();
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
batchOwner = outer;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/** The batch item a query being created belongs to, or `null` for one created outside a batch. @internal */
|
|
305
|
+
const currentQueryDevtoolsBatch = () => batchOwner;
|
|
286
306
|
let suppressStackRegistration = false;
|
|
287
307
|
/**
|
|
288
308
|
* Suppresses devtools registration for the next {@link createQueryStack} call. Used by
|
|
@@ -409,6 +429,9 @@ const QueryRuntimeErrorCode = {
|
|
|
409
429
|
CIRCULAR_QUERY_DEPENDENCY: 800,
|
|
410
430
|
// Query Sequence
|
|
411
431
|
QUERY_SEQUENCE_ALREADY_RUNNING: 900,
|
|
432
|
+
// Query Batch
|
|
433
|
+
QUERY_BATCH_ALREADY_RUNNING: 910,
|
|
434
|
+
QUERY_BATCH_WITH_ARGS_USED: 911,
|
|
412
435
|
// Legacy interop
|
|
413
436
|
LEGACY_PREPARE_WITHOUT_INJECTION_CONTEXT: 950,
|
|
414
437
|
LEGACY_QUERY_STATE_ALREADY_HAS_KEY: 951,
|
|
@@ -507,6 +530,12 @@ const legacyPrepareWithoutInjectionContext = (creatorName, method = 'prepare') =
|
|
|
507
530
|
const querySequenceAlreadyRunning = () => {
|
|
508
531
|
return new RuntimeError(QueryRuntimeErrorCode.QUERY_SEQUENCE_ALREADY_RUNNING, `run() was called on a query sequence that is already running. Wait for the current run to settle before starting another.`);
|
|
509
532
|
};
|
|
533
|
+
const queryBatchAlreadyRunning = () => {
|
|
534
|
+
return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_ALREADY_RUNNING, `run() or retryFailed() was called on a query batch that is already running. Await the current run, or call cancel() first.`);
|
|
535
|
+
};
|
|
536
|
+
const queryBatchWithArgsUsed = () => {
|
|
537
|
+
return new RuntimeError(QueryRuntimeErrorCode.QUERY_BATCH_WITH_ARGS_USED, `The "withArgs()" feature cannot be used inside a query batch. Each item's args come from the batch's "args" option.`);
|
|
538
|
+
};
|
|
510
539
|
|
|
511
540
|
const isClassValidatorError = (error) => {
|
|
512
541
|
return typeof error === 'object' && error !== null && 'statusCode' in error && 'message' in error && 'error' in error;
|
|
@@ -972,12 +1001,16 @@ const setupQueryState = (options) => {
|
|
|
972
1001
|
requestEventsSubscription.unsubscribe();
|
|
973
1002
|
requestEventsSubscription = request.events$.subscribe((event) => requestEvents$.next(event));
|
|
974
1003
|
};
|
|
1004
|
+
options.destroyRef?.onDestroy(() => {
|
|
1005
|
+
requestEventsSubscription.unsubscribe();
|
|
1006
|
+
requestEvents$.complete();
|
|
1007
|
+
});
|
|
975
1008
|
const executionState = computed(() => {
|
|
976
1009
|
const currentResponse = response();
|
|
977
1010
|
const currentError = error();
|
|
978
1011
|
const currentLoading = loading();
|
|
979
1012
|
if (currentLoading) {
|
|
980
|
-
if (currentResponse) {
|
|
1013
|
+
if (currentResponse !== null) {
|
|
981
1014
|
return {
|
|
982
1015
|
type: 'loading',
|
|
983
1016
|
hasCachedResponse: true,
|
|
@@ -991,10 +1024,19 @@ const setupQueryState = (options) => {
|
|
|
991
1024
|
loading: currentLoading,
|
|
992
1025
|
};
|
|
993
1026
|
}
|
|
1027
|
+
else if (currentError && currentResponse !== null) {
|
|
1028
|
+
return {
|
|
1029
|
+
type: 'failure',
|
|
1030
|
+
error: currentError,
|
|
1031
|
+
hasCachedResponse: true,
|
|
1032
|
+
cachedResponse: currentResponse,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
994
1035
|
else if (currentError) {
|
|
995
1036
|
return {
|
|
996
1037
|
type: 'failure',
|
|
997
1038
|
error: currentError,
|
|
1039
|
+
hasCachedResponse: false,
|
|
998
1040
|
};
|
|
999
1041
|
}
|
|
1000
1042
|
else if (currentResponse !== null || latestHttpEvent()?.type === HttpEventType.Response) {
|
|
@@ -1204,11 +1246,13 @@ const createBaseQuery = (options) => {
|
|
|
1204
1246
|
queryConfig: options.queryConfig,
|
|
1205
1247
|
});
|
|
1206
1248
|
return runInInjectionContext(deps.injector, () => {
|
|
1249
|
+
const batchOwner = currentQueryDevtoolsBatch();
|
|
1207
1250
|
const devtoolsStats = createQueryDevtoolsStatsRecorder();
|
|
1208
1251
|
const devtoolsFormLinks = createQueryDevtoolsFormLinksRecorder();
|
|
1209
1252
|
const devtoolsOverrides = createQueryDevtoolsOverridesRecorder();
|
|
1210
1253
|
const state = setupQueryState({
|
|
1211
1254
|
transformResponse: options.creator?.transformResponse,
|
|
1255
|
+
destroyRef: deps.destroyRef,
|
|
1212
1256
|
devtoolsStats,
|
|
1213
1257
|
devtoolsFormLinks,
|
|
1214
1258
|
devtoolsOverrides,
|
|
@@ -1271,6 +1315,8 @@ const createBaseQuery = (options) => {
|
|
|
1271
1315
|
client: deps.client,
|
|
1272
1316
|
element: deps.hostElement,
|
|
1273
1317
|
gqlQuery: options.creatorInternals.query,
|
|
1318
|
+
batch: batchOwner?.batch,
|
|
1319
|
+
batchItemIndex: batchOwner?.index,
|
|
1274
1320
|
},
|
|
1275
1321
|
});
|
|
1276
1322
|
deps.destroyRef.onDestroy(unregister);
|
|
@@ -2924,6 +2970,259 @@ const createIndexedDbQueryPersistenceAdapter = (options) => {
|
|
|
2924
2970
|
return { loadIndex, read, write, remove, clear, isSupported: true };
|
|
2925
2971
|
};
|
|
2926
2972
|
|
|
2973
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2974
|
+
/**
|
|
2975
|
+
* Runs the same mutation over a list of items - the bulk edit / bulk delete shape - with a bounded
|
|
2976
|
+
* number of requests in flight and a per-item outcome.
|
|
2977
|
+
*
|
|
2978
|
+
* Where a query stack fans a reactive read out over many arg sets and keeps every query alive, a
|
|
2979
|
+
* batch is imperative and disposable: it is triggered by a button, it tolerates a partial failure,
|
|
2980
|
+
* and it destroys each query as soon as that item settles.
|
|
2981
|
+
*
|
|
2982
|
+
* Call it from an injection context; the host's destruction stops an in-flight run.
|
|
2983
|
+
*
|
|
2984
|
+
* @example
|
|
2985
|
+
* ```ts
|
|
2986
|
+
* protected archivePosts = createQueryBatch({
|
|
2987
|
+
* queryCreator: patchPost,
|
|
2988
|
+
* args: (post: Post) => ({ pathParams: { id: post.id }, body: { archived: true } }),
|
|
2989
|
+
* concurrency: 6,
|
|
2990
|
+
* });
|
|
2991
|
+
*
|
|
2992
|
+
* archiveSelected() {
|
|
2993
|
+
* this.archivePosts
|
|
2994
|
+
* .run(this.selection())
|
|
2995
|
+
* .pipe(tap((result) => result.ok || this.toast.error(`${result.failed.length} failed`)))
|
|
2996
|
+
* .subscribe();
|
|
2997
|
+
* }
|
|
2998
|
+
* ```
|
|
2999
|
+
*
|
|
3000
|
+
* ```html
|
|
3001
|
+
* <button et-button [loading]="archivePosts.running()" [progress]="archivePosts.progress()">Archive</button>
|
|
3002
|
+
* @if (archivePosts.failed()) {
|
|
3003
|
+
* <button (click)="retry()">Retry {{ archivePosts.failed() }} failed</button>
|
|
3004
|
+
* }
|
|
3005
|
+
* ```
|
|
3006
|
+
*/
|
|
3007
|
+
const createQueryBatch = (options) => {
|
|
3008
|
+
const { queryCreator, args: mapArgs, stopOnError, allowCache, onItemSettled, features = [] } = options;
|
|
3009
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 4));
|
|
3010
|
+
if (features.some((f) => f.type === QueryFeatureType.WITH_ARGS)) {
|
|
3011
|
+
throw queryBatchWithArgsUsed();
|
|
3012
|
+
}
|
|
3013
|
+
const injector = inject(Injector);
|
|
3014
|
+
const destroyRef = inject(DestroyRef);
|
|
3015
|
+
const devtoolsHandle = isQueryDevtoolsEnabled()
|
|
3016
|
+
? { current: null }
|
|
3017
|
+
: undefined;
|
|
3018
|
+
const status = signal('idle', /* @ts-ignore */
|
|
3019
|
+
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
3020
|
+
const running = signal(false, /* @ts-ignore */
|
|
3021
|
+
...(ngDevMode ? [{ debugName: "running" }] : /* istanbul ignore next */ []));
|
|
3022
|
+
const total = signal(0, /* @ts-ignore */
|
|
3023
|
+
...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
|
|
3024
|
+
const inFlight = signal(0, /* @ts-ignore */
|
|
3025
|
+
...(ngDevMode ? [{ debugName: "inFlight" }] : /* istanbul ignore next */ []));
|
|
3026
|
+
const results = signal([], /* @ts-ignore */
|
|
3027
|
+
...(ngDevMode ? [{ debugName: "results" }] : /* istanbul ignore next */ []));
|
|
3028
|
+
const runStartedAt = signal(null, /* @ts-ignore */
|
|
3029
|
+
...(ngDevMode ? [{ debugName: "runStartedAt" }] : /* istanbul ignore next */ []));
|
|
3030
|
+
const lastSettleAt = signal(null, /* @ts-ignore */
|
|
3031
|
+
...(ngDevMode ? [{ debugName: "lastSettleAt" }] : /* istanbul ignore next */ []));
|
|
3032
|
+
const completedBeforeRun = signal(0, /* @ts-ignore */
|
|
3033
|
+
...(ngDevMode ? [{ debugName: "completedBeforeRun" }] : /* istanbul ignore next */ []));
|
|
3034
|
+
let settled = [];
|
|
3035
|
+
let cancelRequested = false;
|
|
3036
|
+
let hostDestroyed = false;
|
|
3037
|
+
const hostDestroyed$ = new Subject();
|
|
3038
|
+
destroyRef.onDestroy(() => {
|
|
3039
|
+
hostDestroyed = true;
|
|
3040
|
+
cancelRequested = true;
|
|
3041
|
+
hostDestroyed$.next();
|
|
3042
|
+
hostDestroyed$.complete();
|
|
3043
|
+
});
|
|
3044
|
+
const publish = () => results.set(settled.filter((r) => !!r));
|
|
3045
|
+
const record = (result) => {
|
|
3046
|
+
settled[result.index] = result;
|
|
3047
|
+
lastSettleAt.set(Date.now());
|
|
3048
|
+
publish();
|
|
3049
|
+
onItemSettled?.(result);
|
|
3050
|
+
};
|
|
3051
|
+
const countOf = (of) => computed(() => results().filter((r) => r.status === of).length);
|
|
3052
|
+
const completed = computed(() => results().length, /* @ts-ignore */
|
|
3053
|
+
...(ngDevMode ? [{ debugName: "completed" }] : /* istanbul ignore next */ []));
|
|
3054
|
+
const failedResults = computed(() => results().filter((r) => r.status === 'error'), /* @ts-ignore */
|
|
3055
|
+
...(ngDevMode ? [{ debugName: "failedResults" }] : /* istanbul ignore next */ []));
|
|
3056
|
+
// Measured from the run's start rather than between the last two settles: items land in waves of
|
|
3057
|
+
// `concurrency`, so an instantaneous rate swings between zero and a burst.
|
|
3058
|
+
const itemsPerSecond = computed(() => {
|
|
3059
|
+
const startedAt = runStartedAt();
|
|
3060
|
+
const measuredAt = lastSettleAt();
|
|
3061
|
+
if (startedAt === null || measuredAt === null)
|
|
3062
|
+
return null;
|
|
3063
|
+
const elapsedMs = measuredAt - startedAt;
|
|
3064
|
+
const settledInRun = completed() - completedBeforeRun();
|
|
3065
|
+
// One settled item says nothing about a run of `concurrency` parallel ones, and a run shorter
|
|
3066
|
+
// than the buffer extrapolates a whole batch out of its own warm-up.
|
|
3067
|
+
if (elapsedMs < SPEED_BUFFER_TIME_IN_MS || settledInRun < Math.min(concurrency, total()))
|
|
3068
|
+
return null;
|
|
3069
|
+
return (settledInRun / elapsedMs) * 1000;
|
|
3070
|
+
}, /* @ts-ignore */
|
|
3071
|
+
...(ngDevMode ? [{ debugName: "itemsPerSecond" }] : /* istanbul ignore next */ []));
|
|
3072
|
+
const remainingTime = computed(() => {
|
|
3073
|
+
const rate = itemsPerSecond();
|
|
3074
|
+
const outstanding = total() - completed();
|
|
3075
|
+
if (rate === null || rate <= 0 || outstanding <= 0)
|
|
3076
|
+
return null;
|
|
3077
|
+
return Math.round((outstanding / rate) * 1000);
|
|
3078
|
+
}, /* @ts-ignore */
|
|
3079
|
+
...(ngDevMode ? [{ debugName: "remainingTime" }] : /* istanbul ignore next */ []));
|
|
3080
|
+
const runEntry = (entry) => defer(() => {
|
|
3081
|
+
if (cancelRequested) {
|
|
3082
|
+
record({ status: 'cancelled', index: entry.index, item: entry.item });
|
|
3083
|
+
return EMPTY;
|
|
3084
|
+
}
|
|
3085
|
+
const args = mapArgs(entry.item, entry.index);
|
|
3086
|
+
if (args === null) {
|
|
3087
|
+
record({ status: 'skipped', index: entry.index, item: entry.item });
|
|
3088
|
+
return EMPTY;
|
|
3089
|
+
}
|
|
3090
|
+
// The args reach the query through `execute()`, never `withArgs()`, so a function route has to
|
|
3091
|
+
// be told the missing feature is intentional or creating the query throws ET100.
|
|
3092
|
+
const create = () => queryCreator({ silenceMissingWithArgsFeatureError: true }, ...features);
|
|
3093
|
+
const query = runInInjectionContext(injector, () => devtoolsHandle ? runInQueryDevtoolsBatch({ batch: devtoolsHandle, index: entry.index }, create) : create());
|
|
3094
|
+
inFlight.update((count) => count + 1);
|
|
3095
|
+
query.execute({ args, options: { allowCache } });
|
|
3096
|
+
const snapshot = query.createSnapshot();
|
|
3097
|
+
return snapshot.isAlive.asObservable().pipe(filter((isAlive) => !isAlive), take(1),
|
|
3098
|
+
// A destroyed host tears the query down without ever settling it, so the run would hang.
|
|
3099
|
+
takeUntil(hostDestroyed$), tap(() => {
|
|
3100
|
+
const error = snapshot.error();
|
|
3101
|
+
if (error) {
|
|
3102
|
+
record({ status: 'error', index: entry.index, item: entry.item, args, error });
|
|
3103
|
+
if (stopOnError)
|
|
3104
|
+
cancelRequested = true;
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
record({
|
|
3108
|
+
status: 'success',
|
|
3109
|
+
index: entry.index,
|
|
3110
|
+
item: entry.item,
|
|
3111
|
+
args,
|
|
3112
|
+
response: snapshot.response(),
|
|
3113
|
+
});
|
|
3114
|
+
}), finalize(() => {
|
|
3115
|
+
inFlight.update((count) => count - 1);
|
|
3116
|
+
// The host's destruction already tore the query's injector down; destroying it again throws.
|
|
3117
|
+
if (!hostDestroyed)
|
|
3118
|
+
query.subtle.destroy();
|
|
3119
|
+
}));
|
|
3120
|
+
});
|
|
3121
|
+
const settleRun = (entries) => {
|
|
3122
|
+
const stoppedEarly = cancelRequested;
|
|
3123
|
+
for (const entry of entries) {
|
|
3124
|
+
settled[entry.index] ??= { status: 'cancelled', index: entry.index, item: entry.item };
|
|
3125
|
+
}
|
|
3126
|
+
publish();
|
|
3127
|
+
running.set(false);
|
|
3128
|
+
const all = results();
|
|
3129
|
+
const succeeded = all.filter((r) => r.status === 'success');
|
|
3130
|
+
const failed = all.filter((r) => r.status === 'error');
|
|
3131
|
+
const skipped = all.filter((r) => r.status === 'skipped');
|
|
3132
|
+
const notAttempted = all.filter((r) => r.status === 'cancelled');
|
|
3133
|
+
if (failed.length === 0)
|
|
3134
|
+
status.set(notAttempted.length === 0 ? 'success' : 'cancelled');
|
|
3135
|
+
else
|
|
3136
|
+
status.set(succeeded.length === 0 ? 'error' : 'partial');
|
|
3137
|
+
return {
|
|
3138
|
+
ok: failed.length === 0 && notAttempted.length === 0,
|
|
3139
|
+
cancelled: stoppedEarly,
|
|
3140
|
+
results: all,
|
|
3141
|
+
succeeded,
|
|
3142
|
+
failed,
|
|
3143
|
+
skipped,
|
|
3144
|
+
notAttempted,
|
|
3145
|
+
};
|
|
3146
|
+
};
|
|
3147
|
+
const runEntries = (collect) => defer(() => {
|
|
3148
|
+
if (running())
|
|
3149
|
+
throw queryBatchAlreadyRunning();
|
|
3150
|
+
const entries = collect();
|
|
3151
|
+
cancelRequested = false;
|
|
3152
|
+
running.set(true);
|
|
3153
|
+
status.set('running');
|
|
3154
|
+
publish();
|
|
3155
|
+
// After `publish()`: a retry clears its entries first, so this reads the count the retried
|
|
3156
|
+
// items are measured against, not the one from before they were dropped.
|
|
3157
|
+
completedBeforeRun.set(completed());
|
|
3158
|
+
runStartedAt.set(Date.now());
|
|
3159
|
+
lastSettleAt.set(null);
|
|
3160
|
+
return from(entries).pipe(mergeMap((entry) => runEntry(entry), concurrency), toArray(), map(() => settleRun(entries)));
|
|
3161
|
+
});
|
|
3162
|
+
const run = (items) => runEntries(() => {
|
|
3163
|
+
settled = new Array(items.length);
|
|
3164
|
+
total.set(items.length);
|
|
3165
|
+
return items.map((item, index) => ({ item, index }));
|
|
3166
|
+
});
|
|
3167
|
+
const retryFailed = () => runEntries(() => {
|
|
3168
|
+
const entries = results()
|
|
3169
|
+
.filter((r) => r.status === 'error' || r.status === 'cancelled')
|
|
3170
|
+
.map((r) => ({ item: r.item, index: r.index }));
|
|
3171
|
+
for (const entry of entries) {
|
|
3172
|
+
settled[entry.index] = undefined;
|
|
3173
|
+
}
|
|
3174
|
+
return entries;
|
|
3175
|
+
});
|
|
3176
|
+
const batch = {
|
|
3177
|
+
status: status.asReadonly(),
|
|
3178
|
+
running: running.asReadonly(),
|
|
3179
|
+
total: total.asReadonly(),
|
|
3180
|
+
completed,
|
|
3181
|
+
inFlight: inFlight.asReadonly(),
|
|
3182
|
+
succeeded: countOf('success'),
|
|
3183
|
+
failed: countOf('error'),
|
|
3184
|
+
skipped: countOf('skipped'),
|
|
3185
|
+
progress: computed(() => {
|
|
3186
|
+
const count = total();
|
|
3187
|
+
return count === 0 ? 0 : (completed() / count) * 100;
|
|
3188
|
+
}),
|
|
3189
|
+
itemsPerSecond,
|
|
3190
|
+
remainingTime,
|
|
3191
|
+
results: results.asReadonly(),
|
|
3192
|
+
errors: computed(() => failedResults().map((r) => r.error)),
|
|
3193
|
+
failedItems: computed(() => failedResults().map((r) => r.item)),
|
|
3194
|
+
run,
|
|
3195
|
+
retryFailed,
|
|
3196
|
+
cancel: () => {
|
|
3197
|
+
cancelRequested = true;
|
|
3198
|
+
},
|
|
3199
|
+
reset: () => {
|
|
3200
|
+
if (running())
|
|
3201
|
+
return;
|
|
3202
|
+
settled = [];
|
|
3203
|
+
total.set(0);
|
|
3204
|
+
results.set([]);
|
|
3205
|
+
status.set('idle');
|
|
3206
|
+
runStartedAt.set(null);
|
|
3207
|
+
lastSettleAt.set(null);
|
|
3208
|
+
completedBeforeRun.set(0);
|
|
3209
|
+
},
|
|
3210
|
+
};
|
|
3211
|
+
if (devtoolsHandle) {
|
|
3212
|
+
devtoolsHandle.current = batch;
|
|
3213
|
+
const internals = queryCreator.subtle.creatorInternals;
|
|
3214
|
+
const unregister = registerQueryDevtoolsEntry({
|
|
3215
|
+
kind: 'query-batch',
|
|
3216
|
+
handle: devtoolsHandle,
|
|
3217
|
+
route: internals.route,
|
|
3218
|
+
clientRef: internals.client,
|
|
3219
|
+
meta: { method: internals.method, concurrency, stopOnError: !!stopOnError },
|
|
3220
|
+
});
|
|
3221
|
+
destroyRef.onDestroy(unregister);
|
|
3222
|
+
}
|
|
3223
|
+
return batch;
|
|
3224
|
+
};
|
|
3225
|
+
|
|
2927
3226
|
/**
|
|
2928
3227
|
* Resolves an {@link QueryInvalidationOptions.url} to the absolute form request URLs are built in, so
|
|
2929
3228
|
* the comparison - and the message the other tabs receive - never depends on who resolves it.
|
|
@@ -3219,13 +3518,14 @@ const createQueryKeyLockManager = (namespace) => {
|
|
|
3219
3518
|
}
|
|
3220
3519
|
keyStates.set(states);
|
|
3221
3520
|
};
|
|
3222
|
-
const hold = (key) => {
|
|
3521
|
+
const hold = (key, options = {}) => {
|
|
3223
3522
|
const isHolder = signal(false, /* @ts-ignore */
|
|
3224
3523
|
...(ngDevMode ? [{ debugName: "isHolder" }] : /* istanbul ignore next */ []));
|
|
3225
3524
|
const abortController = new AbortController();
|
|
3226
3525
|
const activeHold = { key, isHolder: isHolder.asReadonly() };
|
|
3227
3526
|
let releaseHeldLock = null;
|
|
3228
3527
|
let isReleased = false;
|
|
3528
|
+
let wasGranted = false;
|
|
3229
3529
|
activeHolds.add(activeHold);
|
|
3230
3530
|
publishKeyStates();
|
|
3231
3531
|
const release = () => {
|
|
@@ -3245,12 +3545,16 @@ const createQueryKeyLockManager = (namespace) => {
|
|
|
3245
3545
|
abortController.abort();
|
|
3246
3546
|
}
|
|
3247
3547
|
};
|
|
3548
|
+
// A stealing request is granted on the spot, so it never queues and has nothing to abort - and
|
|
3549
|
+
// the platform rejects `signal` together with `steal` outright.
|
|
3550
|
+
const requestOptions = options.steal ? { steal: true } : { signal: abortController.signal };
|
|
3248
3551
|
locks
|
|
3249
|
-
.request(`${namespace}:${key}`,
|
|
3552
|
+
.request(`${namespace}:${key}`, requestOptions, () => {
|
|
3250
3553
|
// Aborting a request the platform has already granted does nothing, so a release that raced
|
|
3251
3554
|
// the grant lands here instead: take the lock and give it straight back.
|
|
3252
3555
|
if (isReleased)
|
|
3253
3556
|
return Promise.resolve();
|
|
3557
|
+
wasGranted = true;
|
|
3254
3558
|
isHolder.set(true);
|
|
3255
3559
|
publishKeyStates();
|
|
3256
3560
|
return new Promise((resolve) => {
|
|
@@ -3262,6 +3566,13 @@ const createQueryKeyLockManager = (namespace) => {
|
|
|
3262
3566
|
// standby hold ends. Nothing to recover from in either case - this tab is simply not the
|
|
3263
3567
|
// holder, which is what the signal already says.
|
|
3264
3568
|
isHolder.set(false);
|
|
3569
|
+
// A hold that had the lock only ends in a rejection one way: another tab stole it. This one
|
|
3570
|
+
// is over, so it leaves the published states behind and lets the caller decide what comes next.
|
|
3571
|
+
if (!wasGranted || isReleased)
|
|
3572
|
+
return;
|
|
3573
|
+
activeHolds.delete(activeHold);
|
|
3574
|
+
publishKeyStates();
|
|
3575
|
+
options.onLost?.();
|
|
3265
3576
|
});
|
|
3266
3577
|
return { isHolder: activeHold.isHolder, release };
|
|
3267
3578
|
};
|
|
@@ -4320,6 +4631,8 @@ const buildSequence = (steps, state, devtoolsHandle) => {
|
|
|
4320
4631
|
state.running.set(false);
|
|
4321
4632
|
return { ok: true, responses: responses, snapshots };
|
|
4322
4633
|
};
|
|
4634
|
+
const completed = computed(() => state.snapshots().length, /* @ts-ignore */
|
|
4635
|
+
...(ngDevMode ? [{ debugName: "completed" }] : /* istanbul ignore next */ []));
|
|
4323
4636
|
const sequence = {
|
|
4324
4637
|
then: (query, mapArgs) => buildSequence([
|
|
4325
4638
|
...steps,
|
|
@@ -4328,6 +4641,8 @@ const buildSequence = (steps, state, devtoolsHandle) => {
|
|
|
4328
4641
|
status: state.status.asReadonly(),
|
|
4329
4642
|
running: state.running.asReadonly(),
|
|
4330
4643
|
currentStep: state.currentStep.asReadonly(),
|
|
4644
|
+
completed,
|
|
4645
|
+
progress: computed(() => (steps.length === 0 ? 0 : (completed() / steps.length) * 100)),
|
|
4331
4646
|
total: steps.length,
|
|
4332
4647
|
queries: steps.map((s) => s.query),
|
|
4333
4648
|
error: state.error.asReadonly(),
|
|
@@ -4500,6 +4815,54 @@ const resolveFieldTreePath = (fieldTree, path) => {
|
|
|
4500
4815
|
return typeof current === 'function' ? current : null;
|
|
4501
4816
|
};
|
|
4502
4817
|
|
|
4818
|
+
/**
|
|
4819
|
+
* Builds a signal form's `submission.action` around a mutation: submitting executes the query with
|
|
4820
|
+
* args derived from the form's value, waits for it to settle, and maps a failed request's violations
|
|
4821
|
+
* back onto the fields that caused them. `form().submitting()` therefore covers the whole round trip.
|
|
4822
|
+
*
|
|
4823
|
+
* This is the submit-time counterpart to {@link validateWithQuery}. The query's args come from the
|
|
4824
|
+
* submitted value rather than a `withArgs()` feature, which also means the query no longer reads the
|
|
4825
|
+
* form it belongs to - so it can be declared before it, and no derivation runs per keystroke.
|
|
4826
|
+
*
|
|
4827
|
+
* ```ts
|
|
4828
|
+
* protected createPost = createQuerySubmission({
|
|
4829
|
+
* queryCreator: postPost,
|
|
4830
|
+
* args: (value) => ({ body: value }),
|
|
4831
|
+
* onSuccess: (post) => this.router.navigate(['/posts', post.id]),
|
|
4832
|
+
* });
|
|
4833
|
+
*
|
|
4834
|
+
* protected form = form(this.model, postSchema, {
|
|
4835
|
+
* submission: { action: this.createPost.action },
|
|
4836
|
+
* });
|
|
4837
|
+
* ```
|
|
4838
|
+
*
|
|
4839
|
+
* ```html
|
|
4840
|
+
* <form [etForm]="form">…</form>
|
|
4841
|
+
* <et-query-error [error]="createPost.query.error()" [query]="createPost.query" />
|
|
4842
|
+
* ```
|
|
4843
|
+
*
|
|
4844
|
+
* Call it from an injection context.
|
|
4845
|
+
*/
|
|
4846
|
+
const createQuerySubmission = (config) => {
|
|
4847
|
+
// The args reach the query through `execute()`, never `withArgs()`, so a function route has to be
|
|
4848
|
+
// told the missing feature is intentional or creating the query throws ET100.
|
|
4849
|
+
const query = config.queryCreator({ silenceMissingWithArgsFeatureError: true });
|
|
4850
|
+
const action = async (field) => {
|
|
4851
|
+
const args = config.args?.(field().value(), field) ?? null;
|
|
4852
|
+
if (config.args && args === null)
|
|
4853
|
+
return undefined;
|
|
4854
|
+
const snapshot = await executeUntilSettled(query, args === null ? undefined : { args });
|
|
4855
|
+
const error = snapshot.error();
|
|
4856
|
+
if (error) {
|
|
4857
|
+
return (config.mapViolations?.(error, field) ??
|
|
4858
|
+
mapViolationsToFormErrors({ fieldTree: field, error, rewritePath: config.rewritePath }));
|
|
4859
|
+
}
|
|
4860
|
+
config.onSuccess?.(snapshot.response(), field);
|
|
4861
|
+
return undefined;
|
|
4862
|
+
};
|
|
4863
|
+
return { query, action };
|
|
4864
|
+
};
|
|
4865
|
+
|
|
4503
4866
|
// Shared core for the query-backed async validators (`validateWithQuery` /
|
|
4504
4867
|
// `validateWithV2Query`). Everything except how the query is executed - the `factory` that turns
|
|
4505
4868
|
// the debounced params into a `Resource` - is identical between the v2 and v3 variants, so it is
|
|
@@ -4986,8 +5349,35 @@ const rescheduleDelayMs = 5000;
|
|
|
4986
5349
|
* leadership over mid-flight simply never acts on it.
|
|
4987
5350
|
*/
|
|
4988
5351
|
const delegatedRefreshRetryMs = 3000;
|
|
4989
|
-
/** How often a delegated refresh is re-asked for before
|
|
5352
|
+
/** How often a delegated refresh is re-asked for before this tab refreshes the tokens itself. */
|
|
4990
5353
|
const maxDelegatedRefreshAttempts = 3;
|
|
5354
|
+
/**
|
|
5355
|
+
* The same budget for a leader that answered with "a refresh started here". It is working on the
|
|
5356
|
+
* refresh, so it gets longer than a silent one - but not forever, because the answer says a refresh
|
|
5357
|
+
* started, not that it will ever finish.
|
|
5358
|
+
*/
|
|
5359
|
+
const maxDelegatedRefreshWindows = 6;
|
|
5360
|
+
/**
|
|
5361
|
+
* How long before its expiry an access token counts as stale, which is the point a follower stops
|
|
5362
|
+
* waiting for the leader. Long enough to run the delegation ladder before the token is worthless,
|
|
5363
|
+
* short enough that a leader whose timer is merely throttled still gets to act first.
|
|
5364
|
+
*/
|
|
5365
|
+
const followerTakeoverGraceMs = 30000;
|
|
5366
|
+
/**
|
|
5367
|
+
* How long a tab that took a refresh over holds the refresh lock at most. It is released as soon as
|
|
5368
|
+
* the refresh reports back; this only bounds the wait for one that never does.
|
|
5369
|
+
*/
|
|
5370
|
+
const takeoverRefreshMaxLockHoldMs = 15000;
|
|
5371
|
+
/**
|
|
5372
|
+
* The longest delay `setTimeout` can hold. Anything above it fires straight away, which would turn a
|
|
5373
|
+
* long wait into a busy loop that spends every re-arm at once.
|
|
5374
|
+
*/
|
|
5375
|
+
const maxTimerDelayMs = 2147483647;
|
|
5376
|
+
/**
|
|
5377
|
+
* The lock a tab must hold to spend the refresh token outside the leadership. Two followers go stale
|
|
5378
|
+
* at the same instant - their access token is the same one - so the takeover needs a mutex of its own.
|
|
5379
|
+
*/
|
|
5380
|
+
const refreshLockName = (providerName) => `ethlete-auth:refresh:${providerName}`;
|
|
4991
5381
|
/**
|
|
4992
5382
|
* How often a scheduled refresh may re-arm before it waits for a new token pair. A tab that keeps
|
|
4993
5383
|
* declining is either not the leader or busy issuing tokens; either way a 401 still forces a refresh.
|
|
@@ -5046,18 +5436,20 @@ const withRefreshQuery = (key, config) => {
|
|
|
5046
5436
|
let unauthorizedRefreshStreak = 0;
|
|
5047
5437
|
let delegatedRefreshAttempts = 0;
|
|
5048
5438
|
let isWatchingDelegatedRefresh = false;
|
|
5439
|
+
let hasLeaderAnsweredDelegation = false;
|
|
5049
5440
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
5050
5441
|
const refreshQuery = () => context.queries[key];
|
|
5051
5442
|
const executeRefresh = (reason) => {
|
|
5052
5443
|
const currentRefreshToken = context.refreshToken();
|
|
5053
5444
|
if (!currentRefreshToken)
|
|
5054
5445
|
return 'noToken';
|
|
5055
|
-
if (!context.isLeader()) {
|
|
5056
|
-
// A scheduled tick on a follower is pure duplicate
|
|
5057
|
-
// identical across tabs, so the leader's own timer is due at the same
|
|
5058
|
-
// only tab allowed to act.
|
|
5059
|
-
//
|
|
5060
|
-
|
|
5446
|
+
if (reason !== 'takeover' && !context.isLeader()) {
|
|
5447
|
+
// A scheduled tick on a follower is pure duplicate while the token still has life left: multi-tab
|
|
5448
|
+
// sync keeps the access token identical across tabs, so the leader's own timer is due at the same
|
|
5449
|
+
// instant and it is the only tab allowed to act. Once the token is stale that reasoning is spent -
|
|
5450
|
+
// the leader had its moment and let it pass - so the tab delegates and, if that goes unanswered,
|
|
5451
|
+
// takes the refresh over.
|
|
5452
|
+
if (reason === 'scheduled' && !isAccessTokenStale())
|
|
5061
5453
|
return 'notLeader';
|
|
5062
5454
|
context.refreshCoordination?.request();
|
|
5063
5455
|
watchDelegatedRefresh(context.accessToken());
|
|
@@ -5087,16 +5479,28 @@ const withRefreshQuery = (key, config) => {
|
|
|
5087
5479
|
// the request has to go out.
|
|
5088
5480
|
lastRefreshTime = now;
|
|
5089
5481
|
refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
|
|
5482
|
+
// Answers every follower waiting on a delegated refresh, not just the one that asked: they all
|
|
5483
|
+
// hold the same access token, so they all go stale at the same moment.
|
|
5484
|
+
context.refreshCoordination?.announceStart();
|
|
5090
5485
|
return 'executed';
|
|
5091
5486
|
};
|
|
5092
5487
|
/**
|
|
5093
|
-
* Asks the leader again if a delegated refresh went unanswered
|
|
5094
|
-
*
|
|
5095
|
-
*
|
|
5096
|
-
*
|
|
5488
|
+
* Asks the leader again if a delegated refresh went unanswered, and refreshes the tokens in this tab
|
|
5489
|
+
* once the leader has run out of chances. A frozen leader keeps the lock that makes it the leader
|
|
5490
|
+
* while it runs no timer and reads no message, so waiting for it is waiting for nothing.
|
|
5491
|
+
*
|
|
5492
|
+
* One watch at a time, and by the time it re-asks this tab may be the leader itself; `executeRefresh`
|
|
5493
|
+
* re-decides that on every attempt.
|
|
5097
5494
|
*/
|
|
5098
5495
|
function watchDelegatedRefresh(tokenAtRequest) {
|
|
5099
|
-
if (isWatchingDelegatedRefresh
|
|
5496
|
+
if (isWatchingDelegatedRefresh)
|
|
5497
|
+
return;
|
|
5498
|
+
// A new ladder judges the leader by what it answers this time round, not by an answer from an hour
|
|
5499
|
+
// ago - the tab that gave it may not even be open any more.
|
|
5500
|
+
if (delegatedRefreshAttempts === 0)
|
|
5501
|
+
hasLeaderAnsweredDelegation = false;
|
|
5502
|
+
const budget = hasLeaderAnsweredDelegation ? maxDelegatedRefreshWindows : maxDelegatedRefreshAttempts;
|
|
5503
|
+
if (delegatedRefreshAttempts >= budget)
|
|
5100
5504
|
return;
|
|
5101
5505
|
delegatedRefreshAttempts++;
|
|
5102
5506
|
isWatchingDelegatedRefresh = true;
|
|
@@ -5107,11 +5511,62 @@ const withRefreshQuery = (key, config) => {
|
|
|
5107
5511
|
// A new access token is the leader's answer - nothing to re-ask for.
|
|
5108
5512
|
if (context.accessToken() !== tokenAtRequest) {
|
|
5109
5513
|
delegatedRefreshAttempts = 0;
|
|
5514
|
+
hasLeaderAnsweredDelegation = false;
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
const spentBudget = hasLeaderAnsweredDelegation ? maxDelegatedRefreshWindows : maxDelegatedRefreshAttempts;
|
|
5518
|
+
if (delegatedRefreshAttempts >= spentBudget) {
|
|
5519
|
+
void takeOverRefresh(tokenAtRequest);
|
|
5110
5520
|
return;
|
|
5111
5521
|
}
|
|
5112
5522
|
executeRefresh('unauthorized');
|
|
5113
5523
|
});
|
|
5114
5524
|
}
|
|
5525
|
+
/** Runs the refresh and keeps the refresh lock until it reports back, or until the bound runs out. */
|
|
5526
|
+
const runTakenOverRefresh = async () => {
|
|
5527
|
+
const attempt = executeRefresh('takeover');
|
|
5528
|
+
// The ladder starts over whatever came of it, including a takeover that could not run yet - a
|
|
5529
|
+
// refresh already in flight, or the floor under the 401-driven ones. Leaving the budget spent
|
|
5530
|
+
// would retire the only path a follower has to a token the leader is not going to issue.
|
|
5531
|
+
delegatedRefreshAttempts = 0;
|
|
5532
|
+
hasLeaderAnsweredDelegation = false;
|
|
5533
|
+
if (attempt !== 'executed')
|
|
5534
|
+
return;
|
|
5535
|
+
await new Promise((resolve) => {
|
|
5536
|
+
race(context.afterTokenRefresh$, timer(takeoverRefreshMaxLockHoldMs))
|
|
5537
|
+
.pipe(take(1), takeUntilDestroyed(destroyRef))
|
|
5538
|
+
.subscribe({ next: () => resolve(), complete: () => resolve() });
|
|
5539
|
+
});
|
|
5540
|
+
};
|
|
5541
|
+
/**
|
|
5542
|
+
* Spends the refresh token in this tab, after the leader failed to. Guarded by a lock of its own
|
|
5543
|
+
* rather than by the leadership, which the unresponsive tab still holds: every follower runs out of
|
|
5544
|
+
* patience at the same instant, and a single-use refresh token must not be spent twice.
|
|
5545
|
+
*
|
|
5546
|
+
* `ifAvailable` makes it a try-lock, so the tab that does not get the lock stands down instead of
|
|
5547
|
+
* queueing for a turn it must no longer take - the winner's new token pair arrives over the sync
|
|
5548
|
+
* channel long before then.
|
|
5549
|
+
*/
|
|
5550
|
+
const takeOverRefresh = async (tokenAtRequest) => {
|
|
5551
|
+
if (context.accessToken() !== tokenAtRequest)
|
|
5552
|
+
return;
|
|
5553
|
+
const locks = typeof navigator === 'undefined' ? undefined : navigator.locks;
|
|
5554
|
+
if (!locks) {
|
|
5555
|
+
await runTakenOverRefresh();
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
await locks.request(refreshLockName(context.name), { ifAvailable: true }, async (lock) => {
|
|
5559
|
+
if (!lock) {
|
|
5560
|
+
// Another tab is spending the refresh token right now. Start the ladder over rather than race
|
|
5561
|
+
// it: this tab hears about the pair it issues, and needs a way back if it never issues one.
|
|
5562
|
+
delegatedRefreshAttempts = 0;
|
|
5563
|
+
return;
|
|
5564
|
+
}
|
|
5565
|
+
if (context.accessToken() !== tokenAtRequest)
|
|
5566
|
+
return;
|
|
5567
|
+
await runTakenOverRefresh();
|
|
5568
|
+
});
|
|
5569
|
+
};
|
|
5115
5570
|
/**
|
|
5116
5571
|
* How long a scheduled refresh that did not spend the refresh token waits before trying again.
|
|
5117
5572
|
* `null` re-arms nothing, because only a new token pair can change the outcome - and that restarts
|
|
@@ -5126,11 +5581,14 @@ const withRefreshQuery = (key, config) => {
|
|
|
5126
5581
|
return Math.max(minRefreshInterval - (Date.now() - lastRefreshTime), rescheduleDelayMs);
|
|
5127
5582
|
case 'busy':
|
|
5128
5583
|
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.
|
|
5131
5584
|
case 'delegated':
|
|
5132
|
-
case 'notLeader':
|
|
5133
5585
|
return minRefreshInterval;
|
|
5586
|
+
// Left to the leader's own timer, which is due at the same instant as this one. Coming back at
|
|
5587
|
+
// the staleness deadline instead of on a fixed interval is what makes the wait bounded: a fixed
|
|
5588
|
+
// interval spends `maxRescheduleAttempts` within minutes of a token that can live for hours, and
|
|
5589
|
+
// the tab is then left with no armed timer at all.
|
|
5590
|
+
case 'notLeader':
|
|
5591
|
+
return Math.min(Math.max(msUntilAccessTokenIsStale() ?? minRefreshInterval, rescheduleDelayMs), maxTimerDelayMs);
|
|
5134
5592
|
}
|
|
5135
5593
|
};
|
|
5136
5594
|
const calculateRefreshBuffer = (tokenLifetimeMs) => {
|
|
@@ -5143,8 +5601,8 @@ const withRefreshQuery = (key, config) => {
|
|
|
5143
5601
|
const calculatedBuffer = tokenLifetimeMs * (1 - percentage);
|
|
5144
5602
|
return Math.max(minBufferMs, Math.min(maxBufferMs, calculatedBuffer));
|
|
5145
5603
|
};
|
|
5146
|
-
/** When this
|
|
5147
|
-
const
|
|
5604
|
+
/** When this token expires, as a timestamp, or `null` if the expiry cannot be read off it. */
|
|
5605
|
+
const tokenExpiresAt = (token) => {
|
|
5148
5606
|
try {
|
|
5149
5607
|
const decoded = context.bearerDecryptFn ? context.bearerDecryptFn(token) : decryptBearer(token);
|
|
5150
5608
|
const bearerDataValue = patchQueryDevtoolsTokenPayload({
|
|
@@ -5159,29 +5617,61 @@ const withRefreshQuery = (key, config) => {
|
|
|
5159
5617
|
}
|
|
5160
5618
|
return null;
|
|
5161
5619
|
}
|
|
5162
|
-
|
|
5163
|
-
if (tokenLifetimeMs <= 0) {
|
|
5164
|
-
if (!refreshIfExpired)
|
|
5165
|
-
return null;
|
|
5166
|
-
if (isDevMode()) {
|
|
5167
|
-
console.warn('Token is already expired, triggering immediate refresh');
|
|
5168
|
-
}
|
|
5169
|
-
return 0;
|
|
5170
|
-
}
|
|
5171
|
-
return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
|
|
5620
|
+
return expiresIn * 1000;
|
|
5172
5621
|
}
|
|
5173
5622
|
catch {
|
|
5174
5623
|
return null;
|
|
5175
5624
|
}
|
|
5176
5625
|
};
|
|
5626
|
+
/** When this access token is due to be refreshed, or `null` if it cannot be scheduled at all. */
|
|
5627
|
+
const scheduledRefreshDelay = (token) => {
|
|
5628
|
+
const expiresAt = tokenExpiresAt(token);
|
|
5629
|
+
if (expiresAt === null)
|
|
5630
|
+
return null;
|
|
5631
|
+
const tokenLifetimeMs = expiresAt - Date.now();
|
|
5632
|
+
if (tokenLifetimeMs <= 0) {
|
|
5633
|
+
if (!refreshIfExpired)
|
|
5634
|
+
return null;
|
|
5635
|
+
if (isDevMode()) {
|
|
5636
|
+
console.warn('Token is already expired, triggering immediate refresh');
|
|
5637
|
+
}
|
|
5638
|
+
return 0;
|
|
5639
|
+
}
|
|
5640
|
+
return Math.max(tokenLifetimeMs - calculateRefreshBuffer(tokenLifetimeMs), 0);
|
|
5641
|
+
};
|
|
5642
|
+
/**
|
|
5643
|
+
* How long this tab can still work with the access token it holds, or `null` without a readable
|
|
5644
|
+
* expiry. Zero or less means a refresh has to happen now, whichever tab does it.
|
|
5645
|
+
*/
|
|
5646
|
+
const msUntilAccessTokenIsStale = () => {
|
|
5647
|
+
const token = context.accessToken();
|
|
5648
|
+
if (!token)
|
|
5649
|
+
return null;
|
|
5650
|
+
const expiresAt = tokenExpiresAt(token);
|
|
5651
|
+
return expiresAt === null ? null : expiresAt - followerTakeoverGraceMs - Date.now();
|
|
5652
|
+
};
|
|
5653
|
+
const isAccessTokenStale = () => {
|
|
5654
|
+
const remainingMs = msUntilAccessTokenIsStale();
|
|
5655
|
+
return remainingMs !== null && remainingMs <= 0;
|
|
5656
|
+
};
|
|
5177
5657
|
/**
|
|
5178
5658
|
* The scheduled refresh, re-armed for as long as it keeps declining to spend the refresh token.
|
|
5179
5659
|
* Without the re-arm a single declined attempt strands the session on a token nothing renews until
|
|
5180
5660
|
* a request fails with a 401 - by which time the refresh token may be gone too.
|
|
5181
5661
|
*/
|
|
5182
5662
|
const scheduledRefresh = (dueInMs, attemptsLeft) => (dueInMs <= 0 ? of(0) : timer(dueInMs)).pipe(concatMap(() => {
|
|
5183
|
-
const
|
|
5184
|
-
|
|
5663
|
+
const attempt = executeRefresh('scheduled');
|
|
5664
|
+
const retryInMs = retryScheduledIn(attempt);
|
|
5665
|
+
if (retryInMs === null)
|
|
5666
|
+
return EMPTY;
|
|
5667
|
+
// Waiting on another tab does not spend the budget. Both outcomes come back at a deadline
|
|
5668
|
+
// rather than on a fixed interval, and both end the moment a token pair arrives, because a
|
|
5669
|
+
// new token restarts the schedule. Spending the budget on them leaves a follower whose
|
|
5670
|
+
// leader never acts with no armed timer at all, which is the one state nothing recovers
|
|
5671
|
+
// from: the tab then holds a dead token and cannot even ask for a new one.
|
|
5672
|
+
if (attempt === 'notLeader' || attempt === 'delegated')
|
|
5673
|
+
return scheduledRefresh(retryInMs, attemptsLeft);
|
|
5674
|
+
return attemptsLeft <= 0 ? EMPTY : scheduledRefresh(retryInMs, attemptsLeft - 1);
|
|
5185
5675
|
}));
|
|
5186
5676
|
// A backgrounded tab's `timer()` does not fire on time - the browser throttles it, and a frozen
|
|
5187
5677
|
// page stops running it altogether while keeping the Web Lock that makes it the leader. So the
|
|
@@ -5212,6 +5702,9 @@ const withRefreshQuery = (key, config) => {
|
|
|
5212
5702
|
.pipe(switchMap(({ dueInMs }) => (dueInMs === null ? EMPTY : scheduledRefresh(dueInMs, maxRescheduleAttempts))), takeUntilDestroyed())
|
|
5213
5703
|
.subscribe();
|
|
5214
5704
|
context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
|
|
5705
|
+
context.refreshCoordination?.starts$.pipe(takeUntilDestroyed()).subscribe(() => {
|
|
5706
|
+
hasLeaderAnsweredDelegation = true;
|
|
5707
|
+
});
|
|
5215
5708
|
const onRefreshFailure = config.onRefreshFailure ??
|
|
5216
5709
|
(({ error, logout }) => {
|
|
5217
5710
|
if (!retryableStatusCodes.includes(error.code))
|
|
@@ -5233,6 +5726,7 @@ const withRefreshQuery = (key, config) => {
|
|
|
5233
5726
|
if (event.type === 'request-success' && event.isSecure) {
|
|
5234
5727
|
unauthorizedRefreshStreak = 0;
|
|
5235
5728
|
delegatedRefreshAttempts = 0;
|
|
5729
|
+
hasLeaderAnsweredDelegation = false;
|
|
5236
5730
|
return;
|
|
5237
5731
|
}
|
|
5238
5732
|
if (event.type !== 'request-error' || !event.isSecure || event.error?.status !== 401)
|
|
@@ -5405,9 +5899,16 @@ const leaderLockName = (name) => `${LEADER_LOCK_NAMESPACE}:${leaderLockKey(name)
|
|
|
5405
5899
|
/**
|
|
5406
5900
|
* Web Locks has no "someone joined" event, so tabs announce themselves on this channel. Two messages
|
|
5407
5901
|
* per tab lifetime, in place of the heartbeat this used to run once a second. It also carries a
|
|
5408
|
-
* follower's request for a refresh
|
|
5902
|
+
* follower's request for a refresh and the leader's answer that one started, and the two messages the
|
|
5903
|
+
* leadership moves to a visible tab with.
|
|
5409
5904
|
*/
|
|
5410
5905
|
const presenceChannelName = (name) => `ethlete-auth-leader:${name}`;
|
|
5906
|
+
/**
|
|
5907
|
+
* How long a visible tab waits for the leader to say it is still running before it takes the lock
|
|
5908
|
+
* over. Long enough for a same-origin channel message and its answer on a busy main thread, short
|
|
5909
|
+
* enough that the user does not sit in front of a session nothing can refresh.
|
|
5910
|
+
*/
|
|
5911
|
+
const leaderClaimTimeoutMs = 1500;
|
|
5411
5912
|
const countLeaderRequests = (snapshot, lockName) => {
|
|
5412
5913
|
const isLeaderLock = (info) => info.name === lockName;
|
|
5413
5914
|
// Held plus pending is exactly the tab count: every tab requests this one lock, one gets it and the
|
|
@@ -5418,9 +5919,22 @@ const countLeaderRequests = (snapshot, lockName) => {
|
|
|
5418
5919
|
* Elects the one tab that refreshes the session's tokens, over the
|
|
5419
5920
|
* [Web Locks API](https://developer.mozilla.org/docs/Web/API/Web_Locks_API).
|
|
5420
5921
|
*
|
|
5421
|
-
* The lock does the
|
|
5422
|
-
*
|
|
5423
|
-
*
|
|
5922
|
+
* The lock does most of the job: requests queue FIFO, so a tab that closes, crashes or navigates away
|
|
5923
|
+
* has its lock released by the platform and the next tab in line takes over. No heartbeat, no timeout
|
|
5924
|
+
* to tune, and no window in which two tabs both believe they are the leader.
|
|
5925
|
+
*
|
|
5926
|
+
* What the queue cannot decide is *which* tab should lead. Its order is the order the tabs opened in,
|
|
5927
|
+
* which says nothing about which of them is awake: a hidden tab has its timers throttled to about
|
|
5928
|
+
* once a minute and a frozen one runs none at all, so a session led from one is a session nothing
|
|
5929
|
+
* refreshes, while the tab the user is looking at sits queued behind it. So the leadership follows
|
|
5930
|
+
* the user instead:
|
|
5931
|
+
*
|
|
5932
|
+
* - A tab that becomes hidden while leading gives the lock up and asks again, which puts it behind
|
|
5933
|
+
* whoever else wants it - and hands it straight back if nobody does.
|
|
5934
|
+
* - A tab that becomes visible without leading claims the leadership, and every hidden tab gives way
|
|
5935
|
+
* to it. This is what brings the leadership back to a tab the user returns to.
|
|
5936
|
+
* - A leader that answers neither - it is frozen, or the machine it runs on is asleep - has the lock
|
|
5937
|
+
* taken off it, because nothing it does not run can hand it over.
|
|
5424
5938
|
*
|
|
5425
5939
|
* Degrades to "this tab is the leader" without Web Locks - the same single-tab behavior the previous
|
|
5426
5940
|
* `localStorage` implementation fell back to, and the only safe default: a session that refreshes its
|
|
@@ -5430,10 +5944,20 @@ const setupLeaderElection = (options) => {
|
|
|
5430
5944
|
const destroyRef = inject(DestroyRef);
|
|
5431
5945
|
const lockName = leaderLockName(options.name);
|
|
5432
5946
|
const lockManager = createQueryKeyLockManager(LEADER_LOCK_NAMESPACE);
|
|
5433
|
-
const hold = lockManager.hold(leaderLockKey(options.name));
|
|
5434
5947
|
const instanceCount = signal(1, /* @ts-ignore */
|
|
5435
5948
|
...(ngDevMode ? [{ debugName: "instanceCount" }] : /* istanbul ignore next */ []));
|
|
5436
5949
|
const refreshRequests = new Subject();
|
|
5950
|
+
const refreshStarts = new Subject();
|
|
5951
|
+
let isDestroyed = false;
|
|
5952
|
+
let isParticipating = true;
|
|
5953
|
+
const requestLeadership = (requestOptions) => lockManager.hold(leaderLockKey(options.name), {
|
|
5954
|
+
steal: requestOptions?.steal,
|
|
5955
|
+
onLost: () => onLeadershipTaken(),
|
|
5956
|
+
});
|
|
5957
|
+
const hold = signal(requestLeadership(), /* @ts-ignore */
|
|
5958
|
+
...(ngDevMode ? [{ debugName: "hold" }] : /* istanbul ignore next */ []));
|
|
5959
|
+
const isLeader = computed(() => hold().isHolder(), /* @ts-ignore */
|
|
5960
|
+
...(ngDevMode ? [{ debugName: "isLeader" }] : /* istanbul ignore next */ []));
|
|
5437
5961
|
const noop = () => {
|
|
5438
5962
|
/* nothing was set up */
|
|
5439
5963
|
};
|
|
@@ -5441,16 +5965,18 @@ const setupLeaderElection = (options) => {
|
|
|
5441
5965
|
// and asks nobody else for a refresh.
|
|
5442
5966
|
if (!lockManager.isSupported) {
|
|
5443
5967
|
return {
|
|
5444
|
-
isLeader
|
|
5968
|
+
isLeader,
|
|
5445
5969
|
instanceCount: instanceCount.asReadonly(),
|
|
5446
5970
|
isSupported: false,
|
|
5447
5971
|
requestRefresh: noop,
|
|
5448
5972
|
refreshRequests$: refreshRequests.asObservable(),
|
|
5973
|
+
announceRefreshStart: noop,
|
|
5974
|
+
refreshStarts$: refreshStarts.asObservable(),
|
|
5449
5975
|
cleanup: noop,
|
|
5450
5976
|
};
|
|
5451
5977
|
}
|
|
5452
5978
|
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(presenceChannelName(options.name));
|
|
5453
|
-
|
|
5979
|
+
const isPageVisible = () => typeof document === 'undefined' || document.visibilityState === 'visible';
|
|
5454
5980
|
const recount = async () => {
|
|
5455
5981
|
const snapshot = await navigator.locks.query();
|
|
5456
5982
|
if (isDestroyed)
|
|
@@ -5459,8 +5985,65 @@ const setupLeaderElection = (options) => {
|
|
|
5459
5985
|
// count of zero would say the app is not running in the tab reading it.
|
|
5460
5986
|
instanceCount.set(Math.max(countLeaderRequests(snapshot, lockName), 1));
|
|
5461
5987
|
};
|
|
5462
|
-
const
|
|
5463
|
-
const
|
|
5988
|
+
const post = (message) => channel?.postMessage(message);
|
|
5989
|
+
const announce = () => post({ type: 'presence' });
|
|
5990
|
+
const requestRefresh = () => post({ type: 'refresh-requested' });
|
|
5991
|
+
const announceRefreshStart = () => post({ type: 'refresh-started' });
|
|
5992
|
+
/** Set while this tab has claimed the leadership and is waiting to hear whether the leader is alive. */
|
|
5993
|
+
let claimTimeout;
|
|
5994
|
+
const cancelClaim = () => {
|
|
5995
|
+
clearTimeout(claimTimeout);
|
|
5996
|
+
claimTimeout = undefined;
|
|
5997
|
+
};
|
|
5998
|
+
/** Asks for the lock again, which puts this tab behind every tab already waiting for it. */
|
|
5999
|
+
const requeue = (requestOptions) => {
|
|
6000
|
+
hold().release();
|
|
6001
|
+
hold.set(requestLeadership(requestOptions));
|
|
6002
|
+
};
|
|
6003
|
+
/**
|
|
6004
|
+
* Steps aside for the tabs that want the leadership. What a hidden tab does: its timers are
|
|
6005
|
+
* throttled to about once a minute, so the refresh it is responsible for happens roughly whenever.
|
|
6006
|
+
* It stays in the election, and gets the lock straight back if no other tab wants it.
|
|
6007
|
+
*/
|
|
6008
|
+
const yieldLeadership = () => {
|
|
6009
|
+
if (isDestroyed || !isParticipating)
|
|
6010
|
+
return;
|
|
6011
|
+
requeue();
|
|
6012
|
+
void recount();
|
|
6013
|
+
};
|
|
6014
|
+
/**
|
|
6015
|
+
* Takes the lock off the tab holding it, once asking has not worked. The last resort of
|
|
6016
|
+
* {@link claimLeadership}: a frozen leader keeps its lock, answers nothing and refreshes nothing,
|
|
6017
|
+
* and a handover between hidden tabs can land the lock on the next hidden tab in the queue.
|
|
6018
|
+
*/
|
|
6019
|
+
const takeLeadershipOver = () => {
|
|
6020
|
+
cancelClaim();
|
|
6021
|
+
if (isDestroyed || !isParticipating || isLeader())
|
|
6022
|
+
return;
|
|
6023
|
+
requeue({ steal: true });
|
|
6024
|
+
announce();
|
|
6025
|
+
void recount();
|
|
6026
|
+
};
|
|
6027
|
+
/**
|
|
6028
|
+
* Asks the tabs ahead of this one to give the leadership up, because the user is looking at this
|
|
6029
|
+
* one. Hidden tabs give way on their own; a leader that says nothing at all has the lock taken off
|
|
6030
|
+
* it once the wait runs out.
|
|
6031
|
+
*/
|
|
6032
|
+
const claimLeadership = () => {
|
|
6033
|
+
if (isDestroyed || !isParticipating || !channel || isLeader() || !isPageVisible())
|
|
6034
|
+
return;
|
|
6035
|
+
if (claimTimeout !== undefined)
|
|
6036
|
+
return;
|
|
6037
|
+
claimTimeout = setTimeout(takeLeadershipOver, leaderClaimTimeoutMs);
|
|
6038
|
+
post({ type: 'claim' });
|
|
6039
|
+
};
|
|
6040
|
+
/** Rejoins the queue after another tab took the lock over, rather than leaving the election. */
|
|
6041
|
+
const onLeadershipTaken = () => {
|
|
6042
|
+
if (isDestroyed || !isParticipating)
|
|
6043
|
+
return;
|
|
6044
|
+
hold.set(requestLeadership());
|
|
6045
|
+
void recount();
|
|
6046
|
+
};
|
|
5464
6047
|
if (channel) {
|
|
5465
6048
|
channel.onmessage = (event) => {
|
|
5466
6049
|
const message = event.data;
|
|
@@ -5468,39 +6051,134 @@ const setupLeaderElection = (options) => {
|
|
|
5468
6051
|
void recount();
|
|
5469
6052
|
return;
|
|
5470
6053
|
}
|
|
6054
|
+
if (message?.type === 'refresh-started') {
|
|
6055
|
+
refreshStarts.next();
|
|
6056
|
+
return;
|
|
6057
|
+
}
|
|
6058
|
+
if (message?.type === 'claim') {
|
|
6059
|
+
// The answer goes out before the handover, and from the leader alone: it is the one thing that
|
|
6060
|
+
// tells the claiming tab that a tab which is not answering *is* a tab that is not running.
|
|
6061
|
+
if (isLeader())
|
|
6062
|
+
post({ type: 'leader-alive', isVisible: isPageVisible() });
|
|
6063
|
+
// Every hidden tab gives way, not only the leader: a claim that only moved the lock one place
|
|
6064
|
+
// along would land it on the next hidden tab in the queue.
|
|
6065
|
+
if (!isPageVisible())
|
|
6066
|
+
yieldLeadership();
|
|
6067
|
+
return;
|
|
6068
|
+
}
|
|
6069
|
+
if (message?.type === 'leader-alive') {
|
|
6070
|
+
// One visible tab leading is as good as another, so a claim against one is over. A hidden
|
|
6071
|
+
// leader is giving way instead, and the claim stays armed until it has: the release hands the
|
|
6072
|
+
// lock to the first tab in the queue, which may be another hidden one.
|
|
6073
|
+
if (message.isVisible)
|
|
6074
|
+
cancelClaim();
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
5471
6077
|
// Every tab hears the request; only the one that may spend the refresh token acts on it.
|
|
5472
|
-
if (message?.type === 'refresh-requested' &&
|
|
6078
|
+
if (message?.type === 'refresh-requested' && isLeader()) {
|
|
5473
6079
|
refreshRequests.next();
|
|
5474
6080
|
}
|
|
5475
6081
|
};
|
|
5476
6082
|
}
|
|
5477
6083
|
effect(() => {
|
|
5478
|
-
if (!
|
|
6084
|
+
if (!isLeader())
|
|
5479
6085
|
return;
|
|
6086
|
+
cancelClaim();
|
|
5480
6087
|
// Becoming the leader means the previous one went away. A tab that closed normally announced it
|
|
5481
6088
|
// itself, one that crashed did not - so this is the moment to tell the others for free.
|
|
5482
6089
|
announce();
|
|
5483
6090
|
void recount();
|
|
5484
6091
|
});
|
|
6092
|
+
/**
|
|
6093
|
+
* Leaves the election, because this tab is about to stop running. A frozen page keeps its lock while
|
|
6094
|
+
* its timers do not fire, so a leader that stays in would be a leader that refreshes nothing, and
|
|
6095
|
+
* every other tab would sit queued behind it until the user came back. The lock is not requested
|
|
6096
|
+
* again here: the platform would grant it to this page just as well while it is frozen.
|
|
6097
|
+
*/
|
|
6098
|
+
const leaveElection = () => {
|
|
6099
|
+
if (isDestroyed || !isParticipating)
|
|
6100
|
+
return;
|
|
6101
|
+
isParticipating = false;
|
|
6102
|
+
cancelClaim();
|
|
6103
|
+
// Release before the goodbye: the tabs that recount on it should no longer see this one queued.
|
|
6104
|
+
hold().release();
|
|
6105
|
+
announce();
|
|
6106
|
+
};
|
|
6107
|
+
/** Takes part again, behind whichever tab took the leadership over in the meantime. */
|
|
6108
|
+
const rejoinElection = () => {
|
|
6109
|
+
if (isDestroyed || isParticipating)
|
|
6110
|
+
return;
|
|
6111
|
+
isParticipating = true;
|
|
6112
|
+
hold.set(requestLeadership());
|
|
6113
|
+
announce();
|
|
6114
|
+
void recount();
|
|
6115
|
+
// A page resumes because the user came back to it, so it is also the tab that should be leading.
|
|
6116
|
+
claimLeadership();
|
|
6117
|
+
};
|
|
6118
|
+
const isRestoredFromCache = (event) => event.persisted === true;
|
|
6119
|
+
const onFreeze = () => leaveElection();
|
|
6120
|
+
const onResume = () => rejoinElection();
|
|
6121
|
+
const onPageHide = (event) => {
|
|
6122
|
+
if (isRestoredFromCache(event))
|
|
6123
|
+
leaveElection();
|
|
6124
|
+
};
|
|
6125
|
+
const onPageShow = (event) => {
|
|
6126
|
+
if (isRestoredFromCache(event))
|
|
6127
|
+
rejoinElection();
|
|
6128
|
+
};
|
|
6129
|
+
const onVisibilityChange = () => {
|
|
6130
|
+
if (isPageVisible()) {
|
|
6131
|
+
claimLeadership();
|
|
6132
|
+
return;
|
|
6133
|
+
}
|
|
6134
|
+
if (isLeader())
|
|
6135
|
+
yieldLeadership();
|
|
6136
|
+
};
|
|
6137
|
+
// `freeze`/`resume` is Chromium's; `pagehide`/`pageshow` with `persisted` is how a page enters and
|
|
6138
|
+
// leaves the back/forward cache, which stops it just as dead in browsers that fire no `freeze`.
|
|
6139
|
+
if (typeof document !== 'undefined') {
|
|
6140
|
+
document.addEventListener('freeze', onFreeze);
|
|
6141
|
+
document.addEventListener('resume', onResume);
|
|
6142
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
6143
|
+
}
|
|
6144
|
+
if (typeof window !== 'undefined') {
|
|
6145
|
+
window.addEventListener('pagehide', onPageHide);
|
|
6146
|
+
window.addEventListener('pageshow', onPageShow);
|
|
6147
|
+
}
|
|
5485
6148
|
announce();
|
|
5486
6149
|
void recount();
|
|
6150
|
+
// A tab that opens while another one sleeps must not wait for a visibility change that already
|
|
6151
|
+
// happened. The claim costs one message and settles itself when the leader answers.
|
|
6152
|
+
claimLeadership();
|
|
5487
6153
|
const cleanup = () => {
|
|
5488
6154
|
if (isDestroyed)
|
|
5489
6155
|
return;
|
|
5490
6156
|
isDestroyed = true;
|
|
5491
|
-
|
|
5492
|
-
|
|
6157
|
+
cancelClaim();
|
|
6158
|
+
if (typeof document !== 'undefined') {
|
|
6159
|
+
document.removeEventListener('freeze', onFreeze);
|
|
6160
|
+
document.removeEventListener('resume', onResume);
|
|
6161
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
6162
|
+
}
|
|
6163
|
+
if (typeof window !== 'undefined') {
|
|
6164
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
6165
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
6166
|
+
}
|
|
6167
|
+
hold().release();
|
|
5493
6168
|
announce();
|
|
5494
6169
|
channel?.close();
|
|
5495
6170
|
refreshRequests.complete();
|
|
6171
|
+
refreshStarts.complete();
|
|
5496
6172
|
};
|
|
5497
6173
|
destroyRef.onDestroy(cleanup);
|
|
5498
6174
|
return {
|
|
5499
|
-
isLeader
|
|
6175
|
+
isLeader,
|
|
5500
6176
|
instanceCount: instanceCount.asReadonly(),
|
|
5501
6177
|
isSupported: true,
|
|
5502
6178
|
requestRefresh,
|
|
5503
6179
|
refreshRequests$: refreshRequests.asObservable(),
|
|
6180
|
+
announceRefreshStart,
|
|
6181
|
+
refreshStarts$: refreshStarts.asObservable(),
|
|
5504
6182
|
cleanup,
|
|
5505
6183
|
};
|
|
5506
6184
|
};
|
|
@@ -5676,10 +6354,38 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5676
6354
|
settleAdoption();
|
|
5677
6355
|
};
|
|
5678
6356
|
let adoptionTimeout;
|
|
6357
|
+
const requestState = () => channel.postMessage({ type: 'state-request' });
|
|
6358
|
+
/**
|
|
6359
|
+
* A page that was frozen or held in the back/forward cache ran nothing while it was away, so the
|
|
6360
|
+
* token pair it wakes up with is whatever it held when it stopped - the rotations it missed were
|
|
6361
|
+
* broadcast to a tab that was not listening. Asking again is what stops it spending a refresh token
|
|
6362
|
+
* that was already spent, which the server answers with a 401 and the provider with a logout for
|
|
6363
|
+
* every tab.
|
|
6364
|
+
*/
|
|
6365
|
+
const resyncAfterResume = () => {
|
|
6366
|
+
if (!syncTokens || !context.accessToken())
|
|
6367
|
+
return;
|
|
6368
|
+
requestState();
|
|
6369
|
+
};
|
|
6370
|
+
const onResume = () => resyncAfterResume();
|
|
6371
|
+
const onPageShow = (event) => {
|
|
6372
|
+
if (event.persisted === true)
|
|
6373
|
+
resyncAfterResume();
|
|
6374
|
+
};
|
|
6375
|
+
if (typeof document !== 'undefined') {
|
|
6376
|
+
document.addEventListener('resume', onResume);
|
|
6377
|
+
}
|
|
6378
|
+
if (typeof window !== 'undefined') {
|
|
6379
|
+
window.addEventListener('pageshow', onPageShow);
|
|
6380
|
+
}
|
|
5679
6381
|
const cleanup = () => {
|
|
5680
6382
|
clearTimeout(adoptionTimeout);
|
|
5681
6383
|
settleAdoption();
|
|
5682
6384
|
remoteActivity.complete();
|
|
6385
|
+
if (typeof document !== 'undefined')
|
|
6386
|
+
document.removeEventListener('resume', onResume);
|
|
6387
|
+
if (typeof window !== 'undefined')
|
|
6388
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
5683
6389
|
channel.close();
|
|
5684
6390
|
};
|
|
5685
6391
|
destroyRef.onDestroy(cleanup);
|
|
@@ -5697,7 +6403,7 @@ const setupMultiTabSync = (config, context) => {
|
|
|
5697
6403
|
};
|
|
5698
6404
|
});
|
|
5699
6405
|
adoptionTimeout = setTimeout(settleAdoption, sessionAdoptionTimeoutMs);
|
|
5700
|
-
|
|
6406
|
+
requestState();
|
|
5701
6407
|
return { isPending: () => isAdoptionPending, settled };
|
|
5702
6408
|
})()
|
|
5703
6409
|
: undefined;
|
|
@@ -5755,7 +6461,8 @@ const SINGLE_TAB = /* @__PURE__ */ (() => {
|
|
|
5755
6461
|
/**
|
|
5756
6462
|
* Keeps one browser session consistent across the user's tabs: a login in one tab logs the others in,
|
|
5757
6463
|
* a logout logs them out, and only one tab (the leader) performs the automatic token refresh so a
|
|
5758
|
-
* single-use refresh token is not spent twice.
|
|
6464
|
+
* single-use refresh token is not spent twice. The leadership follows the tab the user is looking at,
|
|
6465
|
+
* because a hidden tab has its timers throttled and a frozen one runs none at all.
|
|
5759
6466
|
*
|
|
5760
6467
|
* Off by default - a single-tab app, a kiosk or an embedded webview ships neither the
|
|
5761
6468
|
* `BroadcastChannel` sync nor the Web Locks leader election without it. Degrades to "this tab is the
|
|
@@ -5820,7 +6527,12 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5820
6527
|
return {
|
|
5821
6528
|
isLeader: () => election.isLeader(),
|
|
5822
6529
|
leaderElection: instance,
|
|
5823
|
-
refreshCoordination: {
|
|
6530
|
+
refreshCoordination: {
|
|
6531
|
+
request: election.requestRefresh,
|
|
6532
|
+
requests$: election.refreshRequests$,
|
|
6533
|
+
announceStart: election.announceRefreshStart,
|
|
6534
|
+
starts$: election.refreshStarts$,
|
|
6535
|
+
},
|
|
5824
6536
|
sessionAdoption: sync.sessionAdoption,
|
|
5825
6537
|
activityCoordination: sync.activityCoordination,
|
|
5826
6538
|
};
|
|
@@ -7466,7 +8178,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
|
|
|
7466
8178
|
|
|
7467
8179
|
// Generated from package.json by tools/scripts/generate-version.js - do not edit.
|
|
7468
8180
|
/** The version of `@ethlete/query` this build was cut from. */
|
|
7469
|
-
const QUERY_VERSION = '6.0.0-next.
|
|
8181
|
+
const QUERY_VERSION = '6.0.0-next.39';
|
|
7470
8182
|
|
|
7471
8183
|
/** How deep a schema is expanded before a nested branch is given up on. */
|
|
7472
8184
|
const MAX_DEPTH = 12;
|
|
@@ -8447,6 +9159,12 @@ const applyQueryDevtoolsTokenTtl = (options) => {
|
|
|
8447
9159
|
* so the list is capped for the same reason the repository caps its unused entries.
|
|
8448
9160
|
*/
|
|
8449
9161
|
const MAX_QUERY_DEVTOOLS_TOMBSTONES = 50;
|
|
9162
|
+
/**
|
|
9163
|
+
* How many of its settled items' queries one batch keeps. Counted per batch rather than against
|
|
9164
|
+
* {@link MAX_QUERY_DEVTOOLS_TOMBSTONES}, so a bulk run cannot evict the tombstones the panel is read
|
|
9165
|
+
* for - and low, because the batch's own entry already holds every item's args, response and error.
|
|
9166
|
+
*/
|
|
9167
|
+
const MAX_QUERY_BATCH_TOMBSTONES = 20;
|
|
8450
9168
|
const frozen = (value) => () => value;
|
|
8451
9169
|
const noop = () => undefined;
|
|
8452
9170
|
/**
|
|
@@ -8668,8 +9386,8 @@ const registerEntry = (registration) => {
|
|
|
8668
9386
|
if (index === -1)
|
|
8669
9387
|
return list;
|
|
8670
9388
|
const next = list.slice();
|
|
8671
|
-
// Only queries leave a tombstone. A stack, sequence, form or auth provider is a container
|
|
8672
|
-
// interesting state is the queries it owns - each of which tombstones on its own.
|
|
9389
|
+
// Only queries leave a tombstone. A stack, sequence, batch, form or auth provider is a container
|
|
9390
|
+
// whose interesting state is the queries it owns - each of which tombstones on its own.
|
|
8673
9391
|
if (fullEntry.kind !== 'query') {
|
|
8674
9392
|
next.splice(index, 1);
|
|
8675
9393
|
return next;
|
|
@@ -8678,16 +9396,39 @@ const registerEntry = (registration) => {
|
|
|
8678
9396
|
return capTombstones(next);
|
|
8679
9397
|
});
|
|
8680
9398
|
};
|
|
8681
|
-
/**
|
|
9399
|
+
/** The oldest `count` entries of a bucket, which is what a cap drops when the bucket is over it. */
|
|
9400
|
+
const oldestOver = (bucket, cap) => bucket.length <= cap
|
|
9401
|
+
? []
|
|
9402
|
+
: bucket.sort((a, b) => (a.destroyedAt ?? 0) - (b.destroyedAt ?? 0)).slice(0, bucket.length - cap);
|
|
9403
|
+
/**
|
|
9404
|
+
* Caps the tombstones the registry holds. A batch's items are counted per batch rather than against the
|
|
9405
|
+
* shared budget: a batch destroys one query per item as it settles, so a single 500-item run would
|
|
9406
|
+
* otherwise fill the whole buffer and evict every tombstone the panel is actually read for - the `401`
|
|
9407
|
+
* that took its component down with it. Each batch keeps its own recent tail instead.
|
|
9408
|
+
*/
|
|
8682
9409
|
const capTombstones = (list) => {
|
|
8683
|
-
const
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
9410
|
+
const shared = [];
|
|
9411
|
+
const perBatch = new Map();
|
|
9412
|
+
for (const entry of list) {
|
|
9413
|
+
if (!entry.destroyedAt)
|
|
9414
|
+
continue;
|
|
9415
|
+
const batch = entry.meta.batch;
|
|
9416
|
+
if (!batch) {
|
|
9417
|
+
shared.push(entry);
|
|
9418
|
+
continue;
|
|
9419
|
+
}
|
|
9420
|
+
const bucket = perBatch.get(batch);
|
|
9421
|
+
if (bucket)
|
|
9422
|
+
bucket.push(entry);
|
|
9423
|
+
else
|
|
9424
|
+
perBatch.set(batch, [entry]);
|
|
9425
|
+
}
|
|
9426
|
+
const doomed = new Set(oldestOver(shared, MAX_QUERY_DEVTOOLS_TOMBSTONES));
|
|
9427
|
+
for (const bucket of perBatch.values()) {
|
|
9428
|
+
for (const entry of oldestOver(bucket, MAX_QUERY_BATCH_TOMBSTONES))
|
|
9429
|
+
doomed.add(entry);
|
|
9430
|
+
}
|
|
9431
|
+
return doomed.size ? list.filter((e) => !doomed.has(e)) : list;
|
|
8691
9432
|
};
|
|
8692
9433
|
/**
|
|
8693
9434
|
* Forgets destroyed entries the registry is holding - the panel's "clear gone" action. Pass the ids to
|
|
@@ -13868,8 +14609,8 @@ const defineQueryForm = (config) => {
|
|
|
13868
14609
|
// Feed the URL value into the live model & bound controls; the effect no-ops (model === committed).
|
|
13869
14610
|
model.set(clone(next));
|
|
13870
14611
|
};
|
|
13871
|
-
const applyFromUrl = (params) => {
|
|
13872
|
-
const current = { ...
|
|
14612
|
+
const applyFromUrl = (params, base = committed()) => {
|
|
14613
|
+
const current = { ...base };
|
|
13873
14614
|
let changed = false;
|
|
13874
14615
|
for (const [key, def] of Object.entries(fieldDefs)) {
|
|
13875
14616
|
const raw = params[paramKey(key)];
|
|
@@ -13947,7 +14688,9 @@ const defineQueryForm = (config) => {
|
|
|
13947
14688
|
observeOptions = options;
|
|
13948
14689
|
observing.set(true);
|
|
13949
14690
|
if (options?.syncOnNavigation !== false) {
|
|
13950
|
-
|
|
14691
|
+
// A value written before `observe()` sits in the model only, so the URL merges onto the model
|
|
14692
|
+
// here. Against `committed()` any single URL param would drop that value.
|
|
14693
|
+
applyFromUrl(route.snapshot.queryParams, model());
|
|
13951
14694
|
}
|
|
13952
14695
|
// Commit whatever the model holds now (URL-restored or programmatic defaults).
|
|
13953
14696
|
flush();
|
|
@@ -14271,5 +15014,5 @@ const createWebSocketClient = (options) => {
|
|
|
14271
15014
|
* Generated bundle index. Do not edit.
|
|
14272
15015
|
*/
|
|
14273
15016
|
|
|
14274
|
-
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, generateQueryDevtoolsSampleNumber, 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 };
|
|
15017
|
+
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_BATCH_TOMBSTONES, 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, createQueryBatch, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySubmission, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, currentQueryDevtoolsBatch, 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, generateQueryDevtoolsSampleNumber, 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, queryBatchAlreadyRunning, queryBatchWithArgsUsed, 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, runInQueryDevtoolsBatch, 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 };
|
|
14275
15018
|
//# sourceMappingURL=ethlete-query.mjs.map
|