@tanstack/solid-query 6.0.0-rc.1 → 6.0.0-rc.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.
@@ -251,6 +251,33 @@ export { DehydratedState }
251
251
 
252
252
  export { DehydrateOptions }
253
253
 
254
+ /**
255
+ * Waits for every fetch the client has in flight to settle, then
256
+ * dehydrates. This is the extraction half of a single-flight collector:
257
+ * after route data functions run for the mutation's target URL, loaders
258
+ * commonly kick off prefetches without awaiting them — plain
259
+ * `dehydrate()` would snapshot those mid-fetch and ship nothing.
260
+ *
261
+ * ```ts
262
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, (event, outcome) =>
263
+ * loadFlightTarget({
264
+ * router,
265
+ * event,
266
+ * outcome,
267
+ * collect: () => dehydrateSettled(queryClient),
268
+ * }),
269
+ * )
270
+ * ```
271
+ *
272
+ * Settling is chased to quiescence — awaiting one batch of fetches can
273
+ * dispatch more (dependent queries keyed off a first result) — so an
274
+ * unconditionally self-refetching query would keep this pending; that's
275
+ * an app bug mirrored, not guarded.
276
+ */
277
+ declare function dehydrateSettled(client: QueryClient, options?: DehydrateOptions): Promise<DehydratedState>;
278
+ export { dehydrateSettled }
279
+ export { dehydrateSettled as dehydrateSettled_alias_1 }
280
+
254
281
  export { DistributiveOmit }
255
282
 
256
283
  export { EnsureInfiniteQueryDataOptions }
@@ -271,6 +298,40 @@ export { FetchQueryOptions }
271
298
 
272
299
  export { FetchStatus }
273
300
 
301
+ /**
302
+ * The query cache's single-flight source id. Mutation responses can fold
303
+ * fresh data for multiple caches at once (Solid's multi-source
304
+ * single-flight protocol); this is the slice the query cache claims — the
305
+ * provider subscribes its consumer under it, and server collectors
306
+ * register under it to produce the data:
307
+ *
308
+ * ```ts
309
+ * import { registerFlightDataSource } from '@solidjs/web/server-functions/server'
310
+ * import { FLIGHT_DATA_SOURCE, dehydrate } from '@tanstack/solid-query'
311
+ *
312
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, async (event, outcome) => {
313
+ * // rebuild the data for outcome.targetUrl into a QueryClient, then
314
+ * return dehydrate(queryClient)
315
+ * })
316
+ * ```
317
+ *
318
+ * The slice's payload is a `DehydratedState`; the provider consumes it
319
+ * with `hydrate()`, so every mounted query on those keys updates before
320
+ * the mutation's promise resolves — no follow-up refetches.
321
+ *
322
+ * A mutation can additionally declare its invalidation SCOPE with
323
+ * `X-Revalidate` keys (the `revalidate` option of Solid's `reload`/
324
+ * `redirect` response helpers). The payload covers the slice of that scope
325
+ * the server could recompute; whatever a declared key matches beyond it is
326
+ * swept client-side (see the consumer below). For the sweep to be
327
+ * delivered, the collector must contribute a slice — return the dehydrated
328
+ * state even when it holds no queries if `outcome.revalidateKeys` is
329
+ * present, rather than skipping the source.
330
+ */
331
+ declare const FLIGHT_DATA_SOURCE = "sq";
332
+ export { FLIGHT_DATA_SOURCE }
333
+ export { FLIGHT_DATA_SOURCE as FLIGHT_DATA_SOURCE_alias_1 }
334
+
274
335
  export { focusManager }
275
336
 
276
337
  export { GetNextPageParamFunction }
@@ -555,8 +616,14 @@ export { QueryClientContext as QueryClientContext_alias_1 }
555
616
  /**
556
617
  * Provides the QueryClient and manages its mount lifecycle. On the server
557
618
  * it also registers the cache serializer above; on the client, hooks prime
558
- * the cache from their hash-keyed registry entries themselves — see
559
- * `useBaseQuery`.
619
+ * the cache from their hash-keyed registry entries themselves (see
620
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
621
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
622
+ * `DehydratedState` produced by a server collector registered under the
623
+ * same id) hydrate this client before the mutation's promise resolves.
624
+ * Subscribing is inert when no server collector exists — the server just
625
+ * folds nothing — so it is unconditional. One consumer per source: with
626
+ * nested providers, the innermost mounted one owns the slice.
560
627
  */
561
628
  declare const QueryClientProvider: (props: QueryClientProviderProps) => JSX.Element;
562
629
  export { QueryClientProvider }
@@ -251,6 +251,33 @@ export { DehydratedState }
251
251
 
252
252
  export { DehydrateOptions }
253
253
 
254
+ /**
255
+ * Waits for every fetch the client has in flight to settle, then
256
+ * dehydrates. This is the extraction half of a single-flight collector:
257
+ * after route data functions run for the mutation's target URL, loaders
258
+ * commonly kick off prefetches without awaiting them — plain
259
+ * `dehydrate()` would snapshot those mid-fetch and ship nothing.
260
+ *
261
+ * ```ts
262
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, (event, outcome) =>
263
+ * loadFlightTarget({
264
+ * router,
265
+ * event,
266
+ * outcome,
267
+ * collect: () => dehydrateSettled(queryClient),
268
+ * }),
269
+ * )
270
+ * ```
271
+ *
272
+ * Settling is chased to quiescence — awaiting one batch of fetches can
273
+ * dispatch more (dependent queries keyed off a first result) — so an
274
+ * unconditionally self-refetching query would keep this pending; that's
275
+ * an app bug mirrored, not guarded.
276
+ */
277
+ declare function dehydrateSettled(client: QueryClient, options?: DehydrateOptions): Promise<DehydratedState>;
278
+ export { dehydrateSettled }
279
+ export { dehydrateSettled as dehydrateSettled_alias_1 }
280
+
254
281
  export { DistributiveOmit }
255
282
 
256
283
  export { EnsureInfiniteQueryDataOptions }
@@ -271,6 +298,40 @@ export { FetchQueryOptions }
271
298
 
272
299
  export { FetchStatus }
273
300
 
301
+ /**
302
+ * The query cache's single-flight source id. Mutation responses can fold
303
+ * fresh data for multiple caches at once (Solid's multi-source
304
+ * single-flight protocol); this is the slice the query cache claims — the
305
+ * provider subscribes its consumer under it, and server collectors
306
+ * register under it to produce the data:
307
+ *
308
+ * ```ts
309
+ * import { registerFlightDataSource } from '@solidjs/web/server-functions/server'
310
+ * import { FLIGHT_DATA_SOURCE, dehydrate } from '@tanstack/solid-query'
311
+ *
312
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, async (event, outcome) => {
313
+ * // rebuild the data for outcome.targetUrl into a QueryClient, then
314
+ * return dehydrate(queryClient)
315
+ * })
316
+ * ```
317
+ *
318
+ * The slice's payload is a `DehydratedState`; the provider consumes it
319
+ * with `hydrate()`, so every mounted query on those keys updates before
320
+ * the mutation's promise resolves — no follow-up refetches.
321
+ *
322
+ * A mutation can additionally declare its invalidation SCOPE with
323
+ * `X-Revalidate` keys (the `revalidate` option of Solid's `reload`/
324
+ * `redirect` response helpers). The payload covers the slice of that scope
325
+ * the server could recompute; whatever a declared key matches beyond it is
326
+ * swept client-side (see the consumer below). For the sweep to be
327
+ * delivered, the collector must contribute a slice — return the dehydrated
328
+ * state even when it holds no queries if `outcome.revalidateKeys` is
329
+ * present, rather than skipping the source.
330
+ */
331
+ declare const FLIGHT_DATA_SOURCE = "sq";
332
+ export { FLIGHT_DATA_SOURCE }
333
+ export { FLIGHT_DATA_SOURCE as FLIGHT_DATA_SOURCE_alias_1 }
334
+
274
335
  export { focusManager }
