@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.
- package/build/_tsup-dts-rollup.d.cts +85 -0
- package/build/_tsup-dts-rollup.d.ts +85 -0
- package/build/dev.cjs +196 -18
- package/build/dev.js +198 -20
- package/build/index.cjs +196 -18
- package/build/index.js +198 -20
- package/package.json +6 -6
- package/src/QueryClientProvider.tsx +60 -2
- package/src/hydrationChannel.ts +260 -0
- package/src/useBaseQuery.ts +63 -13
package/build/index.js
CHANGED
|
@@ -1,9 +1,161 @@
|
|
|
1
|
-
import { MutationObserver, shouldThrowError, QueriesObserver, QueryClient as QueryClient$1, replaceEqualDeep, noop, notifyManager, QueryObserver, InfiniteQueryObserver } from '@tanstack/query-core';
|
|
1
|
+
import { MutationObserver, shouldThrowError, QueriesObserver, QueryClient as QueryClient$1, replaceEqualDeep, noop, hydrate, notifyManager, QueryObserver, InfiniteQueryObserver } from '@tanstack/query-core';
|
|
2
2
|
export * from '@tanstack/query-core';
|
|
3
|
-
import { createContext, useContext, onCleanup, createMemo, untrack,
|
|
3
|
+
import { createContext, useContext, onCleanup, createSignal, createRenderEffect, createMemo, untrack, createStore, runWithOwner, merge, createEffect, reconcile, isPending, refresh, snapshot } from 'solid-js';
|
|
4
4
|
import { createComponent } from '@solidjs/web';
|
|
5
5
|
|
|
6
6
|
// src/useQuery.ts
|
|
7
|
+
function createServerDehydrationChannel(client) {
|
|
8
|
+
const cache = client.getQueryCache();
|
|
9
|
+
const entryCache = /* @__PURE__ */ new Map();
|
|
10
|
+
const snapshot2 = () => {
|
|
11
|
+
const entries = [];
|
|
12
|
+
for (const query of cache.getAll()) {
|
|
13
|
+
if (query.state.status !== "success") continue;
|
|
14
|
+
let cached = entryCache.get(query.queryHash);
|
|
15
|
+
if (!cached || cached.state !== query.state) {
|
|
16
|
+
cached = {
|
|
17
|
+
state: query.state,
|
|
18
|
+
entry: {
|
|
19
|
+
dehydratedAt: Date.now(),
|
|
20
|
+
state: query.state,
|
|
21
|
+
queryKey: query.queryKey,
|
|
22
|
+
queryHash: query.queryHash,
|
|
23
|
+
...query.meta && { meta: query.meta },
|
|
24
|
+
...query.queryType && { queryType: query.queryType }
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
entryCache.set(query.queryHash, cached);
|
|
28
|
+
}
|
|
29
|
+
entries.push(cached.entry);
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
};
|
|
33
|
+
let closed = false;
|
|
34
|
+
let pull = null;
|
|
35
|
+
const buffered = [];
|
|
36
|
+
const emit = (value) => {
|
|
37
|
+
if (closed) return;
|
|
38
|
+
if (value.done) closed = true;
|
|
39
|
+
if (pull) {
|
|
40
|
+
const resolve = pull;
|
|
41
|
+
pull = null;
|
|
42
|
+
resolve({ done: false, value });
|
|
43
|
+
} else {
|
|
44
|
+
buffered.push(value);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
let closeTimer = null;
|
|
48
|
+
const scheduleCloseCheck = () => {
|
|
49
|
+
if (closed || closeTimer !== null) return;
|
|
50
|
+
closeTimer = setTimeout(() => {
|
|
51
|
+
closeTimer = null;
|
|
52
|
+
if (closed) return;
|
|
53
|
+
if (client.isFetching() === 0) {
|
|
54
|
+
unsubscribe();
|
|
55
|
+
emit({ entries: snapshot2(), done: true });
|
|
56
|
+
}
|
|
57
|
+
}, 0);
|
|
58
|
+
};
|
|
59
|
+
const unsubscribe = cache.subscribe((event) => {
|
|
60
|
+
if (closed) return;
|
|
61
|
+
if (event.type === "updated" && event.action.type === "success") {
|
|
62
|
+
emit({ entries: snapshot2(), done: false });
|
|
63
|
+
}
|
|
64
|
+
scheduleCloseCheck();
|
|
65
|
+
});
|
|
66
|
+
scheduleCloseCheck();
|
|
67
|
+
return {
|
|
68
|
+
[Symbol.asyncIterator]() {
|
|
69
|
+
return {
|
|
70
|
+
next() {
|
|
71
|
+
if (buffered.length > 0) {
|
|
72
|
+
return Promise.resolve({ done: false, value: buffered.shift() });
|
|
73
|
+
}
|
|
74
|
+
if (closed) {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
done: true,
|
|
77
|
+
value: void 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return new Promise(
|
|
81
|
+
(resolve) => {
|
|
82
|
+
pull = resolve;
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
return(value) {
|
|
87
|
+
if (!closed) {
|
|
88
|
+
closed = true;
|
|
89
|
+
unsubscribe();
|
|
90
|
+
if (closeTimer !== null) {
|
|
91
|
+
clearTimeout(closeTimer);
|
|
92
|
+
closeTimer = null;
|
|
93
|
+
}
|
|
94
|
+
const resolve = pull;
|
|
95
|
+
pull = null;
|
|
96
|
+
resolve?.({ done: true, value: void 0 });
|
|
97
|
+
}
|
|
98
|
+
return Promise.resolve({
|
|
99
|
+
done: true,
|
|
100
|
+
value
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createHydrationCoordinator(client) {
|
|
108
|
+
const applied = /* @__PURE__ */ new Map();
|
|
109
|
+
const waiters = /* @__PURE__ */ new Map();
|
|
110
|
+
let channelDone = false;
|
|
111
|
+
const fireWaiters = (queryHash) => {
|
|
112
|
+
const callbacks = waiters.get(queryHash);
|
|
113
|
+
if (!callbacks) return;
|
|
114
|
+
waiters.delete(queryHash);
|
|
115
|
+
for (const callback of callbacks) queueMicrotask(callback);
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
applyYield(value) {
|
|
119
|
+
const fresh = value.entries.filter(
|
|
120
|
+
(entry) => applied.get(entry.queryHash) !== entry.state.dataUpdatedAt
|
|
121
|
+
);
|
|
122
|
+
if (fresh.length > 0) {
|
|
123
|
+
runWithOwner(
|
|
124
|
+
null,
|
|
125
|
+
() => hydrate(client(), { queries: fresh, mutations: [] })
|
|
126
|
+
);
|
|
127
|
+
for (const entry of fresh) {
|
|
128
|
+
applied.set(entry.queryHash, entry.state.dataUpdatedAt);
|
|
129
|
+
fireWaiters(entry.queryHash);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (value.done && !channelDone) {
|
|
133
|
+
channelDone = true;
|
|
134
|
+
const remaining = [...waiters.values()];
|
|
135
|
+
waiters.clear();
|
|
136
|
+
for (const callbacks of remaining) {
|
|
137
|
+
for (const callback of callbacks) queueMicrotask(callback);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
whenQueryPrimed(queryHash, callback) {
|
|
142
|
+
if (channelDone || applied.has(queryHash)) {
|
|
143
|
+
queueMicrotask(callback);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let list = waiters.get(queryHash);
|
|
147
|
+
if (!list) {
|
|
148
|
+
list = [];
|
|
149
|
+
waiters.set(queryHash, list);
|
|
150
|
+
}
|
|
151
|
+
list.push(callback);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
var HydrationCoordinatorContext = createContext(null);
|
|
156
|
+
|
|
157
|
+
// src/QueryClientProvider.tsx
|
|
158
|
+
var isServer = typeof window === "undefined";
|
|
7
159
|
var QueryClientContext = createContext(null);
|
|
8
160
|
var useQueryClient = (queryClient) => {
|
|
9
161
|
if (queryClient) {
|
|
@@ -18,10 +170,22 @@ var useQueryClient = (queryClient) => {
|
|
|
18
170
|
var QueryClientProvider = (props) => {
|
|
19
171
|
props.client.mount();
|
|
20
172
|
onCleanup(() => props.client.unmount());
|
|
173
|
+
const [channelValue] = createSignal(() => isServer ? createServerDehydrationChannel(props.client) : void 0);
|
|
174
|
+
const coordinator = isServer ? null : createHydrationCoordinator(() => props.client);
|
|
175
|
+
createRenderEffect(() => isServer ? void 0 : channelValue(), (value) => {
|
|
176
|
+
if (value && coordinator) {
|
|
177
|
+
coordinator.applyYield(value);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
21
180
|
return createComponent(QueryClientContext, {
|
|
22
181
|
value: () => props.client,
|
|
23
182
|
get children() {
|
|
24
|
-
return
|
|
183
|
+
return createComponent(HydrationCoordinatorContext, {
|
|
184
|
+
value: coordinator,
|
|
185
|
+
get children() {
|
|
186
|
+
return props.children;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
25
189
|
}
|
|
26
190
|
});
|
|
27
191
|
};
|
|
@@ -29,9 +193,9 @@ var IsRestoringContext = createContext(() => false);
|
|
|
29
193
|
var useIsRestoring = () => useContext(IsRestoringContext);
|
|
30
194
|
|
|
31
195
|
// src/useBaseQuery.ts
|
|
32
|
-
var
|
|
196
|
+
var isServer2 = typeof window === "undefined";
|
|
33
197
|
function _stripFnsForSSR(obj) {
|
|
34
|
-
if (!
|
|
198
|
+
if (!isServer2) return obj;
|
|
35
199
|
const out = {};
|
|
36
200
|
for (const k of Object.keys(obj)) {
|
|
37
201
|
if (k === "refetch" || k === "fetchNextPage" || k === "fetchPreviousPage") {
|
|
@@ -62,8 +226,8 @@ function reconcileFn(store, result, reconcileOption, queryHash) {
|
|
|
62
226
|
}
|
|
63
227
|
return { ...result, data };
|
|
64
228
|
}
|
|
65
|
-
var hydratableObserverResult = (
|
|
66
|
-
if (!
|
|
229
|
+
var hydratableObserverResult = (_query, result) => {
|
|
230
|
+
if (!isServer2) return result;
|
|
67
231
|
const obj = {
|
|
68
232
|
...snapshot(result),
|
|
69
233
|
// During SSR, functions cannot be serialized, so we need to remove them
|
|
@@ -74,12 +238,6 @@ var hydratableObserverResult = (query, result) => {
|
|
|
74
238
|
obj.fetchNextPage = void 0;
|
|
75
239
|
obj.fetchPreviousPage = void 0;
|
|
76
240
|
}
|
|
77
|
-
obj.hydrationData = {
|
|
78
|
-
state: query.state,
|
|
79
|
-
queryKey: query.queryKey,
|
|
80
|
-
queryHash: query.queryHash,
|
|
81
|
-
...query.meta && { meta: query.meta }
|
|
82
|
-
};
|
|
83
241
|
return obj;
|
|
84
242
|
};
|
|
85
243
|
function useBaseQuery(options, Observer, queryClient) {
|
|
@@ -90,7 +248,7 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
90
248
|
const defaultOptions = client().defaultQueryOptions(options());
|
|
91
249
|
defaultOptions._optimisticResults = isRestoring() ? "isRestoring" : "optimistic";
|
|
92
250
|
defaultOptions.structuralSharing = false;
|
|
93
|
-
if (
|
|
251
|
+
if (isServer2) {
|
|
94
252
|
defaultOptions.retry = false;
|
|
95
253
|
defaultOptions.throwOnError = true;
|
|
96
254
|
defaultOptions.experimental_prefetchInRender = true;
|
|
@@ -162,11 +320,25 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
162
320
|
}
|
|
163
321
|
let unsubscribe = null;
|
|
164
322
|
let disposed = false;
|
|
323
|
+
const coordinator = useContext(HydrationCoordinatorContext);
|
|
324
|
+
const attachHydratedSubscriber = () => {
|
|
325
|
+
if (!unsubscribe && !disposed && !isRestoring()) {
|
|
326
|
+
unsubscribe = createClientSubscriber();
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const scheduleHydratedAttach = () => {
|
|
330
|
+
const queryHash = untrack(() => observer.getCurrentQuery().queryHash);
|
|
331
|
+
if (coordinator) {
|
|
332
|
+
coordinator.whenQueryPrimed(queryHash, attachHydratedSubscriber);
|
|
333
|
+
} else {
|
|
334
|
+
queueMicrotask(attachHydratedSubscriber);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
165
337
|
let resolver = null;
|
|
166
338
|
const [queryResource] = createSignal(() => {
|
|
167
339
|
const opts = trackedDefaultedOptions();
|
|
168
340
|
const restoring = isRestoring();
|
|
169
|
-
if (
|
|
341
|
+
if (isServer2) {
|
|
170
342
|
const cached = observer.getOptimisticResult(opts);
|
|
171
343
|
if (!cached.isLoading) {
|
|
172
344
|
observerResult = cached;
|
|
@@ -179,9 +351,11 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
179
351
|
);
|
|
180
352
|
}
|
|
181
353
|
}
|
|
182
|
-
|
|
354
|
+
const replayProbe = { executorRan: false };
|
|
355
|
+
const resource = new Promise((resolve, reject) => {
|
|
356
|
+
replayProbe.executorRan = true;
|
|
183
357
|
resolver = resolve;
|
|
184
|
-
if (
|
|
358
|
+
if (isServer2) {
|
|
185
359
|
unsubscribe = createServerSubscriber((data) => {
|
|
186
360
|
resolve(data);
|
|
187
361
|
}, reject);
|
|
@@ -214,10 +388,14 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
214
388
|
);
|
|
215
389
|
}
|
|
216
390
|
});
|
|
391
|
+
if (!isServer2 && !replayProbe.executorRan) {
|
|
392
|
+
scheduleHydratedAttach();
|
|
393
|
+
}
|
|
394
|
+
return resource;
|
|
217
395
|
});
|
|
218
396
|
onCleanup(() => {
|
|
219
397
|
disposed = true;
|
|
220
|
-
if (
|
|
398
|
+
if (isServer2 && isPending(queryResource)) {
|
|
221
399
|
unsubscribeQueued = true;
|
|
222
400
|
return;
|
|
223
401
|
}
|
|
@@ -225,7 +403,7 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
225
403
|
unsubscribe();
|
|
226
404
|
unsubscribe = null;
|
|
227
405
|
}
|
|
228
|
-
if (resolver && !
|
|
406
|
+
if (resolver && !isServer2) {
|
|
229
407
|
resolver(observerResult);
|
|
230
408
|
resolver = null;
|
|
231
409
|
}
|
|
@@ -243,7 +421,7 @@ function useBaseQuery(options, Observer, queryClient) {
|
|
|
243
421
|
if (typeof prop === "symbol") {
|
|
244
422
|
return Reflect.get(target, prop, receiver);
|
|
245
423
|
}
|
|
246
|
-
if (
|
|
424
|
+
if (isServer2) {
|
|
247
425
|
const resolved = queryResource();
|
|
248
426
|
if (prop in resolved) {
|
|
249
427
|
return Reflect.get(resolved, prop);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/solid-query",
|
|
3
|
-
"version": "6.0.0-beta.
|
|
3
|
+
"version": "6.0.0-beta.8",
|
|
4
4
|
"description": "Primitives for managing, caching and syncing asynchronous and remote data in Solid",
|
|
5
5
|
"author": "tannerlinsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
"@babel/core": "^7.28.0",
|
|
53
53
|
"@babel/preset-typescript": "^7.18.6",
|
|
54
54
|
"@solidjs/testing-library": "^0.8.10",
|
|
55
|
-
"@solidjs/web": "2.0.0-beta.
|
|
56
|
-
"babel-preset-solid": "2.0.0-beta.
|
|
55
|
+
"@solidjs/web": "2.0.0-beta.33",
|
|
56
|
+
"babel-preset-solid": "2.0.0-beta.33",
|
|
57
57
|
"npm-run-all2": "^5.0.0",
|
|
58
|
-
"solid-js": "2.0.0-beta.
|
|
58
|
+
"solid-js": "2.0.0-beta.33",
|
|
59
59
|
"tsup-preset-solid": "^2.2.0",
|
|
60
|
-
"vite-plugin-solid": "3.0.0-next.
|
|
60
|
+
"vite-plugin-solid": "3.0.0-next.21",
|
|
61
61
|
"@tanstack/query-test-utils": "0.0.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"solid-js": ">=2.0.0-beta.
|
|
64
|
+
"solid-js": ">=2.0.0-beta.33 <3.0.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"clean": "premove ./build ./coverage ./dist-ts",
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createRenderEffect,
|
|
4
|
+
createSignal,
|
|
5
|
+
onCleanup,
|
|
6
|
+
useContext,
|
|
7
|
+
} from 'solid-js'
|
|
8
|
+
import {
|
|
9
|
+
HydrationCoordinatorContext,
|
|
10
|
+
createHydrationCoordinator,
|
|
11
|
+
createServerDehydrationChannel,
|
|
12
|
+
} from './hydrationChannel'
|
|
13
|
+
import type { DehydrationChannelYield } from './hydrationChannel'
|
|
2
14
|
import type { QueryClient } from './QueryClient'
|
|
3
15
|
import type { JSX } from '@solidjs/web'
|
|
4
16
|
|
|
17
|
+
const isServer = typeof window === 'undefined'
|
|
18
|
+
|
|
5
19
|
export const QueryClientContext = createContext<(() => QueryClient) | null>(
|
|
6
20
|
null,
|
|
7
21
|
)
|
|
@@ -30,9 +44,53 @@ export const QueryClientProvider = (
|
|
|
30
44
|
props.client.mount()
|
|
31
45
|
onCleanup(() => props.client.unmount())
|
|
32
46
|
|
|
47
|
+
// Library-owned serialization channel for SSR dehydration.
|
|
48
|
+
//
|
|
49
|
+
// Server: the computation's value IS the channel's async iterable, so
|
|
50
|
+
// Solid serializes it through its normal per-computation signal path:
|
|
51
|
+
// the server runtime tees the iterator into the hydration serializer
|
|
52
|
+
// (`ctx.serialize(id, tapped)` in solid-js' `processResult`) and
|
|
53
|
+
// seroval streams each cumulative dehydrated-cache snapshot to the
|
|
54
|
+
// client as a chunk riding the SSR stream, with the entry objects
|
|
55
|
+
// inside deduplicated by reference against everything else in the
|
|
56
|
+
// payload. Nothing reads the signal during SSR, so it never suspends
|
|
57
|
+
// anything.
|
|
58
|
+
//
|
|
59
|
+
// Client, hydrating: Solid replays the serialized iterable through the
|
|
60
|
+
// per-computation signal path (`hydrateSignalFromAsyncIterable`).
|
|
61
|
+
// Yields that were still buffered when hydration began are conflated
|
|
62
|
+
// to the LATEST yield (`normalizeIterator`) — lossless here because
|
|
63
|
+
// every yield is a cumulative snapshot — and live yields after that
|
|
64
|
+
// apply one at a time. Requires a solid-js build with the buffered
|
|
65
|
+
// async-iterable replay conflation fix (> 2.0.0-beta.32): before it,
|
|
66
|
+
// the replay dropped every buffered yield after the first, including
|
|
67
|
+
// the terminal `done` snapshot. The render effect below hands each
|
|
68
|
+
// signal value to the coordinator, which primes the QueryClient via
|
|
69
|
+
// query-core hydrate() (newer-wins) and unblocks `useBaseQuery`
|
|
70
|
+
// subscribers waiting on their query's entry.
|
|
71
|
+
//
|
|
72
|
+
// Client, fresh mount: the compute returns undefined and the effect
|
|
73
|
+
// never fires.
|
|
74
|
+
const [channelValue] = createSignal<DehydrationChannelYield | undefined>(
|
|
75
|
+
() => (isServer ? createServerDehydrationChannel(props.client) : undefined),
|
|
76
|
+
)
|
|
77
|
+
const coordinator = isServer
|
|
78
|
+
? null
|
|
79
|
+
: createHydrationCoordinator(() => props.client)
|
|
80
|
+
createRenderEffect(
|
|
81
|
+
() => (isServer ? undefined : channelValue()),
|
|
82
|
+
(value) => {
|
|
83
|
+
if (value && coordinator) {
|
|
84
|
+
coordinator.applyYield(value)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
33
89
|
return (
|
|
34
90
|
<QueryClientContext value={() => props.client}>
|
|
35
|
-
{
|
|
91
|
+
<HydrationCoordinatorContext value={coordinator}>
|
|
92
|
+
{props.children}
|
|
93
|
+
</HydrationCoordinatorContext>
|
|
36
94
|
</QueryClientContext>
|
|
37
95
|
)
|
|
38
96
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { hydrate } from '@tanstack/query-core'
|
|
2
|
+
import { createContext, runWithOwner } from 'solid-js'
|
|
3
|
+
import type { DehydratedState, QueryState } from '@tanstack/query-core'
|
|
4
|
+
import type { QueryClient } from './QueryClient'
|
|
5
|
+
|
|
6
|
+
type DehydratedQueryEntry = DehydratedState['queries'][number]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single message on the dehydration channel. `entries` is *cumulative* —
|
|
10
|
+
* every yield carries all entries settled so far. Two reasons:
|
|
11
|
+
*
|
|
12
|
+
* - It is what makes Solid's signal-path hydration replay lossless.
|
|
13
|
+
* Yields still buffered when hydration begins are conflated to the
|
|
14
|
+
* LATEST one (`normalizeIterator` drains synchronously available
|
|
15
|
+
* results, keeps the last data yield, and delivers the stream's done
|
|
16
|
+
* result on a subsequent pull), so each yield must be self-contained:
|
|
17
|
+
* the latest cumulative snapshot alone carries everything the dropped
|
|
18
|
+
* intermediates did. Requires the solid-js build with that conflation
|
|
19
|
+
* behavior (> 2.0.0-beta.32); earlier betas pinned the replay at the
|
|
20
|
+
* FIRST buffered yield, dropping every later entry and the `done`
|
|
21
|
+
* marker.
|
|
22
|
+
* - Entry objects keep their identity across yields, so seroval's
|
|
23
|
+
* cross-reference serialization emits each entry once and later yields
|
|
24
|
+
* only reference it — the cumulative shape costs bytes proportional to
|
|
25
|
+
* the number of entries, not its square.
|
|
26
|
+
*
|
|
27
|
+
* `done: true` marks the final yield. The client uses it to release
|
|
28
|
+
* subscribers still waiting for entries that will never arrive (e.g.
|
|
29
|
+
* queries that errored during SSR and were not dehydrated).
|
|
30
|
+
*/
|
|
31
|
+
export interface DehydrationChannelYield {
|
|
32
|
+
entries: Array<DehydratedQueryEntry>
|
|
33
|
+
done: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Server side of the library-owned serialization channel.
|
|
38
|
+
*
|
|
39
|
+
* Returns an AsyncIterable that yields a cumulative snapshot of the
|
|
40
|
+
* dehydrated query cache (success entries, per query-core `dehydrate()`
|
|
41
|
+
* shapes) every time a query settles during SSR. `QueryClientProvider`
|
|
42
|
+
* holds it as a signal value, so Solid serializes it through the normal
|
|
43
|
+
* per-computation path: the server runtime tees the iterator into the
|
|
44
|
+
* hydration serializer (`ctx.serialize(id, tapped)` in solid-js'
|
|
45
|
+
* `processResult`) and seroval streams each yield to the client as a
|
|
46
|
+
* script chunk riding the SSR stream.
|
|
47
|
+
*
|
|
48
|
+
* The iterable must terminate for the SSR stream to complete: the
|
|
49
|
+
* hydration serializer's `flush()` only fires its `onDone` once all
|
|
50
|
+
* pending streams have closed, and the render root is disposed *after*
|
|
51
|
+
* that, so neither `onCleanup` nor the serializer itself can close the
|
|
52
|
+
* channel. Instead the channel closes itself on cache quiescence: after
|
|
53
|
+
* every cache event (and once at creation) it schedules a timer-task
|
|
54
|
+
* check; if no query is fetching by then, no further settle can occur —
|
|
55
|
+
* suspense retry passes that start waterfall fetches are scheduled on
|
|
56
|
+
* microtasks, so they have begun before the check runs — and the channel
|
|
57
|
+
* emits its final cumulative snapshot with `done: true` and completes.
|
|
58
|
+
*
|
|
59
|
+
* Single-consumer by design: solid-js creates exactly one iterator from
|
|
60
|
+
* the value and shares it between the memo and the serializer tap.
|
|
61
|
+
*/
|
|
62
|
+
export function createServerDehydrationChannel(
|
|
63
|
+
client: QueryClient,
|
|
64
|
+
): AsyncIterable<DehydrationChannelYield> {
|
|
65
|
+
const cache = client.getQueryCache()
|
|
66
|
+
// Entry objects are reused across yields while the query's state object
|
|
67
|
+
// is unchanged, both so seroval can deduplicate them by reference and
|
|
68
|
+
// so the client can cheaply skip already-applied entries.
|
|
69
|
+
const entryCache = new Map<
|
|
70
|
+
string,
|
|
71
|
+
{ state: QueryState<unknown, unknown>; entry: DehydratedQueryEntry }
|
|
72
|
+
>()
|
|
73
|
+
|
|
74
|
+
const snapshot = (): Array<DehydratedQueryEntry> => {
|
|
75
|
+
const entries: Array<DehydratedQueryEntry> = []
|
|
76
|
+
for (const query of cache.getAll()) {
|
|
77
|
+
// Mirrors query-core's defaultShouldDehydrateQuery.
|
|
78
|
+
if (query.state.status !== 'success') continue
|
|
79
|
+
let cached = entryCache.get(query.queryHash)
|
|
80
|
+
if (!cached || cached.state !== query.state) {
|
|
81
|
+
cached = {
|
|
82
|
+
state: query.state,
|
|
83
|
+
entry: {
|
|
84
|
+
dehydratedAt: Date.now(),
|
|
85
|
+
state: query.state,
|
|
86
|
+
queryKey: query.queryKey,
|
|
87
|
+
queryHash: query.queryHash,
|
|
88
|
+
...(query.meta && { meta: query.meta }),
|
|
89
|
+
...(query.queryType && { queryType: query.queryType }),
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
entryCache.set(query.queryHash, cached)
|
|
93
|
+
}
|
|
94
|
+
entries.push(cached.entry)
|
|
95
|
+
}
|
|
96
|
+
return entries
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let closed = false
|
|
100
|
+
let pull: ((result: IteratorResult<DehydrationChannelYield>) => void) | null =
|
|
101
|
+
null
|
|
102
|
+
const buffered: Array<DehydrationChannelYield> = []
|
|
103
|
+
|
|
104
|
+
const emit = (value: DehydrationChannelYield) => {
|
|
105
|
+
if (closed) return
|
|
106
|
+
if (value.done) closed = true
|
|
107
|
+
if (pull) {
|
|
108
|
+
const resolve = pull
|
|
109
|
+
pull = null
|
|
110
|
+
resolve({ done: false, value })
|
|
111
|
+
} else {
|
|
112
|
+
buffered.push(value)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null
|
|
117
|
+
const scheduleCloseCheck = () => {
|
|
118
|
+
if (closed || closeTimer !== null) return
|
|
119
|
+
closeTimer = setTimeout(() => {
|
|
120
|
+
closeTimer = null
|
|
121
|
+
if (closed) return
|
|
122
|
+
if (client.isFetching() === 0) {
|
|
123
|
+
unsubscribe()
|
|
124
|
+
emit({ entries: snapshot(), done: true })
|
|
125
|
+
}
|
|
126
|
+
}, 0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const unsubscribe = cache.subscribe((event) => {
|
|
130
|
+
if (closed) return
|
|
131
|
+
if (event.type === 'updated' && event.action.type === 'success') {
|
|
132
|
+
emit({ entries: snapshot(), done: false })
|
|
133
|
+
}
|
|
134
|
+
scheduleCloseCheck()
|
|
135
|
+
})
|
|
136
|
+
scheduleCloseCheck()
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
[Symbol.asyncIterator]() {
|
|
140
|
+
return {
|
|
141
|
+
next() {
|
|
142
|
+
if (buffered.length > 0) {
|
|
143
|
+
return Promise.resolve({ done: false, value: buffered.shift()! })
|
|
144
|
+
}
|
|
145
|
+
if (closed) {
|
|
146
|
+
return Promise.resolve({
|
|
147
|
+
done: true as const,
|
|
148
|
+
value: undefined,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
return new Promise<IteratorResult<DehydrationChannelYield>>(
|
|
152
|
+
(resolve) => {
|
|
153
|
+
pull = resolve
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
return(value?: unknown) {
|
|
158
|
+
if (!closed) {
|
|
159
|
+
closed = true
|
|
160
|
+
unsubscribe()
|
|
161
|
+
if (closeTimer !== null) {
|
|
162
|
+
clearTimeout(closeTimer)
|
|
163
|
+
closeTimer = null
|
|
164
|
+
}
|
|
165
|
+
const resolve = pull
|
|
166
|
+
pull = null
|
|
167
|
+
resolve?.({ done: true, value: undefined })
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve({
|
|
170
|
+
done: true as const,
|
|
171
|
+
value: value as DehydrationChannelYield,
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface HydrationCoordinator {
|
|
180
|
+
/**
|
|
181
|
+
* Prime the QueryClient from a channel yield. Entries already applied
|
|
182
|
+
* (same queryHash and dataUpdatedAt) are skipped; the rest go through
|
|
183
|
+
* query-core `hydrate()`, which keeps whichever data is newer.
|
|
184
|
+
*/
|
|
185
|
+
applyYield: (value: DehydrationChannelYield) => void
|
|
186
|
+
/**
|
|
187
|
+
* Invoke `callback` (on a microtask) once the entry for `queryHash` has
|
|
188
|
+
* been applied — or immediately-on-a-microtask if it already was, or
|
|
189
|
+
* when the channel completes without one (SSR-errored queries are not
|
|
190
|
+
* dehydrated, so their components must not wait forever).
|
|
191
|
+
*/
|
|
192
|
+
whenQueryPrimed: (queryHash: string, callback: () => void) => void
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Client side of the channel. Created by `QueryClientProvider` on the
|
|
197
|
+
* client and handed to `useBaseQuery` via context so hydrated components
|
|
198
|
+
* can attach their observers as soon as their query's entry has been
|
|
199
|
+
* primed — per query, not at global hydration end, which keeps
|
|
200
|
+
* early-hydrated components live while other boundaries still stream.
|
|
201
|
+
*/
|
|
202
|
+
export function createHydrationCoordinator(
|
|
203
|
+
client: () => QueryClient,
|
|
204
|
+
): HydrationCoordinator {
|
|
205
|
+
// queryHash -> dataUpdatedAt of the applied entry
|
|
206
|
+
const applied = new Map<string, number>()
|
|
207
|
+
const waiters = new Map<string, Array<() => void>>()
|
|
208
|
+
let channelDone = false
|
|
209
|
+
|
|
210
|
+
const fireWaiters = (queryHash: string) => {
|
|
211
|
+
const callbacks = waiters.get(queryHash)
|
|
212
|
+
if (!callbacks) return
|
|
213
|
+
waiters.delete(queryHash)
|
|
214
|
+
for (const callback of callbacks) queueMicrotask(callback)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
applyYield(value) {
|
|
219
|
+
const fresh = value.entries.filter(
|
|
220
|
+
(entry) => applied.get(entry.queryHash) !== entry.state.dataUpdatedAt,
|
|
221
|
+
)
|
|
222
|
+
if (fresh.length > 0) {
|
|
223
|
+
// hydrate() synchronously notifies cache subscribers which may
|
|
224
|
+
// write to stores/signals; escape the owned scope (this runs
|
|
225
|
+
// inside the provider's render effect) so those writes are
|
|
226
|
+
// allowed.
|
|
227
|
+
runWithOwner(null, () =>
|
|
228
|
+
hydrate(client(), { queries: fresh, mutations: [] }),
|
|
229
|
+
)
|
|
230
|
+
for (const entry of fresh) {
|
|
231
|
+
applied.set(entry.queryHash, entry.state.dataUpdatedAt)
|
|
232
|
+
fireWaiters(entry.queryHash)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (value.done && !channelDone) {
|
|
236
|
+
channelDone = true
|
|
237
|
+
const remaining = [...waiters.values()]
|
|
238
|
+
waiters.clear()
|
|
239
|
+
for (const callbacks of remaining) {
|
|
240
|
+
for (const callback of callbacks) queueMicrotask(callback)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
whenQueryPrimed(queryHash, callback) {
|
|
245
|
+
if (channelDone || applied.has(queryHash)) {
|
|
246
|
+
queueMicrotask(callback)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
let list = waiters.get(queryHash)
|
|
250
|
+
if (!list) {
|
|
251
|
+
list = []
|
|
252
|
+
waiters.set(queryHash, list)
|
|
253
|
+
}
|
|
254
|
+
list.push(callback)
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const HydrationCoordinatorContext =
|
|
260
|
+
createContext<HydrationCoordinator | null>(null)
|