ngx-signal-query 1.4.0 â 1.5.0-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/README.md +58 -28
- package/fesm2022/ngx-signal-query.mjs +212 -56
- package/fesm2022/ngx-signal-query.mjs.map +1 -1
- package/index.d.ts +33 -9
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
# ngx-signal-query
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/ngx-signal-query)
|
|
4
|
+
[](https://bundlejs.com/?q=ngx-signal-query&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22%40angular/core%22%2C%22%40angular/common%22%2C%22rxjs%22%2C%22rxjs/operators%22%2C%22tslib%22%5D%7D%7D)
|
|
4
5
|
[](./LICENSE)
|
|
5
6
|
[](https://dhutaryan.github.io/ngx-signal-query/)
|
|
6
7
|
|
|
7
|
-
A
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
A data fetching, caching and mutation library for Angular, built on signals and
|
|
9
|
+
RxJS. Queries and mutations take an `Observable` as is, so `HttpClient` works
|
|
10
|
+
without `lastValueFrom`, and when no component needs a query any more, its
|
|
11
|
+
request is cancelled. The model (query keys, staleness, invalidation) follows
|
|
12
|
+
[TanStack Query](https://tanstack.com/query), so what you know from it carries
|
|
13
|
+
over.
|
|
14
|
+
|
|
15
|
+
## Why this one
|
|
16
|
+
|
|
17
|
+
- ð **Observables end to end.** Return `http.get()` from a `queryFn` or `mutationFn`. For a query, unsubscribing is the cancellation: once no component needs it, the request is aborted, with no `AbortSignal` plumbing.
|
|
18
|
+
- ð
°ïļ **Angular all the way down.** State is signals, configuration is `provideQueryClient(withDefaultOptions(...))`, and default mutation hooks run in an injection context, so a global `onError` can `inject()` what it needs.
|
|
19
|
+
- ðŠķ **Small.** About 5 KB min+gzip, tree-shakable ESM, no runtime dependencies beyond `tslib`.
|
|
20
|
+
- ð§ **Familiar concepts.** `queryKey`, `staleTime`, `invalidateQueries` and `placeholderData` mean what you'd expect. Moving to TanStack Query later is mostly mechanical: wrap Observables in `lastValueFrom` and swap the provider.
|
|
11
21
|
|
|
12
22
|
## Features
|
|
13
23
|
|
|
14
|
-
- ðĶ
|
|
15
|
-
- ðïļ
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
24
|
+
- ðĶ State as signals: read `data()` or `isPending()` in a template or a `computed`, no subscriptions
|
|
25
|
+
- ðïļ Caching, request deduplication and structural sharing
|
|
26
|
+
- âŧïļ Invalidation, cancellation and manual cache updates
|
|
27
|
+
- ð Dependent, parallel (`injectQueries`) and disabled queries
|
|
28
|
+
- ðžïļ Seed and placeholder data while a query loads (`initialData`, `placeholderData`, `keepPreviousData`)
|
|
29
|
+
- ð Retries, polling, and refetch on window focus and reconnect
|
|
30
|
+
- âïļ Mutations with async lifecycle hooks and optimistic updates
|
|
31
|
+
- ð App-wide defaults and global indicators (`injectIsFetching()`, `injectIsMutating()`)
|
|
32
|
+
|
|
33
|
+
## Not here (yet)
|
|
34
|
+
|
|
35
|
+
Infinite queries, SSR cache hydration, devtools and persistence. If you need
|
|
36
|
+
them today, TanStack Query has them.
|
|
20
37
|
|
|
21
38
|
## Installation
|
|
22
39
|
|
|
@@ -77,26 +94,39 @@ export class TodosComponent {
|
|
|
77
94
|
}
|
|
78
95
|
```
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
### 3. Mutate and reconcile the cache
|
|
98
|
+
|
|
99
|
+
`injectMutation` works the same way: call `mutate()`, read `isPending()` /
|
|
100
|
+
`status()` as signals, and invalidate affected queries once it succeeds.
|
|
81
101
|
|
|
82
102
|
```ts
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
import { Component, inject } from '@angular/core'
|
|
104
|
+
import { HttpClient } from '@angular/common/http'
|
|
105
|
+
import { injectMutation, injectQueryClient } from 'ngx-signal-query'
|
|
106
|
+
|
|
107
|
+
type Todo = { id: number; title: string }
|
|
108
|
+
|
|
109
|
+
@Component({
|
|
110
|
+
selector: 'app-add-todo',
|
|
111
|
+
template: `
|
|
112
|
+
<button
|
|
113
|
+
(click)="addTodo.mutate('Buy milk')"
|
|
114
|
+
[disabled]="addTodo.isPending()"
|
|
115
|
+
>
|
|
116
|
+
Add todo
|
|
117
|
+
</button>
|
|
118
|
+
`,
|
|
119
|
+
})
|
|
120
|
+
export class AddTodoComponent {
|
|
121
|
+
private readonly http = inject(HttpClient)
|
|
122
|
+
private readonly client = injectQueryClient()
|
|
123
|
+
|
|
124
|
+
protected readonly addTodo = injectMutation(() => ({
|
|
125
|
+
mutationFn: (title: string) =>
|
|
126
|
+
this.http.post<Todo>('/api/todos', { title }),
|
|
127
|
+
onSuccess: () => this.client.invalidateQueries({ queryKey: ['todos'] }),
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
100
130
|
```
|
|
101
131
|
|
|
102
132
|
## Documentation
|
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, InjectionToken,
|
|
3
|
-
import {
|
|
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';
|
|
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';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calls `listener` whenever the user comes back to the page, as the tab or
|
|
9
|
+
* the window turns visible again. The window's `focus` event isn't used: it
|
|
10
|
+
* also fires when an alert or a file picker closes, or on coming back from
|
|
11
|
+
* DevTools, with the user on the page all along.
|
|
12
|
+
*
|
|
13
|
+
* Listens until the current injector is destroyed, and only in the browser:
|
|
14
|
+
* on the server there is no page to come back to. Must run in an injection
|
|
15
|
+
* context.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
function onWindowFocus(listener) {
|
|
20
|
+
if (!inBrowser())
|
|
21
|
+
return;
|
|
22
|
+
const document = inject(DOCUMENT);
|
|
23
|
+
fromEvent(document, 'visibilitychange')
|
|
24
|
+
.pipe(filter(() => document.visibilityState !== 'hidden'), takeUntilDestroyed())
|
|
25
|
+
.subscribe(() => listener());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Calls `listener` whenever the browser reports that the network is back
|
|
29
|
+
* (`online`). Each such event means it was off: the browser fires it only
|
|
30
|
+
* when going from offline to online, a page opened offline included.
|
|
31
|
+
*
|
|
32
|
+
* Listens until the current injector is destroyed, and only in the browser.
|
|
33
|
+
* Must run in an injection context.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
function onReconnect(listener) {
|
|
38
|
+
if (!inBrowser())
|
|
39
|
+
return;
|
|
40
|
+
const window = inject(DOCUMENT).defaultView;
|
|
41
|
+
if (!window)
|
|
42
|
+
return;
|
|
43
|
+
fromEvent(window, 'online')
|
|
44
|
+
.pipe(takeUntilDestroyed())
|
|
45
|
+
.subscribe(() => listener());
|
|
46
|
+
}
|
|
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
|
+
}
|
|
4
52
|
|
|
5
53
|
/** @internal */
|
|
6
54
|
class Cache {
|
|
@@ -206,6 +254,28 @@ function resolveRetryDelay(retryDelay, failureCount, error) {
|
|
|
206
254
|
}
|
|
207
255
|
|
|
208
256
|
const DEFAULT_GC_TIME = 5 * 60 * 1000;
|
|
257
|
+
/**
|
|
258
|
+
* What the promise of a query's fetch rejects with when the fetch stops with
|
|
259
|
+
* no data to give: cancelled before the query had any, or thrown away with
|
|
260
|
+
* the query.
|
|
261
|
+
*
|
|
262
|
+
* @internal
|
|
263
|
+
*/
|
|
264
|
+
class CancelledError extends Error {
|
|
265
|
+
constructor() {
|
|
266
|
+
super('The query fetch was cancelled');
|
|
267
|
+
this.name = 'CancelledError';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function deferred() {
|
|
271
|
+
let resolve;
|
|
272
|
+
let reject;
|
|
273
|
+
const promise = new Promise((res, rej) => {
|
|
274
|
+
resolve = res;
|
|
275
|
+
reject = rej;
|
|
276
|
+
});
|
|
277
|
+
return { promise, resolve, reject };
|
|
278
|
+
}
|
|
209
279
|
/** @internal */
|
|
210
280
|
class Query {
|
|
211
281
|
key;
|
|
@@ -225,6 +295,9 @@ class Query {
|
|
|
225
295
|
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
226
296
|
state = this.#state.asReadonly();
|
|
227
297
|
#subscription = null;
|
|
298
|
+
// The promise of the fetch in flight, which every caller that joins the
|
|
299
|
+
// fetch shares, and how to settle it; null while nothing is in flight.
|
|
300
|
+
#outcome = null;
|
|
228
301
|
// The structuralSharing of the fetch whose response the query holds.
|
|
229
302
|
#fetchedStructuralSharing;
|
|
230
303
|
#observers = new Set();
|
|
@@ -268,6 +341,17 @@ class Query {
|
|
|
268
341
|
}
|
|
269
342
|
return null;
|
|
270
343
|
}
|
|
344
|
+
// Refetches the query now that the user is back on the page, if one of its
|
|
345
|
+
// observers wants that: its refetchOnWindowFocus is 'always', or true while
|
|
346
|
+
// the data is stale for its staleTime. The first such observer decides
|
|
347
|
+
// how, as in TanStack.
|
|
348
|
+
onFocus() {
|
|
349
|
+
this.#refetchOn('refetchOnWindowFocus');
|
|
350
|
+
}
|
|
351
|
+
// The same once the network is back, by refetchOnReconnect.
|
|
352
|
+
onOnline() {
|
|
353
|
+
this.#refetchOn('refetchOnReconnect');
|
|
354
|
+
}
|
|
271
355
|
// How data written into the query by hand merges with what it holds: as
|
|
272
356
|
// its first observer says, enabled or not; with none, the way its data was
|
|
273
357
|
// fetched; undefined if neither has a say.
|
|
@@ -279,14 +363,28 @@ class Query {
|
|
|
279
363
|
}
|
|
280
364
|
// Without a structuralSharing setting the response is stored as is: the
|
|
281
365
|
// default is the client's to resolve.
|
|
366
|
+
//
|
|
367
|
+
// Returns a promise of the data as the query stores it, which every caller
|
|
368
|
+
// that joins the fetch shares. A fetch that replaces this one settles it
|
|
369
|
+
// with its own outcome; one stopped with nothing in its place settles it as
|
|
370
|
+
// cancel() or destroy() says. Nobody has to wait for it.
|
|
282
371
|
fetch({ queryFn, retry: retry$1 = 0, retryDelay = defaultRetryDelay, structuralSharing = false, }, { cancelRefetch = false } = {}) {
|
|
283
|
-
|
|
372
|
+
const running = this.#outcome;
|
|
373
|
+
if (running) {
|
|
284
374
|
// Already in-flight: dedupe unless the caller explicitly wants a fresh
|
|
285
|
-
// fetch (e.g. invalidate/refetch) â then
|
|
375
|
+
// fetch (e.g. invalidate/refetch) â then replace the old one.
|
|
286
376
|
if (!cancelRefetch)
|
|
287
|
-
return;
|
|
288
|
-
this
|
|
377
|
+
return running.promise;
|
|
378
|
+
this.#subscription?.unsubscribe();
|
|
289
379
|
}
|
|
380
|
+
const outcome = deferred();
|
|
381
|
+
// Its result or error lands in the state either way, so a caller may
|
|
382
|
+
// ignore the promise: handled here, a failure doesn't surface as an
|
|
383
|
+
// unhandled rejection, and whoever awaits the promise still gets it.
|
|
384
|
+
outcome.promise.catch(() => undefined);
|
|
385
|
+
// Whoever waits for the fetch this one replaces gets this one's outcome.
|
|
386
|
+
running?.resolve(outcome.promise);
|
|
387
|
+
this.#outcome = outcome;
|
|
290
388
|
this.#state.update((state) => ({
|
|
291
389
|
...state,
|
|
292
390
|
isFetching: true,
|
|
@@ -318,6 +416,7 @@ class Query {
|
|
|
318
416
|
map((data) => replaceData(this.state().data, data, structuralSharing)))
|
|
319
417
|
.subscribe({
|
|
320
418
|
next: (data) => {
|
|
419
|
+
this.#outcome = null;
|
|
321
420
|
this.#fetchedStructuralSharing = structuralSharing;
|
|
322
421
|
this.#state.set({
|
|
323
422
|
data,
|
|
@@ -329,14 +428,20 @@ class Query {
|
|
|
329
428
|
failureReason: null,
|
|
330
429
|
updatedAt: Date.now(),
|
|
331
430
|
});
|
|
431
|
+
outcome.resolve(data);
|
|
432
|
+
},
|
|
433
|
+
error: (err) => {
|
|
434
|
+
this.#outcome = null;
|
|
435
|
+
this.#state.update((state) => ({
|
|
436
|
+
...state,
|
|
437
|
+
status: 'error',
|
|
438
|
+
error: err,
|
|
439
|
+
isFetching: false,
|
|
440
|
+
}));
|
|
441
|
+
outcome.reject(err);
|
|
332
442
|
},
|
|
333
|
-
error: (err) => this.#state.update((state) => ({
|
|
334
|
-
...state,
|
|
335
|
-
status: 'error',
|
|
336
|
-
error: err,
|
|
337
|
-
isFetching: false,
|
|
338
|
-
})),
|
|
339
443
|
});
|
|
444
|
+
return outcome.promise;
|
|
340
445
|
}
|
|
341
446
|
// Without a structuralSharing setting the data is stored as is: which one
|
|
342
447
|
// a write uses is the client's to resolve.
|
|
@@ -365,17 +470,18 @@ class Query {
|
|
|
365
470
|
state.isInvalidated ||
|
|
366
471
|
this.#isStale(staleTime));
|
|
367
472
|
}
|
|
473
|
+
// Stops the fetch in flight. Whoever waits for it gets the data the query
|
|
474
|
+
// holds, as in TanStack, or a CancelledError if it holds none.
|
|
368
475
|
cancel() {
|
|
369
|
-
this
|
|
370
|
-
this.#
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (this.state().isFetching) {
|
|
374
|
-
this.#state.update((state) => ({ ...state, isFetching: false }));
|
|
375
|
-
}
|
|
476
|
+
const { data } = this.state();
|
|
477
|
+
this.#stop((outcome) => data === undefined
|
|
478
|
+
? outcome.reject(new CancelledError())
|
|
479
|
+
: outcome.resolve(data));
|
|
376
480
|
}
|
|
481
|
+
// The query is thrown away with its data, so whoever waits for its fetch
|
|
482
|
+
// gets a CancelledError, as in TanStack.
|
|
377
483
|
destroy() {
|
|
378
|
-
this.
|
|
484
|
+
this.#stop((outcome) => outcome.reject(new CancelledError()));
|
|
379
485
|
this.#clearGcTimer();
|
|
380
486
|
}
|
|
381
487
|
// Inclusive: data fetched exactly `staleTime` ago is already stale. This
|
|
@@ -385,6 +491,38 @@ class Query {
|
|
|
385
491
|
#isStale(staleTime) {
|
|
386
492
|
return Date.now() - this.state().updatedAt >= staleTime;
|
|
387
493
|
}
|
|
494
|
+
#inFlight() {
|
|
495
|
+
return this.#outcome !== null;
|
|
496
|
+
}
|
|
497
|
+
// Stops the fetch in flight, if any, and settles its promise with settle.
|
|
498
|
+
#stop(settle) {
|
|
499
|
+
const outcome = this.#outcome;
|
|
500
|
+
this.#subscription?.unsubscribe();
|
|
501
|
+
this.#subscription = null;
|
|
502
|
+
this.#outcome = null;
|
|
503
|
+
if (outcome)
|
|
504
|
+
settle(outcome);
|
|
505
|
+
// The fetch is no longer running; clear the in-flight flag so isFetching
|
|
506
|
+
// doesn't stay stuck true after a cancellation.
|
|
507
|
+
if (this.state().isFetching) {
|
|
508
|
+
this.#state.update((state) => ({ ...state, isFetching: false }));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// A fetch in flight is kept, as in TanStack, and the observers aren't even
|
|
512
|
+
// asked: fetch() would drop the refetch anyway.
|
|
513
|
+
#refetchOn(option) {
|
|
514
|
+
if (this.#inFlight())
|
|
515
|
+
return;
|
|
516
|
+
for (const observer of this.#observers) {
|
|
517
|
+
const options = observer.fetchOptions();
|
|
518
|
+
if (!options || options[option] === false)
|
|
519
|
+
continue;
|
|
520
|
+
if (options[option] === 'always' || this.shouldFetch(options.staleTime)) {
|
|
521
|
+
this.fetch(options);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
388
526
|
#scheduleGc() {
|
|
389
527
|
this.#clearGcTimer();
|
|
390
528
|
this.#gcTimer = setTimeout(() => {
|
|
@@ -671,6 +809,12 @@ class QueryClient {
|
|
|
671
809
|
/** @internal */
|
|
672
810
|
constructor() {
|
|
673
811
|
inject(DestroyRef).onDestroy(() => (this.#destroyed = true));
|
|
812
|
+
// When the user comes back to the page, or the network comes back, each
|
|
813
|
+
// query refetches if one of its observers wants that. Untracked like the
|
|
814
|
+
// methods: an event fired from an effect mustn't make the effect depend
|
|
815
|
+
// on what the refetch reads.
|
|
816
|
+
onWindowFocus(() => untracked(() => this.#cache.getAll().forEach((query) => query.onFocus())));
|
|
817
|
+
onReconnect(() => untracked(() => this.#cache.getAll().forEach((query) => query.onOnline())));
|
|
674
818
|
}
|
|
675
819
|
/** Returns the underlying query cache. Advanced/internal use. */
|
|
676
820
|
getQueryCache() {
|
|
@@ -682,8 +826,9 @@ class QueryClient {
|
|
|
682
826
|
}
|
|
683
827
|
/**
|
|
684
828
|
* Merges per-query options with the configured defaults, filling in
|
|
685
|
-
* `staleTime`, `gcTime`, `retry`, `retryDelay
|
|
686
|
-
* Used internally by
|
|
829
|
+
* `staleTime`, `gcTime`, `retry`, `retryDelay`, `structuralSharing`,
|
|
830
|
+
* `refetchOnWindowFocus` and `refetchOnReconnect`. Used internally by
|
|
831
|
+
* {@link injectQuery}.
|
|
687
832
|
*/
|
|
688
833
|
defaultQueryOptions(options) {
|
|
689
834
|
const defaults = this.#config.defaultOptions?.queries;
|
|
@@ -694,6 +839,8 @@ class QueryClient {
|
|
|
694
839
|
retry: options.retry ?? defaults?.retry ?? 3,
|
|
695
840
|
retryDelay: options.retryDelay ?? defaults?.retryDelay ?? defaultRetryDelay,
|
|
696
841
|
structuralSharing: options.structuralSharing ?? this.#defaultStructuralSharing(),
|
|
842
|
+
refetchOnWindowFocus: options.refetchOnWindowFocus ?? defaults?.refetchOnWindowFocus ?? true,
|
|
843
|
+
refetchOnReconnect: options.refetchOnReconnect ?? defaults?.refetchOnReconnect ?? true,
|
|
697
844
|
};
|
|
698
845
|
}
|
|
699
846
|
/**
|
|
@@ -727,14 +874,17 @@ class QueryClient {
|
|
|
727
874
|
*/
|
|
728
875
|
fetchQuery(key, queryFn, options = {}) {
|
|
729
876
|
untracked(() => {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
|
|
877
|
+
// Resolved as a query's options are, so one set of defaults serves both.
|
|
878
|
+
// cancelRefetch is about this call, not an option of the query.
|
|
879
|
+
const { cancelRefetch, ...queryOptions } = options;
|
|
880
|
+
const defaulted = this.defaultQueryOptions({
|
|
881
|
+
...queryOptions,
|
|
882
|
+
queryKey: key,
|
|
883
|
+
queryFn,
|
|
884
|
+
});
|
|
735
885
|
const query = this.#cache.getOrCreate(key);
|
|
736
|
-
if (query.shouldFetch(staleTime)) {
|
|
737
|
-
query.fetch(
|
|
886
|
+
if (query.shouldFetch(defaulted.staleTime)) {
|
|
887
|
+
query.fetch(defaulted, { cancelRefetch });
|
|
738
888
|
}
|
|
739
889
|
});
|
|
740
890
|
}
|
|
@@ -1102,17 +1252,26 @@ function createQueryObserver(optionsFn, injector) {
|
|
|
1102
1252
|
return;
|
|
1103
1253
|
switchTo(untracked(() => resolve(entry.key)));
|
|
1104
1254
|
});
|
|
1105
|
-
// How this observer would fetch q right now,
|
|
1106
|
-
// while it is disabled, or once the
|
|
1107
|
-
//
|
|
1108
|
-
// must not be fetched with it.
|
|
1109
|
-
//
|
|
1255
|
+
// How this observer would fetch q right now, and when it wants q
|
|
1256
|
+
// refetched, from the live options; null while it is disabled, or once the
|
|
1257
|
+
// key has moved on. Before the key effect above catches up, queryFn already
|
|
1258
|
+
// fetches the next query's data, so q must not be fetched with it. Every
|
|
1259
|
+
// refetch made on our behalf goes by it: through q's subscriber handle the
|
|
1260
|
+
// invalidation one and the window focus and reconnect ones, and polling.
|
|
1110
1261
|
const fetchOptionsFor = (q) => {
|
|
1111
|
-
const { queryKey, queryFn, retry, retryDelay, structuralSharing, enabled } = untracked(defaultedOptions);
|
|
1262
|
+
const { queryKey, queryFn, retry, retryDelay, structuralSharing, staleTime, refetchOnWindowFocus, refetchOnReconnect, enabled, } = untracked(defaultedOptions);
|
|
1112
1263
|
if (enabled === false || hashKey(queryKey) !== q.queryHash) {
|
|
1113
1264
|
return null;
|
|
1114
1265
|
}
|
|
1115
|
-
return {
|
|
1266
|
+
return {
|
|
1267
|
+
queryFn,
|
|
1268
|
+
retry,
|
|
1269
|
+
retryDelay,
|
|
1270
|
+
structuralSharing,
|
|
1271
|
+
staleTime,
|
|
1272
|
+
refetchOnWindowFocus,
|
|
1273
|
+
refetchOnReconnect,
|
|
1274
|
+
};
|
|
1116
1275
|
};
|
|
1117
1276
|
track((cleanup) => {
|
|
1118
1277
|
const q = query();
|
|
@@ -1140,18 +1299,19 @@ function createQueryObserver(optionsFn, injector) {
|
|
|
1140
1299
|
// Invalidation isn't a trigger: invalidateQueries() refetches each active
|
|
1141
1300
|
// query once itself. A query it skipped (unobserved, or its observers
|
|
1142
1301
|
// disabled) stays invalidated, so it fetches here once observed or enabled:
|
|
1143
|
-
// shouldFetch sees the flag.
|
|
1302
|
+
// shouldFetch sees the flag. The key and queryFn come from one read of the
|
|
1303
|
+
// options, so the fetch can't go to another key's query, whatever order the
|
|
1304
|
+
// effects run in.
|
|
1144
1305
|
track(() => {
|
|
1145
1306
|
query();
|
|
1146
1307
|
if (!enabled())
|
|
1147
1308
|
return;
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}));
|
|
1309
|
+
untracked(() => {
|
|
1310
|
+
const options = defaultedOptions();
|
|
1311
|
+
const q = resolve(options.queryKey);
|
|
1312
|
+
if (q.shouldFetch(options.staleTime))
|
|
1313
|
+
q.fetch(options);
|
|
1314
|
+
});
|
|
1155
1315
|
});
|
|
1156
1316
|
// The polling interval this observer asks for, or false for none. Memoized,
|
|
1157
1317
|
// so the polling effect wakes only when the value changes. The function form
|
|
@@ -1234,18 +1394,14 @@ function createQueryObserver(optionsFn, injector) {
|
|
|
1234
1394
|
isPlaceholderData: computed(() => resolved().isPlaceholderData),
|
|
1235
1395
|
failureCount: computed(() => query().state().failureCount),
|
|
1236
1396
|
failureReason: computed(() => query().state().failureReason),
|
|
1237
|
-
// Force a fresh fetch regardless of staleTime, cancelling
|
|
1238
|
-
// request (explicit user intent â get current data).
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
structuralSharing,
|
|
1246
|
-
cancelRefetch: true,
|
|
1247
|
-
});
|
|
1248
|
-
},
|
|
1397
|
+
// Force a fresh fetch regardless of staleTime and enabled, cancelling
|
|
1398
|
+
// any in-flight request (explicit user intent â get current data).
|
|
1399
|
+
// Untracked, as the client's methods are: called from an effect, it
|
|
1400
|
+
// mustn't make the effect depend on the options or on the query.
|
|
1401
|
+
refetch: () => untracked(() => {
|
|
1402
|
+
const options = defaultedOptions();
|
|
1403
|
+
resolve(options.queryKey).fetch(options, { cancelRefetch: true });
|
|
1404
|
+
}),
|
|
1249
1405
|
},
|
|
1250
1406
|
destroy: () => refs.forEach((ref) => ref.destroy()),
|
|
1251
1407
|
};
|