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.
@@ -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 { DOCUMENT, isPlatformBrowser } from '@angular/common';
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, tap, concatMap, catchError, of, endWith, concat, ignoreElements, EMPTY } from 'rxjs';
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), or
256
- * the query was removed (`removeQueries()`, e.g. on sign-out). The request
257
- * didn't fail, so tell the two apart with `instanceof CancelledError`.
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
- #subscription = null;
309
- // The promise of the fetch in flight, which every caller that joins the
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 = DEFAULT_GC_TIME;
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
- setGcTime(ms) {
331
- this.#gcTime = ms;
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: cancel any in-flight fetch (nobody is waiting for it)
343
- // and schedule gc to dispose the query if no observer returns.
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.cancel();
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.#outcome;
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
- if (!cancelRefetch)
394
- return running.promise;
395
- this.#subscription?.unsubscribe();
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
- this.#outcome = outcome;
405
- // A fetch that replaces one in flight keeps the state the first one
406
- // started from.
407
- if (!running)
408
- this.#beforeFetch = this.state();
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
- // defer + from: normalize Observable/Promise and re-invoke queryFn on each
421
- // retry (a Promise is one-shot, so retry must produce a fresh one).
422
- this.#subscription = defer(() => from(queryFn()))
423
- .pipe(take(1), retry({
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
- if (!shouldRetry(retry$1, attemptIndex, error))
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
- return timer(resolveRetryDelay(retryDelay, attemptIndex, error));
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
- .subscribe({
487
+ map((data) => replaceData(this.state().data, data, structuralSharing)));
488
+ inFlight.subscription.add(attempts.subscribe({
444
489
  next: (data) => {
445
- this.#outcome = null;
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.#outcome = null;
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.#beforeFetch = this.state();
490
- // Keep an orphaned query (no observers) alive for another gcTime so a
491
- // setQueryData write isn't collected before anyone subscribes.
492
- if (this.#observers.size === 0)
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 outcome = this.#outcome;
531
- const before = this.#beforeFetch;
532
- this.#subscription?.unsubscribe();
533
- this.#subscription = null;
534
- this.#outcome = null;
535
- this.#beforeFetch = null;
536
- if (outcome)
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
- if (before) {
544
- this.#state.update((state) => ({
545
- ...state,
546
- status: before.status,
547
- error: before.error,
548
- failureCount: before.failureCount,
549
- failureReason: before.failureReason,
550
- isFetching: false,
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.#cache.remove(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 ?? defaults?.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.findAll(filters).forEach((query) => {
1103
- query.destroy();
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
- const { gcTime } = untracked(defaultedOptions);
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),