@tanstack/solid-query 6.0.0-beta.6 → 6.0.0-beta.8

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.
@@ -147,6 +147,15 @@ export { CancelledError }
147
147
 
148
148
  export { CancelOptions }
149
149
 
150
+ /**
151
+ * Client side of the channel. Created by `QueryClientProvider` on the
152
+ * client and handed to `useBaseQuery` via context so hydrated components
153
+ * can attach their observers as soon as their query's entry has been
154
+ * primed — per query, not at global hydration end, which keeps
155
+ * early-hydrated components live while other boundaries still stream.
156
+ */
157
+ export declare function createHydrationCoordinator(client: () => QueryClient): HydrationCoordinator;
158
+
150
159
  export declare const createInfiniteQuery: typeof useInfiniteQuery;
151
160
 
152
161
  export declare const createMutation: typeof useMutation;
@@ -155,6 +164,34 @@ export declare const createQueries: typeof useQueries;
155
164
 
156
165
  export declare const createQuery: typeof useQuery;
157
166
 
167
+ /**
168
+ * Server side of the library-owned serialization channel.
169
+ *
170
+ * Returns an AsyncIterable that yields a cumulative snapshot of the
171
+ * dehydrated query cache (success entries, per query-core `dehydrate()`
172
+ * shapes) every time a query settles during SSR. `QueryClientProvider`
173
+ * holds it as a signal value, so Solid serializes it through the normal
174
+ * per-computation path: the server runtime tees the iterator into the
175
+ * hydration serializer (`ctx.serialize(id, tapped)` in solid-js'
176
+ * `processResult`) and seroval streams each yield to the client as a
177
+ * script chunk riding the SSR stream.
178
+ *
179
+ * The iterable must terminate for the SSR stream to complete: the
180
+ * hydration serializer's `flush()` only fires its `onDone` once all
181
+ * pending streams have closed, and the render root is disposed *after*
182
+ * that, so neither `onCleanup` nor the serializer itself can close the
183
+ * channel. Instead the channel closes itself on cache quiescence: after
184
+ * every cache event (and once at creation) it schedules a timer-task
185
+ * check; if no query is fetching by then, no further settle can occur —
186
+ * suspense retry passes that start waterfall fetches are scheduled on
187
+ * microtasks, so they have begun before the check runs — and the channel
188
+ * emits its final cumulative snapshot with `done: true` and completes.
189
+ *
190
+ * Single-consumer by design: solid-js creates exactly one iterator from
191
+ * the value and shares it between the memo and the serializer tap.
192
+ */
193
+ export declare function createServerDehydrationChannel(client: QueryClient): AsyncIterable<DehydrationChannelYield>;
194
+
158
195
  export { DataTag }
159
196
 
160
197
  export { dataTagErrorSymbol }
@@ -212,10 +249,40 @@ export { DefinedUseQueryResult as DefinedUseQueryResult_alias_1 }
212
249
 
213
250
  export { dehydrate }
214
251
 
252
+ declare type DehydratedQueryEntry = DehydratedState['queries'][number];
253
+
215
254
  export { DehydratedState }
216
255
 
217
256
  export { DehydrateOptions }
218
257
 
258
+ /**
259
+ * A single message on the dehydration channel. `entries` is *cumulative* —
260
+ * every yield carries all entries settled so far. Two reasons:
261
+ *
262
+ * - It is what makes Solid's signal-path hydration replay lossless.
263
+ * Yields still buffered when hydration begins are conflated to the
264
+ * LATEST one (`normalizeIterator` drains synchronously available
265
+ * results, keeps the last data yield, and delivers the stream's done
266
+ * result on a subsequent pull), so each yield must be self-contained:
267
+ * the latest cumulative snapshot alone carries everything the dropped
268
+ * intermediates did. Requires the solid-js build with that conflation
269
+ * behavior (> 2.0.0-beta.32); earlier betas pinned the replay at the
270
+ * FIRST buffered yield, dropping every later entry and the `done`
271
+ * marker.
272
+ * - Entry objects keep their identity across yields, so seroval's
273
+ * cross-reference serialization emits each entry once and later yields
274
+ * only reference it — the cumulative shape costs bytes proportional to
275
+ * the number of entries, not its square.
276
+ *
277
+ * `done: true` marks the final yield. The client uses it to release
278
+ * subscribers still waiting for entries that will never arrive (e.g.
279
+ * queries that errored during SSR and were not dehydrated).
280
+ */
281
+ export declare interface DehydrationChannelYield {
282
+ entries: Array<DehydratedQueryEntry>;
283
+ done: boolean;
284
+ }
285
+
219
286
  export { DistributiveOmit }
