@ethlete/query 6.0.0-next.43 → 6.0.0-next.44
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 +8 -0
- package/fesm2022/ethlete-query.mjs +48 -7
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.44
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#3077](https://github.com/ethlete-io/ethdk/pull/3077) [`8c2cb3a`](https://github.com/ethlete-io/ethdk/commit/8c2cb3a7b8a311363255d66b0f3c07fcfc4f6bdb) Thanks [@github-actions](https://github.com/apps/github-actions)! - Auth: `onRefreshFailure` and the `logout()` it triggers now run outside the reactive context of the effect that detects the failure. A consumer reacting to the session ending could otherwise throw `NG0602` instead of handling the logout.
|
|
8
|
+
|
|
9
|
+
- [`f7d6bb1`](https://github.com/ethlete-io/ethdk/commit/f7d6bb12a799b680ce72c90cbdcfd96ae3ea2fc0) Thanks [@TomTomB](https://github.com/TomTomB)! - A secure query now waits for the token refresh instead of sending an access token that is already expired, so a session seeded by SSO no longer costs a `401` and a retry.
|
|
10
|
+
|
|
3
11
|
## 6.0.0-next.43
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, Injector, effect, DestroyRef, InjectionToken, EnvironmentInjector, ElementRef, ErrorHandler, createEnvironmentInjector, untracked, signal, linkedSignal, computed, runInInjectionContext, isDevMode, PLATFORM_ID, resource, DOCUMENT, VERSION, makeEnvironmentProviders, provideEnvironmentInitializer, ApplicationRef, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, input, Pipe, ViewEncapsulation, Component, NgZone } from '@angular/core';
|
|
3
3
|
import { UrlTree, Router, RedirectCommand, ActivatedRoute } from '@angular/router';
|
|
4
|
-
import { Observable, defer, from, filter, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, EMPTY as EMPTY$1, take, takeUntil, finalize, mergeMap, toArray, map, merge, takeWhile, firstValueFrom,
|
|
4
|
+
import { Observable, defer, from, filter, Subject, Subscription, throwError, of, timer, switchMap, tap, retry, catchError, EMPTY as EMPTY$1, take, takeUntil, finalize, mergeMap, toArray, map, race, NEVER, merge, takeWhile, firstValueFrom, concatMap, fromEvent, throttleTime, interval, startWith, pairwise, BehaviorSubject, combineLatest, ReplaySubject, shareReplay, skip, withLatestFrom, debounceTime } from 'rxjs';
|
|
5
5
|
import { defineStaticProvider, RuntimeError, clamp, randomId, defineRootProvider, isObject, injectUnsavedChangesCoordinator, injectRoute, getCookie, deleteCookie, setCookie, CORE_VERSION, createDestroy, computedTillTruthy, defineProvider, toProvideFn, toInjectFn, nextFrame, injectQueryParams, injectQueryParamChanges, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
6
6
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
7
7
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
@@ -4389,6 +4389,12 @@ const createQueryClient = (options) => defineRootProvider(() => {
|
|
|
4389
4389
|
});
|
|
4390
4390
|
|
|
4391
4391
|
const AUTH_HEADER = 'Authorization';
|
|
4392
|
+
/**
|
|
4393
|
+
* How long a request waits for the refresh of an expired access token before it sends the expired one
|
|
4394
|
+
* anyway. Nothing guarantees the refresh arrives - the refresh token can be spent, and the request can
|
|
4395
|
+
* fail or never come back - and a request that 401s and retries beats one that never goes out.
|
|
4396
|
+
*/
|
|
4397
|
+
const EXPIRED_TOKEN_WAIT_MS = 5000;
|
|
4392
4398
|
const createSecureExecuteFactory = (options) => {
|
|
4393
4399
|
const executeState = setupQueryExecuteState();
|
|
4394
4400
|
const circularChecker = circularQueryDependencyChecker();
|
|
@@ -4479,16 +4485,25 @@ const createSecureExecuteFactory = (options) => {
|
|
|
4479
4485
|
// Calling `authAndExec` purely because the auth query is "done" therefore races
|
|
4480
4486
|
// the token and can run while it is still null. Gate on the token signal instead:
|
|
4481
4487
|
// execute immediately if it is already available, otherwise wait for it.
|
|
4488
|
+
//
|
|
4489
|
+
// A token that is present but already expired is no better than a missing one: it can only earn a
|
|
4490
|
+
// 401, and the refresh that answers that 401 is the one already in flight (a token seed hands over
|
|
4491
|
+
// an expired access token, and the refresh query's schedule fires on it at once). Waiting for it
|
|
4492
|
+
// spends one round trip instead of two.
|
|
4482
4493
|
const authAndExecWhenTokenReady = (executeArgs) => {
|
|
4483
|
-
|
|
4494
|
+
const tokenAtWait = options.authProvider.accessToken();
|
|
4495
|
+
if (tokenAtWait && !options.authProvider.isAccessTokenExpired()) {
|
|
4484
4496
|
authAndExec(executeArgs);
|
|
4485
4497
|
return;
|
|
4486
4498
|
}
|
|
4499
|
+
// The query is on its way out, not idle - without this a consumer renders an empty state for as
|
|
4500
|
+
// long as the wait lasts.
|
|
4501
|
+
options.state.loading.set({ executeTime: Date.now(), progress: null });
|
|
4487
4502
|
tokenWaitSubscription.unsubscribe();
|
|
4488
|
-
tokenWaitSubscription = toObservable(options.authProvider.accessToken, {
|
|
4503
|
+
tokenWaitSubscription = race(toObservable(options.authProvider.accessToken, {
|
|
4489
4504
|
injector: options.deps.injector,
|
|
4490
|
-
})
|
|
4491
|
-
.pipe(
|
|
4505
|
+
}).pipe(filter((token) => !!token && token !== tokenAtWait)), tokenAtWait ? timer(EXPIRED_TOKEN_WAIT_MS) : NEVER)
|
|
4506
|
+
.pipe(take(1), tap(() => authAndExec(executeArgs)))
|
|
4492
4507
|
.subscribe();
|
|
4493
4508
|
};
|
|
4494
4509
|
const exec = (executeArgs) => {
|
|
@@ -5331,6 +5346,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
5331
5346
|
sessionEndCause: sessionEndCause.asReadonly(),
|
|
5332
5347
|
isTabLocalSession: isTabLocalSession.asReadonly(),
|
|
5333
5348
|
});
|
|
5349
|
+
let readAccessTokenExpiry = null;
|
|
5334
5350
|
const querySetupContext = {
|
|
5335
5351
|
name: config.name,
|
|
5336
5352
|
accessToken,
|
|
@@ -5346,6 +5362,9 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
5346
5362
|
latestExecutedQuery: latestExecutedQuery.asReadonly(),
|
|
5347
5363
|
executionState: executionState.asReadonly(),
|
|
5348
5364
|
hasTokenIssuingExecutionInFlight,
|
|
5365
|
+
reportAccessTokenExpiry: (isExpired) => {
|
|
5366
|
+
readAccessTokenExpiry = isExpired;
|
|
5367
|
+
},
|
|
5349
5368
|
};
|
|
5350
5369
|
for (const builder of config.queries) {
|
|
5351
5370
|
builder.setup?.(querySetupContext);
|
|
@@ -5396,6 +5415,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
5396
5415
|
refreshToken: refreshToken.asReadonly(),
|
|
5397
5416
|
bearerData,
|
|
5398
5417
|
isAuthenticated,
|
|
5418
|
+
isAccessTokenExpired: () => readAccessTokenExpiry?.() ?? false,
|
|
5399
5419
|
latestExecutedQuery: latestExecutedQuery.asReadonly(),
|
|
5400
5420
|
latestNonInternalQuery: latestNonInternalQuery.asReadonly(),
|
|
5401
5421
|
executionState: executionState.asReadonly(),
|
|
@@ -5751,6 +5771,24 @@ const withRefreshQuery = (key, config) => {
|
|
|
5751
5771
|
const remainingMs = msUntilAccessTokenIsStale();
|
|
5752
5772
|
return remainingMs !== null && remainingMs <= 0;
|
|
5753
5773
|
};
|
|
5774
|
+
/**
|
|
5775
|
+
* Whether the access token this tab holds is already past its expiry, so sending it can only earn
|
|
5776
|
+
* a 401. Unlike {@link isAccessTokenStale} this has no grace period: a token with seconds of life
|
|
5777
|
+
* left is still worth sending.
|
|
5778
|
+
*/
|
|
5779
|
+
const isAccessTokenExpired = () => {
|
|
5780
|
+
const token = context.accessToken();
|
|
5781
|
+
if (!token)
|
|
5782
|
+
return false;
|
|
5783
|
+
const expiresAt = tokenExpiresAt(token);
|
|
5784
|
+
return expiresAt !== null && expiresAt <= Date.now();
|
|
5785
|
+
};
|
|
5786
|
+
// Only reported when an expired token actually gets refreshed. With `refreshIfExpired: false` the
|
|
5787
|
+
// application asked for the request to go out and fail, so a secure query must not wait for a
|
|
5788
|
+
// refresh that never comes.
|
|
5789
|
+
if (refreshIfExpired) {
|
|
5790
|
+
context.reportAccessTokenExpiry(isAccessTokenExpired);
|
|
5791
|
+
}
|
|
5754
5792
|
/**
|
|
5755
5793
|
* The scheduled refresh, re-armed for as long as it keeps declining to spend the refresh token.
|
|
5756
5794
|
* Without the re-arm a single declined attempt strands the session on a token nothing renews until
|
|
@@ -5822,7 +5860,10 @@ const withRefreshQuery = (key, config) => {
|
|
|
5822
5860
|
!untracked(context.accessToken);
|
|
5823
5861
|
if (state.type !== 'tokenRefresh' && !isRejectedRestore)
|
|
5824
5862
|
return;
|
|
5825
|
-
|
|
5863
|
+
// `logout` tears down every secure cache entry and emits on `events$`, so a consumer that reacts
|
|
5864
|
+
// to a session ending runs inside this effect. Angular refuses `effect()` from a reactive context,
|
|
5865
|
+
// so a listener that creates a query would throw NG0602 instead of handling the logout.
|
|
5866
|
+
untracked(() => onRefreshFailure({ error: state.error, logout: () => context.logout('expired') }));
|
|
5826
5867
|
});
|
|
5827
5868
|
// Auto-retry on 401: Listen to repository events and trigger refresh on 401 errors
|
|
5828
5869
|
const autoRetryOn401 = config.autoRetryOn401 ?? true;
|
|
@@ -9621,7 +9662,7 @@ const armQueryDevtoolsOverrideTransfer = (recorder, ops) => {
|
|
|
9621
9662
|
|
|
9622
9663
|
// Generated from package.json by tools/scripts/generate-version.js - do not edit.
|
|
9623
9664
|
/** The version of `@ethlete/query` this build was cut from. */
|
|
9624
|
-
const QUERY_VERSION = '6.0.0-next.
|
|
9665
|
+
const QUERY_VERSION = '6.0.0-next.44';
|
|
9625
9666
|
|
|
9626
9667
|
/** How deep a schema is expanded before a nested branch is given up on. */
|
|
9627
9668
|
const MAX_DEPTH = 12;
|