@tanstack/solid-query 6.0.0-rc.1 → 6.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,31 @@ 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
+ declare const FLIGHT_DATA_SOURCE = "sq";
323
+ export { FLIGHT_DATA_SOURCE }
324
+ export { FLIGHT_DATA_SOURCE as FLIGHT_DATA_SOURCE_alias_1 }
325
+
274
326
  export { focusManager }
275
327
 
276
328
  export { GetNextPageParamFunction }
@@ -555,8 +607,14 @@ export { QueryClientContext as QueryClientContext_alias_1 }
555
607
  /**
556
608
  * Provides the QueryClient and manages its mount lifecycle. On the server
557
609
  * 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`.
610
+ * the cache from their hash-keyed registry entries themselves (see
611
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
612
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
613
+ * `DehydratedState` produced by a server collector registered under the
614
+ * same id) hydrate this client before the mutation's promise resolves.
615
+ * Subscribing is inert when no server collector exists — the server just
616
+ * folds nothing — so it is unconditional. One consumer per source: with
617
+ * nested providers, the innermost mounted one owns the slice.
560
618
  */
561
619
  declare const QueryClientProvider: (props: QueryClientProviderProps) => JSX.Element;
562
620
  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,31 @@ 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
+ declare const FLIGHT_DATA_SOURCE = "sq";
323
+ export { FLIGHT_DATA_SOURCE }
324
+ export { FLIGHT_DATA_SOURCE as FLIGHT_DATA_SOURCE_alias_1 }
325
+
274
326
  export { focusManager }
275
327
 
276
328
  export { GetNextPageParamFunction }
@@ -555,8 +607,14 @@ export { QueryClientContext as QueryClientContext_alias_1 }
555
607
  /**
556
608
  * Provides the QueryClient and manages its mount lifecycle. On the server
557
609
  * 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`.
610
+ * the cache from their hash-keyed registry entries themselves (see
611
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
612
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
613
+ * `DehydratedState` produced by a server collector registered under the
614
+ * same id) hydrate this client before the mutation's promise resolves.
615
+ * Subscribing is inert when no server collector exists — the server just
616
+ * folds nothing — so it is unconditional. One consumer per source: with
617
+ * nested providers, the innermost mounted one owns the slice.
560
618
  */
561
619
  declare const QueryClientProvider: (props: QueryClientProviderProps) => JSX.Element;
562
620
  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,7 @@ 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";
15
17
  exports.QueryClientContext = solidJs.createContext(null);