220
287
 
221
288
  export { EnsureInfiniteQueryDataOptions }
@@ -280,6 +347,24 @@ export { hydrate }
280
347
 
281
348
  export { HydrateOptions }
282
349
 
350
+ declare interface HydrationCoordinator {
351
+ /**
352
+ * Prime the QueryClient from a channel yield. Entries already applied
353
+ * (same queryHash and dataUpdatedAt) are skipped; the rest go through
354
+ * query-core `hydrate()`, which keeps whichever data is newer.
355
+ */
356
+ applyYield: (value: DehydrationChannelYield) => void;
357
+ /**
358
+ * Invoke `callback` (on a microtask) once the entry for `queryHash` has
359
+ * been applied — or immediately-on-a-microtask if it already was, or
360
+ * when the channel completes without one (SSR-errored queries are not
361
+ * dehydrated, so their components must not wait forever).
362
+ */
363
+ whenQueryPrimed: (queryHash: string, callback: () => void) => void;
364
+ }
365
+
366
+ export declare const HydrationCoordinatorContext: Context<HydrationCoordinator | null>;
367
+
283
368
  export { InferDataFromTag }
284
369
 
285
370
  export { InferErrorFromTag }
@@ -147,6 +147,15 @@ export { CancelledError }
147
147
 
148
148
  export { CancelOptions }
149
149
 
150
+ /**
151
+ * Client side of the channel. Created by `QueryClientProvider` on the
152
+ * client and handed to `useBaseQuery` via context so hydrated components
153
+ * can attach their observers as soon as their query's entry has been
154
+ * primed — per query, not at global hydration end, which keeps
155
+ * early-hydrated components live while other boundaries still stream.
156
+ */
157
+ export declare function createHydrationCoordinator(client: () => QueryClient): HydrationCoordinator;
158
+
150
159
  export declare const createInfiniteQuery: typeof useInfiniteQuery;
151
160
 
152
161
  export declare const createMutation: typeof useMutation;
@@ -155,6 +164,34 @@ export declare const createQueries: typeof useQueries;
155
164
 
156
165
  export declare const createQuery: typeof useQuery;
157
166
 
167
+ /**
168
+ * Server side of the library-owned serialization channel.
169
+ *
170
+ * Returns an AsyncIterable that yields a cumulative snapshot of the
171
+ * dehydrated query cache (success entries, per query-core `dehydrate()`
172
+ * shapes) every time a query settles during SSR. `QueryClientProvider`
173
+ * holds it as a signal value, so Solid serializes it through the normal
174
+ * per-computation path: the server runtime tees the iterator into the
175
+ * hydration serializer (`ctx.serialize(id, tapped)` in solid-js'
176
+ * `processResult`) and seroval streams each yield to the client as a
177
+ * script chunk riding the SSR stream.
178
+ *
179
+ * The iterable must terminate for the SSR stream to complete: the
180
+ * hydration serializer's `flush()` only fires its `onDone` once all
181
+ * pending streams have closed, and the render root is disposed *after*
182
+ * that, so neither `onCleanup` nor the serializer itself can close the
183
+ * channel. Instead the channel closes itself on cache quiescence: after
184
+ * every cache event (and once at creation) it schedules a timer-task
185
+ * check; if no query is fetching by then, no further settle can occur —
186
+ * suspense retry passes that start waterfall fetches are scheduled on
187
+ * microtasks, so they have begun before the check runs — and the channel
188
+ * emits its final cumulative snapshot with `done: true` and completes.
189
+ *
190
+ * Single-consumer by design: solid-js creates exactly one iterator from
191
+ * the value and shares it between the memo and the serializer tap.
192
+ */
193
+ export declare function createServerDehydrationChannel(client: QueryClient): AsyncIterable<DehydrationChannelYield>;
194
+
158
195
  export { DataTag }
