rwsdk 1.0.0-beta.4 → 1.0.0-beta.40

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.
Files changed (139) hide show
  1. package/dist/lib/constants.d.mts +1 -0
  2. package/dist/lib/constants.mjs +7 -4
  3. package/dist/lib/e2e/browser.mjs +6 -2
  4. package/dist/lib/e2e/constants.d.mts +4 -0
  5. package/dist/lib/e2e/constants.mjs +49 -12
  6. package/dist/lib/e2e/dev.mjs +37 -49
  7. package/dist/lib/e2e/environment.d.mts +2 -0
  8. package/dist/lib/e2e/environment.mjs +201 -64
  9. package/dist/lib/e2e/index.d.mts +1 -0
  10. package/dist/lib/e2e/index.mjs +1 -0
  11. package/dist/lib/e2e/poll.d.mts +1 -1
  12. package/dist/lib/e2e/release.d.mts +1 -0
  13. package/dist/lib/e2e/release.mjs +16 -32
  14. package/dist/lib/e2e/tarball.mjs +2 -34
  15. package/dist/lib/e2e/testHarness.d.mts +34 -3
  16. package/dist/lib/e2e/testHarness.mjs +219 -90
  17. package/dist/lib/e2e/utils.d.mts +1 -0
  18. package/dist/lib/e2e/utils.mjs +15 -0
  19. package/dist/runtime/client/client.d.ts +35 -0
  20. package/dist/runtime/client/client.js +35 -0
  21. package/dist/runtime/client/navigation.d.ts +49 -0
  22. package/dist/runtime/client/navigation.js +80 -31
  23. package/dist/runtime/entries/clientSSR.d.ts +1 -0
  24. package/dist/runtime/entries/clientSSR.js +3 -0
  25. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  26. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  27. package/dist/runtime/entries/router.d.ts +1 -0
  28. package/dist/runtime/entries/routerClient.d.ts +1 -0
  29. package/dist/runtime/entries/routerClient.js +1 -0
  30. package/dist/runtime/entries/worker.d.ts +2 -0
  31. package/dist/runtime/entries/worker.js +2 -0
  32. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  33. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  34. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  35. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  36. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  37. package/dist/runtime/lib/db/createDb.js +4 -0
  38. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  39. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  40. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  41. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  42. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  43. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  44. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  45. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  46. package/dist/runtime/lib/links.d.ts +21 -7
  47. package/dist/runtime/lib/links.js +82 -24
  48. package/dist/runtime/lib/links.test.js +20 -0
  49. package/dist/runtime/lib/manifest.d.ts +1 -1
  50. package/dist/runtime/lib/manifest.js +7 -4
  51. package/dist/runtime/lib/realtime/client.js +8 -2
  52. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  53. package/dist/runtime/lib/router.d.ts +153 -36
  54. package/dist/runtime/lib/router.js +169 -20
  55. package/dist/runtime/lib/router.test.js +241 -0
  56. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  57. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  58. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  59. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  60. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  61. package/dist/runtime/lib/types.js +1 -0
  62. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  63. package/dist/runtime/render/renderToStream.d.ts +4 -2
  64. package/dist/runtime/render/renderToStream.js +53 -24
  65. package/dist/runtime/render/renderToString.d.ts +3 -6
  66. package/dist/runtime/requestInfo/types.d.ts +4 -1
  67. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  68. package/dist/runtime/requestInfo/utils.js +44 -0
  69. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  70. package/dist/runtime/requestInfo/worker.js +3 -10
  71. package/dist/runtime/script.d.ts +1 -3
  72. package/dist/runtime/script.js +1 -10
  73. package/dist/runtime/state.d.ts +3 -0
  74. package/dist/runtime/state.js +13 -0
  75. package/dist/runtime/worker.d.ts +3 -1
  76. package/dist/runtime/worker.js +32 -0
  77. package/dist/scripts/debug-sync.mjs +18 -20
  78. package/dist/scripts/worker-run.d.mts +1 -1
  79. package/dist/scripts/worker-run.mjs +59 -113
  80. package/dist/use-synced-state/SyncedStateServer.d.mts +21 -0
  81. package/dist/use-synced-state/SyncedStateServer.mjs +128 -0
  82. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  83. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  84. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  85. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  86. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  87. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  88. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  89. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  90. package/dist/use-synced-state/client-core.d.ts +26 -0
  91. package/dist/use-synced-state/client-core.js +39 -0
  92. package/dist/use-synced-state/client.d.ts +3 -0
  93. package/dist/use-synced-state/client.js +4 -0
  94. package/dist/use-synced-state/constants.d.mts +1 -0
  95. package/dist/use-synced-state/constants.mjs +1 -0
  96. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  97. package/dist/use-synced-state/useSyncedState.js +58 -0
  98. package/dist/use-synced-state/worker.d.mts +13 -0
  99. package/dist/use-synced-state/worker.mjs +69 -0
  100. package/dist/vite/buildApp.mjs +34 -2
  101. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  102. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  103. package/dist/vite/configPlugin.mjs +9 -14
  104. package/dist/vite/constants.d.mts +1 -0
  105. package/dist/vite/constants.mjs +1 -0
  106. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  107. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  108. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  109. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  110. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  111. package/dist/vite/directivesPlugin.mjs +4 -4
  112. package/dist/vite/envResolvers.d.mts +11 -0
  113. package/dist/vite/envResolvers.mjs +20 -0
  114. package/dist/vite/getViteEsbuild.mjs +2 -1
  115. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  116. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  117. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  118. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  119. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  120. package/dist/vite/linkerPlugin.d.mts +2 -1
  121. package/dist/vite/linkerPlugin.mjs +11 -3
  122. package/dist/vite/linkerPlugin.test.mjs +15 -0
  123. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  124. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  125. package/dist/vite/redwoodPlugin.mjs +8 -10
  126. package/dist/vite/runDirectivesScan.mjs +72 -18
  127. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  128. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  129. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  130. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  131. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  132. package/dist/vite/statePlugin.d.mts +4 -0
  133. package/dist/vite/statePlugin.mjs +62 -0
  134. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  135. package/dist/vite/virtualPlugin.mjs +6 -7
  136. package/package.json +27 -10
  137. package/dist/vite/manifestPlugin.d.mts +0 -4
  138. package/dist/vite/manifestPlugin.mjs +0 -63
  139. /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 phase = "outer-head";
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 === "outer-head") {
29
- const { done, value } = await outerReader.read();
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
- if (buffer)
32
- controller.enqueue(encoder.encode(buffer));
33
- controller.close();
34
- return;
207
+ hoistedTagsReady = true;
208
+ phase = "outer-head";
35
209
  }
