rwsdk 1.0.0-beta.3 → 1.0.0-beta.30
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/dist/lib/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +7 -4
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +16 -0
- package/dist/lib/e2e/constants.mjs +77 -0
- package/dist/lib/e2e/dev.mjs +37 -49
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +202 -65
- package/dist/lib/e2e/index.d.mts +1 -0
- package/dist/lib/e2e/index.mjs +1 -0
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +16 -32
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +36 -4
- package/dist/lib/e2e/testHarness.mjs +216 -128
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/runtime/client/client.d.ts +35 -0
- package/dist/runtime/client/client.js +35 -0
- package/dist/runtime/client/navigation.d.ts +49 -0
- package/dist/runtime/client/navigation.js +80 -31
- package/dist/runtime/entries/clientSSR.d.ts +1 -0
- package/dist/runtime/entries/clientSSR.js +3 -0
- package/dist/runtime/entries/router.d.ts +1 -0
- package/dist/runtime/entries/routerClient.d.ts +1 -0
- package/dist/runtime/entries/routerClient.js +1 -0
- package/dist/runtime/entries/worker.d.ts +2 -0
- package/dist/runtime/entries/worker.js +2 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
- package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/createDb.d.ts +1 -2
- package/dist/runtime/lib/db/createDb.js +4 -0
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +18 -7
- package/dist/runtime/lib/links.js +70 -24
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/manifest.d.ts +1 -1
- package/dist/runtime/lib/manifest.js +7 -4
- package/dist/runtime/lib/realtime/client.js +8 -2
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +147 -33
- package/dist/runtime/lib/router.js +169 -20
- package/dist/runtime/lib/router.test.js +241 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
- package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
- package/dist/runtime/lib/types.js +1 -0
- package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.d.ts +4 -2
- package/dist/runtime/render/renderToStream.js +53 -24
- package/dist/runtime/render/renderToString.d.ts +3 -1
- package/dist/runtime/requestInfo/types.d.ts +4 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +44 -0
- package/dist/runtime/requestInfo/worker.js +3 -2
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +26 -0
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/scripts/worker-run.d.mts +1 -1
- package/dist/scripts/worker-run.mjs +52 -113
- package/dist/use-synced-state/SyncStateServer.d.mts +20 -0
- package/dist/use-synced-state/SyncStateServer.mjs +124 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
- package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
- package/dist/use-synced-state/client.d.ts +28 -0
- package/dist/use-synced-state/client.js +39 -0
- package/dist/use-synced-state/constants.d.mts +1 -0
- package/dist/use-synced-state/constants.mjs +1 -0
- package/dist/use-synced-state/useSyncState.d.ts +20 -0
- package/dist/use-synced-state/useSyncState.js +58 -0
- package/dist/use-synced-state/useSyncedState.d.ts +20 -0
- package/dist/use-synced-state/useSyncedState.js +58 -0
- package/dist/use-synced-state/worker.d.mts +14 -0
- package/dist/use-synced-state/worker.mjs +73 -0
- package/dist/vite/buildApp.mjs +34 -2
- package/dist/vite/configPlugin.mjs +8 -14
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/getViteEsbuild.mjs +2 -1
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +68 -0
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +1 -12
- package/dist/vite/linkerPlugin.d.mts +2 -1
- package/dist/vite/linkerPlugin.mjs +11 -3
- package/dist/vite/linkerPlugin.test.mjs +15 -0
- package/dist/vite/miniflareHMRPlugin.mjs +1 -38
- package/dist/vite/moveStaticAssetsPlugin.mjs +14 -4
- package/dist/vite/redwoodPlugin.mjs +6 -10
- package/dist/vite/runDirectivesScan.mjs +59 -14
- package/dist/vite/ssrBridgePlugin.mjs +122 -34
- package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
- package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +69 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/package.json +26 -10
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
|
@@ -15,112 +15,377 @@
|
|
|
15
15
|
* @param startMarker The marker in the document to start injecting the app.
|
|
16
16
|
* @param endMarker The marker in the app stream that signals the end of the initial, non-suspended render.
|
|
17
17
|
*/
|
|
18
|
+
function splitStreamOnFirstNonHoistedTag(sourceStream) {
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const nonHoistedTagPattern = /<(?!(?:\/)?(?:title|meta|link|style|base)[\s>\/])(?![!?])/i;
|
|
22
|
+
let sourceReader;
|
|
23
|
+
let appBodyController = null;
|
|
24
|
+
let buffer = "";
|
|
25
|
+
let hoistedTagsDone = false;
|
|
26
|
+
const hoistedTagsStream = new ReadableStream({
|
|
27
|
+
start(controller) {
|
|
28
|
+
sourceReader = sourceStream.getReader();
|
|
29
|
+
const pump = async () => {
|
|
30
|
+
try {
|
|
31
|
+
if (hoistedTagsDone) {
|
|
32
|
+
controller.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { done, value } = await sourceReader.read();
|
|
36
|
+
if (done) {
|
|
37
|
+
if (buffer) {
|
|
38
|
+
const match = buffer.match(nonHoistedTagPattern);
|
|
39
|
+
if (match && typeof match.index === "number") {
|
|
40
|
+
const hoistedPart = buffer.slice(0, match.index);
|
|
41
|
+
controller.enqueue(encoder.encode(hoistedPart));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
controller.enqueue(encoder.encode(buffer));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
controller.close();
|
|
48
|
+
hoistedTagsDone = true;
|
|
49
|
+
if (appBodyController) {
|
|
50
|
+
appBodyController.close();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
buffer += decoder.decode(value, { stream: true });
|
|
55
|
+
const match = buffer.match(nonHoistedTagPattern);
|
|
56
|
+
if (match && typeof match.index === "number") {
|
|
57
|
+
const hoistedPart = buffer.slice(0, match.index);
|
|
58
|
+
const appPart = buffer.slice(match.index);
|
|
59
|
+
buffer = "";
|
|
60
|
+
controller.enqueue(encoder.encode(hoistedPart));
|
|
61
|
+
controller.close();
|
|
62
|
+
hoistedTagsDone = true;
|
|
63
|
+
if (appBodyController) {
|
|
64
|
+
if (appPart) {
|
|
65
|
+
appBodyController.enqueue(encoder.encode(appPart));
|
|
66
|
+
}
|
|
67
|
+
while (true) {
|
|
68
|
+
const { done, value } = await sourceReader.read();
|
|
69
|
+
if (done) {
|
|
70
|
+
appBodyController.close();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
appBodyController.enqueue(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const flushIndex = buffer.lastIndexOf("\n");
|
|
79
|
+
if (flushIndex !== -1) {
|
|
80
|
+
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
81
|
+
buffer = buffer.slice(flushIndex + 1);
|
|
82
|
+
}
|
|
83
|
+
await pump();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
controller.error(e);
|
|
88
|
+
if (appBodyController) {
|
|
89
|
+
appBodyController.error(e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
pump().catch((e) => {
|
|
94
|
+
controller.error(e);
|
|
95
|
+
if (appBodyController) {
|
|
96
|
+
appBodyController.error(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const appBodyStream = new ReadableStream({
|
|
102
|
+
start(controller) {
|
|
103
|
+
appBodyController = controller;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return [hoistedTagsStream, appBodyStream];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A utility that orchestrates and interleaves three ReadableStreams to produce a
|
|
110
|
+
* single, valid HTML response stream. It uses two special markers:
|
|
111
|
+
*
|
|
112
|
+
* - `startMarker`: Placed in the `outerHtml` stream (the document shell) to
|
|
113
|
+
* designate where the application's content should be injected.
|
|
114
|
+
* - `endMarker`: Injected into the `innerHtml` stream's RSC payload to signal
|
|
115
|
+
* the end of the initial, non-suspended render. This marker is needed for
|
|
116
|
+
* non-blocking hydration, as it allows the stitching process to send the
|
|
117
|
+
* client `<script>` tags before all suspended content has resolved.
|
|
118
|
+
*
|
|
119
|
+
* It manages three main stream readers:
|
|
120
|
+
*
|
|
121
|
+
* - `hoistedTagsReader`: Reads from the `hoistedTagsStream`, which contains only
|
|
122
|
+
* the hoisted meta tags (e.g., `<title>`, `<meta>`).
|
|
123
|
+
* - `outerReader`: Reads from the `outerHtml` stream, which is the server-rendered
|
|
124
|
+
* document shell (containing `<html>`, `<head>`, etc.).
|
|
125
|
+
* - `innerReader`: Reads from the `appBodyStream`, which contains the main
|
|
126
|
+
* application content, stripped of its hoisted tags.
|
|
127
|
+
*
|
|
128
|
+
* The function proceeds through a multi-phase state machine, managed by the
|
|
129
|
+
* `pump` function, to correctly interleave these streams.
|
|
130
|
+
*
|
|
131
|
+
* The state machine moves through the following phases:
|
|
132
|
+
*
|
|
133
|
+
* 1. `read-hoisted`:
|
|
134
|
+
* - **Goal:** Buffer all hoisted tags from the `hoistedTagsStream`.
|
|
135
|
+
* - **Action:** Reads from `hoistedTagsReader` and appends all content into
|
|
136
|
+
* the `hoistedTagsBuffer`. Does not enqueue anything yet.
|
|
137
|
+
* - **Transition:** Moves to `outer-head` when the stream is exhausted.
|
|
138
|
+
*
|
|
139
|
+
* 2. `outer-head`:
|
|
140
|
+
* - **Goal:** Stream the document up to the closing `</head>` tag, inject the
|
|
141
|
+
* hoisted tags, and then continue until the app `startMarker`.
|
|
142
|
+
* - **Action:** Reads from `outerReader`. When it finds `</head>`, it enqueues
|
|
143
|
+
* the content before it, then enqueues the `hoistedTagsBuffer`, and finally
|
|
144
|
+
* enqueues the `</head>` tag itself. It then continues reading from
|
|
145
|
+
* `outerReader` until it finds the `startMarker`.
|
|
146
|
+
* - **Transition:** Moves to `inner-shell` after finding and discarding the
|
|
147
|
+
* `startMarker`.
|
|
148
|
+
*
|
|
149
|
+
* 3. `inner-shell`:
|
|
150
|
+
* - **Goal:** Stream the initial, non-suspended part of the application.
|
|
151
|
+
* - **Action:** Switches to `innerReader`. It enqueues chunks until it finds
|
|
152
|
+
* the `endMarker`. Any content after the marker is stored in
|
|
153
|
+
* `innerSuspendedRemains`.
|
|
154
|
+
* - **Transition:** Moves to `outer-tail` after finding the `endMarker`.
|
|
155
|
+
*
|
|
156
|
+
* 4. `outer-tail`:
|
|
157
|
+
* - **Goal:** Stream the rest of the document's `<body>`, including client
|
|
158
|
+
* `<script>` tags.
|
|
159
|
+
* - **Action:** Switches back to `outerReader` and enqueues chunks until it
|
|
160
|
+
* finds the `</body>` tag.
|
|
161
|
+
* - **Transition:** Moves to `inner-suspended` after finding `</body>`.
|
|
162
|
+
*
|
|
163
|
+
* 5. `inner-suspended`:
|
|
164
|
+
* - **Goal:** Stream any suspended content from the React app.
|
|
165
|
+
* - **Action:** First enqueues any content from `innerSuspendedRemains`, then
|
|
166
|
+
* continues reading from `innerReader` until the stream is exhausted.
|
|
167
|
+
* - **Transition:** Moves to `outer-end` when the stream is exhausted.
|
|
168
|
+
*
|
|
169
|
+
* 6. `outer-end`:
|
|
170
|
+
* - **Goal:** Finish the document.
|
|
171
|
+
* - **Action:** Switches back to `outerReader` for the last time to send the
|
|
172
|
+
* closing `</body>` and `</html>` tags.
|
|
173
|
+
*/
|
|
18
174
|
export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, endMarker) {
|
|
175
|
+
const [hoistedTagsStream, appBodyStream] = splitStreamOnFirstNonHoistedTag(innerHtml);
|
|
19
176
|
const decoder = new TextDecoder();
|
|
20
177
|
const encoder = new TextEncoder();
|
|
21
178
|
let outerReader;
|
|
22
179
|
let innerReader;
|
|
180
|
+
let hoistedTagsReader;
|
|
23
181
|
let buffer = "";
|
|
24
182
|
let outerBufferRemains = "";
|
|
25
|
-
let
|
|
183
|
+
let innerSuspendedRemains = "";
|
|
184
|
+
let hoistedTagsBuffer = "";
|
|
185
|
+
let hoistedTagsReady = false;
|
|
186
|
+
let phase = "read-hoisted";
|
|
26
187
|
const pump = async (controller) => {
|
|
188
|
+
const enqueue = (text) => {
|
|
189
|
+
if (text) {
|
|
190
|
+
controller.enqueue(encoder.encode(text));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const flush = () => {
|
|
194
|
+
const flushIndex = buffer.lastIndexOf("\n");
|
|
195
|
+
if (flushIndex !== -1) {
|
|
196
|
+
enqueue(buffer.slice(0, flushIndex + 1));
|
|
197
|
+
buffer = buffer.slice(flushIndex + 1);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
27
200
|
try {
|
|
28
|
-
if (phase === "
|
|
29
|
-
|
|
201
|
+
if (phase === "read-hoisted") {
|
|
202
|
+
// Continuously read from the hoisted tags stream and buffer the
|
|
203
|
+
// content. Once the stream is finished, transition to the next phase.
|
|
204
|
+
const { done, value } = await hoistedTagsReader.read();
|
|
205
|
+
// When the stream is done, we're ready to process the document head.
|
|
30
206
|
if (done) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
controller.close();
|
|
34
|
-
return;
|
|
207
|
+
hoistedTagsReady = true;
|
|
208
|
+
phase = "outer-head";
|
|
35
209
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
210
|
+
else {
|
|
211
|
+
// Otherwise, keep appending to the buffer.
|
|
212
|
+
hoistedTagsBuffer += decoder.decode(value, { stream: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (phase === "outer-head") {
|
|
216
|
+
// Read from the document stream. Search for the closing `</head>` tag
|
|
217
|
+
// to inject the buffered hoisted tags. Then, search for the
|
|
218
|
+
// `startMarker` to know when to start injecting the app shell. Once
|
|
219
|
+
// the marker is found, transition to the next phase.
|
|
220
|
+
const { done, value } = await outerReader.read();
|
|
221
|
+
// Handle the case where the document stream ends.
|
|
222
|
+
if (done) {
|
|
223
|
+
// If there's content left in the buffer, process it for markers.
|
|
224
|
+
if (buffer) {
|
|
225
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
226
|
+
if (headCloseIndex !== -1 &&
|
|
227
|
+
hoistedTagsReady &&
|
|
228
|
+
hoistedTagsBuffer) {
|
|
229
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
230
|
+
enqueue(hoistedTagsBuffer);
|
|
231
|
+
hoistedTagsBuffer = "";
|
|
232
|
+
enqueue("</head>");
|
|
233
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
234
|
+
}
|
|
235
|
+
const markerIndex = buffer.indexOf(startMarker);
|
|
236
|
+
if (markerIndex !== -1) {
|
|
237
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
238
|
+
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
enqueue(buffer);
|
|
242
|
+
}
|
|
243
|
+
buffer = "";
|
|
244
|
+
}
|
|
245
|
+
else if (hoistedTagsReady && hoistedTagsBuffer) {
|
|
246
|
+
enqueue(hoistedTagsBuffer);
|
|
247
|
+
hoistedTagsBuffer = "";
|
|
248
|
+
}
|
|
249
|
+
// Even if the stream ends, we must proceed to the app shell phase.
|
|
42
250
|
phase = "inner-shell";
|
|
43
251
|
}
|
|
44
252
|
else {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
253
|
+
// As chunks arrive, append them to the buffer.
|
|
254
|
+
buffer += decoder.decode(value, { stream: true });
|
|
255
|
+
// Search for the closing head tag to inject hoisted tags.
|
|
256
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
257
|
+
if (headCloseIndex !== -1 && hoistedTagsReady && hoistedTagsBuffer) {
|
|
258
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
259
|
+
enqueue(hoistedTagsBuffer);
|
|
260
|
+
hoistedTagsBuffer = "";
|
|
261
|
+
enqueue("</head>");
|
|
262
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
263
|
+
}
|
|
264
|
+
// Search for the start marker to switch to the app stream.
|
|
265
|
+
const markerIndex = buffer.indexOf(startMarker);
|
|
266
|
+
if (markerIndex !== -1) {
|
|
267
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
268
|
+
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
269
|
+
buffer = "";
|
|
270
|
+
phase = "inner-shell";
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// If no marker is found yet, flush the buffer up to the last
|
|
274
|
+
// newline to keep the stream flowing.
|
|
275
|
+
flush();
|
|
49
276
|
}
|
|
50
277
|
}
|
|
51
278
|
}
|
|
52
279
|
else if (phase === "inner-shell") {
|
|
280
|
+
// Now read from the app stream. We send the initial part of the app
|
|
281
|
+
// content until we find the `endMarker`. This marker tells us that the
|
|
282
|
+
// non-suspended part of the app is rendered. Any content after this
|
|
283
|
+
// marker is considered suspended and is buffered. Then, transition.
|
|
53
284
|
const { done, value } = await innerReader.read();
|
|
285
|
+
// Handle the case where the app stream ends.
|
|
54
286
|
if (done) {
|
|
55
287
|
if (buffer)
|
|
56
|
-
|
|
288
|
+
enqueue(buffer);
|
|
57
289
|
phase = "outer-tail";
|
|
58
290
|
}
|
|
59
291
|
else {
|
|
292
|
+
// As chunks arrive, append them to the buffer.
|
|
60
293
|
buffer += decoder.decode(value, { stream: true });
|
|
61
294
|
const markerIndex = buffer.indexOf(endMarker);
|
|
295
|
+
// If the end marker is found, enqueue content up to the marker,
|
|
296
|
+
// buffer the rest, and switch to the document tail phase.
|
|
62
297
|
if (markerIndex !== -1) {
|
|
63
298
|
const endOfMarkerIndex = markerIndex + endMarker.length;
|
|
64
|
-
|
|
65
|
-
|
|
299
|
+
enqueue(buffer.slice(0, endOfMarkerIndex));
|
|
300
|
+
innerSuspendedRemains = buffer.slice(endOfMarkerIndex);
|
|
301
|
+
buffer = "";
|
|
66
302
|
phase = "outer-tail";
|
|
67
303
|
}
|
|
68
304
|
else {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
72
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
73
|
-
}
|
|
305
|
+
// If no marker is found yet, flush the buffer.
|
|
306
|
+
flush();
|
|
74
307
|
}
|
|
75
308
|
}
|
|
76
309
|
}
|
|
77
310
|
else if (phase === "outer-tail") {
|
|
311
|
+
// Switch back to the document stream. The goal is to send the rest of
|
|
312
|
+
// the document's body, which critically includes the client-side
|
|
313
|
+
// `<script>` tags for hydration. We stream until we find the closing
|
|
314
|
+
// `</body>` tag and then transition.
|
|
315
|
+
// First, process any leftover buffer from the `outer-head` phase.
|
|
78
316
|
if (outerBufferRemains) {
|
|
79
317
|
buffer = outerBufferRemains;
|
|
80
318
|
outerBufferRemains = "";
|
|
81
319
|
}
|
|
82
320
|
const { done, value } = await outerReader.read();
|
|
321
|
+
// Handle the case where the document stream ends.
|
|
83
322
|
if (done) {
|
|
84
|
-
if (buffer)
|
|
85
|
-
|
|
323
|
+
if (buffer) {
|
|
324
|
+
// Search the remaining buffer for the closing body tag.
|
|
325
|
+
const markerIndex = buffer.indexOf("</body>");
|
|
326
|
+
if (markerIndex !== -1) {
|
|
327
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
328
|
+
buffer = buffer.slice(markerIndex);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
enqueue(buffer);
|
|
332
|
+
buffer = "";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Proceed to the suspended content phase.
|
|
86
336
|
phase = "inner-suspended";
|
|
87
337
|
}
|
|
88
338
|
else {
|
|
339
|
+
// As chunks arrive, append them to the buffer.
|
|
89
340
|
buffer += decoder.decode(value, { stream: true });
|
|
341
|
+
// Search for the closing body tag to switch to suspended content.
|
|
90
342
|
const markerIndex = buffer.indexOf("</body>");
|
|
91
343
|
if (markerIndex !== -1) {
|
|
92
|
-
|
|
344
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
93
345
|
buffer = buffer.slice(markerIndex);
|
|
94
346
|
phase = "inner-suspended";
|
|
95
347
|
}
|
|
96
348
|
else {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
100
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
101
|
-
}
|
|
349
|
+
// If no marker is found yet, flush the buffer.
|
|
350
|
+
flush();
|
|
102
351
|
}
|
|
103
352
|
}
|
|
104
353
|
}
|
|
105
354
|
else if (phase === "inner-suspended") {
|
|
355
|
+
// Switch back to the app stream. First, send any buffered suspended
|
|
356
|
+
// content from the `inner-shell` phase. Then, stream the rest of the
|
|
357
|
+
// app content until it's finished. This is all the content that was
|
|
358
|
+
// behind a `<Suspense>` boundary.
|
|
359
|
+
// First, send any buffered suspended content from the `inner-shell` phase.
|
|
360
|
+
if (innerSuspendedRemains) {
|
|
361
|
+
enqueue(innerSuspendedRemains);
|
|
362
|
+
innerSuspendedRemains = "";
|
|
363
|
+
}
|
|
106
364
|
const { done, value } = await innerReader.read();
|
|
365
|
+
// When the app stream is done, transition to the final phase.
|
|
107
366
|
if (done) {
|
|
108
367
|
phase = "outer-end";
|
|
109
368
|
}
|
|
110
369
|
else {
|
|
370
|
+
// Otherwise, pass through the remaining app content directly.
|
|
111
371
|
controller.enqueue(value);
|
|
112
372
|
}
|
|
113
373
|
}
|
|
114
374
|
else if (phase === "outer-end") {
|
|
375
|
+
// Finally, switch back to the document stream one last time to send
|
|
376
|
+
// the closing `</body>` and `</html>` tags and finish the response.
|
|
377
|
+
// First, send any leftover buffer from the `outer-tail` phase.
|
|
115
378
|
if (buffer) {
|
|
116
|
-
|
|
379
|
+
enqueue(buffer);
|
|
117
380
|
buffer = "";
|
|
118
381
|
}
|
|
119
382
|
const { done, value } = await outerReader.read();
|
|
383
|
+
// When the document stream is done, we're finished.
|
|
120
384
|
if (done) {
|
|
121
385
|
controller.close();
|
|
122
386
|
return;
|
|
123
387
|
}
|
|
388
|
+
// Otherwise, pass through the final document content.
|
|
124
389
|
controller.enqueue(value);
|
|
125
390
|
}
|
|
126
391
|
await pump(controller);
|
|
@@ -132,12 +397,14 @@ export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, e
|
|
|
132
397
|
return new ReadableStream({
|
|
133
398
|
start(controller) {
|
|
134
399
|
outerReader = outerHtml.getReader();
|
|
135
|
-
innerReader =
|
|
400
|
+
innerReader = appBodyStream.getReader();
|
|
401
|
+
hoistedTagsReader = hoistedTagsStream.getReader();
|
|
136
402
|
pump(controller).catch((e) => controller.error(e));
|
|
137
403
|
},
|
|
138
404
|
cancel(reason) {
|
|
139
405
|
outerReader?.cancel(reason);
|
|
140
406
|
innerReader?.cancel(reason);
|
|
407
|
+
hoistedTagsReader?.cancel(reason);
|
|
141
408
|
},
|
|
142
409
|
});
|
|
143
410
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|