159
196
 
160
197
  export { dataTagErrorSymbol }
@@ -212,10 +249,40 @@ export { DefinedUseQueryResult as DefinedUseQueryResult_alias_1 }
212
249
 
213
250
  export { dehydrate }
214
251
 
252
+ declare type DehydratedQueryEntry = DehydratedState['queries'][number];
253
+
215
254
  export { DehydratedState }
216
255
 
217
256
  export { DehydrateOptions }
218
257
 
258
+ /**
259
+ * A single message on the dehydration channel. `entries` is *cumulative* —
260
+ * every yield carries all entries settled so far. Two reasons:
261
+ *
262
+ * - It is what makes Solid's signal-path hydration replay lossless.
263
+ * Yields still buffered when hydration begins are conflated to the
264
+ * LATEST one (`normalizeIterator` drains synchronously available
265
+ * results, keeps the last data yield, and delivers the stream's done
266
+ * result on a subsequent pull), so each yield must be self-contained:
267
+ * the latest cumulative snapshot alone carries everything the dropped
268
+ * intermediates did. Requires the solid-js build with that conflation
269
+ * behavior (> 2.0.0-beta.32); earlier betas pinned the replay at the
270
+ * FIRST buffered yield, dropping every later entry and the `done`
271
+ * marker.
272
+ * - Entry objects keep their identity across yields, so seroval's
273
+ * cross-reference serialization emits each entry once and later yields
274
+ * only reference it — the cumulative shape costs bytes proportional to
275
+ * the number of entries, not its square.
276
+ *
277
+ * `done: true` marks the final yield. The client uses it to release
278
+ * subscribers still waiting for entries that will never arrive (e.g.
279
+ * queries that errored during SSR and were not dehydrated).
280
+ */
281
+ export declare interface DehydrationChannelYield {
282
+ entries: Array<DehydratedQueryEntry>;
283
+ done: boolean;
284
+ }
285
+
219
286
  export { DistributiveOmit }
220
287
 
221
288
  export { EnsureInfiniteQueryDataOptions }
@@ -280,6 +347,24 @@ export { hydrate }
280
347
 
281
348
  export { HydrateOptions }
282
349
 
350
+ declare interface HydrationCoordinator {
351
+ /**
352
+ * Prime the QueryClient from a channel yield. Entries already applied
353
+ * (same queryHash and dataUpdatedAt) are skipped; the rest go through
354
+ * query-core `hydrate()`, which keeps whichever data is newer.
355
+ */
356
+ applyYield: (value: DehydrationChannelYield) => void;
357
+ /**
358
+ * Invoke `callback` (on a microtask) once the entry for `queryHash` has
359
+ * been applied — or immediately-on-a-microtask if it already was, or
360
+ * when the channel completes without one (SSR-errored queries are not
361
+ * dehydrated, so their components must not wait forever).
362
+ */
363
+ whenQueryPrimed: (queryHash: string, callback: () => void) => void;
364
+ }
365
+
366
+ export declare const HydrationCoordinatorContext: Context<HydrationCoordinator | null>;
367
+
283
368
  export { InferDataFromTag }
284
369
 
285
370
  export { InferErrorFromTag }
package/build/dev.cjs CHANGED
@@ -5,6 +5,158 @@ var solidJs = require('solid-js');
5
5
  var web = require('@solidjs/web');
6
6
 
7
7
  // src/useQuery.ts