36
- buffer += decoder.decode(value, { stream: true });
37
- const markerIndex = buffer.indexOf(startMarker);
38
- if (markerIndex !== -1) {
39
- controller.enqueue(encoder.encode(buffer.slice(0, markerIndex)));
40
- outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
41
- buffer = "";
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
- const flushIndex = buffer.lastIndexOf("\n");
46
- if (flushIndex !== -1) {
47
- controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
48
- buffer = buffer.slice(flushIndex + 1);
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
- controller.enqueue(encoder.encode(buffer));
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
- controller.enqueue(encoder.encode(buffer.slice(0, endOfMarkerIndex)));
65
- buffer = buffer.slice(endOfMarkerIndex);
299
+ enqueue(buffer.slice(0, endOfMarkerIndex));
300
+ innerSuspendedRemains = buffer.slice(endOfMarkerIndex);
301
+ buffer = "";
66
302
  phase = "outer-tail";
67
303
  }
68
304
  else {
69
- const flushIndex = buffer.lastIndexOf("\n");
70
- if (flushIndex !== -1) {
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
- controller.enqueue(encoder.encode(buffer));
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
- controller.enqueue(encoder.encode(buffer.slice(0, markerIndex)));
344
+ enqueue(buffer.slice(0, markerIndex));
93
345
  buffer = buffer.slice(markerIndex);
94
346
  phase = "inner-suspended";
95
347
  }
96
348
  else {
97
- const flushIndex = buffer.lastIndexOf("\n");
98
- if (flushIndex !== -1) {
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
- controller.enqueue(encoder.encode(buffer));
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 = innerHtml.getReader();
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
  }