@ethlete/query 6.0.0-next.10 → 6.0.0-next.11
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 +94 -70
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +28 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#2995](https://github.com/ethlete-io/ethdk/pull/2995) [`9038da9`](https://github.com/ethlete-io/ethdk/commit/9038da9dba1af6597ce24056f45354b4073673aa) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix `withSuccessHandling`, `withErrorHandling` and `withLogging` callbacks sometimes being skipped when a query re-executes in quick succession (e.g. polling, auto-refresh, or fast repeated requests). Each execution's result now reliably triggers its handler.
|
|
8
|
+
|
|
9
|
+
- [#2995](https://github.com/ethlete-io/ethdk/pull/2995) [`24b4fd7`](https://github.com/ethlete-io/ethdk/commit/24b4fd7e1aebda8a4844d4a8ec9700aa1a89a0ee) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix secure queries intermittently throwing `tokensNotAvailableInsideAuthAndExec` when the auth query completes on a different reactive timeline than the access token is set on (e.g. a cross-client / secure login query whose token is populated by a separate effect). The secure execute factory now waits for the access token to be available before running the request instead of assuming it is set the moment the auth query has a response, and recovers automatically once the token arrives.
|
|
10
|
+
|
|
3
11
|
## 6.0.0-next.10
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ErrorHandler,
|
|
2
|
+
import { isDevMode, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, ElementRef, input, Pipe, ViewEncapsulation, ChangeDetectionStrategy, Component, DOCUMENT, NgZone } from '@angular/core';
|
|
3
3
|
import { createStaticProvider, RuntimeError, clamp, createDestroy, computedTillTruthy, getCookie, setCookie, deleteCookie, createProvider, nextFrame, createRootProvider, isObject, injectRoute, getDomain, injectQueryParamChanges, injectQueryParams, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
4
|
-
import {
|
|
4
|
+
import { filter, Subject, Subscription, Observable, tap, retry, throwError, timer, catchError, takeUntil, startWith, pairwise, switchMap, of, BehaviorSubject, fromEvent, take, ReplaySubject, map, shareReplay, interval, skip, takeWhile, combineLatest, EMPTY, withLatestFrom, debounceTime, merge, throttleTime } from 'rxjs';
|
|
5
5
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { HttpClient, HttpEventType, HttpErrorResponse, HttpHeaders, HttpStatusCode } from '@angular/common/http';
|
|
7
7
|
import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/signals';
|
|
@@ -46,9 +46,6 @@ const createGqlExecuteFn = (executeOptions) => {
|
|
|
46
46
|
const reset = () => resetExecuteState({ executeState, executeOptions });
|
|
47
47
|
const exec = (executeArgs) => {
|
|
48
48
|
circularChecker.check();
|
|
49
|
-
// Make sure all effects are flushed before reading the args.
|
|
50
|
-
// A withArgs feature effect might still be pending otherwise resulting in the wrong args being read.
|
|
51
|
-
executeOptions.deps.effectScheduler.flush();
|
|
52
49
|
const { args = executeOptions.state.args(), options } = executeArgs ?? {};
|
|
53
50
|
const query = transformGql(executeOptions.creatorInternals.query);
|
|
54
51
|
let gqlParams = query(args?.variables);
|
|
@@ -133,7 +130,6 @@ const setupQueryDependencies = (options) => {
|
|
|
133
130
|
scopeDestroyRef: hostInjector.get(DestroyRef),
|
|
134
131
|
client: hostInjector.get(clientToken),
|
|
135
132
|
injector: undefined, // Will be set after injector creation
|
|
136
|
-
effectScheduler: hostInjector.get(_EffectScheduler),
|
|
137
133
|
ngErrorHandler: hostInjector.get(ErrorHandler),
|
|
138
134
|
httpClient: hostInjector.get(HttpClient),
|
|
139
135
|
};
|
|
@@ -307,17 +303,27 @@ const withArgs = (args) => {
|
|
|
307
303
|
return createQueryFeature({
|
|
308
304
|
type: QueryFeatureType.WITH_ARGS,
|
|
309
305
|
fn: (context) => {
|
|
310
|
-
|
|
306
|
+
// `state.args` is reactively backed by this source, so every reader (exec, polling,
|
|
307
|
+
// auto-refresh) always sees the latest value with no pending-effect window. Sentinels are
|
|
308
|
+
// resolved here: CLEAR -> null, `null` ("don't change") -> keep the previous value.
|
|
309
|
+
let previous = null;
|
|
310
|
+
context.state.subtle.setArgsSource(() => {
|
|
311
|
+
const value = args();
|
|
312
|
+
if (value === CLEAR_QUERY_ARGS) {
|
|
313
|
+
previous = null;
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (value === null)
|
|
317
|
+
return previous;
|
|
318
|
+
previous = value;
|
|
319
|
+
return value;
|
|
320
|
+
});
|
|
321
|
+
// Trigger (auto-)execution whenever the resolved args change.
|
|
311
322
|
nestedEffect(() => {
|
|
312
|
-
const currArgsNow =
|
|
323
|
+
const currArgsNow = context.state.args();
|
|
313
324
|
if (currArgsNow === null)
|
|
314
325
|
return;
|
|
315
326
|
untracked(() => {
|
|
316
|
-
if (currArgsNow === CLEAR_QUERY_ARGS) {
|
|
317
|
-
context.state.args.set(null);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
context.state.args.set(currArgsNow);
|
|
321
327
|
if (context.flags.shouldAutoExecute)
|
|
322
328
|
context.execute({ args: currArgsNow });
|
|
323
329
|
});
|
|
@@ -366,14 +372,9 @@ const withLogging = (options) => {
|
|
|
366
372
|
return createQueryFeature({
|
|
367
373
|
type: QueryFeatureType.WITH_LOGGING,
|
|
368
374
|
fn: (context) => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return;
|
|
373
|
-
untracked(() => {
|
|
374
|
-
options.logFn(event);
|
|
375
|
-
});
|
|
376
|
-
}, { injector: context.deps.injector });
|
|
375
|
+
context.state.events$.pipe(takeUntilDestroyed(context.deps.destroyRef)).subscribe((event) => {
|
|
376
|
+
options.logFn(event);
|
|
377
|
+
});
|
|
377
378
|
},
|
|
378
379
|
});
|
|
379
380
|
};
|
|
@@ -382,14 +383,11 @@ const withErrorHandling = (options) => {
|
|
|
382
383
|
return createQueryFeature({
|
|
383
384
|
type: QueryFeatureType.WITH_ERROR_HANDLING,
|
|
384
385
|
fn: (context) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
options.handler(error);
|
|
391
|
-
});
|
|
392
|
-
}, { injector: context.deps.injector });
|
|
386
|
+
context.state.events$
|
|
387
|
+
.pipe(filter((event) => event.type === 'error'), takeUntilDestroyed(context.deps.destroyRef))
|
|
388
|
+
.subscribe((event) => {
|
|
389
|
+
options.handler(event.error);
|
|
390
|
+
});
|
|
393
391
|
},
|
|
394
392
|
});
|
|
395
393
|
};
|
|
@@ -398,14 +396,11 @@ const withSuccessHandling = (options) => {
|
|
|
398
396
|
return createQueryFeature({
|
|
399
397
|
type: QueryFeatureType.WITH_SUCCESS_HANDLING,
|
|
400
398
|
fn: (context) => {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
options.handler(context.state.response());
|
|
407
|
-
});
|
|
408
|
-
}, { injector: context.deps.injector });
|
|
399
|
+
context.state.events$
|
|
400
|
+
.pipe(filter((event) => event.type === HttpEventType.Response), takeUntilDestroyed(context.deps.destroyRef))
|
|
401
|
+
.subscribe(() => {
|
|
402
|
+
options.handler(context.state.response());
|
|
403
|
+
});
|
|
409
404
|
},
|
|
410
405
|
});
|
|
411
406
|
};
|
|
@@ -469,9 +464,21 @@ const setupQueryState = (options) => {
|
|
|
469
464
|
const loading = linkedSignal(() => request()?.loading() ?? null, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
470
465
|
const error = linkedSignal(() => request()?.error() ?? null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
471
466
|
const latestHttpEvent = linkedSignal(() => request()?.currentEvent() ?? null, ...(ngDevMode ? [{ debugName: "latestHttpEvent" }] : /* istanbul ignore next */ []));
|
|
472
|
-
|
|
467
|
+
// `args` is reactively sourced: reads always reflect the latest value from the installed source
|
|
468
|
+
// (e.g. the `withArgs` feature), so there is no stale window between a source change and an
|
|
469
|
+
// effect writing it. It stays writable (reset / auth-error clear it via `.set(null)`); a manual
|
|
470
|
+
// set is overridden again the next time the source's dependencies change.
|
|
471
|
+
const argsSource = signal(() => null, ...(ngDevMode ? [{ debugName: "argsSource" }] : /* istanbul ignore next */ []));
|
|
472
|
+
const args = linkedSignal(() => argsSource()(), ...(ngDevMode ? [{ debugName: "args" }] : /* istanbul ignore next */ []));
|
|
473
|
+
const setArgsSource = (source) => argsSource.set(source);
|
|
473
474
|
const lastTimeExecutedAt = signal(null, ...(ngDevMode ? [{ debugName: "lastTimeExecutedAt" }] : /* istanbul ignore next */ []));
|
|
474
475
|
const lastTriggeredBy = signal(null, ...(ngDevMode ? [{ debugName: "lastTriggeredBy" }] : /* istanbul ignore next */ []));
|
|
476
|
+
const requestEvents$ = new Subject();
|
|
477
|
+
let requestEventsSubscription = Subscription.EMPTY;
|
|
478
|
+
const bindRequestEvents = (request) => {
|
|
479
|
+
requestEventsSubscription.unsubscribe();
|
|
480
|
+
requestEventsSubscription = request.events$.subscribe((event) => requestEvents$.next(event));
|
|
481
|
+
};
|
|
475
482
|
const executionState = computed(() => {
|
|
476
483
|
const currentResponse = response();
|
|
477
484
|
const currentError = error();
|
|
@@ -517,9 +524,12 @@ const setupQueryState = (options) => {
|
|
|
517
524
|
lastTimeExecutedAt,
|
|
518
525
|
lastTriggeredBy,
|
|
519
526
|
executionState,
|
|
527
|
+
events$: requestEvents$.asObservable(),
|
|
520
528
|
subtle: {
|
|
521
529
|
request,
|
|
522
530
|
rawResponse,
|
|
531
|
+
bindRequestEvents,
|
|
532
|
+
setArgsSource,
|
|
523
533
|
},
|
|
524
534
|
};
|
|
525
535
|
return state;
|
|
@@ -746,6 +756,7 @@ const queryExecute = (options) => {
|
|
|
746
756
|
state.lastTimeExecutedAt.set(Date.now());
|
|
747
757
|
state.lastTriggeredBy.set(runQueryOptions?.triggeredBy ?? null);
|
|
748
758
|
state.subtle.request.set(request);
|
|
759
|
+
state.subtle.bindRequestEvents(request);
|
|
749
760
|
};
|
|
750
761
|
const circularQueryDependencyChecker = () => {
|
|
751
762
|
let lastTriggerTs = 0;
|
|
@@ -779,9 +790,6 @@ const createExecuteFn = (executeOptions) => {
|
|
|
779
790
|
const reset = () => resetExecuteState({ executeState, executeOptions });
|
|
780
791
|
const exec = (executeArgs) => {
|
|
781
792
|
circularChecker.check();
|
|
782
|
-
// Make sure all effects are flushed before reading the args.
|
|
783
|
-
// A withArgs feature effect might still be pending otherwise resulting in the wrong args being read.
|
|
784
|
-
executeOptions.deps.effectScheduler.flush();
|
|
785
793
|
const { args = executeOptions.state.args(), options } = executeArgs ?? {};
|
|
786
794
|
queryExecute({ executeOptions, executeState, args, options });
|
|
787
795
|
};
|
|
@@ -1540,6 +1548,7 @@ const createHttpRequest = (options) => {
|
|
|
1540
1548
|
let currentStreamSubscription = Subscription.EMPTY;
|
|
1541
1549
|
const { args, clientOptions, dependencies } = options;
|
|
1542
1550
|
const currentEvent = signal(null, ...(ngDevMode ? [{ debugName: "currentEvent" }] : /* istanbul ignore next */ []));
|
|
1551
|
+
const event$ = new Subject();
|
|
1543
1552
|
const loading = signal(null, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1544
1553
|
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1545
1554
|
const response = signal(null, ...(ngDevMode ? [{ debugName: "response" }] : /* istanbul ignore next */ []));
|
|
@@ -1601,11 +1610,10 @@ const createHttpRequest = (options) => {
|
|
|
1601
1610
|
return true;
|
|
1602
1611
|
};
|
|
1603
1612
|
const destroy = () => {
|
|
1604
|
-
|
|
1605
|
-
return false;
|
|
1606
|
-
}
|
|
1613
|
+
const wasActive = !currentStreamSubscription.closed;
|
|
1607
1614
|
currentStreamSubscription.unsubscribe();
|
|
1608
|
-
|
|
1615
|
+
event$.complete();
|
|
1616
|
+
return wasActive;
|
|
1609
1617
|
};
|
|
1610
1618
|
const updateState = (event) => {
|
|
1611
1619
|
switch (event.type) {
|
|
@@ -1635,12 +1643,15 @@ const createHttpRequest = (options) => {
|
|
|
1635
1643
|
break;
|
|
1636
1644
|
}
|
|
1637
1645
|
currentEvent.set(event);
|
|
1646
|
+
event$.next(event);
|
|
1638
1647
|
};
|
|
1639
1648
|
const updateErrorState = (errorResponse) => {
|
|
1640
1649
|
const errorRes = createQueryErrorResponse(errorResponse);
|
|
1641
1650
|
error.set(errorRes);
|
|
1642
1651
|
loading.set(null);
|
|
1643
|
-
|
|
1652
|
+
const errorEvent = { type: 'error', error: errorRes };
|
|
1653
|
+
currentEvent.set(errorEvent);
|
|
1654
|
+
event$.next(errorEvent);
|
|
1644
1655
|
options.dependencies.ngErrorHandler.handleError(errorRes.raw);
|
|
1645
1656
|
};
|
|
1646
1657
|
const updateLoadingState = (event) => {
|
|
@@ -1680,6 +1691,7 @@ const createHttpRequest = (options) => {
|
|
|
1680
1691
|
error: error.asReadonly(),
|
|
1681
1692
|
response: response.asReadonly(),
|
|
1682
1693
|
currentEvent: currentEvent.asReadonly(),
|
|
1694
|
+
events$: event$.asObservable(),
|
|
1683
1695
|
isStale,
|
|
1684
1696
|
};
|
|
1685
1697
|
return httpRequest;
|
|
@@ -2623,11 +2635,14 @@ const createSecureExecuteFactory = (options) => {
|
|
|
2623
2635
|
const circularChecker = circularQueryDependencyChecker();
|
|
2624
2636
|
let authQuerySubscription = Subscription.EMPTY;
|
|
2625
2637
|
let tokenRefreshSubscription = Subscription.EMPTY;
|
|
2638
|
+
let tokenWaitSubscription = Subscription.EMPTY;
|
|
2626
2639
|
const reset = () => {
|
|
2627
2640
|
authQuerySubscription.unsubscribe();
|
|
2628
2641
|
authQuerySubscription = Subscription.EMPTY;
|
|
2629
2642
|
tokenRefreshSubscription.unsubscribe();
|
|
2630
2643
|
tokenRefreshSubscription = Subscription.EMPTY;
|
|
2644
|
+
tokenWaitSubscription.unsubscribe();
|
|
2645
|
+
tokenWaitSubscription = Subscription.EMPTY;
|
|
2631
2646
|
resetExecuteState({
|
|
2632
2647
|
executeState,
|
|
2633
2648
|
executeOptions: { deps: options.deps, state: options.state },
|
|
@@ -2668,11 +2683,27 @@ const createSecureExecuteFactory = (options) => {
|
|
|
2668
2683
|
},
|
|
2669
2684
|
}, tokens, headers, executeState);
|
|
2670
2685
|
};
|
|
2686
|
+
// The auth query completing and the access token being populated happen on two
|
|
2687
|
+
// different reactive timelines: the token is set by a separate token-extraction
|
|
2688
|
+
// effect (see `setupBearerQueryRegistry`), and for cross-client / secure auth
|
|
2689
|
+
// queries the completion is delivered through several nested `toObservable` hops.
|
|
2690
|
+
// Calling `authAndExec` purely because the auth query is "done" therefore races
|
|
2691
|
+
// the token and can run while it is still null. Gate on the token signal instead:
|
|
2692
|
+
// execute immediately if it is already available, otherwise wait for it.
|
|
2693
|
+
const authAndExecWhenTokenReady = (executeArgs) => {
|
|
2694
|
+
if (options.authProvider.accessToken()) {
|
|
2695
|
+
authAndExec(executeArgs);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
tokenWaitSubscription.unsubscribe();
|
|
2699
|
+
tokenWaitSubscription = toObservable(options.authProvider.accessToken, {
|
|
2700
|
+
injector: options.deps.injector,
|
|
2701
|
+
})
|
|
2702
|
+
.pipe(filter(Boolean), take(1), tap(() => authAndExec(executeArgs)))
|
|
2703
|
+
.subscribe();
|
|
2704
|
+
};
|
|
2671
2705
|
const exec = (executeArgs) => {
|
|
2672
2706
|
circularChecker.check();
|
|
2673
|
-
// Make sure all effects are flushed before reading the args.
|
|
2674
|
-
// A withArgs feature effect might still be pending otherwise resulting in the wrong args being read.
|
|
2675
|
-
options.deps.effectScheduler.flush();
|
|
2676
2707
|
const execArgsWithDefaults = {
|
|
2677
2708
|
args: executeArgs?.args ?? options.state.args(),
|
|
2678
2709
|
options: executeArgs?.options,
|
|
@@ -2681,6 +2712,8 @@ const createSecureExecuteFactory = (options) => {
|
|
|
2681
2712
|
authQuerySubscription = Subscription.EMPTY;
|
|
2682
2713
|
tokenRefreshSubscription.unsubscribe();
|
|
2683
2714
|
tokenRefreshSubscription = Subscription.EMPTY;
|
|
2715
|
+
tokenWaitSubscription.unsubscribe();
|
|
2716
|
+
tokenWaitSubscription = Subscription.EMPTY;
|
|
2684
2717
|
tokenRefreshSubscription = options.authProvider.afterTokenRefresh$
|
|
2685
2718
|
.pipe(filter(() => {
|
|
2686
2719
|
const currentError = options.state.error();
|
|
@@ -2708,14 +2741,14 @@ const createSecureExecuteFactory = (options) => {
|
|
|
2708
2741
|
error(query);
|
|
2709
2742
|
}
|
|
2710
2743
|
else if (query.response()) {
|
|
2711
|
-
|
|
2744
|
+
authAndExecWhenTokenReady(execArgsWithDefaults);
|
|
2712
2745
|
}
|
|
2713
2746
|
}));
|
|
2714
2747
|
}))
|
|
2715
2748
|
.subscribe();
|
|
2716
2749
|
}
|
|
2717
2750
|
else if (authQuery.response()) {
|
|
2718
|
-
|
|
2751
|
+
authAndExecWhenTokenReady(execArgsWithDefaults);
|
|
2719
2752
|
}
|
|
2720
2753
|
else if (authQuery.error()) {
|
|
2721
2754
|
error(authQuery);
|
|
@@ -5041,26 +5074,17 @@ const createQueryRepository = (config) => {
|
|
|
5041
5074
|
else {
|
|
5042
5075
|
const consumers = new Map([]);
|
|
5043
5076
|
consumers.set(consumerDestroyRef, destroyListener);
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
if (
|
|
5049
|
-
|
|
5050
|
-
type: 'request-error',
|
|
5051
|
-
|
|
5052
|
-
key,
|
|
5053
|
-
isSecure,
|
|
5054
|
-
request,
|
|
5055
|
-
});
|
|
5077
|
+
// Drive repository events off the request's discrete, terminal event stream rather than
|
|
5078
|
+
// recombining the `error` + `response` signals via combineLatest (which could observe stale
|
|
5079
|
+
// pairings). Each settle emits exactly one terminal event.
|
|
5080
|
+
const eventSubscription = request.events$.subscribe((event) => {
|
|
5081
|
+
if (event.type === 'error') {
|
|
5082
|
+
if (event.error.raw instanceof HttpErrorResponse) {
|
|
5083
|
+
eventsSubject.next({ type: 'request-error', error: event.error.raw, key, isSecure, request });
|
|
5084
|
+
}
|
|
5056
5085
|
}
|
|
5057
|
-
else if (
|
|
5058
|
-
eventsSubject.next({
|
|
5059
|
-
type: 'request-success',
|
|
5060
|
-
key,
|
|
5061
|
-
isSecure,
|
|
5062
|
-
request,
|
|
5063
|
-
});
|
|
5086
|
+
else if (event.type === HttpEventType.Response) {
|
|
5087
|
+
eventsSubject.next({ type: 'request-success', key, isSecure, request });
|
|
5064
5088
|
}
|
|
5065
5089
|
});
|
|
5066
5090
|
cache.set(key, {
|