@ethlete/query 6.0.0-next.28 → 6.0.0-next.29
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 +36 -0
- package/fesm2022/ethlete-query-testing.mjs +2 -1
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +639 -172
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query-testing.d.ts +2 -0
- package/types/ethlete-query.d.ts +276 -22
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, makeEnvironmentProviders, assertInInjectionContext, provideEnvironmentInitializer, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
|
|
3
|
-
import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain,
|
|
3
|
+
import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, getDomain, deleteCookie, setCookie, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
4
4
|
import { filter, Subject, Subscription, defer, throwError, timer, switchMap, tap, retry, catchError, take, of, merge, firstValueFrom, fromEvent, throttleTime, interval, combineLatest, map, Observable, takeUntil, startWith, pairwise, BehaviorSubject, ReplaySubject, shareReplay, skip, takeWhile, EMPTY, withLatestFrom, debounceTime } from 'rxjs';
|
|
5
5
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
6
6
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
@@ -50,7 +50,7 @@ const queryDevtoolsFnDetail = (fn, key) => {
|
|
|
50
50
|
return name ? [{ label: key, value: name }] : [];
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const noop$
|
|
53
|
+
const noop$3 = () => undefined;
|
|
54
54
|
let registrar = null;
|
|
55
55
|
/**
|
|
56
56
|
* Installs the real registry. Called by `provideQueryDevtools()`; nothing else may call it.
|
|
@@ -70,7 +70,7 @@ const isQueryDevtoolsEnabled = () => registrar !== null;
|
|
|
70
70
|
* a no-op callback) unless {@link provideQueryDevtools} has been called.
|
|
71
71
|
* @internal
|
|
72
72
|
*/
|
|
73
|
-
const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$
|
|
73
|
+
const registerQueryDevtoolsEntry = (entry) => registrar?.(entry) ?? noop$3;
|
|
74
74
|
let statsFactory = null;
|
|
75
75
|
/**
|
|
76
76
|
* Installs the stats recorder factory. Called by `provideQueryDevtools()`; nothing else may call it.
|
|
@@ -1023,7 +1023,7 @@ const createBaseQuery = (options) => {
|
|
|
1023
1023
|
if (devtoolsStats) {
|
|
1024
1024
|
const statsSubscription = state.events$.subscribe((event) => {
|
|
1025
1025
|
if (event.type === 'error') {
|
|
1026
|
-
devtoolsStats.recordError({ faulted: event.faulted });
|
|
1026
|
+
devtoolsStats.recordError({ faulted: event.faulted, error: event.error });
|
|
1027
1027
|
}
|
|
1028
1028
|
else if (event.type === HttpEventType.Response) {
|
|
1029
1029
|
devtoolsStats.recordResponse({ headers: event.headers, body: event.body });
|
|
@@ -1085,7 +1085,7 @@ const setupQueryExecuteState = () => {
|
|
|
1085
1085
|
};
|
|
1086
1086
|
};
|
|
1087
1087
|
const queryExecute = (options) => {
|
|
1088
|
-
const { executeOptions, args, executeState, options: runQueryOptions, isSecure } = options;
|
|
1088
|
+
const { executeOptions, args, executeState, options: runQueryOptions, isSecure, isRefreshable } = options;
|
|
1089
1089
|
const { deps, state, creator, creatorInternals, queryConfig } = executeOptions;
|
|
1090
1090
|
const { key, request, executed } = deps.client.repository.request({
|
|
1091
1091
|
route: creatorInternals.route,
|
|
@@ -1098,6 +1098,7 @@ const queryExecute = (options) => {
|
|
|
1098
1098
|
previousKey: executeState.previousKey(),
|
|
1099
1099
|
runQueryOptions,
|
|
1100
1100
|
isSecure,
|
|
1101
|
+
isRefreshable,
|
|
1101
1102
|
silenceUncacheableAllowCacheError: queryConfig.silenceUncacheableAllowCacheError,
|
|
1102
1103
|
});
|
|
1103
1104
|
executeState.previousKey.set(key);
|
|
@@ -1850,8 +1851,13 @@ const createQueryStack = (options) => {
|
|
|
1850
1851
|
removeStrategy === 'newest' ? finalQueries.slice(0, maxQueries) : finalQueries.slice(-maxQueries);
|
|
1851
1852
|
}
|
|
1852
1853
|
queries.set(finalQueries);
|
|
1853
|
-
|
|
1854
|
-
|
|
1854
|
+
// After the eviction, not before: `removeStrategy: 'newest'` destroys the query `appendFn`
|
|
1855
|
+
// just returned, and `lastQuery` must never point at a query that is no longer in the stack.
|
|
1856
|
+
const last = lastAppendedQuery && finalQueries.includes(lastAppendedQuery)
|
|
1857
|
+
? lastAppendedQuery
|
|
1858
|
+
: (finalQueries[finalQueries.length - 1] ?? null);
|
|
1859
|
+
lastQuery.set(last);
|
|
1860
|
+
return last;
|
|
1855
1861
|
}
|
|
1856
1862
|
else {
|
|
1857
1863
|
const keyFn = argsKeyFn ?? ((a) => JSON.stringify(a));
|
|
@@ -2084,7 +2090,7 @@ const createPagedQueryStack = (options) => {
|
|
|
2084
2090
|
...(ngDevMode ? [{ debugName: "minPagination" }] : /* istanbul ignore next */ []));
|
|
2085
2091
|
const isFirstPageLoaded = computed(() => {
|
|
2086
2092
|
const min = minPagination();
|
|
2087
|
-
return min ? loadedMinPage() ===
|
|
2093
|
+
return min ? loadedMinPage() === 1 : false;
|
|
2088
2094
|
}, /* @ts-ignore */
|
|
2089
2095
|
...(ngDevMode ? [{ debugName: "isFirstPageLoaded" }] : /* istanbul ignore next */ []));
|
|
2090
2096
|
const isLastPageLoaded = computed(() => {
|
|
@@ -2164,14 +2170,16 @@ const createPagedQueryStack = (options) => {
|
|
|
2164
2170
|
};
|
|
2165
2171
|
const canFetchPreviousPage = computed(() => {
|
|
2166
2172
|
const currentMinPagination = minPagination();
|
|
2167
|
-
if (!currentMinPagination)
|
|
2173
|
+
if (!currentMinPagination || stack.anyLoading())
|
|
2168
2174
|
return false;
|
|
2169
2175
|
return loadedMinPage() > 1;
|
|
2170
2176
|
}, /* @ts-ignore */
|
|
2171
2177
|
...(ngDevMode ? [{ debugName: "canFetchPreviousPage" }] : /* istanbul ignore next */ []));
|
|
2172
2178
|
const canFetchNextPage = computed(() => {
|
|
2173
2179
|
const currentMaxPagination = maxPagination();
|
|
2174
|
-
|
|
2180
|
+
if (!currentMaxPagination || stack.anyLoading())
|
|
2181
|
+
return false;
|
|
2182
|
+
return loadedMaxPage() < currentMaxPagination.totalPages;
|
|
2175
2183
|
}, /* @ts-ignore */
|
|
2176
2184
|
...(ngDevMode ? [{ debugName: "canFetchNextPage" }] : /* istanbul ignore next */ []));
|
|
2177
2185
|
const items = computed(() => {
|
|
@@ -2891,9 +2899,9 @@ const shouldRetryRequest = (options) => {
|
|
|
2891
2899
|
return { retry: false };
|
|
2892
2900
|
};
|
|
2893
2901
|
|
|
2894
|
-
const noop$
|
|
2902
|
+
const noop$2 = () => undefined;
|
|
2895
2903
|
/** Shared because it never changes: without Web Locks, every hold is granted and never lost. */
|
|
2896
|
-
const alwaysHolder = { isHolder: /* @__PURE__ */ signal(true).asReadonly(), release: noop$
|
|
2904
|
+
const alwaysHolder = { isHolder: /* @__PURE__ */ signal(true).asReadonly(), release: noop$2 };
|
|
2897
2905
|
const noKeyStates = /* @__PURE__ */ signal({}).asReadonly();
|
|
2898
2906
|
const createQueryKeyLockManager = (namespace) => {
|
|
2899
2907
|
const locks = typeof navigator === 'undefined' ? undefined : navigator.locks;
|
|
@@ -3081,10 +3089,10 @@ const unwrapQuerySyncMessage = (data) => {
|
|
|
3081
3089
|
return null;
|
|
3082
3090
|
};
|
|
3083
3091
|
|
|
3084
|
-
const noop = () => undefined;
|
|
3092
|
+
const noop$1 = () => undefined;
|
|
3085
3093
|
const createQuerySyncTransport = (channelName) => {
|
|
3086
3094
|
if (typeof BroadcastChannel === 'undefined') {
|
|
3087
|
-
return { post: noop, listen: () => noop, destroy: noop, isSupported: false };
|
|
3095
|
+
return { post: noop$1, listen: () => noop$1, destroy: noop$1, isSupported: false };
|
|
3088
3096
|
}
|
|
3089
3097
|
const channel = new BroadcastChannel(channelName);
|
|
3090
3098
|
const listeners = new Set();
|
|
@@ -3361,6 +3369,7 @@ const createQueryRepository = (config) => {
|
|
|
3361
3369
|
const shouldCache = creatorOptions?.subtle?.useQueryRepositoryCache === false
|
|
3362
3370
|
? false
|
|
3363
3371
|
: shouldCacheQuery(options.method) || creatorOptions?.subtle?.useQueryRepositoryCache === true;
|
|
3372
|
+
const isRefreshable = shouldCache && (options.isRefreshable ?? shouldCacheQuery(options.method));
|
|
3364
3373
|
if (!shouldCache && options.key)
|
|
3365
3374
|
throw uncacheableRequestHasCacheKeyParam(options.key);
|
|
3366
3375
|
// `allowCache` is never read on the uncacheable path below - there is no cache entry to reuse - so this throw
|
|
@@ -3407,6 +3416,7 @@ const createQueryRepository = (config) => {
|
|
|
3407
3416
|
request: cacheEntry.request,
|
|
3408
3417
|
isSecure,
|
|
3409
3418
|
isCached: true,
|
|
3419
|
+
isRefreshable,
|
|
3410
3420
|
isMultiTabSyncEnabled,
|
|
3411
3421
|
isPersistEnabled,
|
|
3412
3422
|
keepUnusedFor,
|
|
@@ -3435,6 +3445,7 @@ const createQueryRepository = (config) => {
|
|
|
3435
3445
|
request,
|
|
3436
3446
|
isSecure,
|
|
3437
3447
|
isCached: shouldCache,
|
|
3448
|
+
isRefreshable,
|
|
3438
3449
|
isMultiTabSyncEnabled,
|
|
3439
3450
|
isPersistEnabled,
|
|
3440
3451
|
keepUnusedFor,
|
|
@@ -3446,11 +3457,13 @@ const createQueryRepository = (config) => {
|
|
|
3446
3457
|
return { key: trackingKey, request, executed };
|
|
3447
3458
|
};
|
|
3448
3459
|
/** Tears an entry down for good, whether it currently has consumers or not. */
|
|
3449
|
-
const destroyEntry = (key, cacheEntry) => {
|
|
3460
|
+
const destroyEntry = (key, cacheEntry, cause) => {
|
|
3461
|
+
const { method, url } = cacheEntry.request;
|
|
3450
3462
|
clearTimeout(cacheEntry.evictTimer);
|
|
3451
3463
|
cacheEntry.request.destroy();
|
|
3452
3464
|
cacheEntry.eventSubscription?.unsubscribe();
|
|
3453
3465
|
cache.delete(key);
|
|
3466
|
+
eventsSubject.next({ type: 'entry-destroyed', key, isSecure: cacheEntry.isSecure, method, url, cause });
|
|
3454
3467
|
};
|
|
3455
3468
|
const cancelEviction = (cacheEntry) => {
|
|
3456
3469
|
if (cacheEntry.evictTimer === undefined)
|
|
@@ -3466,12 +3479,12 @@ const createQueryRepository = (config) => {
|
|
|
3466
3479
|
return;
|
|
3467
3480
|
unused.sort(([, a], [, b]) => (a.unusedSince ?? 0) - (b.unusedSince ?? 0));
|
|
3468
3481
|
for (const [key, entry] of unused.slice(0, unused.length - MAX_UNUSED_ENTRIES)) {
|
|
3469
|
-
destroyEntry(key, entry);
|
|
3482
|
+
destroyEntry(key, entry, 'unused-cap');
|
|
3470
3483
|
}
|
|
3471
3484
|
};
|
|
3472
3485
|
const retain = (key, cacheEntry) => {
|
|
3473
3486
|
cacheEntry.unusedSince = Date.now();
|
|
3474
|
-
cacheEntry.evictTimer = setTimeout(() => evict(key), cacheEntry.keepUnusedFor);
|
|
3487
|
+
cacheEntry.evictTimer = setTimeout(() => evict(key, 'expired'), cacheEntry.keepUnusedFor);
|
|
3475
3488
|
enforceUnusedEntryLimit();
|
|
3476
3489
|
};
|
|
3477
3490
|
const unbind = (key, consumerDestroyRef) => {
|
|
@@ -3488,7 +3501,7 @@ const createQueryRepository = (config) => {
|
|
|
3488
3501
|
retain(key, cacheEntry);
|
|
3489
3502
|
}
|
|
3490
3503
|
else {
|
|
3491
|
-
destroyEntry(key, cacheEntry);
|
|
3504
|
+
destroyEntry(key, cacheEntry, 'unbind');
|
|
3492
3505
|
}
|
|
3493
3506
|
}
|
|
3494
3507
|
bumpCacheVersion();
|
|
@@ -3501,7 +3514,7 @@ const createQueryRepository = (config) => {
|
|
|
3501
3514
|
// Force the teardown instead of routing through `unbind`: a logged out session must not leave a
|
|
3502
3515
|
// retained response body behind waiting out its `keepUnusedFor` window.
|
|
3503
3516
|
cacheEntry.consumers.clear();
|
|
3504
|
-
destroyEntry(key, cacheEntry);
|
|
3517
|
+
destroyEntry(key, cacheEntry, 'logout');
|
|
3505
3518
|
}
|
|
3506
3519
|
bumpCacheVersion();
|
|
3507
3520
|
eventsSubject.next({ type: 'unbind-all-secure' });
|
|
@@ -3511,10 +3524,11 @@ const createQueryRepository = (config) => {
|
|
|
3511
3524
|
for (const cacheEntry of cache.values()) {
|
|
3512
3525
|
if (cacheEntry.consumers.size === 0)
|
|
3513
3526
|
continue;
|
|
3514
|
-
// Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed
|
|
3515
|
-
//
|
|
3516
|
-
// the cache
|
|
3517
|
-
|
|
3527
|
+
// Re-firing a mutation would be a side effect nobody asked for, so only reads are refreshed:
|
|
3528
|
+
// a cacheable method, or a GQL query transported via POST. Opting into the cache alone does not
|
|
3529
|
+
// qualify - the auth queries do that for a stable cache key, and replaying a login POST would
|
|
3530
|
+
// end the session.
|
|
3531
|
+
if (!cacheEntry.isRefreshable)
|
|
3518
3532
|
continue;
|
|
3519
3533
|
if (filter && !filter(cacheEntry.request))
|
|
3520
3534
|
continue;
|
|
@@ -3548,7 +3562,7 @@ const createQueryRepository = (config) => {
|
|
|
3548
3562
|
return true;
|
|
3549
3563
|
};
|
|
3550
3564
|
const bind = (options) => {
|
|
3551
|
-
const { key, consumerDestroyRef, request, isSecure, isCached, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
|
|
3565
|
+
const { key, consumerDestroyRef, request, isSecure, isCached, isRefreshable, isMultiTabSyncEnabled, isPersistEnabled, keepUnusedFor, } = options;
|
|
3552
3566
|
const destroyListener = consumerDestroyRef.onDestroy(() => unbind(key, consumerDestroyRef));
|
|
3553
3567
|
const cacheEntry = cache.get(key);
|
|
3554
3568
|
if (cacheEntry && isCached) {
|
|
@@ -3585,6 +3599,7 @@ const createQueryRepository = (config) => {
|
|
|
3585
3599
|
eventSubscription,
|
|
3586
3600
|
keepUnusedFor,
|
|
3587
3601
|
isCached,
|
|
3602
|
+
isRefreshable,
|
|
3588
3603
|
isMultiTabSyncEnabled,
|
|
3589
3604
|
isPersistEnabled,
|
|
3590
3605
|
});
|
|
@@ -3598,11 +3613,11 @@ const createQueryRepository = (config) => {
|
|
|
3598
3613
|
isUnused: entry.consumers.size === 0,
|
|
3599
3614
|
request: entry.request,
|
|
3600
3615
|
}));
|
|
3601
|
-
const evict = (key) => {
|
|
3616
|
+
const evict = (key, cause = 'manual') => {
|
|
3602
3617
|
const entry = cache.get(key);
|
|
3603
3618
|
if (!entry)
|
|
3604
3619
|
return;
|
|
3605
|
-
destroyEntry(key, entry);
|
|
3620
|
+
destroyEntry(key, entry, cause);
|
|
3606
3621
|
bumpCacheVersion();
|
|
3607
3622
|
};
|
|
3608
3623
|
const repository = {
|
|
@@ -4295,61 +4310,103 @@ const deriveExecutionStateType = (builder, triggeredBy) => {
|
|
|
4295
4310
|
return 'revocation';
|
|
4296
4311
|
return builder.key;
|
|
4297
4312
|
};
|
|
4298
|
-
const setupBearerQueryRegistry = (builders,
|
|
4313
|
+
const setupBearerQueryRegistry = (builders, context) => {
|
|
4314
|
+
const { injector, latestExecutedQuery, latestNonInternalQuery, applyTokens, executionState, sessionStatus } = context;
|
|
4299
4315
|
const queries = {};
|
|
4300
4316
|
const querySnapshots = new Map();
|
|
4317
|
+
// Supersession is provider-wide, not per key. Within one key the reused query already handles it,
|
|
4318
|
+
// but a token refresh landing while a login is in flight would otherwise apply its tokens over the
|
|
4319
|
+
// login's and report its own outcome as the session's - two writers, two different executions.
|
|
4320
|
+
let latestExecutionId = 0;
|
|
4321
|
+
const currentExecutions = [];
|
|
4301
4322
|
for (const builder of builders) {
|
|
4302
4323
|
const querySnapshot = signal(null, /* @ts-ignore */
|
|
4303
4324
|
...(ngDevMode ? [{ debugName: "querySnapshot" }] : /* istanbul ignore next */ []));
|
|
4304
4325
|
querySnapshots.set(builder.key, querySnapshot);
|
|
4305
4326
|
const extractTokens = builder.config.extractTokens ?? defaultExtractTokens;
|
|
4327
|
+
// A 2xx whose body carries no usable tokens is a failed execution, not a successful one - the
|
|
4328
|
+
// state effect below reads this so it cannot report `success` for an attempt that left the tab
|
|
4329
|
+
// unauthenticated.
|
|
4330
|
+
const extractionError = signal(null, /* @ts-ignore */
|
|
4331
|
+
...(ngDevMode ? [{ debugName: "extractionError" }] : /* istanbul ignore next */ []));
|
|
4332
|
+
const currentExecution = signal(null, /* @ts-ignore */
|
|
4333
|
+
...(ngDevMode ? [{ debugName: "currentExecution" }] : /* istanbul ignore next */ []));
|
|
4334
|
+
currentExecutions.push(currentExecution);
|
|
4335
|
+
const isSuperseded = (execution) => execution.id !== null && execution.id !== latestExecutionId;
|
|
4336
|
+
// Runs before the state effect below, which reads the extraction error this one writes. Angular
|
|
4337
|
+
// runs effects in creation order, so the two must stay in this order.
|
|
4306
4338
|
effect(() => {
|
|
4307
|
-
const
|
|
4308
|
-
if (!
|
|
4339
|
+
const execution = currentExecution();
|
|
4340
|
+
if (!execution)
|
|
4309
4341
|
return;
|
|
4342
|
+
const { snapshot } = execution;
|
|
4310
4343
|
const response = snapshot.response();
|
|
4311
4344
|
const loading = snapshot.loading();
|
|
4312
4345
|
const error = snapshot.error();
|
|
4313
|
-
if (response && !loading && !error) {
|
|
4346
|
+
if (response && !loading && !error && !isSuperseded(execution)) {
|
|
4314
4347
|
try {
|
|
4315
4348
|
const tokens = extractTokens(response);
|
|
4349
|
+
extractionError.set(null);
|
|
4316
4350
|
applyTokens(tokens.accessToken, tokens.refreshToken);
|
|
4317
4351
|
}
|
|
4318
4352
|
catch (extractError) {
|
|
4353
|
+
extractionError.set(createQueryErrorResponse(extractError));
|
|
4319
4354
|
if (isDevMode()) {
|
|
4320
4355
|
console.error(`Failed to extract tokens from ${builder.key} response:`, extractError);
|
|
4321
4356
|
}
|
|
4322
4357
|
}
|
|
4323
4358
|
}
|
|
4324
4359
|
});
|
|
4360
|
+
effect(() => {
|
|
4361
|
+
const execution = currentExecution();
|
|
4362
|
+
if (!execution)
|
|
4363
|
+
return;
|
|
4364
|
+
const { type, snapshot } = execution;
|
|
4365
|
+
const response = snapshot.response();
|
|
4366
|
+
const loading = snapshot.loading();
|
|
4367
|
+
const error = snapshot.error();
|
|
4368
|
+
if (loading || isSuperseded(execution))
|
|
4369
|
+
return;
|
|
4370
|
+
if (error) {
|
|
4371
|
+
executionState.set({ type, state: 'error', error });
|
|
4372
|
+
}
|
|
4373
|
+
else if (response) {
|
|
4374
|
+
const failedExtraction = extractionError();
|
|
4375
|
+
if (failedExtraction) {
|
|
4376
|
+
executionState.set({ type, state: 'error', error: failedExtraction });
|
|
4377
|
+
}
|
|
4378
|
+
else {
|
|
4379
|
+
executionState.set({ type, state: 'success', response });
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
// Keyed on `isAlive` rather than on the branches above: a cancelled restore produces neither a
|
|
4383
|
+
// response nor an error, and would otherwise leave `sessionStatus` at `restoring` forever.
|
|
4384
|
+
if (type === 'autoLogin' && !snapshot.isAlive() && !untracked(context.isAuthenticated)) {
|
|
4385
|
+
sessionStatus.set('anonymous');
|
|
4386
|
+
}
|
|
4387
|
+
});
|
|
4388
|
+
// One query per builder, reused by every execution. A query owns a child injector, a devtools
|
|
4389
|
+
// entry and a repository binding holding the request and response bodies - creating one per login
|
|
4390
|
+
// attempt or token refresh grows all of that for as long as the tab lives.
|
|
4391
|
+
let query = null;
|
|
4325
4392
|
const execute = (args, options) => {
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
injector,
|
|
4329
|
-
});
|
|
4393
|
+
query ??= builder.config.queryCreator({ onlyManualExecution: true, injector });
|
|
4394
|
+
extractionError.set(null);
|
|
4330
4395
|
query.execute({ args, options });
|
|
4331
4396
|
const snapshot = query.createSnapshot();
|
|
4332
4397
|
const stateType = deriveExecutionStateType(builder, options?.triggeredBy);
|
|
4333
|
-
|
|
4398
|
+
const isRevocation = stateType === 'revocation';
|
|
4399
|
+
if (!isRevocation) {
|
|
4334
4400
|
latestExecutedQuery.set({ key: builder.key, snapshot });
|
|
4335
4401
|
if (!snapshot.triggeredBy()) {
|
|
4336
4402
|
latestNonInternalQuery.set({ key: builder.key, snapshot });
|
|
4337
4403
|
}
|
|
4338
4404
|
}
|
|
4339
4405
|
executionState.set({ type: stateType, state: 'loading' });
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
if (loading)
|
|
4345
|
-
return;
|
|
4346
|
-
if (error) {
|
|
4347
|
-
executionState.set({ type: stateType, state: 'error', error });
|
|
4348
|
-
}
|
|
4349
|
-
else if (response) {
|
|
4350
|
-
executionState.set({ type: stateType, state: 'success', response });
|
|
4351
|
-
}
|
|
4352
|
-
}, { injector });
|
|
4406
|
+
if (stateType === 'autoLogin') {
|
|
4407
|
+
sessionStatus.set('restoring');
|
|
4408
|
+
}
|
|
4409
|
+
currentExecution.set({ id: isRevocation ? null : ++latestExecutionId, type: stateType, snapshot });
|
|
4353
4410
|
querySnapshot.set(snapshot);
|
|
4354
4411
|
return snapshot;
|
|
4355
4412
|
};
|
|
@@ -4358,7 +4415,12 @@ const setupBearerQueryRegistry = (builders, injector, latestExecutedQuery, lates
|
|
|
4358
4415
|
snapshot: querySnapshot.asReadonly(),
|
|
4359
4416
|
};
|
|
4360
4417
|
}
|
|
4361
|
-
|
|
4418
|
+
const hasTokenIssuingExecutionInFlight = computed(() => currentExecutions.some((execution) => {
|
|
4419
|
+
const current = execution();
|
|
4420
|
+
return !!current && current.id !== null && current.snapshot.isAlive();
|
|
4421
|
+
}), /* @ts-ignore */
|
|
4422
|
+
...(ngDevMode ? [{ debugName: "hasTokenIssuingExecutionInFlight" }] : /* istanbul ignore next */ []));
|
|
4423
|
+
return { queries, querySnapshots, hasTokenIssuingExecutionInFlight };
|
|
4362
4424
|
};
|
|
4363
4425
|
const setupFeatures = (featureBuilders, context) => {
|
|
4364
4426
|
const features = {};
|
|
@@ -4403,6 +4465,7 @@ const alwaysLeader = () => true;
|
|
|
4403
4465
|
const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
4404
4466
|
let isLeaderFn = alwaysLeader;
|
|
4405
4467
|
let leaderElectionContext;
|
|
4468
|
+
let refreshCoordination;
|
|
4406
4469
|
for (const featureBuilder of featureBuilders ?? []) {
|
|
4407
4470
|
const earlySetup = featureBuilder.earlySetup;
|
|
4408
4471
|
if (!earlySetup)
|
|
@@ -4410,8 +4473,9 @@ const runEarlyFeatureSetup = (featureBuilders, context) => {
|
|
|
4410
4473
|
const result = earlySetup(context);
|
|
4411
4474
|
isLeaderFn = result.isLeader ?? isLeaderFn;
|
|
4412
4475
|
leaderElectionContext = result.leaderElection ?? leaderElectionContext;
|
|
4476
|
+
refreshCoordination = result.refreshCoordination ?? refreshCoordination;
|
|
4413
4477
|
}
|
|
4414
|
-
return { isLeaderFn, leaderElectionContext };
|
|
4478
|
+
return { isLeaderFn, leaderElectionContext, refreshCoordination };
|
|
4415
4479
|
};
|
|
4416
4480
|
const createBearerAuthProviderImpl = (config) => {
|
|
4417
4481
|
const injector = inject(Injector);
|
|
@@ -4446,9 +4510,15 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4446
4510
|
...(ngDevMode ? [{ debugName: "latestNonInternalQuery" }] : /* istanbul ignore next */ []));
|
|
4447
4511
|
const executionState = signal(null, /* @ts-ignore */
|
|
4448
4512
|
...(ngDevMode ? [{ debugName: "executionState" }] : /* istanbul ignore next */ []));
|
|
4513
|
+
const sessionStatus = signal('unknown', /* @ts-ignore */
|
|
4514
|
+
...(ngDevMode ? [{ debugName: "sessionStatus" }] : /* istanbul ignore next */ []));
|
|
4515
|
+
const sessionEndCause = signal(null, /* @ts-ignore */
|
|
4516
|
+
...(ngDevMode ? [{ debugName: "sessionEndCause" }] : /* istanbul ignore next */ []));
|
|
4449
4517
|
const applyTokens = (access, refresh) => {
|
|
4450
4518
|
accessToken.set(access);
|
|
4451
4519
|
refreshToken.set(refresh);
|
|
4520
|
+
sessionStatus.set('authenticated');
|
|
4521
|
+
sessionEndCause.set(null);
|
|
4452
4522
|
afterTokenRefresh$.next();
|
|
4453
4523
|
};
|
|
4454
4524
|
// Public entry point only - a query-driven login already reports its own, more specific
|
|
@@ -4458,20 +4528,37 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4458
4528
|
applyTokens(access, refresh);
|
|
4459
4529
|
executionState.set({ type: 'tokenSeed', state: 'success' });
|
|
4460
4530
|
};
|
|
4461
|
-
const { queries } = setupBearerQueryRegistry(config.queries,
|
|
4462
|
-
|
|
4531
|
+
const { queries, hasTokenIssuingExecutionInFlight } = setupBearerQueryRegistry(config.queries, {
|
|
4532
|
+
injector,
|
|
4533
|
+
latestExecutedQuery,
|
|
4534
|
+
latestNonInternalQuery,
|
|
4535
|
+
applyTokens,
|
|
4536
|
+
executionState,
|
|
4537
|
+
sessionStatus,
|
|
4538
|
+
isAuthenticated,
|
|
4539
|
+
});
|
|
4540
|
+
const logout = (cause = 'user') => {
|
|
4463
4541
|
accessToken.set(null);
|
|
4464
4542
|
refreshToken.set(null);
|
|
4465
4543
|
queryClient.repository.unbindAllSecure();
|
|
4466
4544
|
latestExecutedQuery.set(null);
|
|
4467
4545
|
latestNonInternalQuery.set(null);
|
|
4468
4546
|
executionState.set({ type: 'logout', state: 'success' });
|
|
4547
|
+
sessionStatus.set('anonymous');
|
|
4548
|
+
sessionEndCause.set(cause);
|
|
4469
4549
|
// Unsaved edits can no longer be saved once the session is gone. Guarding them past this point
|
|
4470
4550
|
// only strands a "discard your changes?" dialog over the login page the app redirects to, and
|
|
4471
4551
|
// leaves the tab locked against closing. See `injectUnsavedChangesCoordinator`.
|
|
4472
4552
|
unsavedChanges.abandonAll('logout');
|
|
4473
4553
|
};
|
|
4474
|
-
const isLeader = runEarlyFeatureSetup(config.features, {
|
|
4554
|
+
const isLeader = runEarlyFeatureSetup(config.features, {
|
|
4555
|
+
accessToken,
|
|
4556
|
+
refreshToken,
|
|
4557
|
+
queryClient,
|
|
4558
|
+
name: config.name,
|
|
4559
|
+
applyTokens,
|
|
4560
|
+
logout,
|
|
4561
|
+
});
|
|
4475
4562
|
const querySetupContext = {
|
|
4476
4563
|
accessToken,
|
|
4477
4564
|
refreshToken,
|
|
@@ -4479,8 +4566,11 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4479
4566
|
queryClient,
|
|
4480
4567
|
repository: queryClient.repository,
|
|
4481
4568
|
isLeader: isLeader.isLeaderFn,
|
|
4569
|
+
refreshCoordination: isLeader.refreshCoordination,
|
|
4570
|
+
logout,
|
|
4482
4571
|
afterTokenRefresh$,
|
|
4483
4572
|
queries: queries,
|
|
4573
|
+
hasTokenIssuingExecutionInFlight,
|
|
4484
4574
|
};
|
|
4485
4575
|
for (const builder of config.queries) {
|
|
4486
4576
|
builder.setup?.(querySetupContext);
|
|
@@ -4498,8 +4588,15 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4498
4588
|
afterTokenRefresh$,
|
|
4499
4589
|
queries: queries,
|
|
4500
4590
|
executionState,
|
|
4591
|
+
sessionStatus: sessionStatus.asReadonly(),
|
|
4592
|
+
sessionEndCause: sessionEndCause.asReadonly(),
|
|
4501
4593
|
};
|
|
4502
4594
|
const { features, applied: appliedFeatures } = setupFeatures(config.features, featureSetupContext);
|
|
4595
|
+
// Runs after the features, because `withPersistentAuth` starts its auto-login during setup. Still
|
|
4596
|
+
// `unknown` here means nothing is trying to restore a session, so there is nothing to wait for.
|
|
4597
|
+
if (sessionStatus() === 'unknown') {
|
|
4598
|
+
sessionStatus.set('anonymous');
|
|
4599
|
+
}
|
|
4503
4600
|
const provider = {
|
|
4504
4601
|
queries,
|
|
4505
4602
|
features: features,
|
|
@@ -4510,6 +4607,8 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
4510
4607
|
latestExecutedQuery: latestExecutedQuery.asReadonly(),
|
|
4511
4608
|
latestNonInternalQuery: latestNonInternalQuery.asReadonly(),
|
|
4512
4609
|
executionState: executionState.asReadonly(),
|
|
4610
|
+
sessionStatus: sessionStatus.asReadonly(),
|
|
4611
|
+
sessionEndCause: sessionEndCause.asReadonly(),
|
|
4513
4612
|
setTokens,
|
|
4514
4613
|
logout,
|
|
4515
4614
|
afterTokenRefresh$,
|
|
@@ -4539,7 +4638,9 @@ const withAuthenticationQuery = (key, config, setup) => ({
|
|
|
4539
4638
|
key,
|
|
4540
4639
|
config: {
|
|
4541
4640
|
...config,
|
|
4542
|
-
|
|
4641
|
+
// A spent credential entry must not sit out a retention window: the request body holds the
|
|
4642
|
+
// username and password, the response the tokens it was exchanged for.
|
|
4643
|
+
queryCreator: config.queryCreator.clone({ keepUnusedFor: 0, subtle: { useQueryRepositoryCache: true } }),
|
|
4543
4644
|
},
|
|
4544
4645
|
setup,
|
|
4545
4646
|
});
|
|
@@ -4574,20 +4675,30 @@ const withRefreshQuery = (key, config) => {
|
|
|
4574
4675
|
const minRefreshInterval = config.minRefreshInterval ?? 30000; // 30 seconds default
|
|
4575
4676
|
const refreshIfExpired = config.refreshIfExpired ?? true;
|
|
4576
4677
|
let lastRefreshTime = 0;
|
|
4577
|
-
|
|
4678
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4679
|
+
const refreshQuery = () => context.queries[key];
|
|
4680
|
+
const executeRefresh = (reason) => {
|
|
4578
4681
|
const currentRefreshToken = context.refreshToken();
|
|
4579
|
-
if (!currentRefreshToken
|
|
4682
|
+
if (!currentRefreshToken)
|
|
4580
4683
|
return;
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4684
|
+
if (!context.isLeader()) {
|
|
4685
|
+
// Only the leader may spend a single-use refresh token, but the event is still real - hand it
|
|
4686
|
+
// over instead of dropping it, or this tab waits out the leader's timer with a dead token.
|
|
4687
|
+
context.refreshCoordination?.request();
|
|
4585
4688
|
return;
|
|
4586
|
-
|
|
4587
|
-
//
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4689
|
+
}
|
|
4690
|
+
// Any token-issuing execution, not just another refresh: a login already in flight is about to
|
|
4691
|
+
// issue a token pair of its own, and the refresh token this would spend belongs to the session
|
|
4692
|
+
// that login is replacing.
|
|
4693
|
+
if (context.hasTokenIssuingExecutionInFlight())
|
|
4694
|
+
return;
|
|
4695
|
+
if (reason === 'scheduled') {
|
|
4696
|
+
const now = Date.now();
|
|
4697
|
+
if (now - lastRefreshTime < minRefreshInterval)
|
|
4698
|
+
return;
|
|
4699
|
+
lastRefreshTime = now;
|
|
4700
|
+
}
|
|
4701
|
+
refreshQuery().execute(buildArgs(currentRefreshToken), { triggeredBy: 'token-refresh' });
|
|
4591
4702
|
};
|
|
4592
4703
|
const calculateRefreshBuffer = (tokenLifetimeMs) => {
|
|
4593
4704
|
if (typeof config.refreshStrategy === 'number') {
|
|
@@ -4638,9 +4749,24 @@ const withRefreshQuery = (key, config) => {
|
|
|
4638
4749
|
}), takeUntilDestroyed())
|
|
4639
4750
|
.subscribe((shouldRefresh) => {
|
|
4640
4751
|
if (shouldRefresh) {
|
|
4641
|
-
executeRefresh(
|
|
4752
|
+
executeRefresh('scheduled');
|
|
4642
4753
|
}
|
|
4643
4754
|
});
|
|
4755
|
+
context.refreshCoordination?.requests$.pipe(takeUntilDestroyed()).subscribe(() => executeRefresh('unauthorized'));
|
|
4756
|
+
const onRefreshFailure = config.onRefreshFailure ??
|
|
4757
|
+
(({ error, logout }) => {
|
|
4758
|
+
if (!retryableStatusCodes.includes(error.code))
|
|
4759
|
+
logout();
|
|
4760
|
+
});
|
|
4761
|
+
effect(() => {
|
|
4762
|
+
const snapshot = refreshQuery().snapshot();
|
|
4763
|
+
if (!snapshot || snapshot.loading())
|
|
4764
|
+
return;
|
|
4765
|
+
const error = snapshot.error();
|
|
4766
|
+
if (!error)
|
|
4767
|
+
return;
|
|
4768
|
+
onRefreshFailure({ error, logout: () => context.logout('expired') });
|
|
4769
|
+
});
|
|
4644
4770
|
// Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
|
|
4645
4771
|
const autoRetryOn401 = config.autoRetryOn401 ?? true;
|
|
4646
4772
|
if (autoRetryOn401) {
|
|
@@ -4656,7 +4782,7 @@ const withRefreshQuery = (key, config) => {
|
|
|
4656
4782
|
return true;
|
|
4657
4783
|
}), takeUntilDestroyed())
|
|
4658
4784
|
.subscribe(() => {
|
|
4659
|
-
executeRefresh(
|
|
4785
|
+
executeRefresh('unauthorized');
|
|
4660
4786
|
});
|
|
4661
4787
|
}
|
|
4662
4788
|
};
|
|
@@ -4665,7 +4791,11 @@ const withRefreshQuery = (key, config) => {
|
|
|
4665
4791
|
key,
|
|
4666
4792
|
config: {
|
|
4667
4793
|
...config,
|
|
4668
|
-
queryCreator: config.queryCreator.clone({
|
|
4794
|
+
queryCreator: config.queryCreator.clone({
|
|
4795
|
+
retryFn: refreshRetryFn,
|
|
4796
|
+
keepUnusedFor: 0,
|
|
4797
|
+
subtle: { useQueryRepositoryCache: true },
|
|
4798
|
+
}),
|
|
4669
4799
|
},
|
|
4670
4800
|
setup,
|
|
4671
4801
|
buildArgs,
|
|
@@ -4697,7 +4827,7 @@ const withInactivityLogout = (config = {}) => {
|
|
|
4697
4827
|
});
|
|
4698
4828
|
const inactivityLogout$ = activity$.pipe(switchMap(() => timer(inactivityTimeout)), filter(() => enabled() && !!context.accessToken()));
|
|
4699
4829
|
const logoutSubscription = inactivityLogout$.subscribe(() => {
|
|
4700
|
-
context.logout();
|
|
4830
|
+
context.logout('inactivity');
|
|
4701
4831
|
});
|
|
4702
4832
|
effect(() => {
|
|
4703
4833
|
const token = context.accessToken();
|
|
@@ -4747,18 +4877,20 @@ const withInactivityLogout = (config = {}) => {
|
|
|
4747
4877
|
/**
|
|
4748
4878
|
* Namespace and key of the one lock the whole election is: whoever holds it is the leader, and every
|
|
4749
4879
|
* other tab of the app is queued behind it. The lock name is what the instance count is derived from,
|
|
4750
|
-
* so the two can never disagree about who is taking part.
|
|
4880
|
+
* so the two can never disagree about who is taking part. Both carry the provider's name, so two
|
|
4881
|
+
* providers reachable from one origin elect a leader each instead of one between them.
|
|
4751
4882
|
*/
|
|
4752
4883
|
const LEADER_LOCK_NAMESPACE = 'ethlete-auth';
|
|
4753
|
-
const
|
|
4754
|
-
const
|
|
4884
|
+
const leaderLockKey = (name) => `leader:${name}`;
|
|
4885
|
+
const leaderLockName = (name) => `${LEADER_LOCK_NAMESPACE}:${leaderLockKey(name)}`;
|
|
4755
4886
|
/**
|
|
4756
4887
|
* Web Locks has no "someone joined" event, so tabs announce themselves on this channel. Two messages
|
|
4757
|
-
* per tab lifetime, in place of the heartbeat this used to run once a second.
|
|
4888
|
+
* per tab lifetime, in place of the heartbeat this used to run once a second. It also carries a
|
|
4889
|
+
* follower's request for a refresh, which is the other thing a tab can only say to the leader.
|
|
4758
4890
|
*/
|
|
4759
|
-
const
|
|
4760
|
-
const countLeaderRequests = (snapshot) => {
|
|
4761
|
-
const isLeaderLock = (info) => info.name ===
|
|
4891
|
+
const presenceChannelName = (name) => `ethlete-auth-leader:${name}`;
|
|
4892
|
+
const countLeaderRequests = (snapshot, lockName) => {
|
|
4893
|
+
const isLeaderLock = (info) => info.name === lockName;
|
|
4762
4894
|
// Held plus pending is exactly the tab count: every tab requests this one lock, one gets it and the
|
|
4763
4895
|
// rest queue. The platform drops a crashed tab from both lists on its own.
|
|
4764
4896
|
return (snapshot.held?.filter(isLeaderLock).length ?? 0) + (snapshot.pending?.filter(isLeaderLock).length ?? 0);
|
|
@@ -4775,20 +4907,29 @@ const countLeaderRequests = (snapshot) => {
|
|
|
4775
4907
|
* `localStorage` implementation fell back to, and the only safe default: a session that refreshes its
|
|
4776
4908
|
* token in every tab is wasteful, one that refreshes it in none logs the user out.
|
|
4777
4909
|
*/
|
|
4778
|
-
const setupLeaderElection = () => {
|
|
4910
|
+
const setupLeaderElection = (options) => {
|
|
4779
4911
|
const destroyRef = inject(DestroyRef);
|
|
4912
|
+
const lockName = leaderLockName(options.name);
|
|
4780
4913
|
const lockManager = createQueryKeyLockManager(LEADER_LOCK_NAMESPACE);
|
|
4781
|
-
const hold = lockManager.hold(
|
|
4914
|
+
const hold = lockManager.hold(leaderLockKey(options.name));
|
|
4782
4915
|
const instanceCount = signal(1, /* @ts-ignore */
|
|
4783
4916
|
...(ngDevMode ? [{ debugName: "instanceCount" }] : /* istanbul ignore next */ []));
|
|
4784
|
-
const
|
|
4917
|
+
const refreshRequests = new Subject();
|
|
4918
|
+
const noop = () => {
|
|
4785
4919
|
/* nothing was set up */
|
|
4786
4920
|
};
|
|
4787
|
-
// `hold.isHolder` is already `true` in this case, so the tab reads as the leader it effectively is
|
|
4921
|
+
// `hold.isHolder` is already `true` in this case, so the tab reads as the leader it effectively is,
|
|
4922
|
+
// and asks nobody else for a refresh.
|
|
4788
4923
|
if (!lockManager.isSupported) {
|
|
4789
|
-
return {
|
|
4924
|
+
return {
|
|
4925
|
+
isLeader: hold.isHolder,
|
|
4926
|
+
instanceCount: instanceCount.asReadonly(),
|
|
4927
|
+
requestRefresh: noop,
|
|
4928
|
+
refreshRequests$: refreshRequests.asObservable(),
|
|
4929
|
+
cleanup: noop,
|
|
4930
|
+
};
|
|
4790
4931
|
}
|
|
4791
|
-
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(
|
|
4932
|
+
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(presenceChannelName(options.name));
|
|
4792
4933
|
let isDestroyed = false;
|
|
4793
4934
|
const recount = async () => {
|
|
4794
4935
|
const snapshot = await navigator.locks.query();
|
|
@@ -4796,14 +4937,21 @@ const setupLeaderElection = () => {
|
|
|
4796
4937
|
return;
|
|
4797
4938
|
// Never below one: the snapshot can be taken before this tab's own request is registered, and a
|
|
4798
4939
|
// count of zero would say the app is not running in the tab reading it.
|
|
4799
|
-
instanceCount.set(Math.max(countLeaderRequests(snapshot), 1));
|
|
4940
|
+
instanceCount.set(Math.max(countLeaderRequests(snapshot, lockName), 1));
|
|
4800
4941
|
};
|
|
4801
4942
|
const announce = () => channel?.postMessage({ type: 'presence' });
|
|
4943
|
+
const requestRefresh = () => channel?.postMessage({ type: 'refresh-requested' });
|
|
4802
4944
|
if (channel) {
|
|
4803
4945
|
channel.onmessage = (event) => {
|
|
4804
|
-
|
|
4946
|
+
const message = event.data;
|
|
4947
|
+
if (message?.type === 'presence') {
|
|
4948
|
+
void recount();
|
|
4805
4949
|
return;
|
|
4806
|
-
|
|
4950
|
+
}
|
|
4951
|
+
// Every tab hears the request; only the one that may spend the refresh token acts on it.
|
|
4952
|
+
if (message?.type === 'refresh-requested' && hold.isHolder()) {
|
|
4953
|
+
refreshRequests.next();
|
|
4954
|
+
}
|
|
4807
4955
|
};
|
|
4808
4956
|
}
|
|
4809
4957
|
effect(() => {
|
|
@@ -4824,9 +4972,16 @@ const setupLeaderElection = () => {
|
|
|
4824
4972
|
hold.release();
|
|
4825
4973
|
announce();
|
|
4826
4974
|
channel?.close();
|
|
4975
|
+
refreshRequests.complete();
|
|
4827
4976
|
};
|
|
4828
4977
|
destroyRef.onDestroy(cleanup);
|
|
4829
|
-
return {
|
|
4978
|
+
return {
|
|
4979
|
+
isLeader: hold.isHolder,
|
|
4980
|
+
instanceCount: instanceCount.asReadonly(),
|
|
4981
|
+
requestRefresh,
|
|
4982
|
+
refreshRequests$: refreshRequests.asObservable(),
|
|
4983
|
+
cleanup,
|
|
4984
|
+
};
|
|
4830
4985
|
};
|
|
4831
4986
|
|
|
4832
4987
|
const ENCRYPTION_KEY_STORAGE = '__eth_ek';
|
|
@@ -4909,13 +5064,19 @@ const resetEncryptionKey = () => {
|
|
|
4909
5064
|
}
|
|
4910
5065
|
};
|
|
4911
5066
|
|
|
4912
|
-
|
|
5067
|
+
/**
|
|
5068
|
+
* The channel a provider syncs on unless the consumer names one. Carries the provider's name, so two
|
|
5069
|
+
* providers reachable from the same origin do not push each other's tokens around.
|
|
5070
|
+
*/
|
|
5071
|
+
const defaultSyncChannelName = (name) => `ethlete-auth-sync:${name}`;
|
|
5072
|
+
/** How a logout is recorded in {@link lastSyncedState}, where a token pair records its two tokens. */
|
|
5073
|
+
const LOGGED_OUT = 'logout';
|
|
5074
|
+
const tokenState = (access, refresh) => `${access}\0${refresh}`;
|
|
5075
|
+
const setupMultiTabSync = (config, context) => {
|
|
4913
5076
|
const destroyRef = inject(DestroyRef);
|
|
4914
|
-
const channelName = config.channelName ??
|
|
5077
|
+
const channelName = config.channelName ?? defaultSyncChannelName(context.name);
|
|
4915
5078
|
const syncTokens = config.syncTokens ?? true;
|
|
4916
5079
|
const syncLogout = config.syncLogout ?? true;
|
|
4917
|
-
let channel = null;
|
|
4918
|
-
let isProcessingExternalUpdate = false;
|
|
4919
5080
|
if (typeof BroadcastChannel === 'undefined') {
|
|
4920
5081
|
if (isDevMode()) {
|
|
4921
5082
|
console.warn('BroadcastChannel is not supported in this environment. Multi-tab sync will be disabled.');
|
|
@@ -4926,60 +5087,67 @@ const setupMultiTabSync = (config, accessToken, refreshToken, queryClient) => {
|
|
|
4926
5087
|
},
|
|
4927
5088
|
};
|
|
4928
5089
|
}
|
|
4929
|
-
channel = new BroadcastChannel(channelName);
|
|
5090
|
+
const channel = new BroadcastChannel(channelName);
|
|
5091
|
+
// The session state this tab last sent or received, which is what stops an incoming message from being
|
|
5092
|
+
// echoed straight back out. A flag cleared once the message handler returns cannot do that job: the
|
|
5093
|
+
// broadcasting effects below run after it, so they would always see it cleared.
|
|
5094
|
+
let lastSyncedState = null;
|
|
5095
|
+
let hadTokens = false;
|
|
4930
5096
|
channel.onmessage = (event) => {
|
|
4931
5097
|
const message = event.data;
|
|
4932
|
-
|
|
4933
|
-
|
|
5098
|
+
if (message.type === 'logout' && syncLogout) {
|
|
5099
|
+
lastSyncedState = LOGGED_OUT;
|
|
5100
|
+
hadTokens = false;
|
|
5101
|
+
context.logout('otherTab');
|
|
4934
5102
|
return;
|
|
4935
|
-
isProcessingExternalUpdate = true;
|
|
4936
|
-
try {
|
|
4937
|
-
if (message.type === 'logout' && syncLogout) {
|
|
4938
|
-
accessToken.set(null);
|
|
4939
|
-
refreshToken.set(null);
|
|
4940
|
-
queryClient.repository.unbindAllSecure();
|
|
4941
|
-
}
|
|
4942
|
-
else if (message.type === 'tokens-updated' && syncTokens) {
|
|
4943
|
-
accessToken.set(decryptToken(message.accessToken));
|
|
4944
|
-
refreshToken.set(decryptToken(message.refreshToken));
|
|
4945
|
-
}
|
|
4946
|
-
}
|
|
4947
|
-
finally {
|
|
4948
|
-
isProcessingExternalUpdate = false;
|
|
4949
5103
|
}
|
|
5104
|
+
if (message.type !== 'tokens-updated' || !syncTokens)
|
|
5105
|
+
return;
|
|
5106
|
+
const access = decryptToken(message.accessToken);
|
|
5107
|
+
const refresh = decryptToken(message.refreshToken);
|
|
5108
|
+
if (!access || !refresh)
|
|
5109
|
+
return;
|
|
5110
|
+
lastSyncedState = tokenState(access, refresh);
|
|
5111
|
+
hadTokens = true;
|
|
5112
|
+
context.applyTokens(access, refresh);
|
|
4950
5113
|
};
|
|
4951
5114
|
const cleanup = () => {
|
|
4952
|
-
channel
|
|
5115
|
+
channel.close();
|
|
4953
5116
|
};
|
|
4954
5117
|
destroyRef.onDestroy(cleanup);
|
|
4955
5118
|
if (syncTokens) {
|
|
4956
5119
|
effect(() => {
|
|
4957
|
-
const access = accessToken();
|
|
4958
|
-
const refresh = refreshToken();
|
|
4959
|
-
if (access
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
5120
|
+
const access = context.accessToken();
|
|
5121
|
+
const refresh = context.refreshToken();
|
|
5122
|
+
if (!access || !refresh)
|
|
5123
|
+
return;
|
|
5124
|
+
const state = tokenState(access, refresh);
|
|
5125
|
+
if (state === lastSyncedState)
|
|
5126
|
+
return;
|
|
5127
|
+
lastSyncedState = state;
|
|
5128
|
+
const message = {
|
|
5129
|
+
type: 'tokens-updated',
|
|
5130
|
+
accessToken: encryptToken(access),
|
|
5131
|
+
refreshToken: encryptToken(refresh),
|
|
5132
|
+
};
|
|
5133
|
+
channel.postMessage(message);
|
|
4967
5134
|
});
|
|
4968
5135
|
}
|
|
4969
5136
|
if (syncLogout) {
|
|
4970
|
-
let hadTokens = false;
|
|
4971
5137
|
effect(() => {
|
|
4972
|
-
const access = accessToken();
|
|
5138
|
+
const access = context.accessToken();
|
|
4973
5139
|
if (access) {
|
|
4974
5140
|
hadTokens = true;
|
|
5141
|
+
return;
|
|
4975
5142
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
}
|
|
5143
|
+
if (!hadTokens)
|
|
5144
|
+
return;
|
|
5145
|
+
hadTokens = false;
|
|
5146
|
+
if (lastSyncedState === LOGGED_OUT)
|
|
5147
|
+
return;
|
|
5148
|
+
lastSyncedState = LOGGED_OUT;
|
|
5149
|
+
const message = { type: 'logout' };
|
|
5150
|
+
channel.postMessage(message);
|
|
4983
5151
|
});
|
|
4984
5152
|
}
|
|
4985
5153
|
return { cleanup };
|
|
@@ -5013,11 +5181,12 @@ const SINGLE_TAB = /* @__PURE__ */ (() => {
|
|
|
5013
5181
|
*/
|
|
5014
5182
|
const withBearerAuthMultiTabSync = (config = {}) => {
|
|
5015
5183
|
let instance = SINGLE_TAB;
|
|
5184
|
+
let channelName = config.channelName ?? '';
|
|
5016
5185
|
const setup = (_context) => ({
|
|
5017
5186
|
type: BearerAuthFeatureType.MULTI_TAB_SYNC,
|
|
5018
5187
|
instance,
|
|
5019
5188
|
devtools: () => [
|
|
5020
|
-
{ label: 'channel', value:
|
|
5189
|
+
{ label: 'channel', value: channelName },
|
|
5021
5190
|
{ label: 'tokens', value: config.syncTokens === false ? 'tab local' : 'synced' },
|
|
5022
5191
|
{ label: 'logout', value: config.syncLogout === false ? 'tab local' : 'synced' },
|
|
5023
5192
|
{
|
|
@@ -5029,12 +5198,23 @@ const withBearerAuthMultiTabSync = (config = {}) => {
|
|
|
5029
5198
|
// The elected leader gates the auth queries' automatic refresh, which is wired up before the
|
|
5030
5199
|
// regular feature setup runs - so this half has to happen earlier than `setup`.
|
|
5031
5200
|
const earlySetup = (context) => {
|
|
5032
|
-
|
|
5201
|
+
channelName = config.channelName ?? defaultSyncChannelName(context.name);
|
|
5202
|
+
setupMultiTabSync({ channelName, syncTokens: config.syncTokens, syncLogout: config.syncLogout }, {
|
|
5203
|
+
accessToken: context.accessToken,
|
|
5204
|
+
refreshToken: context.refreshToken,
|
|
5205
|
+
name: context.name,
|
|
5206
|
+
applyTokens: context.applyTokens,
|
|
5207
|
+
logout: context.logout,
|
|
5208
|
+
});
|
|
5033
5209
|
if (config.leaderElection === false)
|
|
5034
5210
|
return {};
|
|
5035
|
-
const election = setupLeaderElection();
|
|
5211
|
+
const election = setupLeaderElection({ name: context.name });
|
|
5036
5212
|
instance = { isLeader: election.isLeader, instanceCount: election.instanceCount };
|
|
5037
|
-
return {
|
|
5213
|
+
return {
|
|
5214
|
+
isLeader: () => election.isLeader(),
|
|
5215
|
+
leaderElection: instance,
|
|
5216
|
+
refreshCoordination: { request: election.requestRefresh, requests$: election.refreshRequests$ },
|
|
5217
|
+
};
|
|
5038
5218
|
};
|
|
5039
5219
|
return Object.assign(setup, { earlySetup });
|
|
5040
5220
|
};
|
|
@@ -5059,7 +5239,7 @@ const withPersistentAuth = (config) => {
|
|
|
5059
5239
|
};
|
|
5060
5240
|
};
|
|
5061
5241
|
const createPersistentAuthFeature = (config, context) => {
|
|
5062
|
-
const { refreshToken } = context;
|
|
5242
|
+
const { refreshToken, executionState } = context;
|
|
5063
5243
|
const cookieName = config.cookie?.name ?? 'etAuth';
|
|
5064
5244
|
const rememberMeStorageKey = `${cookieName}-rememberMe`;
|
|
5065
5245
|
const route = injectRoute();
|
|
@@ -5076,25 +5256,36 @@ const createPersistentAuthFeature = (config, context) => {
|
|
|
5076
5256
|
};
|
|
5077
5257
|
const rememberMeSignal = signal(initializeRememberMe(), /* @ts-ignore */
|
|
5078
5258
|
...(ngDevMode ? [{ debugName: "rememberMeSignal" }] : /* istanbul ignore next */ []));
|
|
5259
|
+
const cookieDomain = () => config.cookie?.domain ?? getDomain() ?? 'localhost';
|
|
5260
|
+
const cookiePath = () => config.cookie?.path ?? '/';
|
|
5261
|
+
const removeCookie = () => deleteCookie(cookieName, cookiePath(), cookieDomain());
|
|
5079
5262
|
effect(() => {
|
|
5080
5263
|
const token = refreshToken();
|
|
5081
5264
|
const rememberMe = rememberMeSignal();
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5265
|
+
// A missing token is not a reason to drop the cookie. It is missing on every startup - `tryLogin`
|
|
5266
|
+
// reads the cookie synchronously and the auto-login only resolves a tick later - so deleting here
|
|
5267
|
+
// would throw away a 30-day refresh token before it was ever used. Deletion is driven by the events
|
|
5268
|
+
// that actually end a session instead, below.
|
|
5269
|
+
if (!token)
|
|
5270
|
+
return;
|
|
5271
|
+
const expiresInDays = rememberMe ? (config.cookie?.expiresInDays ?? 30) : null;
|
|
5272
|
+
setCookie(cookieName, encryptToken(token), expiresInDays, cookieDomain(), cookiePath(), config.cookie?.sameSite ?? 'lax');
|
|
5273
|
+
});
|
|
5274
|
+
effect(() => {
|
|
5275
|
+
const state = executionState();
|
|
5276
|
+
if (!state)
|
|
5277
|
+
return;
|
|
5278
|
+
if (state.type === 'logout') {
|
|
5279
|
+
removeCookie();
|
|
5280
|
+
return;
|
|
5097
5281
|
}
|
|
5282
|
+
// A token the server itself rejected is worth nothing on the next visit; anything else (offline, a
|
|
5283
|
+
// 500, an aborted startup) leaves it in place so a reload can try again.
|
|
5284
|
+
const wasRejected = state.state === 'error' &&
|
|
5285
|
+
(state.type === 'tokenRefresh' || state.type === 'autoLogin') &&
|
|
5286
|
+
(state.error.code === 401 || state.error.code === 403);
|
|
5287
|
+
if (wasRejected)
|
|
5288
|
+
removeCookie();
|
|
5098
5289
|
});
|
|
5099
5290
|
const tryLogin = () => {
|
|
5100
5291
|
const currentRoute = route();
|
|
@@ -5340,7 +5531,7 @@ const createTrackingFeature = (context, config) => {
|
|
|
5340
5531
|
const refreshToken = context.refreshToken();
|
|
5341
5532
|
const hasTokens = !!(accessToken || refreshToken);
|
|
5342
5533
|
if (hadTokens && !hasTokens) {
|
|
5343
|
-
emit('logout',
|
|
5534
|
+
emit('logout', { cause: untracked(context.sessionEndCause) });
|
|
5344
5535
|
}
|
|
5345
5536
|
hadTokens = hasTokens;
|
|
5346
5537
|
});
|
|
@@ -5487,7 +5678,12 @@ const createQueryDevtoolsFormLinks = () => {
|
|
|
5487
5678
|
};
|
|
5488
5679
|
};
|
|
5489
5680
|
|
|
5490
|
-
const
|
|
5681
|
+
const isPathAtOrBelow = (path, root) => path.length >= root.length && root.every((step, index) => step === path[index]);
|
|
5682
|
+
/**
|
|
5683
|
+
* Whether any of `entries` is armed at `path` or anywhere below it - what the panel's menu asks before
|
|
5684
|
+
* offering "Reset", so the destructive action is hidden where it would be a silent no-op.
|
|
5685
|
+
*/
|
|
5686
|
+
const hasQueryDevtoolsOverridesAtPath = (entries, path) => entries.some((entry) => isPathAtOrBelow(entry.op.path, path));
|
|
5491
5687
|
/**
|
|
5492
5688
|
* Walks `path` into `root`, stopping as soon as a step's container doesn't exist. An empty path
|
|
5493
5689
|
* resolves to `root` itself, which is how an op targets the response's top-level value.
|
|
@@ -5695,15 +5891,84 @@ const resizePagination = (value, shape, mode, amount) => {
|
|
|
5695
5891
|
const STRING_PRESETS = {
|
|
5696
5892
|
short: 'Ab',
|
|
5697
5893
|
long: /* @__PURE__ */ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt. '.repeat(4),
|
|
5894
|
+
longWord: 'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
|
|
5698
5895
|
unicode: 'مرحبا بالعالم 👋 日本語テスト 🎉 Ñoño',
|
|
5699
5896
|
};
|
|
5700
|
-
const stringPresetValue = (preset, custom) => preset === 'custom' ?
|
|
5897
|
+
const stringPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? '' : STRING_PRESETS[preset]);
|
|
5701
5898
|
const NUMBER_PRESETS = {
|
|
5702
5899
|
zero: 0,
|
|
5703
5900
|
negative: -1,
|
|
5704
5901
|
huge: Number.MAX_SAFE_INTEGER,
|
|
5705
5902
|
};
|
|
5706
|
-
const numberPresetValue = (preset, custom) => preset === 'custom' ?
|
|
5903
|
+
const numberPresetValue = (preset, custom) => custom ?? (preset === 'custom' ? 0 : NUMBER_PRESETS[preset]);
|
|
5904
|
+
const pickRandom = (values) => values[Math.floor(Math.random() * values.length)];
|
|
5905
|
+
const randomIntBetween = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
|
|
5906
|
+
const randomHex = (length) => {
|
|
5907
|
+
let out = '';
|
|
5908
|
+
while (out.length < length)
|
|
5909
|
+
out += Math.random().toString(16).slice(2);
|
|
5910
|
+
return out.slice(0, length);
|
|
5911
|
+
};
|
|
5912
|
+
const SHORT_WORDS = ['Ok', 'Ada', 'Fig', 'Kiwi', 'Nova', 'Echo', 'Amber', 'Delta', 'Mango', 'Quartz', 'Zephyr'];
|
|
5913
|
+
const LOREM_SENTENCE = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ';
|
|
5914
|
+
const LONG_COMPOUNDS = [
|
|
5915
|
+
'Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz',
|
|
5916
|
+
'Donaudampfschifffahrtsgesellschaftskapitänskajütenschlüssel',
|
|
5917
|
+
'Kraftfahrzeughaftpflichtversicherungsbescheinigung',
|
|
5918
|
+
];
|
|
5919
|
+
const UNICODE_SAMPLES = [
|
|
5920
|
+
'مرحبا بالعالم 👋 اختبار النص',
|
|
5921
|
+
'日本語テスト 🎉 カタカナとひらがな',
|
|
5922
|
+
'한국어 테스트 💡 조합형 한글',
|
|
5923
|
+
'עברית מימין לשמאל 🚀 בדיקה',
|
|
5924
|
+
'Ñoño über façade — Ω≈ç√∫ 🧪',
|
|
5925
|
+
];
|
|
5926
|
+
/**
|
|
5927
|
+
* Generates a fresh sample value for a string preset, so filling twenty fields yields twenty different
|
|
5928
|
+
* values instead of twenty identical ones. Call it at arm time and store the result in the op's
|
|
5929
|
+
* `custom` field - generating inside apply would reshuffle the response on every refetch. `longWord`
|
|
5930
|
+
* is a single unbreakable token (a compound word, a URL, a hex blob) - it tests overflow, where
|
|
5931
|
+
* `long` tests wrapping.
|
|
5932
|
+
*/
|
|
5933
|
+
const generateQueryDevtoolsStringPreset = (preset) => {
|
|
5934
|
+
switch (preset) {
|
|
5935
|
+
case 'short':
|
|
5936
|
+
return pickRandom(SHORT_WORDS);
|
|
5937
|
+
case 'long': {
|
|
5938
|
+
const length = randomIntBetween(80, 480);
|
|
5939
|
+
return LOREM_SENTENCE.repeat(Math.ceil(length / LOREM_SENTENCE.length))
|
|
5940
|
+
.slice(0, length)
|
|
5941
|
+
.trimEnd();
|
|
5942
|
+
}
|
|
5943
|
+
case 'longWord':
|
|
5944
|
+
return pickRandom([
|
|
5945
|
+
() => pickRandom(LONG_COMPOUNDS),
|
|
5946
|
+
() => `https://api.example.com/v2/resources/${randomHex(24)}/attachments?signature=${randomHex(randomIntBetween(24, 96))}`,
|
|
5947
|
+
() => randomHex(randomIntBetween(64, 160)),
|
|
5948
|
+
])();
|
|
5949
|
+
case 'unicode':
|
|
5950
|
+
return pickRandom(UNICODE_SAMPLES);
|
|
5951
|
+
}
|
|
5952
|
+
};
|
|
5953
|
+
/**
|
|
5954
|
+
* Generates a fresh sample value for a number preset - same arm-time contract as
|
|
5955
|
+
* {@link generateQueryDevtoolsStringPreset}. `negative` and `huge` vary in magnitude so a batch fill
|
|
5956
|
+
* produces varied digit counts; `zero` is always `0`.
|
|
5957
|
+
*/
|
|
5958
|
+
const generateQueryDevtoolsNumberPreset = (preset) => {
|
|
5959
|
+
switch (preset) {
|
|
5960
|
+
case 'zero':
|
|
5961
|
+
return 0;
|
|
5962
|
+
case 'negative': {
|
|
5963
|
+
const magnitude = 10 ** randomIntBetween(0, 8);
|
|
5964
|
+
return -randomIntBetween(magnitude, magnitude * 10 - 1);
|
|
5965
|
+
}
|
|
5966
|
+
case 'huge': {
|
|
5967
|
+
const magnitude = 10 ** randomIntBetween(9, 15);
|
|
5968
|
+
return Math.min(randomIntBetween(magnitude, magnitude * 10 - 1), Number.MAX_SAFE_INTEGER);
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
};
|
|
5707
5972
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5708
5973
|
/** `invalid` is deliberately unparseable - "the API sent a date my code can't parse" is a real bug class. */
|
|
5709
5974
|
const datePresetValue = (preset) => {
|
|
@@ -5795,7 +6060,7 @@ const createQueryDevtoolsOverrides = () => {
|
|
|
5795
6060
|
let counter = 0;
|
|
5796
6061
|
const arm = (op) => {
|
|
5797
6062
|
if (op.type === 'reset') {
|
|
5798
|
-
list.update((current) => current.filter((entry) => !
|
|
6063
|
+
list.update((current) => current.filter((entry) => !isPathAtOrBelow(entry.op.path, op.path)));
|
|
5799
6064
|
return;
|
|
5800
6065
|
}
|
|
5801
6066
|
list.update((current) => [...current, { id: `override-${++counter}`, op }]);
|
|
@@ -5847,10 +6112,27 @@ const estimateBodySize = (body) => {
|
|
|
5847
6112
|
/** How many runs a query keeps. Enough to see a stampede or a chain without growing unbounded. */
|
|
5848
6113
|
const RUN_HISTORY = 25;
|
|
5849
6114
|
/**
|
|
5850
|
-
* How many of those runs keep
|
|
5851
|
-
*
|
|
6115
|
+
* How many of those runs keep a body - a response, or the body an error came with. Bodies dominate what
|
|
6116
|
+
* the buffer retains, which is why this is far below {@link RUN_HISTORY} and why raising it is a memory
|
|
6117
|
+
* decision the app has to make for itself.
|
|
5852
6118
|
*/
|
|
5853
|
-
const
|
|
6119
|
+
const DEFAULT_RESPONSE_HISTORY = 5;
|
|
6120
|
+
let responseHistory = DEFAULT_RESPONSE_HISTORY;
|
|
6121
|
+
/**
|
|
6122
|
+
* How many bodies each query retains - {@link QueryDevtoolsOptions.responseHistory}, or `5`. Only a run
|
|
6123
|
+
* that still holds one can be an end of a response diff, so the panel reads this to say why an older run
|
|
6124
|
+
* can no longer be picked.
|
|
6125
|
+
*
|
|
6126
|
+
* Part of the devtools contract. **Not part of the general public contract.**
|
|
6127
|
+
*/
|
|
6128
|
+
const queryDevtoolsResponseHistory = () => responseHistory;
|
|
6129
|
+
/**
|
|
6130
|
+
* Sets how many bodies each query retains. Called by `provideQueryDevtools()`; nothing else may call it.
|
|
6131
|
+
* @internal
|
|
6132
|
+
*/
|
|
6133
|
+
const setQueryDevtoolsResponseHistory = (count) => {
|
|
6134
|
+
responseHistory = count === undefined ? DEFAULT_RESPONSE_HISTORY : Math.max(1, Math.floor(count));
|
|
6135
|
+
};
|
|
5854
6136
|
/** The index of the newest run still in flight, or -1 when none is. */
|
|
5855
6137
|
const lastPendingRunIndex = (runs) => {
|
|
5856
6138
|
for (let index = runs.length - 1; index >= 0; index--) {
|
|
@@ -5860,17 +6142,25 @@ const lastPendingRunIndex = (runs) => {
|
|
|
5860
6142
|
return -1;
|
|
5861
6143
|
};
|
|
5862
6144
|
/**
|
|
5863
|
-
* Drops the
|
|
5864
|
-
*
|
|
6145
|
+
* Drops the body of every run past the newest {@link queryDevtoolsResponseHistory} that hold one - a
|
|
6146
|
+
* response body and an error body count against the same budget, since a run only ever has one of the
|
|
6147
|
+
* two. Mutates the array it is given, which is always one this module just built.
|
|
5865
6148
|
*/
|
|
5866
|
-
const
|
|
6149
|
+
const trimRetainedBodies = (runs) => {
|
|
6150
|
+
const budget = responseHistory;
|
|
5867
6151
|
let kept = 0;
|
|
5868
6152
|
for (let index = runs.length - 1; index >= 0; index--) {
|
|
5869
6153
|
const run = runs[index];
|
|
5870
|
-
if (!run?.
|
|
6154
|
+
if (!run || (!run.hasResponse && !run.error?.hasBody))
|
|
6155
|
+
continue;
|
|
6156
|
+
if (++kept <= budget)
|
|
5871
6157
|
continue;
|
|
5872
|
-
|
|
5873
|
-
|
|
6158
|
+
runs[index] = {
|
|
6159
|
+
...run,
|
|
6160
|
+
response: null,
|
|
6161
|
+
hasResponse: false,
|
|
6162
|
+
error: run.error?.hasBody ? { ...run.error, body: null, hasBody: false } : run.error,
|
|
6163
|
+
};
|
|
5874
6164
|
}
|
|
5875
6165
|
return runs;
|
|
5876
6166
|
};
|
|
@@ -5907,7 +6197,7 @@ const createQueryDevtoolsStats = () => {
|
|
|
5907
6197
|
let runCounter = 0;
|
|
5908
6198
|
const appendRun = (run, previous) => {
|
|
5909
6199
|
const appended = { ...run, index: ++runCounter };
|
|
5910
|
-
runs.set(
|
|
6200
|
+
runs.set(trimRetainedBodies([...previous, appended].slice(-RUN_HISTORY)));
|
|
5911
6201
|
};
|
|
5912
6202
|
/**
|
|
5913
6203
|
* Ends the newest run still in flight. A response with no run of the query's own waiting for it came
|
|
@@ -5920,7 +6210,7 @@ const createQueryDevtoolsStats = () => {
|
|
|
5920
6210
|
appendRun({ ...end, startedAt: end.endedAt, didRequest: false, url: lastUrl, sentBytes: 0, attempts: 0 }, current);
|
|
5921
6211
|
return;
|
|
5922
6212
|
}
|
|
5923
|
-
runs.set(
|
|
6213
|
+
runs.set(trimRetainedBodies(current.map((run, index) => (index === pending ? { ...run, ...end } : run))));
|
|
5924
6214
|
};
|
|
5925
6215
|
const recordExecution = ({ didRequest, body, url }) => {
|
|
5926
6216
|
const now = Date.now();
|
|
@@ -5950,6 +6240,7 @@ const createQueryDevtoolsStats = () => {
|
|
|
5950
6240
|
receivedBytes: 0,
|
|
5951
6241
|
response: null,
|
|
5952
6242
|
hasResponse: false,
|
|
6243
|
+
error: null,
|
|
5953
6244
|
}, runs().map((run) => (run.status === 'pending' ? { ...run, status: 'aborted', endedAt: now } : run)));
|
|
5954
6245
|
};
|
|
5955
6246
|
const recordResponse = (payload) => {
|
|
@@ -5972,15 +6263,31 @@ const createQueryDevtoolsStats = () => {
|
|
|
5972
6263
|
receivedBytes: received.bytes,
|
|
5973
6264
|
response: payload.body,
|
|
5974
6265
|
hasResponse: true,
|
|
6266
|
+
error: null,
|
|
5975
6267
|
});
|
|
5976
6268
|
};
|
|
5977
6269
|
const recordError = (options) => {
|
|
6270
|
+
const error = options?.error;
|
|
5978
6271
|
stats.update((current) => ({
|
|
5979
6272
|
...current,
|
|
5980
6273
|
errors: current.errors + 1,
|
|
5981
6274
|
lastResponseWasFaulted: options?.faulted ?? false,
|
|
5982
6275
|
}));
|
|
5983
|
-
endRun({
|
|
6276
|
+
endRun({
|
|
6277
|
+
endedAt: Date.now(),
|
|
6278
|
+
status: 'error',
|
|
6279
|
+
receivedBytes: 0,
|
|
6280
|
+
response: null,
|
|
6281
|
+
hasResponse: false,
|
|
6282
|
+
error: error
|
|
6283
|
+
? {
|
|
6284
|
+
code: error.code,
|
|
6285
|
+
messages: queryErrorMessages(error),
|
|
6286
|
+
body: error.raw.error,
|
|
6287
|
+
hasBody: error.raw.error !== null && error.raw.error !== undefined,
|
|
6288
|
+
}
|
|
6289
|
+
: null,
|
|
6290
|
+
});
|
|
5984
6291
|
};
|
|
5985
6292
|
const recordRetry = ({ attempt }) => {
|
|
5986
6293
|
const current = runs();
|
|
@@ -6042,6 +6349,106 @@ const sumQueryDevtoolsStats = (handles) => {
|
|
|
6042
6349
|
return total;
|
|
6043
6350
|
};
|
|
6044
6351
|
|
|
6352
|
+
/**
|
|
6353
|
+
* How many destroyed queries the registry keeps. A tombstone holds the last response body it captured,
|
|
6354
|
+
* so the list is capped for the same reason the repository caps its unused entries.
|
|
6355
|
+
*/
|
|
6356
|
+
const MAX_QUERY_DEVTOOLS_TOMBSTONES = 50;
|
|
6357
|
+
const frozen = (value) => () => value;
|
|
6358
|
+
const noop = () => undefined;
|
|
6359
|
+
/**
|
|
6360
|
+
* Reads one signal off a handle that is being destroyed. A query whose injector is already gone can
|
|
6361
|
+
* throw here, and losing one field is better than losing the whole tombstone.
|
|
6362
|
+
*/
|
|
6363
|
+
const read = (get, fallback) => {
|
|
6364
|
+
try {
|
|
6365
|
+
return get();
|
|
6366
|
+
}
|
|
6367
|
+
catch {
|
|
6368
|
+
return fallback;
|
|
6369
|
+
}
|
|
6370
|
+
};
|
|
6371
|
+
/**
|
|
6372
|
+
* A destroyed query's last state, shaped like the query itself: every signal the panel reads is
|
|
6373
|
+
* replaced by a function returning the value it held when the query went away, and every action is a
|
|
6374
|
+
* no-op. Rendering a tombstone therefore costs the panel no second code path - the drawer reads
|
|
6375
|
+
* `entry.handle` exactly as it does for a live query, and gates its actions on
|
|
6376
|
+
* {@link QueryDevtoolsEntry.destroyedAt} instead.
|
|
6377
|
+
*
|
|
6378
|
+
* The captured request is a plain copy, not the request itself: the real one is destroyed the moment
|
|
6379
|
+
* its cache entry is, and holding it would keep its subscriptions and its cache entry alive.
|
|
6380
|
+
*/
|
|
6381
|
+
const snapshotQueryDevtoolsHandle = (handle) => {
|
|
6382
|
+
const query = handle;
|
|
6383
|
+
if (!query || typeof query !== 'object')
|
|
6384
|
+
return handle;
|
|
6385
|
+
const live = read(() => query.subtle?.request() ?? null, null);
|
|
6386
|
+
const request = live
|
|
6387
|
+
? {
|
|
6388
|
+
method: live.method,
|
|
6389
|
+
url: live.url,
|
|
6390
|
+
args: live.args,
|
|
6391
|
+
execute: () => false,
|
|
6392
|
+
destroy: () => false,
|
|
6393
|
+
isStale: frozen(false),
|
|
6394
|
+
loading: frozen(null),
|
|
6395
|
+
error: read(() => frozen(live.error()), frozen(null)),
|
|
6396
|
+
response: read(() => frozen(live.response()), frozen(null)),
|
|
6397
|
+
currentEvent: read(() => frozen(live.currentEvent()), frozen(null)),
|
|
6398
|
+
expiresAt: read(() => frozen(live.expiresAt()), frozen(null)),
|
|
6399
|
+
subtle: {
|
|
6400
|
+
retryState: frozen(null),
|
|
6401
|
+
attempts: read(() => frozen(live.subtle.attempts()), frozen(1)),
|
|
6402
|
+
lastDurationMs: read(() => frozen(live.subtle.lastDurationMs()), frozen(null)),
|
|
6403
|
+
resolveHeaders: read(() => frozen(live.subtle.resolveHeaders()), frozen(undefined)),
|
|
6404
|
+
lastPersistedResponseAt: read(() => frozen(live.subtle.lastPersistedResponseAt()), frozen(null)),
|
|
6405
|
+
lastExternalResponseAt: read(() => frozen(live.subtle.lastExternalResponseAt()), frozen(null)),
|
|
6406
|
+
},
|
|
6407
|
+
}
|
|
6408
|
+
: null;
|
|
6409
|
+
return {
|
|
6410
|
+
id: read(() => frozen(query.id()), frozen(null)),
|
|
6411
|
+
args: read(() => frozen(query.args()), frozen(null)),
|
|
6412
|
+
response: read(() => frozen(query.response()), frozen(null)),
|
|
6413
|
+
error: read(() => frozen(query.error()), frozen(null)),
|
|
6414
|
+
latestHttpEvent: read(() => frozen(query.latestHttpEvent()), frozen(null)),
|
|
6415
|
+
lastTimeExecutedAt: read(() => frozen(query.lastTimeExecutedAt()), frozen(null)),
|
|
6416
|
+
triggeredBy: read(() => frozen(query.triggeredBy()), frozen(null)),
|
|
6417
|
+
executionState: read(() => frozen(query.executionState()), frozen(null)),
|
|
6418
|
+
// A tombstone never loads: a forced loading state captured mid-flight would spin forever.
|
|
6419
|
+
loading: frozen(null),
|
|
6420
|
+
execute: () => false,
|
|
6421
|
+
reset: noop,
|
|
6422
|
+
subtle: {
|
|
6423
|
+
request: frozen(request),
|
|
6424
|
+
destroy: noop,
|
|
6425
|
+
setResponse: noop,
|
|
6426
|
+
setLoading: noop,
|
|
6427
|
+
setError: noop,
|
|
6428
|
+
},
|
|
6429
|
+
};
|
|
6430
|
+
};
|
|
6431
|
+
/**
|
|
6432
|
+
* The entry a destroyed query leaves behind: the same id, kind and metadata, a frozen
|
|
6433
|
+
* {@link snapshotQueryDevtoolsHandle} in place of the live query, and no `element` - a DOM node whose
|
|
6434
|
+
* component is gone must not be kept alive by the panel.
|
|
6435
|
+
*
|
|
6436
|
+
* `stats` is carried over as-is. The recorder closes over its own signals rather than over the query,
|
|
6437
|
+
* so the run history that makes a failed request readable survives without retaining anything.
|
|
6438
|
+
*/
|
|
6439
|
+
const tombstoneOf = (entry, destroyedAt) => {
|
|
6440
|
+
const { element: _element, ...meta } = entry.meta;
|
|
6441
|
+
return {
|
|
6442
|
+
id: entry.id,
|
|
6443
|
+
kind: entry.kind,
|
|
6444
|
+
meta,
|
|
6445
|
+
handle: snapshotQueryDevtoolsHandle(entry.handle),
|
|
6446
|
+
createdAt: entry.createdAt,
|
|
6447
|
+
destroyedAt,
|
|
6448
|
+
stats: entry.stats,
|
|
6449
|
+
};
|
|
6450
|
+
};
|
|
6451
|
+
|
|
6045
6452
|
const entries = /* @__PURE__ */ signal([], /* @ts-ignore */
|
|
6046
6453
|
...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
|
|
6047
6454
|
/**
|
|
@@ -6161,9 +6568,39 @@ const registerEntry = (registration) => {
|
|
|
6161
6568
|
formLinks: registration.formLinks,
|
|
6162
6569
|
overrides: registration.overrides,
|
|
6163
6570
|
};
|
|
6164
|
-
|
|
6165
|
-
|
|
6571
|
+
// A registration that brings its own id can repeat one a tombstone still holds; the live entry wins.
|
|
6572
|
+
entries.update((list) => [...list.filter((e) => !(e.id === id && e.destroyedAt)), fullEntry]);
|
|
6573
|
+
return () => entries.update((list) => {
|
|
6574
|
+
const index = list.findIndex((e) => e.id === fullEntry.id);
|
|
6575
|
+
if (index === -1)
|
|
6576
|
+
return list;
|
|
6577
|
+
const next = list.slice();
|
|
6578
|
+
// Only queries leave a tombstone. A stack, sequence, form or auth provider is a container whose
|
|
6579
|
+
// interesting state is the queries it owns - each of which tombstones on its own.
|
|
6580
|
+
if (fullEntry.kind !== 'query') {
|
|
6581
|
+
next.splice(index, 1);
|
|
6582
|
+
return next;
|
|
6583
|
+
}
|
|
6584
|
+
next[index] = tombstoneOf(fullEntry, Date.now());
|
|
6585
|
+
return capTombstones(next);
|
|
6586
|
+
});
|
|
6166
6587
|
};
|
|
6588
|
+
/** Drops the oldest tombstones once there are more than {@link MAX_QUERY_DEVTOOLS_TOMBSTONES} of them. */
|
|
6589
|
+
const capTombstones = (list) => {
|
|
6590
|
+
const excess = list.filter((e) => e.destroyedAt).length - MAX_QUERY_DEVTOOLS_TOMBSTONES;
|
|
6591
|
+
if (excess <= 0)
|
|
6592
|
+
return list;
|
|
6593
|
+
const doomed = new Set(list
|
|
6594
|
+
.filter((e) => e.destroyedAt)
|
|
6595
|
+
.sort((a, b) => (a.destroyedAt ?? 0) - (b.destroyedAt ?? 0))
|
|
6596
|
+
.slice(0, excess));
|
|
6597
|
+
return list.filter((e) => !doomed.has(e));
|
|
6598
|
+
};
|
|
6599
|
+
/**
|
|
6600
|
+
* Forgets every destroyed entry the registry is holding - the panel's "clear gone" action. Live
|
|
6601
|
+
* entries are untouched.
|
|
6602
|
+
*/
|
|
6603
|
+
const clearQueryDevtoolsTombstones = () => entries.update((list) => list.filter((e) => !e.destroyedAt));
|
|
6167
6604
|
/**
|
|
6168
6605
|
* Enables the `@ethlete/query` devtools. Add this to your application providers (e.g. in
|
|
6169
6606
|
* `bootstrapApplication`) to make query clients, queries, stacks, sequences and auth providers
|
|
@@ -6179,7 +6616,8 @@ const registerEntry = (registration) => {
|
|
|
6179
6616
|
* });
|
|
6180
6617
|
* ```
|
|
6181
6618
|
*/
|
|
6182
|
-
const provideQueryDevtools = () => {
|
|
6619
|
+
const provideQueryDevtools = (options) => {
|
|
6620
|
+
setQueryDevtoolsResponseHistory(options?.responseHistory);
|
|
6183
6621
|
setQueryDevtoolsRegistrar(registerEntry);
|
|
6184
6622
|
setQueryDevtoolsStatsFactory(createQueryDevtoolsStats);
|
|
6185
6623
|
setQueryDevtoolsFormLinksFactory(createQueryDevtoolsFormLinks);
|
|
@@ -6262,6 +6700,7 @@ const createGqlExecuteFn = (executeOptions) => {
|
|
|
6262
6700
|
executeState,
|
|
6263
6701
|
args: computedArgs,
|
|
6264
6702
|
options,
|
|
6703
|
+
isRefreshable: executeOptions.creatorInternals.method === 'QUERY',
|
|
6265
6704
|
});
|
|
6266
6705
|
};
|
|
6267
6706
|
exec['reset'] = reset;
|
|
@@ -11074,6 +11513,7 @@ const buildDefaults = (fields) => {
|
|
|
11074
11513
|
}
|
|
11075
11514
|
return out;
|
|
11076
11515
|
};
|
|
11516
|
+
const MAX_RESET_PASSES = 10;
|
|
11077
11517
|
const changedKeysBetween = (previous, current) => Object.keys(current).filter((key) => !equal(previous?.[key], current[key]));
|
|
11078
11518
|
/** Best-effort URL string → value coercion, mirroring the auto-transform of the legacy QueryForm. */
|
|
11079
11519
|
const autoCoerce = (raw, defaultValue) => {
|
|
@@ -11236,6 +11676,26 @@ const defineQueryForm = (config) => {
|
|
|
11236
11676
|
}
|
|
11237
11677
|
return next;
|
|
11238
11678
|
};
|
|
11679
|
+
/**
|
|
11680
|
+
* Resets are transitive: a field this pass reset counts as changed in the next one, so
|
|
11681
|
+
* `country → league → team` clears `team` as well when only `country` moved. Passes repeat until
|
|
11682
|
+
* nothing moves, which has to happen before `committed` is written - one query execution for the
|
|
11683
|
+
* whole chain, not one per hop. The cap only guards a cyclic `isResetBy` graph; a field already at
|
|
11684
|
+
* its default stops triggering, so a well-formed graph settles in as many passes as it is deep.
|
|
11685
|
+
*/
|
|
11686
|
+
const resolveResets = (prev, live) => {
|
|
11687
|
+
let next = live;
|
|
11688
|
+
for (let pass = 0; pass < MAX_RESET_PASSES; pass++) {
|
|
11689
|
+
const applied = applyResets(next, changedKeysBetween(prev, next));
|
|
11690
|
+
if (equal(applied, next))
|
|
11691
|
+
return applied;
|
|
11692
|
+
next = applied;
|
|
11693
|
+
}
|
|
11694
|
+
if (isDevMode()) {
|
|
11695
|
+
console.warn(`defineQueryForm: isResetBy did not settle within ${MAX_RESET_PASSES} passes. Check for a cycle.`);
|
|
11696
|
+
}
|
|
11697
|
+
return next;
|
|
11698
|
+
};
|
|
11239
11699
|
const flush = () => {
|
|
11240
11700
|
clearTimer();
|
|
11241
11701
|
const live = model();
|
|
@@ -11244,8 +11704,7 @@ const defineQueryForm = (config) => {
|
|
|
11244
11704
|
skipNextResets = false;
|
|
11245
11705
|
return;
|
|
11246
11706
|
}
|
|
11247
|
-
const
|
|
11248
|
-
const next = skipNextResets ? { ...live } : applyResets(live, changedKeys);
|
|
11707
|
+
const next = skipNextResets ? { ...live } : resolveResets(prev, live);
|
|
11249
11708
|
skipNextResets = false;
|
|
11250
11709
|
previous.set(clone(prev));
|
|
11251
11710
|
committed.set(clone(next));
|
|
@@ -11589,12 +12048,15 @@ const createWebSocketClient = (options) => {
|
|
|
11589
12048
|
const join = (name) => {
|
|
11590
12049
|
emit({ event: 'join-room', data: name, room: name });
|
|
11591
12050
|
const existingRoom = rooms.get(name);
|
|
11592
|
-
if (existingRoom)
|
|
12051
|
+
if (existingRoom) {
|
|
12052
|
+
existingRoom.joinCount++;
|
|
11593
12053
|
return existingRoom;
|
|
12054
|
+
}
|
|
11594
12055
|
const message = signal(null, /* @ts-ignore */
|
|
11595
12056
|
...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
|
|
11596
12057
|
const newRoom = {
|
|
11597
12058
|
latestMessage: message,
|
|
12059
|
+
joinCount: 1,
|
|
11598
12060
|
};
|
|
11599
12061
|
rooms.set(name, newRoom);
|
|
11600
12062
|
syncDevtoolsRooms();
|
|
@@ -11628,10 +12090,15 @@ const createWebSocketClient = (options) => {
|
|
|
11628
12090
|
return roomData.asReadonly();
|
|
11629
12091
|
};
|
|
11630
12092
|
const leaveRoom = (room) => {
|
|
11631
|
-
|
|
12093
|
+
const joinedRoom = rooms.get(room);
|
|
12094
|
+
if (!joinedRoom) {
|
|
11632
12095
|
if (isDevMode())
|
|
11633
12096
|
throw roomNotJoined(room);
|
|
12097
|
+
return;
|
|
11634
12098
|
}
|
|
12099
|
+
joinedRoom.joinCount--;
|
|
12100
|
+
if (joinedRoom.joinCount > 0)
|
|
12101
|
+
return;
|
|
11635
12102
|
emit({ event: 'leave-room', data: room, room });
|
|
11636
12103
|
rooms.delete(room);
|
|
11637
12104
|
syncDevtoolsRooms();
|
|
@@ -11696,5 +12163,5 @@ const createWebSocketClient = (options) => {
|
|
|
11696
12163
|
* Generated bundle index. Do not edit.
|
|
11697
12164
|
*/
|
|
11698
12165
|
|
|
11699
|
-
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_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsFaults, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsFaultInjectionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsOverridesFactory, setQueryDevtoolsRegistrar, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
|
|
12166
|
+
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DEFAULT_KEEP_UNUSED_FOR, DEFAULT_QUERY_PERSISTENCE_MAX_AGE, DEFAULT_QUERY_PERSISTENCE_MAX_ENTRIES, DEFAULT_QUERY_PERSISTENCE_VERSION, DEFAULT_QUERY_PERSISTENCE_WRITE_DELAY, DateArrayQueryField, DateQueryField, EMPTY_QUERY_DEVTOOLS_FAULT, EntityStore, IGNORED_FILTER_COUNT_FIELDS, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, MAX_QUERY_DEVTOOLS_TOMBSTONES, MAX_UNUSED_ENTRIES, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QUERY_DEVTOOLS_FAULT_STATUSES, QUERY_PERSISTENCE_STORE_VERSION, QUERY_SYNC_PROTOCOL_VERSION, QueryClientFeatureType, QueryDevtoolsComponent, QueryDirective, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryDevtoolsOverrides, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, booleanArrayQueryField, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, clearQueryDevtoolsFaults, clearQueryDevtoolsTombstones, collectLeafPaths, collectQueryFormLinks, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, consumeSuppressQueryStackDevtools, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createIndexedDbQueryPersistenceAdapter, createInfinityQueryConfig, createLegacyQueryCreator, createNoopQueryPersistenceAdapter, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryDevtoolsFormLinks, createQueryDevtoolsFormLinksRecorder, createQueryDevtoolsOverrides, createQueryDevtoolsOverridesRecorder, createQueryDevtoolsStats, createQueryDevtoolsStatsRecorder, createQueryErrorResponse, createQueryFeature, createQueryInvalidationFilter, createQueryKeyLockManager, createQueryObject, createQueryPersistenceEngine, createQueryRepository, createQuerySnapshotFn, createQueryStack, createQuerySyncEngine, createQuerySyncTransport, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, dateArrayQueryField, dateQueryField, decryptBearer, deepFreeze, def, defineQueryForm, describeQueryDevtoolsFeatures, detectContentTypeHeader, detectPaginationShape, dynLikePaginationAdapter, effectComputed, emptyQueryArgs, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractHtmlErrorMessage, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, formatQueryDevtoolsDuration, generateQueryDevtoolsNumberPreset, generateQueryDevtoolsStringPreset, getBearerAuthProviderName, getDefaultHeaders, getQueryClientName, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, hasQueryDevtoolsOverridesAtPath, htmlErrorPayload, htmlQueryErrorParser, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isDateShapedLeaf, isEmptyString, isGqlQueryConfig, isHtmlErrorPayload, isLegacyQuery, isNaN$1 as isNaN, isPageOutOfRangeError, isQuery, isQueryCollection, isQueryDevtoolsEnabled, isQueryDevtoolsFaultArmed, isQueryDevtoolsFaultInjectionEnabled, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, isUnderInvalidatedUrl, legacyPrepareFallbackInjector, legacyPrepareWithoutInjectionContext, mapToPaginated, mapViolationsToFormErrors, maybeExecute, measureQueryDevtoolsPayload, mergeHeaders, nestedEffect, noteQueryFormRead, numberArrayQueryField, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, parseQueryRoute, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideLegacyPrepareFallback, provideQueryClientForDevtools, provideQueryDevtools, queryArrayComputed, queryClientFeatureUsedMultipleTimes, queryComputed, queryComputedTillTruthy, queryDevtoolsEntries, queryDevtoolsFaults, queryDevtoolsFnDetail, queryDevtoolsResponseHistory, queryErrorMessage, queryErrorMessages, queryExecute, queryFeatureUsedMultipleTimes, queryField, querySequence, querySequenceAlreadyRunning, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, registerQueryDevtoolsEntry, registerQueryErrorParser, removeFrom, request, resetExecuteState, resetPageOnError, resolveInvalidationUrl, resolveQueryDevtoolsFault, resolveQueryDevtoolsFaultForAttempt, runDefaultQueryRetry, runQueryErrorParsers, searchQueryField, serializeBody, setDefaultQueryRetryFn, setQueryDevtoolsFault, setQueryDevtoolsFaultResolver, setQueryDevtoolsFormLinksFactory, setQueryDevtoolsOverridesFactory, setQueryDevtoolsRegistrar, setQueryDevtoolsResponseHistory, setQueryDevtoolsStatsFactory, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, smartDuplicateArrayItem, snapshotQueryDevtoolsHandle, sortQueryField, splitQueryConfig, stringArrayQueryField, stringifyQueryRouteParts, sumQueryDevtoolsStats, suppressNextQueryStackDevtools, switchQueryCollectionState, switchQueryState, symfonyQueryErrorParser, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, tombstoneOf, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, unwrapQuerySyncMessage, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withBearerAuthMultiTabSync, withDefaultRetry, withErrorHandling, withEthleteApiErrors, withHtmlErrorParsing, withInactivityLogout, withLogging, withMultiTabSync, withPageResetOnError, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withQueryPersistence, withRefreshQuery, withResponseUpdate, withSuccessHandling, withSymfonyErrors, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal, wrapQuerySyncMessage };
|
|
11700
12167
|
//# sourceMappingURL=ethlete-query.mjs.map
|