275
336
 
276
337
  export { GetNextPageParamFunction }
@@ -555,8 +616,14 @@ export { QueryClientContext as QueryClientContext_alias_1 }
555
616
  /**
556
617
  * Provides the QueryClient and manages its mount lifecycle. On the server
557
618
  * it also registers the cache serializer above; on the client, hooks prime
558
- * the cache from their hash-keyed registry entries themselves — see
559
- * `useBaseQuery`.
619
+ * the cache from their hash-keyed registry entries themselves (see
620
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
621
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
622
+ * `DehydratedState` produced by a server collector registered under the
623
+ * same id) hydrate this client before the mutation's promise resolves.
624
+ * Subscribing is inert when no server collector exists — the server just
625
+ * folds nothing — so it is unconditional. One consumer per source: with
626
+ * nested providers, the innermost mounted one owns the slice.
560
627
  */
561
628
  declare const QueryClientProvider: (props: QueryClientProviderProps) => JSX.Element;
562
629
  export { QueryClientProvider }
package/build/dev.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  var queryCore = require('@tanstack/query-core');
4
4
  var solidJs = require('solid-js');
5
5
  var web = require('@solidjs/web');
6
+ var serverFunctions = require('@solidjs/web/server-functions');
6
7
 
7
8
  // src/index.ts