16
18
  exports.useQueryClient = (queryClient) => {
17
19
  if (queryClient) {
@@ -27,9 +29,11 @@ function serializeCacheOnServer(client) {
27
29
  const ctx = solidJs.sharedConfig.context;
28
30
  if (!ctx || !ctx.async || ctx.noHydrate) return;
29
31
  const cache = client.getQueryCache();
32
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
30
33
  const seen = /* @__PURE__ */ new Set();
31
34
  const serializeQuery = (query) => {
32
35
  if (seen.has(query.queryHash)) return;
36
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
33
37
  const state = query.state;
34
38
  if (state.status === "success") {
35
39
  seen.add(query.queryHash);
@@ -53,7 +57,17 @@ function serializeCacheOnServer(client) {
53
57
  exports.QueryClientProvider = (props) => {
54
58
  props.client.mount();
55
59
  solidJs.onCleanup(() => props.client.unmount());
56
- if (isServer) serializeCacheOnServer(props.client);
60
+ if (isServer) {
61
+ serializeCacheOnServer(props.client);
62
+ solidJs.onCleanup(() => {
63
+ props.client.cancelQueries().catch(() => void 0);
64
+ props.client.clear();
65
+ });
66
+ } else {
67
+ solidJs.onCleanup(serverFunctions.subscribeFlightData(exports.FLIGHT_DATA_SOURCE, (data) => {
68
+ queryCore.hydrate(props.client, data);
69
+ }));
70
+ }
57
71
  return web.createComponent(exports.QueryClientContext, {
58
72
  value: () => props.client,
59
73
  get children() {
@@ -432,6 +446,15 @@ function useQuery(options, queryClient) {
432
446
  function queryOptions(options) {
433
447
  return options;
434
448
  }
449
+ async function dehydrateSettled(client, options) {
450
+ const cache = client.getQueryCache();
451
+ for (; ; ) {
452
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
453
+ if (pending.length === 0) break;
454
+ await Promise.allSettled(pending);
455
+ }
456
+ return queryCore.dehydrate(client, options);
457
+ }
435
458
  var isServer3 = typeof window === "undefined";
436
459
  function createCacheAggregate(subscribe, read, empty) {
437
460
  const hydratedMount = !isServer3 && solidJs.sharedConfig.hydrating === true;
@@ -592,15 +615,13 @@ function useMutation(options, queryClient) {
592
615
  const [flight, setFlight] = solidJs.createOptimistic(null);
593
616
  const [settled, setSettled] = solidJs.createSignal(IDLE);
594
617
  let activeMutation = null;
618
+ let unsubscribeFlight = null;
595
619
  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
- }
620
+ if (!isServer4)
621
+ solidJs.onCleanup(() => {
622
+ unsubscribeFlight?.();
623
+ unsubscribeFlight = null;
601
624
  });
602
- solidJs.onCleanup(unsubscribe);
603
- }
604
625
  async function* run(variables) {
605
626
  const opts = solidJs.untrack(options);
606
627
  const callbackContext = {
@@ -610,7 +631,8 @@ function useMutation(options, queryClient) {
610
631
  };
611
632
  if (!isServer4) setFlight({ variables });
612
633
  opts.onMutate?.(variables, callbackContext);
613
- const mutation = client().getMutationCache().build(client(), {
634
+ const c = solidJs.untrack(client);
635
+ const mutation = c.getMutationCache().build(c, {
614
636
  ...opts,
615
637
  // Options-level callbacks re-run below, inside the transaction —
616
638
  // executing them here (inside the await gap) would let their
@@ -621,6 +643,20 @@ function useMutation(options, queryClient) {
621
643
  onSettled: void 0
622
644
  });
623
645
  activeMutation = mutation;
646
+ if (!isServer4) {
647
+ unsubscribeFlight?.();
648
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
649
+ if ("mutation" in event && event.mutation === activeMutation) {
650
+ setFlightVersion((v) => v + 1);
651
+ }
652
+ });
653
+ }
654
+ const settleFlight = () => {
655
+ if (activeMutation !== mutation) return;
656
+ activeMutation = null;
657
+ unsubscribeFlight?.();
658
+ unsubscribeFlight = null;
659
+ };
624
660
  async function* settleFailure(error) {
625
661
  try {
626
662
  if (opts.onError)
@@ -648,7 +684,7 @@ function useMutation(options, queryClient) {
648
684
  variables,
649
685
  submittedAt: mutation.state.submittedAt
650
686
  });
651
- activeMutation = null;
687
+ settleFlight();
652
688
  throw error;
653
689
  }
654
690
  let result2;
@@ -688,7 +724,7 @@ function useMutation(options, queryClient) {
688
724
  variables,
689
725
  submittedAt: mutation.state.submittedAt
690
726
  });
691
- activeMutation = null;
727
+ settleFlight();
692
728
  return result2;
693
729
  }
694
730
  const invoke = isServer4 ? (variables) => drain(run(variables)) : solidJs.action(run);
@@ -826,6 +862,7 @@ function useQueries(queriesOptions, queryClient) {
826
862
  });
827
863
  }
828
864
 
865
+ exports.dehydrateSettled = dehydrateSettled;
829
866
  exports.infiniteQueryOptions = infiniteQueryOptions;
830
867
  exports.mutationOptions = mutationOptions;
831
868
  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
4
  import { createComponent } 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,7 @@ 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";
14
16
  var QueryClientContext = createContext(null);
15
17
  var useQueryClient = (queryClient) => {
16
18
  if (queryClient) {
@@ -26,9 +28,11 @@ function serializeCacheOnServer(client) {
26
28
  const ctx = sharedConfig.context;
27
29
  if (!ctx || !ctx.async || ctx.noHydrate) return;
28
30
  const cache = client.getQueryCache();
31
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
29
32
  const seen = /* @__PURE__ */ new Set();
30
33
  const serializeQuery = (query) => {
31
34
  if (seen.has(query.queryHash)) return;
35
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
32
36
  const state = query.state;
33
37
  if (state.status === "success") {
34
38
  seen.add(query.queryHash);
@@ -52,7 +56,17 @@ function serializeCacheOnServer(client) {
52
56
  var QueryClientProvider = (props) => {
53
57
  props.client.mount();
54
58
  onCleanup(() => props.client.unmount());
55
- if (isServer) serializeCacheOnServer(props.client);
59
+ if (isServer) {
60
+ serializeCacheOnServer(props.client);
61
+ onCleanup(() => {
62
+ props.client.cancelQueries().catch(() => void 0);
63
+ props.client.clear();
64
+ });
65
+ } else {
66
+ onCleanup(subscribeFlightData(FLIGHT_DATA_SOURCE, (data) => {
67
+ hydrate(props.client, data);
68
+ }));
69
+ }
56
70
  return createComponent(QueryClientContext, {
57
71
  value: () => props.client,
58
72
  get children() {
@@ -431,6 +445,15 @@ function useQuery(options, queryClient) {
431
445
  function queryOptions(options) {
432
446
  return options;
433
447
  }
448
+ async function dehydrateSettled(client, options) {
449
+ const cache = client.getQueryCache();
450
+ for (; ; ) {
451
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
452
+ if (pending.length === 0) break;
453
+ await Promise.allSettled(pending);
454
+ }
455
+ return dehydrate(client, options);
456
+ }
434
457
  var isServer3 = typeof window === "undefined";
435
458
  function createCacheAggregate(subscribe, read, empty) {
436
459
  const hydratedMount = !isServer3 && sharedConfig.hydrating === true;
@@ -591,15 +614,13 @@ function useMutation(options, queryClient) {
591
614
  const [flight, setFlight] = createOptimistic(null);
592
615
  const [settled, setSettled] = createSignal(IDLE);
593
616
  let activeMutation = null;
617
+ let unsubscribeFlight = null;
594
618
  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
- }
619
+ if (!isServer4)
620
+ onCleanup(() => {
621
+ unsubscribeFlight?.();
622
+ unsubscribeFlight = null;
600
623
  });
601
- onCleanup(unsubscribe);
602
- }
603
624
  async function* run(variables) {
604
625
  const opts = untrack(options);
605
626
  const callbackContext = {
@@ -609,7 +630,8 @@ function useMutation(options, queryClient) {
609
630
  };
610
631
  if (!isServer4) setFlight({ variables });
611
632
  opts.onMutate?.(variables, callbackContext);
612
- const mutation = client().getMutationCache().build(client(), {
633
+ const c = untrack(client);
634
+ const mutation = c.getMutationCache().build(c, {
613
635
  ...opts,
614
636
  // Options-level callbacks re-run below, inside the transaction —
615
637
  // executing them here (inside the await gap) would let their
@@ -620,6 +642,20 @@ function useMutation(options, queryClient) {
620
642
  onSettled: void 0
621
643
  });
622
644
  activeMutation = mutation;
645
+ if (!isServer4) {
646
+ unsubscribeFlight?.();
647
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
648
+ if ("mutation" in event && event.mutation === activeMutation) {
649
+ setFlightVersion((v) => v + 1);
650
+ }
651
+ });
652
+ }
653
+ const settleFlight = () => {
654
+ if (activeMutation !== mutation) return;
655
+ activeMutation = null;
656
+ unsubscribeFlight?.();
657
+ unsubscribeFlight = null;
658
+ };
623
659
  async function* settleFailure(error) {
624
660
  try {
625
661
  if (opts.onError)
@@ -647,7 +683,7 @@ function useMutation(options, queryClient) {
647
683
  variables,
648
684
  submittedAt: mutation.state.submittedAt
649
685
  });
650
- activeMutation = null;
686
+ settleFlight();
651
687
  throw error;
652
688
  }
653
689
  let result2;
@@ -687,7 +723,7 @@ function useMutation(options, queryClient) {
687
723
  variables,
688
724
  submittedAt: mutation.state.submittedAt
689
725
  });
690
- activeMutation = null;
726
+ settleFlight();
691
727
  return result2;
692
728
  }
693
729
  const invoke = isServer4 ? (variables) => drain(run(variables)) : action(run);
@@ -825,4 +861,4 @@ function useQueries(queriesOptions, queryClient) {
825
861
  });
826
862
  }
827
863
 
828
- export { IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
864
+ 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,7 @@ 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";
15
17
  exports.QueryClientContext = solidJs.createContext(null);
16
18
  exports.useQueryClient = (queryClient) => {
17
19
  if (queryClient) {
@@ -27,9 +29,11 @@ function serializeCacheOnServer(client) {
27
29
  const ctx = solidJs.sharedConfig.context;
28
30
  if (!ctx || !ctx.async || ctx.noHydrate) return;
29
31
  const cache = client.getQueryCache();
32
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
30
33
  const seen = /* @__PURE__ */ new Set();
31
34
  const serializeQuery = (query) => {
32
35
  if (seen.has(query.queryHash)) return;
36
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
33
37
  const state = query.state;
34
38
  if (state.status === "success") {
35
39
  seen.add(query.queryHash);
@@ -53,7 +57,17 @@ function serializeCacheOnServer(client) {
53
57
  exports.QueryClientProvider = (props) => {
54
58
  props.client.mount();
55
59
  solidJs.onCleanup(() => props.client.unmount());
56
- if (isServer) serializeCacheOnServer(props.client);
60
+ if (isServer) {
61
+ serializeCacheOnServer(props.client);
62
+ solidJs.onCleanup(() => {
63
+ props.client.cancelQueries().catch(() => void 0);
64
+ props.client.clear();
65
+ });
66
+ } else {
67
+ solidJs.onCleanup(serverFunctions.subscribeFlightData(exports.FLIGHT_DATA_SOURCE, (data) => {
68
+ queryCore.hydrate(props.client, data);
69
+ }));
70
+ }
57
71
  return web.createComponent(exports.QueryClientContext, {
58
72
  value: () => props.client,
59
73
  get children() {
@@ -432,6 +446,15 @@ function useQuery(options, queryClient) {
432
446
  function queryOptions(options) {
433
447
  return options;
434
448
  }
449
+ async function dehydrateSettled(client, options) {
450
+ const cache = client.getQueryCache();
451
+ for (; ; ) {
452
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
453
+ if (pending.length === 0) break;
454
+ await Promise.allSettled(pending);
455
+ }
456
+ return queryCore.dehydrate(client, options);
457
+ }
435
458
  var isServer3 = typeof window === "undefined";
436
459
  function createCacheAggregate(subscribe, read, empty) {
437
460
  const hydratedMount = !isServer3 && solidJs.sharedConfig.hydrating === true;
@@ -592,15 +615,13 @@ function useMutation(options, queryClient) {
592
615
  const [flight, setFlight] = solidJs.createOptimistic(null);
593
616
  const [settled, setSettled] = solidJs.createSignal(IDLE);
594
617
  let activeMutation = null;
618
+ let unsubscribeFlight = null;
595
619
  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
- }
620
+ if (!isServer4)
621
+ solidJs.onCleanup(() => {
622
+ unsubscribeFlight?.();
623
+ unsubscribeFlight = null;
601
624
  });
602
- solidJs.onCleanup(unsubscribe);
603
- }
604
625
  async function* run(variables) {
605
626
  const opts = solidJs.untrack(options);
606
627
  const callbackContext = {
@@ -610,7 +631,8 @@ function useMutation(options, queryClient) {
610
631
  };
611
632
  if (!isServer4) setFlight({ variables });
612
633
  opts.onMutate?.(variables, callbackContext);
613
- const mutation = client().getMutationCache().build(client(), {
634
+ const c = solidJs.untrack(client);
635
+ const mutation = c.getMutationCache().build(c, {
614
636
  ...opts,
615
637
  // Options-level callbacks re-run below, inside the transaction —
616
638
  // executing them here (inside the await gap) would let their
@@ -621,6 +643,20 @@ function useMutation(options, queryClient) {
621
643
  onSettled: void 0
622
644
  });
623
645
  activeMutation = mutation;
646
+ if (!isServer4) {
647
+ unsubscribeFlight?.();
648
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
649
+ if ("mutation" in event && event.mutation === activeMutation) {
650
+ setFlightVersion((v) => v + 1);
651
+ }
652
+ });
653
+ }
654
+ const settleFlight = () => {
655
+ if (activeMutation !== mutation) return;
656
+ activeMutation = null;
657
+ unsubscribeFlight?.();
658
+ unsubscribeFlight = null;
659
+ };
624
660
  async function* settleFailure(error) {
625
661
  try {
626
662
  if (opts.onError)
@@ -648,7 +684,7 @@ function useMutation(options, queryClient) {
648
684
  variables,
649
685
  submittedAt: mutation.state.submittedAt
650
686
  });
651
- activeMutation = null;
687
+ settleFlight();
652
688
  throw error;
653
689
  }
654
690
  let result2;
@@ -688,7 +724,7 @@ function useMutation(options, queryClient) {
688
724
  variables,
689
725
  submittedAt: mutation.state.submittedAt
690
726
  });
691
- activeMutation = null;
727
+ settleFlight();
692
728
  return result2;
693
729
  }
694
730
  const invoke = isServer4 ? (variables) => drain(run(variables)) : solidJs.action(run);
@@ -826,6 +862,7 @@ function useQueries(queriesOptions, queryClient) {
826
862
  });
827
863
  }
828
864
 
865
+ exports.dehydrateSettled = dehydrateSettled;
829
866
  exports.infiniteQueryOptions = infiniteQueryOptions;
830
867
  exports.mutationOptions = mutationOptions;
831
868
  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
4
  import { createComponent } 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,7 @@ 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";
14
16
  var QueryClientContext = createContext(null);
15
17
  var useQueryClient = (queryClient) => {
16
18
  if (queryClient) {
@@ -26,9 +28,11 @@ function serializeCacheOnServer(client) {
26
28
  const ctx = sharedConfig.context;
27
29
  if (!ctx || !ctx.async || ctx.noHydrate) return;
28
30
  const cache = client.getQueryCache();
31
+ const shouldDehydrateQuery = client.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
29
32
  const seen = /* @__PURE__ */ new Set();
30
33
  const serializeQuery = (query) => {
31
34
  if (seen.has(query.queryHash)) return;
35
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return;
32
36
  const state = query.state;
33
37
  if (state.status === "success") {
34
38
  seen.add(query.queryHash);
@@ -52,7 +56,17 @@ function serializeCacheOnServer(client) {
52
56
  var QueryClientProvider = (props) => {
53
57
  props.client.mount();
54
58
  onCleanup(() => props.client.unmount());
55
- if (isServer) serializeCacheOnServer(props.client);
59
+ if (isServer) {
60
+ serializeCacheOnServer(props.client);
61
+ onCleanup(() => {
62
+ props.client.cancelQueries().catch(() => void 0);
63
+ props.client.clear();
64
+ });
65
+ } else {
66
+ onCleanup(subscribeFlightData(FLIGHT_DATA_SOURCE, (data) => {
67
+ hydrate(props.client, data);
68
+ }));
69
+ }
56
70
  return createComponent(QueryClientContext, {
57
71
  value: () => props.client,
58
72
  get children() {
@@ -431,6 +445,15 @@ function useQuery(options, queryClient) {
431
445
  function queryOptions(options) {
432
446
  return options;
433
447
  }
448
+ async function dehydrateSettled(client, options) {
449
+ const cache = client.getQueryCache();
450
+ for (; ; ) {
451
+ const pending = cache.getAll().filter((query) => query.state.fetchStatus !== "idle").map((query) => query.promise);
452
+ if (pending.length === 0) break;
453
+ await Promise.allSettled(pending);
454
+ }
455
+ return dehydrate(client, options);
456
+ }
434
457
  var isServer3 = typeof window === "undefined";
435
458
  function createCacheAggregate(subscribe, read, empty) {
436
459
  const hydratedMount = !isServer3 && sharedConfig.hydrating === true;
@@ -591,15 +614,13 @@ function useMutation(options, queryClient) {
591
614
  const [flight, setFlight] = createOptimistic(null);
592
615
  const [settled, setSettled] = createSignal(IDLE);
593
616
  let activeMutation = null;
617
+ let unsubscribeFlight = null;
594
618
  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
- }
619
+ if (!isServer4)
620
+ onCleanup(() => {
621
+ unsubscribeFlight?.();
622
+ unsubscribeFlight = null;
600
623
  });
601
- onCleanup(unsubscribe);
602
- }
603
624
  async function* run(variables) {
604
625
  const opts = untrack(options);
605
626
  const callbackContext = {
@@ -609,7 +630,8 @@ function useMutation(options, queryClient) {
609
630
  };
610
631
  if (!isServer4) setFlight({ variables });
611
632
  opts.onMutate?.(variables, callbackContext);
612
- const mutation = client().getMutationCache().build(client(), {
633
+ const c = untrack(client);
634
+ const mutation = c.getMutationCache().build(c, {
613
635
  ...opts,
614
636
  // Options-level callbacks re-run below, inside the transaction —
615
637
  // executing them here (inside the await gap) would let their
@@ -620,6 +642,20 @@ function useMutation(options, queryClient) {
620
642
  onSettled: void 0
621
643
  });
622
644
  activeMutation = mutation;
645
+ if (!isServer4) {
646
+ unsubscribeFlight?.();
647
+ unsubscribeFlight = c.getMutationCache().subscribe((event) => {
648
+ if ("mutation" in event && event.mutation === activeMutation) {
649
+ setFlightVersion((v) => v + 1);
650
+ }
651
+ });
652
+ }
653
+ const settleFlight = () => {
654
+ if (activeMutation !== mutation) return;
655
+ activeMutation = null;
656
+ unsubscribeFlight?.();
657
+ unsubscribeFlight = null;
658
+ };
623
659
  async function* settleFailure(error) {
624
660
  try {
625
661
  if (opts.onError)
@@ -647,7 +683,7 @@ function useMutation(options, queryClient) {
647
683
  variables,
648
684
  submittedAt: mutation.state.submittedAt
649
685
  });
650
- activeMutation = null;
686
+ settleFlight();
651
687
  throw error;
652
688
  }
653
689
  let result2;
@@ -687,7 +723,7 @@ function useMutation(options, queryClient) {
687
723
  variables,
688
724
  submittedAt: mutation.state.submittedAt
689
725
  });
690
- activeMutation = null;
726
+ settleFlight();
691
727
  return result2;
692
728
  }
693
729
  const invoke = isServer4 ? (variables) => drain(run(variables)) : action(run);
@@ -825,4 +861,4 @@ function useQueries(queriesOptions, queryClient) {
825
861
  });
826
862
  }
827
863
 
828
- export { IsRestoringContext, QueryClient, QueryClientContext, QueryClientProvider, infiniteQueryOptions, mutationOptions, queryOptions, useInfiniteQuery, useIsFetching, useIsMutating, useIsRestoring, useMutation, useMutationState, useQueries, useQuery, useQueryClient };
864
+ 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.2",
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,7 @@
1
1
  import { createContext, onCleanup, sharedConfig, useContext } from 'solid-js'
2
- import type { Query } from '@tanstack/query-core'
2
+ import { hydrate } from '@tanstack/query-core'
3
+ import { subscribeFlightData } from '@solidjs/web/server-functions'
4
+ import type { DehydratedState, Query } from '@tanstack/query-core'
3
5
  import type { QueryClient } from './QueryClient'
4
6
  import type { JSX } from '@solidjs/web'
5
7
 
@@ -13,6 +15,29 @@ const isServer = typeof window === 'undefined'
13
15
  */
14
16
  export const HYDRATION_KEY_PREFIX = 'sq:'
15
17
 
18
+ /**
19
+ * The query cache's single-flight source id. Mutation responses can fold
20
+ * fresh data for multiple caches at once (Solid's multi-source
21
+ * single-flight protocol); this is the slice the query cache claims — the
22
+ * provider subscribes its consumer under it, and server collectors
23
+ * register under it to produce the data:
24
+ *
25
+ * ```ts
26
+ * import { registerFlightDataSource } from '@solidjs/web/server-functions/server'
27
+ * import { FLIGHT_DATA_SOURCE, dehydrate } from '@tanstack/solid-query'
28
+ *
29
+ * registerFlightDataSource(FLIGHT_DATA_SOURCE, async (event, outcome) => {
30
+ * // rebuild the data for outcome.targetUrl into a QueryClient, then
31
+ * return dehydrate(queryClient)
32
+ * })
33
+ * ```
34
+ *
35
+ * The slice's payload is a `DehydratedState`; the provider consumes it
36
+ * with `hydrate()`, so every mounted query on those keys updates before
37
+ * the mutation's promise resolves — no follow-up refetches.
38
+ */
39
+ export const FLIGHT_DATA_SOURCE = 'sq'
40
+
16
41
  export const QueryClientContext = createContext<(() => QueryClient) | null>(
17
42
  null,
18
43
  )
@@ -64,9 +89,18 @@ function serializeCacheOnServer(client: QueryClient): void {
64
89
  if (!ctx || !ctx.async || ctx.noHydrate) return
65
90
 
66
91
  const cache = client.getQueryCache()
92
+ // The standard dehydrate filter gates the wire here too, so apps keep
93
+ // sensitive or oversized queries out of the HTML with the same option
94
+ // they'd pass any other transport.
95
+ const shouldDehydrateQuery =
96
+ client.getDefaultOptions().dehydrate?.shouldDehydrateQuery
67
97
  const seen = new Set<string>()
68
98
  const serializeQuery = (query: Query<any, any, any, any>) => {
69
99
  if (seen.has(query.queryHash)) return
100
+ // Consulted per cache event until it passes, so a filter that rejects
101
+ // pending queries (e.g. the core default) still admits the settled
102
+ // value if it lands while the request's serialization context is live.
103
+ if (shouldDehydrateQuery && !shouldDehydrateQuery(query)) return
70
104
  const state = query.state
71
105
  if (state.status === 'success') {
72
106
  seen.add(query.queryHash)
@@ -97,15 +131,40 @@ function serializeCacheOnServer(client: QueryClient): void {
97
131
  /**
98
132
  * Provides the QueryClient and manages its mount lifecycle. On the server
99
133
  * 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`.
134
+ * the cache from their hash-keyed registry entries themselves (see
135
+ * `useBaseQuery`) and the provider subscribes the cache's single-flight
136
+ * consumer: mutation responses carrying a `FLIGHT_DATA_SOURCE` slice (a
137
+ * `DehydratedState` produced by a server collector registered under the
138
+ * same id) hydrate this client before the mutation's promise resolves.
139
+ * Subscribing is inert when no server collector exists — the server just
140
+ * folds nothing — so it is unconditional. One consumer per source: with
141
+ * nested providers, the innermost mounted one owns the slice.
102
142
  */
103
143
  export const QueryClientProvider = (
104
144
  props: QueryClientProviderProps,
105
145
  ): JSX.Element => {
106
146
  props.client.mount()
107
147
  onCleanup(() => props.client.unmount())
108
- if (isServer) serializeCacheOnServer(props.client)
148
+ if (isServer) {
149
+ serializeCacheOnServer(props.client)
150
+ // Render disposal ends the request: abort what's still in flight and
151
+ // drop the cache so user-configured finite gcTime timers can't pin
152
+ // the per-request client (and whatever its queries closed over) until
153
+ // they fire. Serialized promises are already in seroval's hands, so
154
+ // clearing here can't affect the streamed payload.
155
+ onCleanup(() => {
156
+ props.client.cancelQueries().catch(() => undefined)
157
+ props.client.clear()
158
+ })
159
+ } else {
160
+ // Client-only: the server's consumer registry is module state shared
161
+ // across requests — registering there would leak between them.
162
+ onCleanup(
163
+ subscribeFlightData<DehydratedState>(FLIGHT_DATA_SOURCE, (data) => {
164
+ hydrate(props.client, data)
165
+ }),
166
+ )
167
+ }
109
168
 
110
169
  return (
111
170
  <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