8
+ function createServerDehydrationChannel(client) {
9
+ const cache = client.getQueryCache();
10
+ const entryCache = /* @__PURE__ */ new Map();
11
+ const snapshot2 = () => {
12
+ const entries = [];
13
+ for (const query of cache.getAll()) {
14
+ if (query.state.status !== "success") continue;
15
+ let cached = entryCache.get(query.queryHash);
16
+ if (!cached || cached.state !== query.state) {
17
+ cached = {
18
+ state: query.state,
19
+ entry: {
20
+ dehydratedAt: Date.now(),
21
+ state: query.state,
22
+ queryKey: query.queryKey,
23
+ queryHash: query.queryHash,
24
+ ...query.meta && { meta: query.meta },
25
+ ...query.queryType && { queryType: query.queryType }
26
+ }
27
+ };
28
+ entryCache.set(query.queryHash, cached);
29
+ }
30
+ entries.push(cached.entry);
31
+ }
32
+ return entries;
33
+ };
34
+ let closed = false;
35
+ let pull = null;
36
+ const buffered = [];
37
+ const emit = (value) => {
38
+ if (closed) return;
39
+ if (value.done) closed = true;
40
+ if (pull) {
41
+ const resolve = pull;
42
+ pull = null;
43
+ resolve({ done: false, value });
44
+ } else {
45
+ buffered.push(value);
46
+ }
47
+ };
48
+ let closeTimer = null;
49
+ const scheduleCloseCheck = () => {
50
+ if (closed || closeTimer !== null) return;
51
+ closeTimer = setTimeout(() => {
52
+ closeTimer = null;
53
+ if (closed) return;
54
+ if (client.isFetching() === 0) {
55
+ unsubscribe();
56
+ emit({ entries: snapshot2(), done: true });
57
+ }
58
+ }, 0);
59
+ };
60
+ const unsubscribe = cache.subscribe((event) => {
61
+ if (closed) return;
62
+ if (event.type === "updated" && event.action.type === "success") {
63
+ emit({ entries: snapshot2(), done: false });
64
+ }
65
+ scheduleCloseCheck();
66
+ });
67
+ scheduleCloseCheck();
68
+ return {
69
+ [Symbol.asyncIterator]() {
70
+ return {
71
+ next() {
72
+ if (buffered.length > 0) {
73
+ return Promise.resolve({ done: false, value: buffered.shift() });
74
+ }
75
+ if (closed) {
76
+ return Promise.resolve({
77
+ done: true,
78
+ value: void 0
79
+ });
80
+ }
81
+ return new Promise(
82
+ (resolve) => {
83
+ pull = resolve;
84
+ }
85
+ );
86
+ },
87
+ return(value) {
88
+ if (!closed) {
89
+ closed = true;
90
+ unsubscribe();
91
+ if (closeTimer !== null) {
92
+ clearTimeout(closeTimer);
93
+ closeTimer = null;
94
+ }
95
+ const resolve = pull;
96
+ pull = null;
97
+ resolve?.({ done: true, value: void 0 });
98
+ }
99
+ return Promise.resolve({
100
+ done: true,
101
+ value
102
+ });
103
+ }
104
+ };
105
+ }
106
+ };
107
+ }
108
+ function createHydrationCoordinator(client) {
109
+ const applied = /* @__PURE__ */ new Map();
110
+ const waiters = /* @__PURE__ */ new Map();
111
+ let channelDone = false;
112
+ const fireWaiters = (queryHash) => {
113
+ const callbacks = waiters.get(queryHash);
114
+ if (!callbacks) return;
115
+ waiters.delete(queryHash);
116
+ for (const callback of callbacks) queueMicrotask(callback);
117
+ };
118
+ return {
119
+ applyYield(value) {
120
+ const fresh = value.entries.filter(
121
+ (entry) => applied.get(entry.queryHash) !== entry.state.dataUpdatedAt
122
+ );
123
+ if (fresh.length > 0) {
124
+ solidJs.runWithOwner(
125
+ null,
126
+ () => queryCore.hydrate(client(), { queries: fresh, mutations: [] })
127
+ );
128
+ for (const entry of fresh) {
129
+ applied.set(entry.queryHash, entry.state.dataUpdatedAt);
130
+ fireWaiters(entry.queryHash);
131
+ }
132
+ }
133
+ if (value.done && !channelDone) {
134
+ channelDone = true;
135
+ const remaining = [...waiters.values()];
136
+ waiters.clear();
137
+ for (const callbacks of remaining) {
138
+ for (const callback of callbacks) queueMicrotask(callback);
139
+ }
140
+ }
141
+ },
142
+ whenQueryPrimed(queryHash, callback) {
143
+ if (channelDone || applied.has(queryHash)) {
144
+ queueMicrotask(callback);
145
+ return;
146
+ }
147
+ let list = waiters.get(queryHash);
148
+ if (!list) {
149
+ list = [];
150
+ waiters.set(queryHash, list);
151
+ }
152
+ list.push(callback);
153
+ }
154
+ };
155
+ }
156
+ var HydrationCoordinatorContext = solidJs.createContext(null);
157
+
158
+ // src/QueryClientProvider.tsx
159
+ var isServer = typeof window === "undefined";
8
160
  exports.QueryClientContext = solidJs.createContext(null);
