ngx-signal-query 1.5.0 → 1.5.1-dev.2
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/fesm2022/ngx-signal-query.mjs +187 -93
- package/fesm2022/ngx-signal-query.mjs.map +1 -1
- package/index.d.ts +46 -9
- package/package.json +1 -1
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, PLATFORM_ID, signal, Injectable, InjectionToken, EnvironmentInjector, DestroyRef, untracked, runInInjectionContext, makeEnvironmentProviders, assertInInjectionContext, effect, computed, Injector } from '@angular/core';
|
|
3
|
-
import {
|
|
3
|
+
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
4
4
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
-
import { fromEvent, filter, defer, from, take, retry, timer, throwIfEmpty, map,
|
|
5
|
+
import { fromEvent, filter, Subscription, defer, isObservable, from, take, retry, timer, tap, throwIfEmpty, map, concatMap, catchError, of, endWith, concat, ignoreElements, EMPTY } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether the app runs in the browser rather than on the server. On the
|
|
9
|
+
* server, DOCUMENT is an emulated document that has listeners, a window and
|
|
10
|
+
* no visibilityState: only the platform tells. Must run in an injection
|
|
11
|
+
* context.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
function inBrowser() {
|
|
16
|
+
return isPlatformBrowser(inject(PLATFORM_ID));
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
20
|
* Calls `listener` whenever the user comes back to the page, as the tab or
|
|
@@ -44,11 +56,6 @@ function onReconnect(listener) {
|
|
|
44
56
|
.pipe(takeUntilDestroyed())
|
|
45
57
|
.subscribe(() => listener());
|
|
46
58
|
}
|
|
47
|
-
// On the server, DOCUMENT is an emulated document that has listeners,
|
|
48
|
-
// a window and no visibilityState: only the platform tells.
|
|
49
|
-
function inBrowser() {
|
|
50
|
-
return isPlatformBrowser(inject(PLATFORM_ID));
|
|
51
|
-
}
|
|
52
59
|
|
|
53
60
|
/** @internal */
|
|
54
61
|
class Cache {
|
|
@@ -252,9 +259,10 @@ function deferred() {
|
|
|
252
259
|
/**
|
|
253
260
|
* The error a promise of a query's data rejects with when the fetch stops
|
|
254
261
|
* with no data to give: it was cancelled before the query had any
|
|
255
|
-
* (`cancelQueries()`, or the last component showing the query went away
|
|
256
|
-
*
|
|
257
|
-
* didn't fail, so tell
|
|
262
|
+
* (`cancelQueries()`, or the last component showing the query went away
|
|
263
|
+
* during an `Observable` fetch), or the query was removed
|
|
264
|
+
* (`removeQueries()`, e.g. on sign-out). The request didn't fail, so tell
|
|
265
|
+
* the two apart with `instanceof CancelledError`.
|
|
258
266
|
*
|
|
259
267
|
* A mutation's `mutateAsync()` rejects with one too, when the mutation cache
|
|
260
268
|
* is cleared while its run is in flight.
|
|
@@ -286,7 +294,6 @@ function resolveRetryDelay(retryDelay, failureCount, error) {
|
|
|
286
294
|
: retryDelay;
|
|
287
295
|
}
|
|
288
296
|
|
|
289
|
-
const DEFAULT_GC_TIME = 5 * 60 * 1000;
|
|
290
297
|
/** @internal */
|
|
291
298
|
class Query {
|
|
292
299
|
key;
|
|
@@ -305,30 +312,32 @@ class Query {
|
|
|
305
312
|
// field it reads during initialization (field init order).
|
|
306
313
|
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
307
314
|
state = this.#state.asReadonly();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// fetch shares, and how to settle it; null while nothing is in flight.
|
|
311
|
-
#outcome = null;
|
|
312
|
-
// The state the fetch in flight started from, or the last write made
|
|
313
|
-
// during it: stopping the fetch with nothing in its place goes back to it
|
|
314
|
-
// (see #stop). null while nothing is in flight.
|
|
315
|
-
#beforeFetch = null;
|
|
315
|
+
// The fetch in flight; null while nothing is.
|
|
316
|
+
#inFlight = null;
|
|
316
317
|
// The structuralSharing of the fetch whose response the query holds.
|
|
317
318
|
#fetchedStructuralSharing;
|
|
318
319
|
#observers = new Set();
|
|
319
|
-
#gcTime =
|
|
320
|
+
#gcTime = 0;
|
|
320
321
|
#gcTimer = null;
|
|
321
322
|
#cache;
|
|
322
|
-
constructor(key, queryHash, cache) {
|
|
323
|
+
constructor({ key, queryHash, cache, gcTime }) {
|
|
323
324
|
this.key = key;
|
|
324
325
|
this.queryHash = queryHash;
|
|
325
326
|
this.#cache = cache;
|
|
327
|
+
this.updateGcTime(gcTime);
|
|
328
|
+
// A new query has nothing using it yet: its creator is about to, and if
|
|
329
|
+
// nothing ever does, gc removes it.
|
|
330
|
+
this.#scheduleGc();
|
|
326
331
|
}
|
|
327
332
|
get observerCount() {
|
|
328
333
|
return this.#observers.size;
|
|
329
334
|
}
|
|
330
|
-
|
|
331
|
-
|
|
335
|
+
// Keeps the longest gcTime asked for by anything that used the query, as
|
|
336
|
+
// in TanStack: whoever created it, each component that showed it, each
|
|
337
|
+
// fetchQuery. A shorter one doesn't cut short a longer one asked for
|
|
338
|
+
// before. Never below 0.
|
|
339
|
+
updateGcTime(ms) {
|
|
340
|
+
this.#gcTime = Math.max(this.#gcTime, ms);
|
|
332
341
|
}
|
|
333
342
|
addObserver(observer) {
|
|
334
343
|
this.#observers.add(observer);
|
|
@@ -339,10 +348,16 @@ class Query {
|
|
|
339
348
|
// the fetch or schedule gc while others still watch.
|
|
340
349
|
if (!this.#observers.delete(observer))
|
|
341
350
|
return;
|
|
342
|
-
// No observers left:
|
|
343
|
-
//
|
|
351
|
+
// No observers left: nobody waits for the fetch in flight any more. Stop
|
|
352
|
+
// it if that aborts its request; a request that runs on anyway is let
|
|
353
|
+
// land, so its response isn't thrown away, but nobody is left to want a
|
|
354
|
+
// failure tried again. Then schedule gc to dispose the query if no
|
|
355
|
+
// observer returns.
|
|
344
356
|
if (this.#observers.size === 0) {
|
|
345
|
-
this
|
|
357
|
+
if (this.#inFlight?.abortable)
|
|
358
|
+
this.cancel();
|
|
359
|
+
else if (this.#inFlight)
|
|
360
|
+
this.#inFlight.retriesStopped = true;
|
|
346
361
|
this.#scheduleGc();
|
|
347
362
|
}
|
|
348
363
|
}
|
|
@@ -386,13 +401,18 @@ class Query {
|
|
|
386
401
|
// with its own outcome; one stopped with nothing in its place settles it as
|
|
387
402
|
// cancel() or destroy() says. Nobody has to wait for it.
|
|
388
403
|
fetch({ queryFn, retry: retry$1 = 0, retryDelay = defaultRetryDelay, structuralSharing = false, }, { cancelRefetch = false } = {}) {
|
|
389
|
-
const running = this.#
|
|
404
|
+
const running = this.#inFlight;
|
|
390
405
|
if (running) {
|
|
391
406
|
// Already in-flight: dedupe unless the caller explicitly wants a fresh
|
|
392
|
-
// fetch (e.g. invalidate/refetch) — then replace the old one.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
407
|
+
// fetch (e.g. invalidate/refetch) — then replace the old one. A caller
|
|
408
|
+
// that joins hands its queryFn over for the retries to come, and wants
|
|
409
|
+
// them made, even after the observers the fetch was started for left.
|
|
410
|
+
if (!cancelRefetch) {
|
|
411
|
+
running.queryFn = queryFn;
|
|
412
|
+
running.retriesStopped = false;
|
|
413
|
+
return running.outcome.promise;
|
|
414
|
+
}
|
|
415
|
+
running.subscription.unsubscribe();
|
|
396
416
|
}
|
|
397
417
|
const outcome = deferred();
|
|
398
418
|
// Its result or error lands in the state either way, so a caller may
|
|
@@ -400,12 +420,18 @@ class Query {
|
|
|
400
420
|
// unhandled rejection, and whoever awaits the promise still gets it.
|
|
401
421
|
outcome.promise.catch(() => undefined);
|
|
402
422
|
// Whoever waits for the fetch this one replaces gets this one's outcome.
|
|
403
|
-
running?.resolve(outcome.promise);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
423
|
+
running?.outcome.resolve(outcome.promise);
|
|
424
|
+
const inFlight = {
|
|
425
|
+
outcome,
|
|
426
|
+
subscription: new Subscription(),
|
|
427
|
+
// A fetch that replaces one in flight keeps the state the first one
|
|
428
|
+
// started from.
|
|
429
|
+
before: running?.before ?? this.state(),
|
|
430
|
+
queryFn,
|
|
431
|
+
abortable: false,
|
|
432
|
+
retriesStopped: false,
|
|
433
|
+
};
|
|
434
|
+
this.#inFlight = inFlight;
|
|
409
435
|
// A query with no data reads like a first load while it fetches, even
|
|
410
436
|
// after a failure: 'pending' with no error, so a loading state and
|
|
411
437
|
// placeholderData show rather than the old error. One with data keeps
|
|
@@ -417,10 +443,19 @@ class Query {
|
|
|
417
443
|
failureCount: 0,
|
|
418
444
|
failureReason: null,
|
|
419
445
|
}));
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
446
|
+
// The first attempt runs the caller's queryFn, each retry the one
|
|
447
|
+
// #retryQueryFn picks. defer + from: normalize Observable/Promise and
|
|
448
|
+
// make a fresh request for each attempt (a Promise is one-shot).
|
|
449
|
+
let firstAttempt = true;
|
|
450
|
+
const attempts = defer(() => {
|
|
451
|
+
const attemptQueryFn = firstAttempt
|
|
452
|
+
? queryFn
|
|
453
|
+
: this.#retryQueryFn(inFlight);
|
|
454
|
+
firstAttempt = false;
|
|
455
|
+
const request = attemptQueryFn();
|
|
456
|
+
inFlight.abortable = isObservable(request);
|
|
457
|
+
return from(request);
|
|
458
|
+
}).pipe(take(1), retry({
|
|
424
459
|
delay: (error, retryCount) => {
|
|
425
460
|
// retryCount (1-based) is the number of failures so far; the retry
|
|
426
461
|
// predicate/delay take a 0-based attempt index (0 = first retry).
|
|
@@ -430,20 +465,29 @@ class Query {
|
|
|
430
465
|
failureReason: error,
|
|
431
466
|
}));
|
|
432
467
|
const attemptIndex = retryCount - 1;
|
|
433
|
-
|
|
468
|
+
// Once the last observer left, the failure ends the fetch at once.
|
|
469
|
+
if (inFlight.retriesStopped ||
|
|
470
|
+
!shouldRetry(retry$1, attemptIndex, error)) {
|
|
434
471
|
throw error;
|
|
435
|
-
|
|
472
|
+
}
|
|
473
|
+
// The retry goes out when the wait is over. If the last observer
|
|
474
|
+
// left during the wait, an error thrown when the timer fires ends
|
|
475
|
+
// the fetch with this error instead, as in TanStack; whoever joins
|
|
476
|
+
// meanwhile clears the flag, and the retry goes out on time.
|
|
477
|
+
return timer(resolveRetryDelay(retryDelay, attemptIndex, error)).pipe(tap(() => {
|
|
478
|
+
if (inFlight.retriesStopped)
|
|
479
|
+
throw error;
|
|
480
|
+
}));
|
|
436
481
|
},
|
|
437
482
|
}), throwIfEmpty(() => new Error('Query function completed without emitting a value')),
|
|
438
483
|
// Keeps what didn't change from the data the query holds when the
|
|
439
484
|
// response lands, a write made during the fetch included. After the
|
|
440
485
|
// retries: a structuralSharing function that throws fails the fetch
|
|
441
486
|
// rather than retrying it, or leaving it in flight.
|
|
442
|
-
map((data) => replaceData(this.state().data, data, structuralSharing)))
|
|
443
|
-
|
|
487
|
+
map((data) => replaceData(this.state().data, data, structuralSharing)));
|
|
488
|
+
inFlight.subscription.add(attempts.subscribe({
|
|
444
489
|
next: (data) => {
|
|
445
|
-
this.#
|
|
446
|
-
this.#beforeFetch = null;
|
|
490
|
+
this.#inFlight = null;
|
|
447
491
|
this.#fetchedStructuralSharing = structuralSharing;
|
|
448
492
|
this.#state.set({
|
|
449
493
|
data,
|
|
@@ -456,10 +500,10 @@ class Query {
|
|
|
456
500
|
updatedAt: Date.now(),
|
|
457
501
|
});
|
|
458
502
|
outcome.resolve(data);
|
|
503
|
+
this.#scheduleGc();
|
|
459
504
|
},
|
|
460
505
|
error: (err) => {
|
|
461
|
-
this.#
|
|
462
|
-
this.#beforeFetch = null;
|
|
506
|
+
this.#inFlight = null;
|
|
463
507
|
this.#state.update((state) => ({
|
|
464
508
|
...state,
|
|
465
509
|
status: 'error',
|
|
@@ -467,8 +511,9 @@ class Query {
|
|
|
467
511
|
isFetching: false,
|
|
468
512
|
}));
|
|
469
513
|
outcome.reject(err);
|
|
514
|
+
this.#scheduleGc();
|
|
470
515
|
},
|
|
471
|
-
});
|
|
516
|
+
}));
|
|
472
517
|
return outcome.promise;
|
|
473
518
|
}
|
|
474
519
|
// Without a structuralSharing setting the data is stored as is: which one
|
|
@@ -485,12 +530,11 @@ class Query {
|
|
|
485
530
|
updatedAt,
|
|
486
531
|
}));
|
|
487
532
|
// A write made during a fetch is what stopping the fetch goes back to.
|
|
488
|
-
if (this.#inFlight
|
|
489
|
-
this.#
|
|
490
|
-
//
|
|
491
|
-
// setQueryData write isn't collected before anyone
|
|
492
|
-
|
|
493
|
-
this.#scheduleGc();
|
|
533
|
+
if (this.#inFlight)
|
|
534
|
+
this.#inFlight.before = this.state();
|
|
535
|
+
// A write counts as use: a query nobody observes is kept for another
|
|
536
|
+
// gcTime, so a setQueryData write isn't collected before anyone shows it.
|
|
537
|
+
this.#scheduleGc();
|
|
494
538
|
}
|
|
495
539
|
invalidate() {
|
|
496
540
|
this.#state.update((state) => ({ ...state, isInvalidated: true }));
|
|
@@ -505,9 +549,14 @@ class Query {
|
|
|
505
549
|
// holds, as in TanStack, or a CancelledError if it holds none.
|
|
506
550
|
cancel() {
|
|
507
551
|
const { data } = this.state();
|
|
552
|
+
const fetching = this.#inFlight !== null;
|
|
508
553
|
this.#stop((outcome) => data === undefined
|
|
509
554
|
? outcome.reject(new CancelledError())
|
|
510
555
|
: outcome.resolve(data));
|
|
556
|
+
// Only a fetch that stops can leave the query unused: cancelling none
|
|
557
|
+
// mustn't put off its gc.
|
|
558
|
+
if (fetching)
|
|
559
|
+
this.#scheduleGc();
|
|
511
560
|
}
|
|
512
561
|
// The query is thrown away with its data, so whoever waits for its fetch
|
|
513
562
|
// gets a CancelledError, as in TanStack.
|
|
@@ -522,39 +571,33 @@ class Query {
|
|
|
522
571
|
#isStale(staleTime) {
|
|
523
572
|
return Date.now() - this.state().updatedAt >= staleTime;
|
|
524
573
|
}
|
|
525
|
-
#inFlight() {
|
|
526
|
-
return this.#outcome !== null;
|
|
527
|
-
}
|
|
528
574
|
// Stops the fetch in flight, if any, and settles its promise with settle.
|
|
529
575
|
#stop(settle) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.#
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
settle(outcome);
|
|
576
|
+
const inFlight = this.#inFlight;
|
|
577
|
+
if (!inFlight)
|
|
578
|
+
return;
|
|
579
|
+
inFlight.subscription.unsubscribe();
|
|
580
|
+
this.#inFlight = null;
|
|
581
|
+
settle(inFlight.outcome);
|
|
582
|
+
const { before } = inFlight;
|
|
538
583
|
// The fetch is no longer running, so isFetching mustn't stay stuck true,
|
|
539
584
|
// and what the fetch wrote over goes back to how it was: a query that
|
|
540
585
|
// failed shows its error again, as in TanStack. Data and the stale mark
|
|
541
586
|
// aren't the fetch's to put back: they stay as written meanwhile, so an
|
|
542
587
|
// invalidation made during the fetch still holds.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}));
|
|
552
|
-
}
|
|
588
|
+
this.#state.update((state) => ({
|
|
589
|
+
...state,
|
|
590
|
+
status: before.status,
|
|
591
|
+
error: before.error,
|
|
592
|
+
failureCount: before.failureCount,
|
|
593
|
+
failureReason: before.failureReason,
|
|
594
|
+
isFetching: false,
|
|
595
|
+
}));
|
|
553
596
|
}
|
|
554
597
|
// A fetch in flight is kept, as in TanStack, and the observers aren't even
|
|
555
598
|
// asked: fetch() would drop the refetch anyway.
|
|
556
599
|
#refetchOn(option) {
|
|
557
|
-
if (this.#inFlight
|
|
600
|
+
if (this.#inFlight)
|
|
558
601
|
return;
|
|
559
602
|
for (const observer of this.#observers) {
|
|
560
603
|
const options = observer.fetchOptions();
|
|
@@ -566,11 +609,36 @@ class Query {
|
|
|
566
609
|
}
|
|
567
610
|
}
|
|
568
611
|
}
|
|
612
|
+
// The queryFn a retry runs: that of the first observer that wants the
|
|
613
|
+
// query fetched, as for a refetch, else that of whoever asked for the
|
|
614
|
+
// fetch last. Not simply the one the fetch started with: a queryFn may
|
|
615
|
+
// read its key's value only when it runs, as `() => api(this.id())` does,
|
|
616
|
+
// and by the retry its component may have moved on to another key. An
|
|
617
|
+
// observer whose key moved on doesn't want this query fetched.
|
|
618
|
+
#retryQueryFn(inFlight) {
|
|
619
|
+
for (const observer of this.#observers) {
|
|
620
|
+
const options = observer.fetchOptions();
|
|
621
|
+
if (options)
|
|
622
|
+
return options.queryFn;
|
|
623
|
+
}
|
|
624
|
+
return inFlight.queryFn;
|
|
625
|
+
}
|
|
626
|
+
// Removes the query gcTime from now, unless something uses it by then: an
|
|
627
|
+
// observer, or a fetch of it in flight. Called whenever the query may have
|
|
628
|
+
// become unused: when it's created, when its last observer leaves, when a
|
|
629
|
+
// fetch of it ends, when data is written into it by hand.
|
|
569
630
|
#scheduleGc() {
|
|
570
631
|
this.#clearGcTimer();
|
|
632
|
+
// While observed, the query is in use: the last observer leaving calls
|
|
633
|
+
// this again. Infinity, or a gcTime no timer can hold, keeps the query
|
|
634
|
+
// until something removes it: a browser would run the timer at once.
|
|
635
|
+
if (this.#observers.size > 0 || !isValidTimeout(this.#gcTime))
|
|
636
|
+
return;
|
|
571
637
|
this.#gcTimer = setTimeout(() => {
|
|
572
638
|
this.#gcTimer = null;
|
|
573
|
-
this
|
|
639
|
+
// A fetch still in flight keeps it: the fetch's end calls this again.
|
|
640
|
+
if (!this.#inFlight)
|
|
641
|
+
this.#cache.remove(this);
|
|
574
642
|
}, this.#gcTime);
|
|
575
643
|
}
|
|
576
644
|
#clearGcTimer() {
|
|
@@ -583,12 +651,12 @@ class Query {
|
|
|
583
651
|
|
|
584
652
|
/** @internal */
|
|
585
653
|
class QueryCache extends Cache {
|
|
586
|
-
getOrCreate(key) {
|
|
654
|
+
getOrCreate(key, gcTime) {
|
|
587
655
|
const queryHash = hashKey(key);
|
|
588
656
|
const exist = this.getEntry(queryHash);
|
|
589
657
|
if (exist)
|
|
590
658
|
return exist;
|
|
591
|
-
return this.addEntry(queryHash, new Query(key, queryHash, this));
|
|
659
|
+
return this.addEntry(queryHash, new Query({ key, queryHash, cache: this, gcTime }));
|
|
592
660
|
}
|
|
593
661
|
get(key) {
|
|
594
662
|
return this.getEntry(hashKey(key));
|
|
@@ -619,6 +687,9 @@ class QueryCache extends Cache {
|
|
|
619
687
|
// Guard on identity, not just the hash: a stale instance (already removed
|
|
620
688
|
// and recreated under the same key) must not evict the current query.
|
|
621
689
|
if (this.getEntry(query.queryHash) === query) {
|
|
690
|
+
// Whoever removes it, gc or removeQueries, it leaves cleaned up: its
|
|
691
|
+
// fetch cancelled and its gc timer cleared.
|
|
692
|
+
query.destroy();
|
|
622
693
|
this.removeEntry(query.queryHash);
|
|
623
694
|
}
|
|
624
695
|
}
|
|
@@ -837,6 +908,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.21", ngImpo
|
|
|
837
908
|
/** @internal */
|
|
838
909
|
const QUERY_CLIENT_CONFIG = new InjectionToken('QUERY_CLIENT_CONFIG');
|
|
839
910
|
|
|
911
|
+
const DEFAULT_GC_TIME = 5 * 60 * 1000;
|
|
840
912
|
/**
|
|
841
913
|
* Central registry and cache for queries and mutations.
|
|
842
914
|
*
|
|
@@ -861,6 +933,7 @@ class QueryClient {
|
|
|
861
933
|
#cache = inject(QueryCache);
|
|
862
934
|
#mutationCache = inject(MutationCache);
|
|
863
935
|
#config = inject(QUERY_CLIENT_CONFIG, { optional: true }) ?? {};
|
|
936
|
+
#onServer = !inBrowser();
|
|
864
937
|
/** @internal */
|
|
865
938
|
#injector = inject(EnvironmentInjector);
|
|
866
939
|
/**
|
|
@@ -899,7 +972,7 @@ class QueryClient {
|
|
|
899
972
|
return {
|
|
900
973
|
...options,
|
|
901
974
|
staleTime: options.staleTime ?? defaults?.staleTime ?? 0,
|
|
902
|
-
gcTime: options.gcTime
|
|
975
|
+
gcTime: this.#gcTime(options.gcTime),
|
|
903
976
|
retry: options.retry ?? defaults?.retry ?? 3,
|
|
904
977
|
retryDelay: options.retryDelay ?? defaults?.retryDelay ?? defaultRetryDelay,
|
|
905
978
|
structuralSharing: options.structuralSharing ?? this.#defaultStructuralSharing(),
|
|
@@ -944,6 +1017,9 @@ class QueryClient {
|
|
|
944
1017
|
* set here or in {@link withDefaultOptions}, so whoever waits for the data
|
|
945
1018
|
* hears of the failure at once.
|
|
946
1019
|
*
|
|
1020
|
+
* A query no component shows is removed `gcTime` after the fetch lands (the
|
|
1021
|
+
* configured default, or 5 minutes), as any unused query is.
|
|
1022
|
+
*
|
|
947
1023
|
* @param key - The query key to fetch and cache under.
|
|
948
1024
|
* @param queryFn - Function returning the data as an `Observable` or `Promise`.
|
|
949
1025
|
* @param options - Fetch tuning (`staleTime`, `retry`, `retryDelay`,
|
|
@@ -977,7 +1053,10 @@ class QueryClient {
|
|
|
977
1053
|
this.#config.defaultOptions?.queries?.retry ??
|
|
978
1054
|
false,
|
|
979
1055
|
});
|
|
980
|
-
const query = this.#cache.getOrCreate(key);
|
|
1056
|
+
const query = this.#cache.getOrCreate(key, defaulted.gcTime);
|
|
1057
|
+
// A query already in the cache keeps this call's gcTime if it's
|
|
1058
|
+
// longer, as it does for each component showing it.
|
|
1059
|
+
query.updateGcTime(defaulted.gcTime);
|
|
981
1060
|
return query.shouldFetch(defaulted.staleTime)
|
|
982
1061
|
? query.fetch(defaulted, { cancelRefetch })
|
|
983
1062
|
: Promise.resolve(query.state().data);
|
|
@@ -1005,6 +1084,9 @@ class QueryClient {
|
|
|
1005
1084
|
* nothing. It merges as the query showing the key asks; with none showing
|
|
1006
1085
|
* it, the way the cached data was fetched; otherwise by the default.
|
|
1007
1086
|
*
|
|
1087
|
+
* A key no component shows is kept for `gcTime` after the last write, then
|
|
1088
|
+
* removed.
|
|
1089
|
+
*
|
|
1008
1090
|
* @param key - The query key to write.
|
|
1009
1091
|
* @param updater - The new data, or a function `(prev) => next`.
|
|
1010
1092
|
*
|
|
@@ -1015,7 +1097,7 @@ class QueryClient {
|
|
|
1015
1097
|
*/
|
|
1016
1098
|
setQueryData(key, updater) {
|
|
1017
1099
|
untracked(() => {
|
|
1018
|
-
const query = this.#cache.getOrCreate(key);
|
|
1100
|
+
const query = this.#cache.getOrCreate(key, this.#gcTime());
|
|
1019
1101
|
const data = functionalUpdate(updater, query.state().data);
|
|
1020
1102
|
// Matches TanStack: an updater returning undefined is a no-op.
|
|
1021
1103
|
if (data === undefined)
|
|
@@ -1099,10 +1181,9 @@ class QueryClient {
|
|
|
1099
1181
|
* @param filters - Which queries to remove; omit to remove all.
|
|
1100
1182
|
*/
|
|
1101
1183
|
removeQueries(filters) {
|
|
1102
|
-
untracked(() => this.#cache
|
|
1103
|
-
|
|
1104
|
-
this.#cache.remove(query);
|
|
1105
|
-
}));
|
|
1184
|
+
untracked(() => this.#cache
|
|
1185
|
+
.findAll(filters)
|
|
1186
|
+
.forEach((query) => this.#cache.remove(query)));
|
|
1106
1187
|
}
|
|
1107
1188
|
/**
|
|
1108
1189
|
* Returns the number of matching queries currently fetching. A snapshot: it
|
|
@@ -1127,6 +1208,22 @@ class QueryClient {
|
|
|
1127
1208
|
isMutating() {
|
|
1128
1209
|
return untracked(() => this.#mutationCache.findAll({ status: 'pending' }).length);
|
|
1129
1210
|
}
|
|
1211
|
+
/**
|
|
1212
|
+
* How long a query stays in the cache once nothing uses it: the gcTime it
|
|
1213
|
+
* asks for, else the configured default, else 5 minutes. Every path that
|
|
1214
|
+
* puts a query into the cache takes it from here.
|
|
1215
|
+
*
|
|
1216
|
+
* On the server it's Infinity, whatever was asked: the cache lives as long
|
|
1217
|
+
* as the app that renders the request, and a pending timer would hold up a
|
|
1218
|
+
* render with zone.js.
|
|
1219
|
+
*
|
|
1220
|
+
* @internal
|
|
1221
|
+
*/
|
|
1222
|
+
#gcTime(time) {
|
|
1223
|
+
if (this.#onServer)
|
|
1224
|
+
return Infinity;
|
|
1225
|
+
return (time ?? this.#config.defaultOptions?.queries?.gcTime ?? DEFAULT_GC_TIME);
|
|
1226
|
+
}
|
|
1130
1227
|
/**
|
|
1131
1228
|
* The configured default structuralSharing, or `true`. One default serves
|
|
1132
1229
|
* every query whatever its data, so a function there takes and returns
|
|
@@ -1320,7 +1417,7 @@ function createQueryObserver(optionsFn, injector) {
|
|
|
1320
1417
|
// writes signals (the cache's entries, the query's state), so callers run it
|
|
1321
1418
|
// untracked: reached from a computed, a tracked write throws NG0600.
|
|
1322
1419
|
const resolve = (key) => {
|
|
1323
|
-
const q = cache.getOrCreate(key);
|
|
1420
|
+
const q = cache.getOrCreate(key, untracked(defaultedOptions).gcTime);
|
|
1324
1421
|
applyInitialData(q);
|
|
1325
1422
|
return q;
|
|
1326
1423
|
};
|
|
@@ -1393,10 +1490,7 @@ function createQueryObserver(optionsFn, injector) {
|
|
|
1393
1490
|
};
|
|
1394
1491
|
track((cleanup) => {
|
|
1395
1492
|
const q = query();
|
|
1396
|
-
|
|
1397
|
-
if (gcTime !== undefined) {
|
|
1398
|
-
q.setGcTime(gcTime);
|
|
1399
|
-
}
|
|
1493
|
+
q.updateGcTime(untracked(defaultedOptions).gcTime);
|
|
1400
1494
|
// This observer as q sees it.
|
|
1401
1495
|
const subscriber = {
|
|
1402
1496
|
fetchOptions: () => fetchOptionsFor(q),
|