ngx-signal-query 1.8.0-dev.1 → 1.8.0-dev.3
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 +3 -2
- package/fesm2022/ngx-signal-query.mjs +285 -58
- package/fesm2022/ngx-signal-query.mjs.map +1 -1
- package/index.d.ts +71 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ know from it carries over.
|
|
|
29
29
|
- 🔁 Retries, polling, and refetch on window focus and reconnect
|
|
30
30
|
- ✏️ Mutations with async lifecycle hooks and optimistic updates
|
|
31
31
|
- 🌍 App-wide defaults and global indicators (`injectIsFetching()`, `injectIsMutating()`)
|
|
32
|
+
- 🖥️ Server rendering: the server waits for every fetch, and `withHydration()` brings the data over to the browser
|
|
32
33
|
|
|
33
34
|
## Not here (yet)
|
|
34
35
|
|
|
35
|
-
Infinite queries,
|
|
36
|
-
|
|
36
|
+
Infinite queries, devtools and persistence. If you need them today, TanStack
|
|
37
|
+
Query has them.
|
|
37
38
|
|
|
38
39
|
## Installation
|
|
39
40
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, PLATFORM_ID, signal, Injectable, InjectionToken, EnvironmentInjector,
|
|
2
|
+
import { inject, PLATFORM_ID, makeStateKey, TransferState, NgZone, signal, PendingTasks, DestroyRef, Injectable, InjectionToken, EnvironmentInjector, untracked, runInInjectionContext, makeEnvironmentProviders, assertInInjectionContext, effect, computed, Injector } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
4
4
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { fromEvent, filter, Subscription, defer, isObservable, from, take, retry, timer, tap, throwIfEmpty, map, concatMap, catchError, of, endWith, concat, ignoreElements, EMPTY } from 'rxjs';
|
|
@@ -70,48 +70,6 @@ function onReconnect(listener) {
|
|
|
70
70
|
.subscribe(() => listener());
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** @internal */
|
|
74
|
-
class Cache {
|
|
75
|
-
#entries = signal([], ...(ngDevMode ? [{ debugName: "#entries" }] : []));
|
|
76
|
-
// Reactive snapshot of all entries; updated on add/remove/clear so that
|
|
77
|
-
// findAll() (and its computed consumers like isFetching) react to the
|
|
78
|
-
// collection. Protected: only subclasses read it, never external code.
|
|
79
|
-
entries = this.#entries.asReadonly();
|
|
80
|
-
#entriesMap = new Map();
|
|
81
|
-
getAll() {
|
|
82
|
-
return Array.from(this.#entriesMap.values());
|
|
83
|
-
}
|
|
84
|
-
clear() {
|
|
85
|
-
this.#entriesMap.clear();
|
|
86
|
-
this.#sync();
|
|
87
|
-
}
|
|
88
|
-
// Updates the snapshot again once the current computation is over, in a
|
|
89
|
-
// microtask. An entry added in the middle of a computation is in the
|
|
90
|
-
// snapshot at once, but a computed that read the snapshot earlier in that
|
|
91
|
-
// computation doesn't hear of it: it only starts listening once its
|
|
92
|
-
// computation is done. The second update reaches it.
|
|
93
|
-
notifyLater() {
|
|
94
|
-
queueMicrotask(() => this.#sync());
|
|
95
|
-
}
|
|
96
|
-
addEntry(key, entry) {
|
|
97
|
-
this.#entriesMap.set(key, entry);
|
|
98
|
-
this.#sync();
|
|
99
|
-
return entry;
|
|
100
|
-
}
|
|
101
|
-
getEntry(key) {
|
|
102
|
-
return this.#entriesMap.get(key);
|
|
103
|
-
}
|
|
104
|
-
removeEntry(key) {
|
|
105
|
-
const removed = this.#entriesMap.delete(key);
|
|
106
|
-
if (removed)
|
|
107
|
-
this.#sync();
|
|
108
|
-
return removed;
|
|
109
|
-
}
|
|
110
|
-
#sync() {
|
|
111
|
-
this.#entries.set(Array.from(this.#entriesMap.values()));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
73
|
// Resolves an updater: calls it with the previous value if it's a function,
|
|
116
74
|
// otherwise uses it as the value directly.
|
|
117
75
|
/** @internal */
|
|
@@ -245,6 +203,54 @@ function replaceData(prevData, data, structuralSharing) {
|
|
|
245
203
|
}
|
|
246
204
|
return structuralSharing ? replaceEqualDeep(prevData, data) : data;
|
|
247
205
|
}
|
|
206
|
+
// Past this many levels survivesJson gives up: a value that references
|
|
207
|
+
// itself never ends, and real data is never this deep.
|
|
208
|
+
const MAX_JSON_DEPTH = 500;
|
|
209
|
+
// Whether JSON carries the value over as it is: JSON.parse gives back a value
|
|
210
|
+
// equal to it, of the same kind. Strings, finite numbers, booleans, null,
|
|
211
|
+
// plain objects and arrays without holes do. A Date, a Map, a class instance,
|
|
212
|
+
// a BigInt, NaN, undefined in an array, or a value that references itself
|
|
213
|
+
// don't. An object property that's undefined counts as carried: JSON drops
|
|
214
|
+
// it, and reading it gives undefined all the same. Symbol keys and extra
|
|
215
|
+
// properties on an array aren't checked, and don't make it over.
|
|
216
|
+
/** @internal */
|
|
217
|
+
function survivesJson(value, depth = 0) {
|
|
218
|
+
if (value === null)
|
|
219
|
+
return true;
|
|
220
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
221
|
+
return true;
|
|
222
|
+
// NaN and Infinity come back as null.
|
|
223
|
+
if (typeof value === 'number')
|
|
224
|
+
return Number.isFinite(value);
|
|
225
|
+
// undefined, a function or a symbol is dropped, or comes back as null; a
|
|
226
|
+
// BigInt can't be written at all.
|
|
227
|
+
if (typeof value !== 'object')
|
|
228
|
+
return false;
|
|
229
|
+
if (depth > MAX_JSON_DEPTH)
|
|
230
|
+
return false;
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
for (let i = 0; i < value.length; i++) {
|
|
233
|
+
// An item that's undefined, a hole included, comes back as null.
|
|
234
|
+
if (!survivesJson(value[i], depth + 1))
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
// JSON.parse makes objects whose prototype is Object.prototype. Whatever
|
|
240
|
+
// has another one, a Date, a Map, a class instance, comes back as something
|
|
241
|
+
// else. One with none comes back as an ordinary object, which reads the
|
|
242
|
+
// same.
|
|
243
|
+
const prototype = Object.getPrototypeOf(value);
|
|
244
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
245
|
+
return false;
|
|
246
|
+
// for...in rather than Object.keys(): no array to allocate per object.
|
|
247
|
+
for (const key in value) {
|
|
248
|
+
const item = value[key];
|
|
249
|
+
if (item !== undefined && !survivesJson(item, depth + 1))
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
248
254
|
// The longest delay a browser timer holds, in ms. A longer one overflows and
|
|
249
255
|
// the timer fires at once.
|
|
250
256
|
const MAX_TIMEOUT = 2 ** 31 - 1;
|
|
@@ -277,6 +283,126 @@ function deferred() {
|
|
|
277
283
|
return { promise, resolve, reject };
|
|
278
284
|
}
|
|
279
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Where in Angular's TransferState the page carries the cache.
|
|
288
|
+
*
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
const TRANSFER_KEY = makeStateKey('ngx-signal-query');
|
|
292
|
+
/**
|
|
293
|
+
* Hands the cache from a server render over to the browser through Angular's
|
|
294
|
+
* TransferState: on the server, the queries go into the page once the render
|
|
295
|
+
* is done; in the browser, they go back into the cache, as fetched when the
|
|
296
|
+
* server fetched them. Must run in an injection context.
|
|
297
|
+
*
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
function transferCache(cache, gcTime) {
|
|
301
|
+
const transferState = inject(TransferState);
|
|
302
|
+
if (!inBrowser()) {
|
|
303
|
+
// Called as Angular writes the state into the page, once the app is
|
|
304
|
+
// stable: every fetch of the render has landed by then.
|
|
305
|
+
transferState.onSerialize(TRANSFER_KEY, () => dehydrate(cache));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const queries = transferState.get(TRANSFER_KEY, null);
|
|
309
|
+
if (!queries)
|
|
310
|
+
return;
|
|
311
|
+
// Read once: from now on the cache holds the data.
|
|
312
|
+
transferState.remove(TRANSFER_KEY);
|
|
313
|
+
// Outside Angular's zone: with zone.js, the gc timer of a query no
|
|
314
|
+
// component shows yet would keep the app from counting as stable, and
|
|
315
|
+
// Angular's HTTP transfer cache would go on answering its requests with the
|
|
316
|
+
// server's responses until then.
|
|
317
|
+
inject(NgZone).runOutsideAngular(() => hydrate(cache, queries, gcTime));
|
|
318
|
+
}
|
|
319
|
+
// The queries the page carries: those that succeeded, with data JSON carries
|
|
320
|
+
// as is. Any other is left to the browser, which fetches it as it would
|
|
321
|
+
// without a server render. Data JSON can't carry would reach the browser as
|
|
322
|
+
// something else, a Date as a string, or not at all: a BigInt or a value that
|
|
323
|
+
// references itself fails the whole page's state, Angular's own included.
|
|
324
|
+
function dehydrate(cache) {
|
|
325
|
+
return cache.getAll().flatMap((query) => {
|
|
326
|
+
const { status, data, updatedAt, isInvalidated } = query.state();
|
|
327
|
+
if (status !== 'success')
|
|
328
|
+
return [];
|
|
329
|
+
if (!survivesJson(data)) {
|
|
330
|
+
warnLeftOut(query.queryHash);
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
return [
|
|
334
|
+
{
|
|
335
|
+
key: query.key,
|
|
336
|
+
hash: query.queryHash,
|
|
337
|
+
data,
|
|
338
|
+
updatedAt,
|
|
339
|
+
...(isInvalidated && { isInvalidated }),
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Puts the queries the page carries into a cache nothing has read yet. One
|
|
345
|
+
// that nothing uses is kept for gcTime, as data written with setQueryData is.
|
|
346
|
+
function hydrate(cache, queries, gcTime) {
|
|
347
|
+
for (const { key, hash, data, updatedAt, isInvalidated } of queries) {
|
|
348
|
+
const query = cache.getOrCreate(key, gcTime, hash);
|
|
349
|
+
query.setData(data, { updatedAt });
|
|
350
|
+
if (isInvalidated)
|
|
351
|
+
query.invalidate();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Tells the developer why a query loads again in the browser. Development
|
|
355
|
+
// builds only: a production build drops it with the rest of Angular's
|
|
356
|
+
// development checks.
|
|
357
|
+
function warnLeftOut(queryHash) {
|
|
358
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
359
|
+
// eslint-disable-next-line no-console
|
|
360
|
+
console.warn(`ngx-signal-query: query ${queryHash} stays on the server: JSON can't carry its data as is (a Date, a Map or a class instance, say), so the browser fetches it again. Return plain data from its queryFn to bring it over.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @internal */
|
|
365
|
+
class Cache {
|
|
366
|
+
#entries = signal([], ...(ngDevMode ? [{ debugName: "#entries" }] : []));
|
|
367
|
+
// Reactive snapshot of all entries; updated on add/remove/clear so that
|
|
368
|
+
// findAll() (and its computed consumers like isFetching) react to the
|
|
369
|
+
// collection. Protected: only subclasses read it, never external code.
|
|
370
|
+
entries = this.#entries.asReadonly();
|
|
371
|
+
#entriesMap = new Map();
|
|
372
|
+
getAll() {
|
|
373
|
+
return Array.from(this.#entriesMap.values());
|
|
374
|
+
}
|
|
375
|
+
clear() {
|
|
376
|
+
this.#entriesMap.clear();
|
|
377
|
+
this.#sync();
|
|
378
|
+
}
|
|
379
|
+
// Updates the snapshot again once the current computation is over, in a
|
|
380
|
+
// microtask. An entry added in the middle of a computation is in the
|
|
381
|
+
// snapshot at once, but a computed that read the snapshot earlier in that
|
|
382
|
+
// computation doesn't hear of it: it only starts listening once its
|
|
383
|
+
// computation is done. The second update reaches it.
|
|
384
|
+
notifyLater() {
|
|
385
|
+
queueMicrotask(() => this.#sync());
|
|
386
|
+
}
|
|
387
|
+
addEntry(key, entry) {
|
|
388
|
+
this.#entriesMap.set(key, entry);
|
|
389
|
+
this.#sync();
|
|
390
|
+
return entry;
|
|
391
|
+
}
|
|
392
|
+
getEntry(key) {
|
|
393
|
+
return this.#entriesMap.get(key);
|
|
394
|
+
}
|
|
395
|
+
removeEntry(key) {
|
|
396
|
+
const removed = this.#entriesMap.delete(key);
|
|
397
|
+
if (removed)
|
|
398
|
+
this.#sync();
|
|
399
|
+
return removed;
|
|
400
|
+
}
|
|
401
|
+
#sync() {
|
|
402
|
+
this.#entries.set(Array.from(this.#entriesMap.values()));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
280
406
|
/**
|
|
281
407
|
* The error a promise of a query's data rejects with when the fetch stops
|
|
282
408
|
* with no data to give: it was cancelled before the query had any
|
|
@@ -338,10 +464,12 @@ class Query {
|
|
|
338
464
|
#gcTime = 0;
|
|
339
465
|
#gcTimer = null;
|
|
340
466
|
#cache;
|
|
341
|
-
|
|
467
|
+
#pendingTasks;
|
|
468
|
+
constructor({ key, queryHash, cache, gcTime, pendingTasks }) {
|
|
342
469
|
this.key = key;
|
|
343
470
|
this.queryHash = queryHash;
|
|
344
471
|
this.#cache = cache;
|
|
472
|
+
this.#pendingTasks = pendingTasks;
|
|
345
473
|
this.updateGcTime(gcTime);
|
|
346
474
|
// A new query has nothing using it yet: its creator is about to, and if
|
|
347
475
|
// nothing ever does, gc removes it.
|
|
@@ -453,8 +581,12 @@ class Query {
|
|
|
453
581
|
queryFn,
|
|
454
582
|
abortable: false,
|
|
455
583
|
retriesStopped: false,
|
|
584
|
+
removeTask: this.#pendingTasks.add(),
|
|
456
585
|
};
|
|
457
586
|
this.#inFlight = inFlight;
|
|
587
|
+
// Only now that this fetch holds its task: the app mustn't count as
|
|
588
|
+
// stable in between.
|
|
589
|
+
running?.removeTask();
|
|
458
590
|
// A query with no data reads like a first load while it fetches, even
|
|
459
591
|
// after a failure: 'pending' with no error, so a loading state and
|
|
460
592
|
// placeholderData show rather than the old error. One with data keeps
|
|
@@ -535,6 +667,8 @@ class Query {
|
|
|
535
667
|
});
|
|
536
668
|
outcome.resolve(data);
|
|
537
669
|
this.#scheduleGc();
|
|
670
|
+
// Last: whatever waits for the app to settle finds the data in.
|
|
671
|
+
inFlight.removeTask();
|
|
538
672
|
},
|
|
539
673
|
error: (err) => {
|
|
540
674
|
this.#inFlight = null;
|
|
@@ -546,6 +680,7 @@ class Query {
|
|
|
546
680
|
}));
|
|
547
681
|
outcome.reject(err);
|
|
548
682
|
this.#scheduleGc();
|
|
683
|
+
inFlight.removeTask();
|
|
549
684
|
},
|
|
550
685
|
}));
|
|
551
686
|
return outcome.promise;
|
|
@@ -627,6 +762,7 @@ class Query {
|
|
|
627
762
|
failureReason: before.failureReason,
|
|
628
763
|
isFetching: false,
|
|
629
764
|
}));
|
|
765
|
+
inFlight.removeTask();
|
|
630
766
|
}
|
|
631
767
|
// A fetch in flight is kept, as in TanStack, and the observers aren't even
|
|
632
768
|
// asked: fetch() would drop the refetch anyway.
|
|
@@ -685,12 +821,28 @@ class Query {
|
|
|
685
821
|
|
|
686
822
|
/** @internal */
|
|
687
823
|
class QueryCache extends Cache {
|
|
688
|
-
|
|
689
|
-
|
|
824
|
+
// Handed to each query it creates, which has no injector of its own.
|
|
825
|
+
#pendingTasks = inject(PendingTasks);
|
|
826
|
+
constructor() {
|
|
827
|
+
super();
|
|
828
|
+
// The cache goes with the injector that provides it. Its queries stop
|
|
829
|
+
// with it: once nothing can reach them, no fetch of theirs may keep the
|
|
830
|
+
// app busy, and no timer of theirs may run on.
|
|
831
|
+
inject(DestroyRef).onDestroy(() => this.clear());
|
|
832
|
+
}
|
|
833
|
+
// The hash is the key's, unless the caller brings the one the query had
|
|
834
|
+
// elsewhere, as a query the server hands over does.
|
|
835
|
+
getOrCreate(key, gcTime, queryHash = hashKey(key)) {
|
|
690
836
|
const exist = this.getEntry(queryHash);
|
|
691
837
|
if (exist)
|
|
692
838
|
return exist;
|
|
693
|
-
return this.addEntry(queryHash, new Query({
|
|
839
|
+
return this.addEntry(queryHash, new Query({
|
|
840
|
+
key,
|
|
841
|
+
queryHash,
|
|
842
|
+
cache: this,
|
|
843
|
+
gcTime,
|
|
844
|
+
pendingTasks: this.#pendingTasks,
|
|
845
|
+
}));
|
|
694
846
|
}
|
|
695
847
|
get(key) {
|
|
696
848
|
return this.getEntry(hashKey(key));
|
|
@@ -731,12 +883,12 @@ class QueryCache extends Cache {
|
|
|
731
883
|
this.getAll().forEach((query) => query.destroy());
|
|
732
884
|
super.clear();
|
|
733
885
|
}
|
|
734
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.21", ngImport: i0, type: QueryCache, deps:
|
|
886
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.21", ngImport: i0, type: QueryCache, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
735
887
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.21", ngImport: i0, type: QueryCache });
|
|
736
888
|
}
|
|
737
889
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.21", ngImport: i0, type: QueryCache, decorators: [{
|
|
738
890
|
type: Injectable
|
|
739
|
-
}] });
|
|
891
|
+
}], ctorParameters: () => [] });
|
|
740
892
|
|
|
741
893
|
/** @internal */
|
|
742
894
|
function getInitialState() {
|
|
@@ -762,10 +914,12 @@ class Mutation {
|
|
|
762
914
|
#observers = 0;
|
|
763
915
|
#options;
|
|
764
916
|
#cache;
|
|
765
|
-
|
|
917
|
+
#pendingTasks;
|
|
918
|
+
constructor(mutationId, options, cache, pendingTasks) {
|
|
766
919
|
this.mutationId = mutationId;
|
|
767
920
|
this.#options = options;
|
|
768
921
|
this.#cache = cache;
|
|
922
|
+
this.#pendingTasks = pendingTasks;
|
|
769
923
|
}
|
|
770
924
|
get observerCount() {
|
|
771
925
|
return this.#observers;
|
|
@@ -792,6 +946,11 @@ class Mutation {
|
|
|
792
946
|
*/
|
|
793
947
|
execute(variables) {
|
|
794
948
|
const run = deferred();
|
|
949
|
+
// One of the app's pending tasks, held until the last hook is done:
|
|
950
|
+
// Angular counts the app as busy meanwhile, so a server render and
|
|
951
|
+
// whenStable() wait for the run, whatever its mutationFn sends the write
|
|
952
|
+
// with. A run that outlives its component holds it all the same.
|
|
953
|
+
const removeTask = this.#pendingTasks.add();
|
|
795
954
|
this.#state.set({
|
|
796
955
|
status: 'pending',
|
|
797
956
|
data: undefined,
|
|
@@ -829,6 +988,8 @@ class Mutation {
|
|
|
829
988
|
else {
|
|
830
989
|
run.reject(outcome.error);
|
|
831
990
|
}
|
|
991
|
+
// Last: whatever waits for the app to settle finds the outcome in.
|
|
992
|
+
removeTask();
|
|
832
993
|
});
|
|
833
994
|
return run.promise;
|
|
834
995
|
}
|
|
@@ -901,8 +1062,10 @@ function runHook(hook) {
|
|
|
901
1062
|
/** @internal */
|
|
902
1063
|
class MutationCache extends Cache {
|
|
903
1064
|
#lastId = 0;
|
|
1065
|
+
// Handed to each mutation it builds, which has no injector of its own.
|
|
1066
|
+
#pendingTasks = inject(PendingTasks);
|
|
904
1067
|
build(options) {
|
|
905
|
-
const mutation = new Mutation(++this.#lastId, options, this);
|
|
1068
|
+
const mutation = new Mutation(++this.#lastId, options, this, this.#pendingTasks);
|
|
906
1069
|
this.addEntry(String(mutation.mutationId), mutation);
|
|
907
1070
|
return mutation;
|
|
908
1071
|
}
|
|
@@ -925,6 +1088,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.21", ngImpo
|
|
|
925
1088
|
|
|
926
1089
|
/** @internal */
|
|
927
1090
|
const QUERY_CLIENT_CONFIG = new InjectionToken('QUERY_CLIENT_CONFIG');
|
|
1091
|
+
/**
|
|
1092
|
+
* Set by {@link withHydration}: the client hands its cache from the server
|
|
1093
|
+
* render over to the browser.
|
|
1094
|
+
*
|
|
1095
|
+
* @internal
|
|
1096
|
+
*/
|
|
1097
|
+
const QUERY_CLIENT_HYDRATION = new InjectionToken('QUERY_CLIENT_HYDRATION');
|
|
928
1098
|
|
|
929
1099
|
const DEFAULT_GC_TIME = 5 * 60 * 1000;
|
|
930
1100
|
/**
|
|
@@ -964,6 +1134,12 @@ class QueryClient {
|
|
|
964
1134
|
/** @internal */
|
|
965
1135
|
constructor() {
|
|
966
1136
|
inject(DestroyRef).onDestroy(() => (this.#destroyed = true));
|
|
1137
|
+
// With withHydration(), the cache carries over from the server render.
|
|
1138
|
+
// Here, before anything can read the cache: whatever reads it first, a
|
|
1139
|
+
// component or an app initializer, finds the server's data in it.
|
|
1140
|
+
if (inject(QUERY_CLIENT_HYDRATION, { optional: true })) {
|
|
1141
|
+
transferCache(this.#cache, this.#gcTime());
|
|
1142
|
+
}
|
|
967
1143
|
// When the user comes back to the page, or the network comes back, each
|
|
968
1144
|
// query refetches if one of its observers wants that. Untracked like the
|
|
969
1145
|
// methods: an event fired from an effect mustn't make the effect depend
|
|
@@ -1041,7 +1217,9 @@ class QueryClient {
|
|
|
1041
1217
|
* if the fetch fails. A cancelled fetch resolves with the data the cache
|
|
1042
1218
|
* holds, or rejects with a {@link CancelledError} if it holds none; a
|
|
1043
1219
|
* removed query always rejects with one. Nothing has to wait for the
|
|
1044
|
-
* promise: a prefetch that fails only leaves the query in `'error'`.
|
|
1220
|
+
* promise: a prefetch that fails only leaves the query in `'error'`. A
|
|
1221
|
+
* server render and `whenStable()` wait for the fetch all the same, as for
|
|
1222
|
+
* a component's query, so its data is in the cache by then.
|
|
1045
1223
|
*
|
|
1046
1224
|
* Unlike a component's query in the browser, it doesn't retry a failure
|
|
1047
1225
|
* unless `retry` is set here or in {@link withDefaultOptions}, so whoever
|
|
@@ -1226,7 +1404,8 @@ class QueryClient {
|
|
|
1226
1404
|
* `refetch()`. A mutation in flight isn't stopped: its write
|
|
1227
1405
|
* has most likely reached the server, so it finishes and runs its hooks, as
|
|
1228
1406
|
* when its component goes away, and a component showing it sees how it
|
|
1229
|
-
* went. A hook that writes into the
|
|
1407
|
+
* went. The app counts as busy until it's done. A hook that writes into the
|
|
1408
|
+
* cache puts that key back.
|
|
1230
1409
|
*
|
|
1231
1410
|
* @example
|
|
1232
1411
|
* ```ts
|
|
@@ -1326,6 +1505,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.21", ngImpo
|
|
|
1326
1505
|
var QueryClientFeatureKind;
|
|
1327
1506
|
(function (QueryClientFeatureKind) {
|
|
1328
1507
|
QueryClientFeatureKind[QueryClientFeatureKind["DefaultOptions"] = 0] = "DefaultOptions";
|
|
1508
|
+
QueryClientFeatureKind[QueryClientFeatureKind["Hydration"] = 1] = "Hydration";
|
|
1329
1509
|
})(QueryClientFeatureKind || (QueryClientFeatureKind = {}));
|
|
1330
1510
|
/** @internal */
|
|
1331
1511
|
function queryClientFeature(kind, providers) {
|
|
@@ -1336,11 +1516,15 @@ function queryClientFeature(kind, providers) {
|
|
|
1336
1516
|
* Registers the {@link QueryClient} and its caches for the application.
|
|
1337
1517
|
*
|
|
1338
1518
|
* Call once at the app root (in `ApplicationConfig.providers` or a root
|
|
1339
|
-
* `bootstrapApplication`).
|
|
1340
|
-
*
|
|
1519
|
+
* `bootstrapApplication`). A client provided by another environment injector
|
|
1520
|
+
* goes with it: once that injector is destroyed, its queries stop, and none
|
|
1521
|
+
* of their fetches keeps the app busy. Pass features such as
|
|
1522
|
+
* {@link withDefaultOptions} to configure defaults, or {@link withHydration}
|
|
1523
|
+
* to bring the data of a server render over to the browser. Registering the
|
|
1524
|
+
* same feature twice throws.
|
|
1341
1525
|
*
|
|
1342
1526
|
* @param features - Optional {@link QueryClientFeature}s (e.g.
|
|
1343
|
-
* {@link withDefaultOptions}).
|
|
1527
|
+
* {@link withDefaultOptions}, {@link withHydration}).
|
|
1344
1528
|
* @returns Environment providers to add to the app's provider list.
|
|
1345
1529
|
*
|
|
1346
1530
|
* @example
|
|
@@ -1907,7 +2091,9 @@ function injectQueries(optionsFn, options) {
|
|
|
1907
2091
|
* context. Destroying it doesn't cancel a run in flight: the write has most
|
|
1908
2092
|
* likely reached the server, so the run finishes and fires its hooks. A
|
|
1909
2093
|
* `mutate()` that comes after that, say from a timer, runs and fires its
|
|
1910
|
-
* hooks too.
|
|
2094
|
+
* hooks too. From `mutate()` until its last hook is done, a run keeps the app
|
|
2095
|
+
* busy, whether its component is still there or not: a server render and
|
|
2096
|
+
* `whenStable()` wait for it.
|
|
1911
2097
|
*
|
|
1912
2098
|
* Must run in an injection context, or be given an explicit `injector`.
|
|
1913
2099
|
*
|
|
@@ -2178,6 +2364,47 @@ function withDefaultOptions(options) {
|
|
|
2178
2364
|
]);
|
|
2179
2365
|
}
|
|
2180
2366
|
|
|
2367
|
+
/**
|
|
2368
|
+
* Feature for {@link provideQueryClient} that brings the data a server render
|
|
2369
|
+
* fetched over to the browser, so the page shows it at once rather than a
|
|
2370
|
+
* loading state. Add it to the providers the server and the browser share. It
|
|
2371
|
+
* works with Angular's `provideClientHydration()` or without it.
|
|
2372
|
+
*
|
|
2373
|
+
* Only queries that succeeded go over, and only data JSON carries as is:
|
|
2374
|
+
* strings, numbers, booleans, `null`, plain objects and arrays. A query whose
|
|
2375
|
+
* data holds a `Date`, a `Map` or a class instance stays on the server, and
|
|
2376
|
+
* the browser fetches it again; in development the server logs a warning
|
|
2377
|
+
* naming it.
|
|
2378
|
+
*
|
|
2379
|
+
* In the browser the data counts as fetched when the server fetched it, and
|
|
2380
|
+
* `staleTime` decides as for any data in the cache: with the default of `0`,
|
|
2381
|
+
* a component refetches its query in the background as it appears, showing
|
|
2382
|
+
* the server's data meanwhile. Set a `staleTime` to skip that request.
|
|
2383
|
+
*
|
|
2384
|
+
* Give it to one client per app: clients provided by other environment
|
|
2385
|
+
* injectors would share one place in the page, and only one of them would get
|
|
2386
|
+
* its data over.
|
|
2387
|
+
*
|
|
2388
|
+
* @returns A {@link QueryClientFeature} to pass to {@link provideQueryClient}.
|
|
2389
|
+
*
|
|
2390
|
+
* @example
|
|
2391
|
+
* ```ts
|
|
2392
|
+
* export const appConfig: ApplicationConfig = {
|
|
2393
|
+
* providers: [
|
|
2394
|
+
* provideQueryClient(
|
|
2395
|
+
* withDefaultOptions({ queries: { staleTime: 30_000 } }),
|
|
2396
|
+
* withHydration(),
|
|
2397
|
+
* ),
|
|
2398
|
+
* ],
|
|
2399
|
+
* }
|
|
2400
|
+
* ```
|
|
2401
|
+
*/
|
|
2402
|
+
function withHydration() {
|
|
2403
|
+
return queryClientFeature(QueryClientFeatureKind.Hydration, [
|
|
2404
|
+
{ provide: QUERY_CLIENT_HYDRATION, useValue: true },
|
|
2405
|
+
]);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2181
2408
|
/*
|
|
2182
2409
|
* Public API Surface of ngx-query
|
|
2183
2410
|
*/
|
|
@@ -2186,5 +2413,5 @@ function withDefaultOptions(options) {
|
|
|
2186
2413
|
* Generated bundle index. Do not edit.
|
|
2187
2414
|
*/
|
|
2188
2415
|
|
|
2189
|
-
export { CancelledError, QueryClient, QueryClientFeatureKind, injectIsFetching, injectIsMutating, injectMutation, injectQueries, injectQuery, injectQueryClient, keepPreviousData, mutationOptions, provideQueryClient, queryOptions, withDefaultOptions };
|
|
2416
|
+
export { CancelledError, QueryClient, QueryClientFeatureKind, injectIsFetching, injectIsMutating, injectMutation, injectQueries, injectQuery, injectQueryClient, keepPreviousData, mutationOptions, provideQueryClient, queryOptions, withDefaultOptions, withHydration };
|
|
2190
2417
|
//# sourceMappingURL=ngx-signal-query.mjs.map
|