8
9
  exports.QueryClient = class QueryClient extends queryCore.QueryClient {
@@ -12,6 +13,18 @@ exports.QueryClient = class QueryClient extends queryCore.QueryClient {
12
13
  };
13
14
  var isServer = typeof window === "undefined";
14
15
  var HYDRATION_KEY_PREFIX = "sq:";
16
+ exports.FLIGHT_DATA_SOURCE = "sq";
17
+ function sweepRevalidatedQueries(client, payload, response) {
18
+ const keys = response.headers.get(web.REVALIDATE_HEADER)?.split(",");
19
+ if (!keys) return;
20
+ const covered = new Set(payload.queries.map((query) => query.queryHash));
21
+ for (const key of keys) {
22
+ client.invalidateQueries({
23
+ queryKey: [key],
24
+ predicate: (query) => !covered.has(query.queryHash)
25
+ }).catch(() => void 0);
26
+ }
27
+ }
15
28
  exports.QueryClientContext = solidJs.createContext(null);
16
29
  exports.useQueryClient = (queryClient) => {
17
30
  if (queryClient) {
@@ -27,9 +40,11 @@ function serializeCacheOnServer(client) {
27
40
  const ctx = solidJs.sharedConfig.context;
28
41
  if (!ctx || !ctx.async || ctx.noHydrate) return;
29
42
  const cache = client.getQueryCache();
43
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
30
44
  const seen = /* @__PURE__ */ new Set();
31
45
  const serializeQuery = (query) => {
32
46
  if (seen.has(query.queryHash)) return;
47
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
33
48
  const state = query.state;
34
49
  if (state.status === "success") {
35
50
  seen.add(query.queryHash);
@@ -53,7 +68,18 @@ function serializeCacheOnServer(client) {
53
68
  exports.QueryClientProvider = (props) => {
54
69
  props.client.mount();
55
70
  solidJs.onCleanup(() => props.client.unmount());
56
- if (isServer) serializeCacheOnServer(props.client);
71
+ if (isServer) {
72
+ serializeCacheOnServer(props.client);
73
+ solidJs.onCleanup(() => {
74
+ props.client.cancelQueries().catch(() => void 0);
75
+ props.client.clear();
76
+ });
77
+ } else {
78
+ solidJs.onCleanup(serverFunctions.subscribeFlightData(exports.FLIGHT_DATA_SOURCE, (data, context) => {
79
+ queryCore.hydrate(props.client, data);
80
+ sweepRevalidatedQueries(props.client, data, context.response);
81
+ }));
82
+ }
57
83
  return web.createComponent(exports.QueryClientContext, {
58
84
  value: () => props.client,
59
85
  get children() {
@@ -432,6 +458,15 @@ function useQuery(options, queryClient) {
432
458
  function queryOptions(options) {
433
459
  return options;
434
460
  }
461
+ async function dehydrateSettled(client, options) {
462
+ const cache = client.getQueryCache();
463
+ for (; ; ) {
464
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
465
+ if (pending.length === 0) break;
466
+ await Promise.allSettled(pending);
467
+ }
468
+ return queryCore.dehydrate(client, options);
469
+ }
435
470
  var isServer3 = typeof window === "undefined";
436
471
  function createCacheAggregate(subscribe, read, empty) {
437
472
  const hydratedMount = !isServer3 && solidJs.sharedConfig.hydrating === true;
@@ -592,15 +627,13 @@ function useMutation(options, queryClient) {
592
627
  const [flight, setFlight] = solidJs.createOptimistic(null);
593
628
  const [settled, setSettled] = solidJs.createSignal(IDLE);
594
629
  let activeMutation = null;
630
+ let unsubscribeFlight = null;
595
631
  const [flightVersion, setFlightVersion] = solidJs.createSignal(0);
596
- if (!isServer4) {
597
- const unsubscribe = client().getMutationCache().subscribe((event) => {
598
- if ("mutation" in event && event.mutation === activeMutation) {
599
- setFlightVersion((v) => v + 1);
600
- }
632
+ if (!isServer4)
633
+ solidJs.onCleanup(() => {
634
+ unsubscribeFlight?.();
635
+ unsubscribeFlight = null;
601
636
  });
602
- solidJs.onCleanup(unsubscribe);
603
- }
604
637
  async function* run(variables) {
605
638
  const opts = solidJs.untrack(options);
606
639
  const callbackContext = {
@@ -610,7 +643,8 @@ function useMutation(options, queryClient) {
610
643
  };
611
644
  if (!isServer4) setFlight({ variables });
612
645
  opts.onMutate?.(variables, callbackContext);
613
- const mutation = client().getMutationCache().build(client(), {
646
+ const c = solidJs.untrack(client);
647
+ const mutation = c.getMutationCache().build(c, {
614
648
  ...opts,
615
649
  // Options-level callbacks re-run below, inside the transaction —
616
650
  // executing them here (inside the await gap) would let their
@@ -621,6 +655,20 @@ function useMutation(options, queryClient) {
621
655
  onSettled: void 0
622
656
  });
623
657
  activeMutation = mutation;
658
+ if (!isServer4) {
659
+ unsubscribeFlight?.();
660
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
661
+ if ("mutation" in event && event.mutation === activeMutation) {
662
+ setFlightVersion((v) => v + 1);
663
+ }
664
+ });
665
+ }
666
+ const settleFlight = () => {
667
+ if (activeMutation !== mutation) return;
668
+ activeMutation = null;
669
+ unsubscribeFlight?.();
670
+ unsubscribeFlight = null;
671
+ };
624
672
  async function* settleFailure(error) {
625
673
  try {
626
674
  if (opts.onError)
@@ -648,7 +696,7 @@ function useMutation(options, queryClient) {
648
696
  variables,
649
697
  submittedAt: mutation.state.submittedAt
650
698
  });
651
- activeMutation = null;
699
+ settleFlight();
652
700
  throw error;
653
701
  }
654
702
  let result2;
@@ -688,7 +736,7 @@ function useMutation(options, queryClient) {
688
736
  variables,
689
737
  submittedAt: mutation.state.submittedAt
690
738
  });
691
- activeMutation = null;
739
+ settleFlight();
692
740
  return result2;
693
741
  }
694
742
  const invoke = isServer4 ? (variables) => drain(run(variables)) : solidJs.action(run);
@@ -826,6 +874,7 @@ function useQueries(queriesOptions, queryClient) {
826
874
  });
827
875
  }
828
876
 
877
+ exports.dehydrateSettled = dehydrateSettled;
829
878
  exports.infiniteQueryOptions = infiniteQueryOptions;
830
879
  exports.mutationOptions = mutationOptions;
831
880
  exports.queryOptions = queryOptions;
package/build/dev.d.cts CHANGED
@@ -23,10 +23,12 @@ export { useQuery } from './_tsup-dts-rollup.cjs';
23
23
  export { queryOptions } from './_tsup-dts-rollup.cjs';
24
24
  export { DefinedInitialDataOptions } from './_tsup-dts-rollup.cjs';
25
25
  export { UndefinedInitialDataOptions } from './_tsup-dts-rollup.cjs';
26
+ export { FLIGHT_DATA_SOURCE_alias_1 as FLIGHT_DATA_SOURCE } from './_tsup-dts-rollup.cjs';
26
27
  export { QueryClientContext_alias_1 as QueryClientContext } from './_tsup-dts-rollup.cjs';
27
28
  export { QueryClientProvider_alias_1 as QueryClientProvider } from './_tsup-dts-rollup.cjs';
28
29
  export { useQueryClient_alias_1 as useQueryClient } from './_tsup-dts-rollup.cjs';
29
30
  export { QueryClientProviderProps_alias_1 as QueryClientProviderProps } from './_tsup-dts-rollup.cjs';
31
+ export { dehydrateSettled_alias_1 as dehydrateSettled } from './_tsup-dts-rollup.cjs';
30
32
  export { useIsFetching } from './_tsup-dts-rollup.cjs';
31
33
  export { useInfiniteQuery } from './_tsup-dts-rollup.cjs';
32
34
  export { infiniteQueryOptions } from './_tsup-dts-rollup.cjs';
package/build/dev.d.ts CHANGED
@@ -23,10 +23,12 @@ export { useQuery } from './_tsup-dts-rollup.js';
23
23
  export { queryOptions } from './_tsup-dts-rollup.js';
24
24
  export { DefinedInitialDataOptions } from './_tsup-dts-rollup.js';
25
25
  export { UndefinedInitialDataOptions } from './_tsup-dts-rollup.js';
26
+ export { FLIGHT_DATA_SOURCE_alias_1 as FLIGHT_DATA_SOURCE } from './_tsup-dts-rollup.js';
26
27
  export { QueryClientContext_alias_1 as QueryClientContext } from './_tsup-dts-rollup.js';
27
28
  export { QueryClientProvider_alias_1 as QueryClientProvider } from './_tsup-dts-rollup.js';
28
29
  export { useQueryClient_alias_1 as useQueryClient } from './_tsup-dts-rollup.js';
29
30
  export { QueryClientProviderProps_alias_1 as QueryClientProviderProps } from './_tsup-dts-rollup.js';
31
+ export { dehydrateSettled_alias_1 as dehydrateSettled } from './_tsup-dts-rollup.js';
30
32
  export { useIsFetching } from './_tsup-dts-rollup.js';
31
33
  export { useInfiniteQuery } from './_tsup-dts-rollup.js';
32
34
  export { infiniteQueryOptions } from './_tsup-dts-rollup.js';
package/build/dev.js CHANGED
@@ -1,7 +1,8 @@
1
- import { noop, QueryClient as QueryClient$1, shouldThrowError, InfiniteQueryObserver, hydrate, QueryObserver, replaceEqualDeep } from '@tanstack/query-core';
1
+ import { noop, QueryClient as QueryClient$1, hydrate, dehydrate, shouldThrowError, InfiniteQueryObserver, QueryObserver, replaceEqualDeep } from '@tanstack/query-core';
2
2
  export * from '@tanstack/query-core';
3
3
  import { createContext, useContext, onCleanup, createMemo, createOptimistic, createSignal, action, createRenderEffect, untrack, repeat, $TRACK, sharedConfig, createEffect, createProjection, runWithOwner, isPending, resolve } from 'solid-js';
4
- import { createComponent } from '@solidjs/web';
4
+ import { createComponent, REVALIDATE_HEADER } from '@solidjs/web';
5
+ import { subscribeFlightData } from '@solidjs/web/server-functions';
5
6
 
6
7
  // src/index.ts
7
8
  var QueryClient = class extends QueryClient$1 {
@@ -11,6 +12,18 @@ var QueryClient = class extends QueryClient$1 {
11
12
  };
12
13
  var isServer = typeof window === "undefined";
13
14
  var HYDRATION_KEY_PREFIX = "sq:";
15
+ var FLIGHT_DATA_SOURCE = "sq";
16
+ function sweepRevalidatedQueries(client, payload, response) {
17
+ const keys = response.headers.get(REVALIDATE_HEADER)?.split(",");
18
+ if (!keys) return;
19
+ const covered = new Set(payload.queries.map((query) => query.queryHash));
20
+ for (const key of keys) {
21
+ client.invalidateQueries({
22
+ queryKey: [key],
23
+ predicate: (query) => !covered.has(query.queryHash)
24
+ }).catch(() => void 0);
25
+ }
26
+ }
14
27
  var QueryClientContext = createContext(null);
15
28
  var useQueryClient = (queryClient) => {
16
29
  if (queryClient) {
@@ -26,9 +39,11 @@ function serializeCacheOnServer(client) {
26
39
  const ctx = sharedConfig.context;
27
40
  if (!ctx || !ctx.async || ctx.noHydrate) return;
28
41
  const cache = client.getQueryCache();
42
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
29
43
  const seen = /* @__PURE__ */ new Set();
30
44
  const serializeQuery = (query) => {
31
45
  if (seen.has(query.queryHash)) return;
46
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
32
47
  const state = query.state;
33
48
  if (state.status === "success") {
34
49
  seen.add(query.queryHash);
@@ -52,7 +67,18 @@ function serializeCacheOnServer(client) {
52
67
  var QueryClientProvider = (props) => {
53
68
  props.client.mount();
54
69
  onCleanup(() => props.client.unmount());
55
- if (isServer) serializeCacheOnServer(props.client);
70
+ if (isServer) {
71
+ serializeCacheOnServer(props.client);
72
+ onCleanup(() => {
73
+ props.client.cancelQueries().catch(() => void 0);
74
+ props.client.clear();
75
+ });
76
+ } else {
77
+ onCleanup(subscribeFlightData(FLIGHT_DATA_SOURCE, (data, context) => {
78
+ hydrate(props.client, data);
79
+ sweepRevalidatedQueries(props.client, data, context.response);
80
+ }));
81
+ }
56
82
  return createComponent(QueryClientContext, {
57
83
  value: () => props.client,
58
84
  get children() {
@@ -431,6 +457,15 @@ function useQuery(options, queryClient) {
431
457
  function queryOptions(options) {
432
458
  return options;
433
459
  }
460
+ async function dehydrateSettled(client, options) {
461
+ const cache = client.getQueryCache();
462
+ for (; ; ) {
463
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
464
+ if (pending.length === 0) break;
465
+ await Promise.allSettled(pending);
466
+ }
467
+ return dehydrate(client, options);
468
+ }
434
469
  var isServer3 = typeof window === "undefined";
435
470
  function createCacheAggregate(subscribe, read, empty) {
436
471
  const hydratedMount = !isServer3 && sharedConfig.hydrating === true;
@@ -591,15 +626,13 @@ function useMutation(options, queryClient) {
591
626
  const [flight, setFlight] = createOptimistic(null);
592
627
  const [settled, setSettled] = createSignal(IDLE);
593
628
  let activeMutation = null;
629
+ let unsubscribeFlight = null;
594
630
  const [flightVersion, setFlightVersion] = createSignal(0);
595
- if (!isServer4) {
596
- const unsubscribe = client().getMutationCache().subscribe((event) => {
597
- if ("mutation" in event && event.mutation === activeMutation) {
598
- setFlightVersion((v) => v + 1);
599
- }
631
+ if (!isServer4)
632
+ onCleanup(() => {
633
+ unsubscribeFlight?.();
634
+ unsubscribeFlight = null;
600
635
  });
601
- onCleanup(unsubscribe);
602
- }
603
636
  async function* run(variables) {
604
637
  const opts = untrack(options);
605
638
  const callbackContext = {
@@ -609,7 +642,8 @@ function useMutation(options, queryClient) {
609
642
  };
610
643
  if (!isServer4) setFlight({ variables });
611
644
  opts.onMutate?.(variables, callbackContext);
612
- const mutation = client().getMutationCache().build(client(), {
645
+ const c = untrack(client);
646
+ const mutation = c.getMutationCache().build(c, {
613
647
  ...opts,
614
648
  // Options-level callbacks re-run below, inside the transaction —
615
649
  // executing them here (inside the await gap) would let their
@@ -620,6 +654,20 @@ function useMutation(options, queryClient) {
620
654
  onSettled: void 0
621
655
  });
622
656
  activeMutation = mutation;
657
+ if (!isServer4) {
658
+ unsubscribeFlight?.();
659
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
660
+ if ("mutation" in event && event.mutation === activeMutation) {
661
+ setFlightVersion((v) => v + 1);
662
+ }
663
+ });
664
+ }
665
+ const settleFlight = () => {
666
+ if (activeMutation !== mutation) return;
667
+ activeMutation = null;
668
+ unsubscribeFlight?.();
669
+ unsubscribeFlight = null;
670
+ };
623
671
  async function* settleFailure(error) {
624
672
  try {
625
673
  if (opts.onError)
@@ -647,7 +695,7 @@ function useMutation(options, queryClient) {
647
695
  variables,
648
696
  submittedAt: mutation.state.submittedAt
649
697
  });
650
- activeMutation = null;
698
+ settleFlight();
651
699
  throw error;
652
700
  }
653
701
  let result2;
@@ -687,7 +735,7 @@ function useMutation(options, queryClient) {
687
735
  variables,
688
736
  submittedAt: mutation.state.submittedAt
689
737
  });
690
- activeMutation = null;
738
+ settleFlight();
691
739
  return result2;
692
740
  }
693
741
  const invoke = isServer4 ? (variables) => drain(run(variables)) : action(run);
@@ -825,4 +873,4 @@ function useQueries(queriesOptions, queryClient) {
825
873
  });
826
874
  }
827
875
 
828
- export { IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
876
+ export { FLIGHT_DATA_SOURCE, IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, dehydrateSettled, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
package/build/index.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  var queryCore = require('@tanstack/query-core');
4
4
  var solidJs = require('solid-js');
5
5
  var web = require('@solidjs/web');
6
+ var serverFunctions = require('@solidjs/web/server-functions');
6
7
 
7
8
  // src/index.ts
8
9
  exports.QueryClient = class QueryClient extends queryCore.QueryClient {
@@ -12,6 +13,18 @@ exports.QueryClient = class QueryClient extends queryCore.QueryClient {
12
13
  };
13
14
  var isServer = typeof window === "undefined";
14
15
  var HYDRATION_KEY_PREFIX = "sq:";
16
+ exports.FLIGHT_DATA_SOURCE = "sq";
17
+ function sweepRevalidatedQueries(client, payload, response) {
18
+ const keys = response.headers.get(web.REVALIDATE_HEADER)?.split(",");
19
+ if (!keys) return;
20
+ const covered = new Set(payload.queries.map((query) => query.queryHash));
21
+ for (const key of keys) {
22
+ client.invalidateQueries({
23
+ queryKey: [key],
24
+ predicate: (query) => !covered.has(query.queryHash)
25
+ }).catch(() => void 0);
26
+ }
27
+ }
15
28
  exports.QueryClientContext = solidJs.createContext(null);
16
29
  exports.useQueryClient = (queryClient) => {
17
30
  if (queryClient) {
@@ -27,9 +40,11 @@ function serializeCacheOnServer(client) {
27
40
  const ctx = solidJs.sharedConfig.context;
28
41
  if (!ctx || !ctx.async || ctx.noHydrate) return;
29
42
  const cache = client.getQueryCache();
43
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
30
44
  const seen = /* @__PURE__ */ new Set();
31
45
  const serializeQuery = (query) => {
32
46
  if (seen.has(query.queryHash)) return;
47
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
33
48
  const state = query.state;
34
49
  if (state.status === "success") {
35
50
  seen.add(query.queryHash);
@@ -53,7 +68,18 @@ function serializeCacheOnServer(client) {
53
68
  exports.QueryClientProvider = (props) => {
54
69
  props.client.mount();
55
70
  solidJs.onCleanup(() => props.client.unmount());
56
- if (isServer) serializeCacheOnServer(props.client);
71
+ if (isServer) {
72
+ serializeCacheOnServer(props.client);
73
+ solidJs.onCleanup(() => {
74
+ props.client.cancelQueries().catch(() => void 0);
75
+ props.client.clear();
76
+ });
77
+ } else {
78
+ solidJs.onCleanup(serverFunctions.subscribeFlightData(exports.FLIGHT_DATA_SOURCE, (data, context) => {
79
+ queryCore.hydrate(props.client, data);
80
+ sweepRevalidatedQueries(props.client, data, context.response);
81
+ }));
82
+ }
57
83
  return web.createComponent(exports.QueryClientContext, {
58
84
  value: () => props.client,
59
85
  get children() {
@@ -432,6 +458,15 @@ function useQuery(options, queryClient) {
432
458
  function queryOptions(options) {
433
459
  return options;
434
460
  }
461
+ async function dehydrateSettled(client, options) {
462
+ const cache = client.getQueryCache();
463
+ for (; ; ) {
464
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
465
+ if (pending.length === 0) break;
466
+ await Promise.allSettled(pending);
467
+ }
468
+ return queryCore.dehydrate(client, options);
469
+ }
435
470
  var isServer3 = typeof window === "undefined";
436
471
  function createCacheAggregate(subscribe, read, empty) {
437
472
  const hydratedMount = !isServer3 && solidJs.sharedConfig.hydrating === true;
@@ -592,15 +627,13 @@ function useMutation(options, queryClient) {
592
627
  const [flight, setFlight] = solidJs.createOptimistic(null);
593
628
  const [settled, setSettled] = solidJs.createSignal(IDLE);
594
629
  let activeMutation = null;
630
+ let unsubscribeFlight = null;
595
631
  const [flightVersion, setFlightVersion] = solidJs.createSignal(0);
596
- if (!isServer4) {
597
- const unsubscribe = client().getMutationCache().subscribe((event) => {
598
- if ("mutation" in event && event.mutation === activeMutation) {
599
- setFlightVersion((v) => v + 1);
600
- }
632
+ if (!isServer4)
633
+ solidJs.onCleanup(() => {
634
+ unsubscribeFlight?.();
635
+ unsubscribeFlight = null;
601
636
  });
602
- solidJs.onCleanup(unsubscribe);
603
- }
604
637
  async function* run(variables) {
605
638
  const opts = solidJs.untrack(options);
606
639
  const callbackContext = {
@@ -610,7 +643,8 @@ function useMutation(options, queryClient) {
610
643
  };
611
644
  if (!isServer4) setFlight({ variables });
612
645
  opts.onMutate?.(variables, callbackContext);
613
- const mutation = client().getMutationCache().build(client(), {
646
+ const c = solidJs.untrack(client);
647
+ const mutation = c.getMutationCache().build(c, {
614
648
  ...opts,
615
649
  // Options-level callbacks re-run below, inside the transaction —
616
650
  // executing them here (inside the await gap) would let their
@@ -621,6 +655,20 @@ function useMutation(options, queryClient) {
621
655
  onSettled: void 0
622
656
  });
623
657
  activeMutation = mutation;
658
+ if (!isServer4) {
659
+ unsubscribeFlight?.();
660
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
661
+ if ("mutation" in event && event.mutation === activeMutation) {
662
+ setFlightVersion((v) => v + 1);
663
+ }
664
+ });
665
+ }
666
+ const settleFlight = () => {
667
+ if (activeMutation !== mutation) return;
668
+ activeMutation = null;
669
+ unsubscribeFlight?.();
670
+ unsubscribeFlight = null;
671
+ };
624
672
  async function* settleFailure(error) {
625
673
  try {
626
674
  if (opts.onError)
@@ -648,7 +696,7 @@ function useMutation(options, queryClient) {
648
696
  variables,
649
697
  submittedAt: mutation.state.submittedAt
650
698
  });
651
- activeMutation = null;
699
+ settleFlight();
652
700
  throw error;
653
701
  }
654
702
  let result2;
@@ -688,7 +736,7 @@ function useMutation(options, queryClient) {
688
736
  variables,
689
737
  submittedAt: mutation.state.submittedAt
690
738
  });
691
- activeMutation = null;
739
+ settleFlight();
692
740
  return result2;
693
741
  }
694
742
  const invoke = isServer4 ? (variables) => drain(run(variables)) : solidJs.action(run);
@@ -826,6 +874,7 @@ function useQueries(queriesOptions, queryClient) {
826
874
  });
827
875
  }
828
876
 
877
+ exports.dehydrateSettled = dehydrateSettled;
829
878
  exports.infiniteQueryOptions = infiniteQueryOptions;
830
879
  exports.mutationOptions = mutationOptions;
831
880
  exports.queryOptions = queryOptions;
package/build/index.d.cts CHANGED
@@ -23,10 +23,12 @@ export { useQuery } from './_tsup-dts-rollup.cjs';
23
23
  export { queryOptions } from './_tsup-dts-rollup.cjs';
24
24
  export { DefinedInitialDataOptions } from './_tsup-dts-rollup.cjs';
25
25
  export { UndefinedInitialDataOptions } from './_tsup-dts-rollup.cjs';
26
+ export { FLIGHT_DATA_SOURCE_alias_1 as FLIGHT_DATA_SOURCE } from './_tsup-dts-rollup.cjs';
26
27
  export { QueryClientContext_alias_1 as QueryClientContext } from './_tsup-dts-rollup.cjs';
27
28
  export { QueryClientProvider_alias_1 as QueryClientProvider } from './_tsup-dts-rollup.cjs';
28
29
  export { useQueryClient_alias_1 as useQueryClient } from './_tsup-dts-rollup.cjs';
29
30
  export { QueryClientProviderProps_alias_1 as QueryClientProviderProps } from './_tsup-dts-rollup.cjs';
31
+ export { dehydrateSettled_alias_1 as dehydrateSettled } from './_tsup-dts-rollup.cjs';
30
32
  export { useIsFetching } from './_tsup-dts-rollup.cjs';
31
33
  export { useInfiniteQuery } from './_tsup-dts-rollup.cjs';
32
34
  export { infiniteQueryOptions } from './_tsup-dts-rollup.cjs';
package/build/index.d.ts CHANGED
@@ -23,10 +23,12 @@ export { useQuery } from './_tsup-dts-rollup.js';
23
23
  export { queryOptions } from './_tsup-dts-rollup.js';
24
24
  export { DefinedInitialDataOptions } from './_tsup-dts-rollup.js';
25
25
  export { UndefinedInitialDataOptions } from './_tsup-dts-rollup.js';
26
+ export { FLIGHT_DATA_SOURCE_alias_1 as FLIGHT_DATA_SOURCE } from './_tsup-dts-rollup.js';
26
27
  export { QueryClientContext_alias_1 as QueryClientContext } from './_tsup-dts-rollup.js';
27
28
  export { QueryClientProvider_alias_1 as QueryClientProvider } from './_tsup-dts-rollup.js';
28
29
  export { useQueryClient_alias_1 as useQueryClient } from './_tsup-dts-rollup.js';
29
30
  export { QueryClientProviderProps_alias_1 as QueryClientProviderProps } from './_tsup-dts-rollup.js';
31
+ export { dehydrateSettled_alias_1 as dehydrateSettled } from './_tsup-dts-rollup.js';
30
32
  export { useIsFetching } from './_tsup-dts-rollup.js';
31
33
  export { useInfiniteQuery } from './_tsup-dts-rollup.js';
32
34
  export { infiniteQueryOptions } from './_tsup-dts-rollup.js';
package/build/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { noop, QueryClient as QueryClient$1, shouldThrowError, InfiniteQueryObserver, hydrate, QueryObserver, replaceEqualDeep } from '@tanstack/query-core';
1
+ import { noop, QueryClient as QueryClient$1, hydrate, dehydrate, shouldThrowError, InfiniteQueryObserver, QueryObserver, replaceEqualDeep } from '@tanstack/query-core';
2
2
  export * from '@tanstack/query-core';
3
3
  import { createContext, useContext, onCleanup, createMemo, createOptimistic, createSignal, action, createRenderEffect, untrack, repeat, $TRACK, sharedConfig, createEffect, createProjection, runWithOwner, isPending, resolve } from 'solid-js';
4
- import { createComponent } from '@solidjs/web';
4
+ import { createComponent, REVALIDATE_HEADER } from '@solidjs/web';
5
+ import { subscribeFlightData } from '@solidjs/web/server-functions';
5
6
 
6
7
  // src/index.ts
7
8
  var QueryClient = class extends QueryClient$1 {
@@ -11,6 +12,18 @@ var QueryClient = class extends QueryClient$1 {
11
12
  };
12
13
  var isServer = typeof window === "undefined";
13
14
  var HYDRATION_KEY_PREFIX = "sq:";
15
+ var FLIGHT_DATA_SOURCE = "sq";
16
+ function sweepRevalidatedQueries(client, payload, response) {
17
+ const keys = response.headers.get(REVALIDATE_HEADER)?.split(",");
18
+ if (!keys) return;
19
+ const covered = new Set(payload.queries.map((query) => query.queryHash));
20
+ for (const key of keys) {
21
+ client.invalidateQueries({
22
+ queryKey: [key],
23
+ predicate: (query) => !covered.has(query.queryHash)
24
+ }).catch(() => void 0);
25
+ }
26
+ }
14
27
  var QueryClientContext = createContext(null);
15
28
  var useQueryClient = (queryClient) => {
16
29
  if (queryClient) {
@@ -26,9 +39,11 @@ function serializeCacheOnServer(client) {
26
39
  const ctx = sharedConfig.context;
27
40
  if (!ctx || !ctx.async || ctx.noHydrate) return;
28
41
  const cache = client.getQueryCache();
42
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
29
43
  const seen = /* @__PURE__ */ new Set();
30
44
  const serializeQuery = (query) => {
31
45
  if (seen.has(query.queryHash)) return;
46
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
32
47
  const state = query.state;
33
48
  if (state.status === "success") {
34
49
  seen.add(query.queryHash);
@@ -52,7 +67,18 @@ function serializeCacheOnServer(client) {
52
67
  var QueryClientProvider = (props) => {
53
68
  props.client.mount();
54
69
  onCleanup(() => props.client.unmount());
55
- if (isServer) serializeCacheOnServer(props.client);
70
+ if (isServer) {
71
+ serializeCacheOnServer(props.client);
72
+ onCleanup(() => {
73
+ props.client.cancelQueries().catch(() => void 0);
74
+ props.client.clear();
75
+ });
76
+ } else {
77
+ onCleanup(subscribeFlightData(FLIGHT_DATA_SOURCE, (data, context) => {
78
+ hydrate(props.client, data);
79
+ sweepRevalidatedQueries(props.client, data, context.response);
80
+ }));
81
+ }
56
82
  return createComponent(QueryClientContext, {
57
83
  value: () => props.client,
58
84
  get children() {
@@ -431,6 +457,15 @@ function useQuery(options, queryClient) {
431
457
  function queryOptions(options) {
432
458
  return options;
433
459
  }
460
+ async function dehydrateSettled(client, options) {
461
+ const cache = client.getQueryCache();
462
+ for (; ; ) {
463
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
464
+ if (pending.length === 0) break;
465
+ await Promise.allSettled(pending);
466
+ }
467
+ return dehydrate(client, options);
468
+ }
434
469
  var isServer3 = typeof window === "undefined";
435
470
  function createCacheAggregate(subscribe, read, empty) {
436
471
  const hydratedMount = !isServer3 && sharedConfig.hydrating === true;
@@ -591,15 +626,13 @@ function useMutation(options, queryClient) {
591
626
  const [flight, setFlight] = createOptimistic(null);
592
627
  const [settled, setSettled] = createSignal(IDLE);
593
628
  let activeMutation = null;
629
+ let unsubscribeFlight = null;
594
630
  const [flightVersion, setFlightVersion] = createSignal(0);
595
- if (!isServer4) {
596
- const unsubscribe = client().getMutationCache().subscribe((event) => {
597
- if ("mutation" in event && event.mutation === activeMutation) {
598
- setFlightVersion((v) => v + 1);
599
- }
631
+ if (!isServer4)
632
+ onCleanup(() => {
633
+ unsubscribeFlight?.();
634
+ unsubscribeFlight = null;
600
635
  });
601
- onCleanup(unsubscribe);
602
- }
603
636
  async function* run(variables) {
604
637
  const opts = untrack(options);
605
638
  const callbackContext = {
@@ -609,7 +642,8 @@ function useMutation(options, queryClient) {
609
642
  };
610
643
  if (!isServer4) setFlight({ variables });
611
644
  opts.onMutate?.(variables, callbackContext);
612
- const mutation = client().getMutationCache().build(client(), {
645
+ const c = untrack(client);
646
+ const mutation = c.getMutationCache().build(c, {
613
647
  ...opts,
614
648
  // Options-level callbacks re-run below, inside the transaction —
615
649
  // executing them here (inside the await gap) would let their
@@ -620,6 +654,20 @@ function useMutation(options, queryClient) {
620
654
  onSettled: void 0
621
655
  });
622
656
  activeMutation = mutation;
657
+ if (!isServer4) {
658
+ unsubscribeFlight?.();
659
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
660
+ if ("mutation" in event && event.mutation === activeMutation) {
661
+ setFlightVersion((v) => v + 1);
662
+ }
663
+ });
664
+ }
665
+ const settleFlight = () => {
666
+ if (activeMutation !== mutation) return;
667
+ activeMutation = null;
668
+ unsubscribeFlight?.();
669
+ unsubscribeFlight = null;
670
+ };
623
671
  async function* settleFailure(error) {
624
672
  try {
625
673
  if (opts.onError)
@@ -647,7 +695,7 @@ function useMutation(options, queryClient) {
647
695
  variables,
648
696
  submittedAt: mutation.state.submittedAt
649
697
  });
650
- activeMutation = null;
698
+ settleFlight();
651
699
  throw error;
652
700
  }
653
701
  let result2;
@@ -687,7 +735,7 @@ function useMutation(options, queryClient) {
687
735
  variables,
688
736
  submittedAt: mutation.state.submittedAt
689
737
  });
690
- activeMutation = null;
738
+ settleFlight();
691
739
  return result2;
692
740
  }
693
741
  const invoke = isServer4 ? (variables) => drain(run(variables)) : action(run);
@@ -825,4 +873,4 @@ function useQueries(queriesOptions, queryClient) {
825
873
  });
826
874
  }
827
875
 
828
- export { IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
876
+ export { FLIGHT_DATA_SOURCE, IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, dehydrateSettled, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/solid-query",
3
- "version": "6.0.0-rc.1",
3
+ "version": "6.0.0-rc.3",
4
4
  "description": "Primitives for managing, caching and syncing asynchronous and remote data in Solid",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -53,15 +53,16 @@
53
53
  "@babel/preset-typescript": "^7.18.6",
54
54
  "@solidjs/testing-library": "^0.8.10",
55
55
  "@solidjs/vite-plugin": "^3.0.0-next.27",
56
- "@solidjs/web": "^2.0.0-rc.3",
56
+ "@solidjs/web": "^2.0.0-rc.6",
57
57
  "babel-preset-solid": "^2.0.0-rc.2",
58
58
  "npm-run-all2": "^5.0.0",
59
- "solid-js": "^2.0.0-rc.4",
59
+ "solid-js": "^2.0.0-rc.6",
60
60
  "tsup-preset-solid": "^2.2.0",
61
61
  "@tanstack/query-test-utils": "0.0.0"
62
62
  },
63
63
  "peerDependencies": {
64
- "solid-js": ">=2.0.0-rc.4 <3.0.0"
64
+ "@solidjs/web": ">=2.0.0-rc.6 <3.0.0",
65
+ "solid-js": ">=2.0.0-rc.6 <3.0.0"
65
66
  },
66
67
  "scripts": {
67
68
  "clean": "premove ./build ./coverage ./dist-ts",
@@ -1,5 +1,8 @@
1
1
  import { createContext, onCleanup, sharedConfig, useContext } from 'solid-js'
2
- import type { Query } from '@tanstack/query-core'
2
+ import { REVALIDATE_HEADER } from '@solidjs/web'
3
+ import { hydrate } from '@tanstack/query-core'
4
+ import { subscribeFlightData } from '@solidjs/web/server-functions'
5
+ import type { DehydratedState, Query } from '@tanstack/query-core'
3
6
  import type { QueryClient } from './QueryClient'
4
7
  import type { JSX } from '@solidjs/web'
5
8
 
@@ -13,6 +16,73 @@ const isServer = typeof window === 'undefined'
13
16
  */
14
17
  export const HYDRATION_KEY_PREFIX = 'sq:'
15
18
 
19
+ /**
20
+ * The query cache's single-flight source id. Mutation responses can fold
21
+ * fresh data for multiple caches at once (Solid's multi-source
22
+ * single-flight protocol); this is the slice the query cache claims — the
23
+ * provider subscribes its consumer under it, and server collectors
24
+ * register under it to produce the data:
25
+ *
26
+ * ```ts
27
+ * import { registerFlightDataSource } from '@solidjs/web/server-functions/server'
28
+ * import { FLIGHT_DATA_SOURCE, dehydrate } from '@tanstack/solid-query'
29
+ *
30
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, async (event, outcome) => {
31
+ * // rebuild the data for outcome.targetUrl into a QueryClient, then
32
+ * return dehydrate(queryClient)
33
+ * })
34
+ * ```
35
+ *
36
+ * The slice's payload is a `DehydratedState`; the provider consumes it
37
+ * with `hydrate()`, so every mounted query on those keys updates before
38
+ * the mutation's promise resolves — no follow-up refetches.
39
+ *
40
+ * A mutation can additionally declare its invalidation SCOPE with
41
+ * `X-Revalidate` keys (the `revalidate` option of Solid's `reload`/
42
+ * `redirect` response helpers). The payload covers the slice of that scope
43
+ * the server could recompute; whatever a declared key matches beyond it is
44
+ * swept client-side (see the consumer below). For the sweep to be
45
+ * delivered, the collector must contribute a slice — return the dehydrated
46
+ * state even when it holds no queries if `outcome.revalidateKeys` is
47
+ * present, rather than skipping the source.
48
+ */
49
+ export const FLIGHT_DATA_SOURCE = 'sq'
50
+
51
+ /**
52
+ * The client half of key-scoped invalidation. A mutation declares its
53
+ * invalidation scope with `X-Revalidate` keys; the flight payload covers
54
+ * the part of that scope the server's collector recomputed (typically the
55
+ * target page's loader graph). Whatever a declared key matches BEYOND the
56
+ * payload — parameterized instances only this client holds, queries no
57
+ * loader owns — is invalidated here: active queries refetch in the
58
+ * background, inactive ones are marked stale for their next mount. This
59
+ * mirrors how Solid Router consumes the same header for its own cache.
60
+ *
61
+ * A key matches by queryKey prefix — `revalidate: 'users'` sweeps every
62
+ * query whose key begins with `'users'`. Payload-covered hashes are exempt:
63
+ * they hydrated with fresh data a moment ago, and refetching them would
64
+ * spend the round trip single flight just saved.
65
+ */
66
+ function sweepRevalidatedQueries(
67
+ client: QueryClient,
68
+ payload: DehydratedState,
69
+ response: Response,
70
+ ): void {
71
+ const keys = response.headers.get(REVALIDATE_HEADER)?.split(',')
72
+ if (!keys) return
73
+ const covered = new Set(payload.queries.map((query) => query.queryHash))
74
+ for (const key of keys) {
75
+ client
76
+ .invalidateQueries({
77
+ queryKey: [key],
78
+ predicate: (query) => !covered.has(query.queryHash),
79
+ })
80
+ // Background refetch failures surface through the queries' own error
81
+ // states; an unhandled rejection here would fail the mutation instead.
82
+ .catch(() => undefined)
83
+ }
84
+ }
85
+
16
86
  export const QueryClientContext = createContext<(() => QueryClient) | null>(
17
87
  null,
18
88
  )
@@ -64,9 +134,18 @@ function serializeCacheOnServer(client: QueryClient): void {
64
134
  if (!ctx || !ctx.async || ctx.noHydrate) return
65
135
 
66
136
  const cache = client.getQueryCache()
137
+ // The standard dehydrate filter gates the wire here too, so apps keep
138
+ // sensitive or oversized queries out of the HTML with the same option
139
+ // they'd pass any other transport.
140
+ const shouldDehydrateQuery =
141
+ client.getDefaultOptions().dehydrate?.shouldDehydrateQuery
67
142
  const seen = new Set<string>()
68
143
  const serializeQuery = (query: Query<any, any, any, any>) => {
69
144
  if (seen.has(query.queryHash)) return
145
+ // Consulted per cache event until it passes, so a filter that rejects
146
+ // pending queries (e.g. the core default) still admits the settled
147
+ // value if it lands while the request's serialization context is live.
148
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return
70
149
  const state = query.state
71
150
  if (state.status === 'success') {
72
151
  seen.add(query.queryHash)
@@ -97,15 +176,47 @@ function serializeCacheOnServer(client: QueryClient): void {
97
176
  /**
98
177
  * Provides the QueryClient and manages its mount lifecycle. On the server
99
178
  * it also registers the cache serializer above; on the client, hooks prime
100
- * the cache from their hash-keyed registry entries themselves — see
101
- * `useBaseQuery`.
179
+ * the cache from their hash-keyed registry entries themselves (see
180
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
181
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
182
+ * `DehydratedState` produced by a server collector registered under the
183
+ * same id) hydrate this client before the mutation's promise resolves.
184
+ * Subscribing is inert when no server collector exists — the server just
185
+ * folds nothing — so it is unconditional. One consumer per source: with
186
+ * nested providers, the innermost mounted one owns the slice.
102
187
  */
103
188
  export const QueryClientProvider = (
104
189
  props: QueryClientProviderProps,
105
190
  ): JSX.Element => {
106
191
  props.client.mount()
107
192
  onCleanup(() => props.client.unmount())
108
- if (isServer) serializeCacheOnServer(props.client)
193
+ if (isServer) {
194
+ serializeCacheOnServer(props.client)
195
+ // Render disposal ends the request: abort what's still in flight and
196
+ // drop the cache so user-configured finite gcTime timers can't pin
197
+ // the per-request client (and whatever its queries closed over) until
198
+ // they fire. Serialized promises are already in seroval's hands, so
199
+ // clearing here can't affect the streamed payload.
200
+ onCleanup(() => {
201
+ props.client.cancelQueries().catch(() => undefined)
202
+ props.client.clear()
203
+ })
204
+ } else {
205
+ // Client-only: the server's consumer registry is module state shared
206
+ // across requests — registering there would leak between them.
207
+ onCleanup(
208
+ subscribeFlightData<DehydratedState>(
209
+ FLIGHT_DATA_SOURCE,
210
+ (data, context) => {
211
+ hydrate(props.client, data)
212
+ // Fresh data first, then the declared-scope sweep: stale marks
213
+ // land synchronously, refetches run in the background — the
214
+ // mutation's promise never waits on them.
215
+ sweepRevalidatedQueries(props.client, data, context.response)
216
+ },
217
+ ),
218
+ )
219
+ }
109
220
 
110
221
  return (
111
222
  <QueryClientContext value={() => props.client}>
@@ -0,0 +1,42 @@
1
+ import { dehydrate } from '@tanstack/query-core'
2
+ import type { DehydrateOptions, DehydratedState } from '@tanstack/query-core'
3
+ import type { QueryClient } from './QueryClient'
4
+
5
+ /**
6
+ * Waits for every fetch the client has in flight to settle, then
7
+ * dehydrates. This is the extraction half of a single-flight collector:
8
+ * after route data functions run for the mutation's target URL, loaders
9
+ * commonly kick off prefetches without awaiting them — plain
10
+ * `dehydrate()` would snapshot those mid-fetch and ship nothing.
11
+ *
12
+ * ```ts
13
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, (event, outcome) =>
14
+ * loadFlightTarget({
15
+ * router,
16
+ * event,
17
+ * outcome,
18
+ * collect: () => dehydrateSettled(queryClient),
19
+ * }),
20
+ * )
21
+ * ```
22
+ *
23
+ * Settling is chased to quiescence — awaiting one batch of fetches can
24
+ * dispatch more (dependent queries keyed off a first result) — so an
25
+ * unconditionally self-refetching query would keep this pending; that's
26
+ * an app bug mirrored, not guarded.
27
+ */
28
+ export async function dehydrateSettled(
29
+ client: QueryClient,
30
+ options?: DehydrateOptions,
31
+ ): Promise<DehydratedState> {
32
+ const cache = client.getQueryCache()
33
+ for (;;) {
34
+ const pending = cache
35
+ .getAll()
36
+ .filter((query) => query.state.fetchStatus !== 'idle')
37
+ .map((query) => query.promise)
38
+ if (pending.length === 0) break
39
+ await Promise.allSettled(pending)
40
+ }
41
+ return dehydrate(client, options)
42
+ }
package/src/index.ts CHANGED
@@ -39,11 +39,13 @@ export type {
39
39
  UndefinedInitialDataOptions,
40
40
  } from './queryOptions'
41
41
  export {
42
+ FLIGHT_DATA_SOURCE,
42
43
  QueryClientContext,
43
44
  QueryClientProvider,
44
45
  useQueryClient,
45
46
  } from './QueryClientProvider'
46
47
  export type { QueryClientProviderProps } from './QueryClientProvider'
48
+ export { dehydrateSettled } from './dehydrateSettled'
47
49
  export { useIsFetching } from './useIsFetching'
48
50
  export { useInfiniteQuery } from './useInfiniteQuery'
49
51
  export { infiniteQueryOptions } from './infiniteQueryOptions'
@@ -114,20 +114,20 @@ export function useMutation<
114
114
  * Flight metadata (retry failures, offline pause) lives on the Mutation
115
115
  * instance and changes during the await gap; a version bump per cache
116
116
  * event for the active mutation keeps reads current without cloning
117
- * observer results.
117
+ * observer results. The subscription is per-flight, created in `run`
118
+ * against the same client the mutation is built on: nothing reactive is
119
+ * read at setup (no client read to untrack), the listener and the
120
+ * mutation can never sit on different clients, and an idle hook holds no
121
+ * cache subscription at all.
118
122
  */
119
123
  let activeMutation: Mutation<TData, TError, TVariables> | null = null
124
+ let unsubscribeFlight: (() => void) | null = null
120
125
  const [flightVersion, setFlightVersion] = createSignal(0)
121
- if (!isServer) {
122
- const unsubscribe = client()
123
- .getMutationCache()
124
- .subscribe((event) => {
125
- if ('mutation' in event && event.mutation === activeMutation) {
126
- setFlightVersion((v) => v + 1)
127
- }
128
- })
129
- onCleanup(unsubscribe)
130
- }
126
+ if (!isServer)
127
+ onCleanup(() => {
128
+ unsubscribeFlight?.()
129
+ unsubscribeFlight = null
130
+ })
131
131
 
132
132
  async function* run(
133
133
  variables: TVariables,
@@ -144,19 +144,33 @@ export function useMutation<
144
144
  if (!isServer) setFlight({ variables })
145
145
  opts.onMutate?.(variables, callbackContext)
146
146
 
147
- const mutation = client()
148
- .getMutationCache()
149
- .build(client(), {
150
- ...opts,
151
- // Options-level callbacks re-run below, inside the transaction —
152
- // executing them here (inside the await gap) would let their
153
- // cache writes and invalidations escape the atomic settle.
154
- onMutate: undefined,
155
- onSuccess: undefined,
156
- onError: undefined,
157
- onSettled: undefined,
158
- }) as unknown as Mutation<TData, TError, TVariables>
147
+ const c = untrack(client)
148
+ const mutation = c.getMutationCache().build(c, {
149
+ ...opts,
150
+ // Options-level callbacks re-run below, inside the transaction —
151
+ // executing them here (inside the await gap) would let their
152
+ // cache writes and invalidations escape the atomic settle.
153
+ onMutate: undefined,
154
+ onSuccess: undefined,
155
+ onError: undefined,
156
+ onSettled: undefined,
157
+ }) as unknown as Mutation<TData, TError, TVariables>
159
158
  activeMutation = mutation
159
+ if (!isServer) {
160
+ // A rapid re-mutate replaces the previous flight's listener.
161
+ unsubscribeFlight?.()
162
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
163
+ if ('mutation' in event && event.mutation === activeMutation) {
164
+ setFlightVersion((v) => v + 1)
165
+ }
166
+ })
167
+ }
168
+ const settleFlight = () => {
169
+ if (activeMutation !== mutation) return
170
+ activeMutation = null
171
+ unsubscribeFlight?.()
172
+ unsubscribeFlight = null
173
+ }
160
174
 
161
175
  /**
162
176
  * Failure settle, shared by both paths. Callback routing mirrors
@@ -202,7 +216,7 @@ export function useMutation<
202
216
  variables,
203
217
  submittedAt: mutation.state.submittedAt,
204
218
  })
205
- activeMutation = null
219
+ settleFlight()
206
220
  throw error
207
221
  }
208
222
 
@@ -250,7 +264,7 @@ export function useMutation<
250
264
  variables,
251
265
  submittedAt: mutation.state.submittedAt,
252
266
  })
253
- activeMutation = null
267
+ settleFlight()
254
268
  return result
255
269
  }
256
270