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 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, SSR cache hydration, devtools and persistence. If you need
36
- them today, TanStack Query has them.
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, DestroyRef, untracked, runInInjectionContext, makeEnvironmentProviders, assertInInjectionContext, effect, computed, Injector } from '@angular/core';
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
- constructor({ key, queryHash, cache, gcTime }) {
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
- getOrCreate(key, gcTime) {
689
- const queryHash = hashKey(key);
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({ key, queryHash, cache: this, gcTime }));
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: null, target: i0.ɵɵFactoryTarget.Injectable });
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
- constructor(mutationId, options, cache) {
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 cache puts that key back.
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`). Pass features such as {@link withDefaultOptions} to
1340
- * configure defaults. Registering the same feature twice throws.
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