9
161
  exports.useQueryClient = (queryClient) => {
10
162
  if (queryClient) {
@@ -19,10 +171,22 @@ exports.useQueryClient = (queryClient) => {
19
171
  exports.QueryClientProvider = (props) => {
20
172
  props.client.mount();
21
173
  solidJs.onCleanup(() => props.client.unmount());
174
+ const [channelValue] = solidJs.createSignal(() => isServer ? createServerDehydrationChannel(props.client) : void 0);
175
+ const coordinator = isServer ? null : createHydrationCoordinator(() => props.client);
176
+ solidJs.createRenderEffect(() => isServer ? void 0 : channelValue(), (value) => {
177
+ if (value && coordinator) {
178
+ coordinator.applyYield(value);
179
+ }
180
+ });
22
181
  return web.createComponent(exports.QueryClientContext, {
23
182
  value: () => props.client,
24
183
  get children() {
25
- return props.children;
184
+ return web.createComponent(HydrationCoordinatorContext, {
185
+ value: coordinator,
186
+ get children() {
187
+ return props.children;
188
+ }
189
+ });
26
190
  }
27
191
  });
28
192
  };
@@ -30,9 +194,9 @@ exports.IsRestoringContext = solidJs.createContext(() => false);
30
194
  exports.useIsRestoring = () => solidJs.useContext(exports.IsRestoringContext);
31
195
 
32
196
  // src/useBaseQuery.ts
33
- var isServer = typeof window === "undefined";
197
+ var isServer2 = typeof window === "undefined";
34
198
  function _stripFnsForSSR(obj) {
35
- if (!isServer) return obj;
199
+ if (!isServer2) return obj;
36
200
  const out = {};
37
201
  for (const k of Object.keys(obj)) {
38
202
  if (k === "refetch" || k === "fetchNextPage" || k === "fetchPreviousPage") {
@@ -72,8 +236,8 @@ function reconcileFn(store, result, reconcileOption, queryHash) {
72
236
  }
73
237
  return { ...result, data };
74
238
  }
75
- var hydratableObserverResult = (query, result) => {
76
- if (!isServer) return result;
239
+ var hydratableObserverResult = (_query, result) => {
240
+ if (!isServer2) return result;
77
241
  const obj = {
78
242
  ...solidJs.snapshot(result),
79
243
  // During SSR, functions cannot be serialized, so we need to remove them
@@ -84,12 +248,6 @@ var hydratableObserverResult = (query, result) => {
84
248
  obj.fetchNextPage = void 0;
85
249
  obj.fetchPreviousPage = void 0;
86
250
  }
87
- obj.hydrationData = {
88
- state: query.state,
89
- queryKey: query.queryKey,
90
- queryHash: query.queryHash,
91
- ...query.meta && { meta: query.meta }
92
- };
93
251
  return obj;
94
252
  };
95
253
  function useBaseQuery(options, Observer, queryClient) {
@@ -100,7 +258,7 @@ function useBaseQuery(options, Observer, queryClient) {
100
258
  const defaultOptions = client().defaultQueryOptions(options());
101
259
  defaultOptions._optimisticResults = isRestoring() ? "isRestoring" : "optimistic";
102
260
  defaultOptions.structuralSharing = false;
103
- if (isServer) {
261
+ if (isServer2) {
104
262
  defaultOptions.retry = false;
105
263
  defaultOptions.throwOnError = true;
106
264
  defaultOptions.experimental_prefetchInRender = true;
@@ -172,11 +330,25 @@ function useBaseQuery(options, Observer, queryClient) {
172
330
  }
173
331
  let unsubscribe = null;
174
332
  let disposed = false;
333
+ const coordinator = solidJs.useContext(HydrationCoordinatorContext);
334
+ const attachHydratedSubscriber = () => {
335
+ if (!unsubscribe && !disposed && !isRestoring()) {
336
+ unsubscribe = createClientSubscriber();
337
+ }
338
+ };
339
+ const scheduleHydratedAttach = () => {
340
+ const queryHash = solidJs.untrack(() => observer.getCurrentQuery().queryHash);
341
+ if (coordinator) {
342
+ coordinator.whenQueryPrimed(queryHash, attachHydratedSubscriber);
343
+ } else {
344
+ queueMicrotask(attachHydratedSubscriber);
345
+ }
346
+ };
175
347
  let resolver = null;
176
348
  const [queryResource] = solidJs.createSignal(() => {
177
349
  const opts = trackedDefaultedOptions();
178
350
  const restoring = isRestoring();
179
- if (isServer) {
351
+ if (isServer2) {
180
352
  const cached = observer.getOptimisticResult(opts);
181
353
  if (!cached.isLoading) {
182
354
  observerResult = cached;
@@ -189,9 +361,11 @@ function useBaseQuery(options, Observer, queryClient) {
189
361
  );
190
362
  }
191
363
  }
192
- return new Promise((resolve, reject) => {
364
+ const replayProbe = { executorRan: false };
365
+ const resource = new Promise((resolve, reject) => {
366
+ replayProbe.executorRan = true;
193
367
  resolver = resolve;
194
- if (isServer) {
368
+ if (isServer2) {
195
369
  unsubscribe = createServerSubscriber((data) => {
196
370
  resolve(data);
197
371
  }, reject);
@@ -224,10 +398,14 @@ function useBaseQuery(options, Observer, queryClient) {
224
398
  );
225
399
  }
226
400
  });
401
+ if (!isServer2 && !replayProbe.executorRan) {
402
+ scheduleHydratedAttach();
403
+ }
404
+ return resource;
227
405
  });
228
406
  solidJs.onCleanup(() => {
229
407
  disposed = true;
230
- if (isServer && solidJs.isPending(queryResource)) {
408
+ if (isServer2 && solidJs.isPending(queryResource)) {
231
409
  unsubscribeQueued = true;
232
410
  return;
233
411
  }
@@ -235,7 +413,7 @@ function useBaseQuery(options, Observer, queryClient) {
235
413
  unsubscribe();
236
414
  unsubscribe = null;
237
415
  }
238
- if (resolver && !isServer) {
416
+ if (resolver && !isServer2) {
239
417
  resolver(observerResult);
240
418
  resolver = null;
241
419
  }
@@ -253,7 +431,7 @@ function useBaseQuery(options, Observer, queryClient) {
253
431
  if (typeof prop === "symbol") {
254
432
  return Reflect.get(target, prop, receiver);
255
433
  }
256
- if (isServer) {
434
+ if (isServer2) {
257
435
  const resolved = queryResource();
258
436
  if (prop in resolved) {
259
437
  return Reflect.get(resolved, prop);