Skip to content

Commit 8aca00c

Browse files
committed
chore(rsc): debug rsc-html-stream uncaught enqueue error
1 parent 8804446 commit 8aca00c

File tree

3 files changed

+143
-3
lines changed

3 files changed

+143
-3
lines changed

packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ export async function renderHTML(
4141

4242
let responseStream: ReadableStream<Uint8Array> = htmlStream
4343
if (!options?.debugNojs) {
44+
console.log('[responseStream.pipeThrough:before]')
4445
// initial RSC stream is injected in HTML stream as <script>...FLIGHT_DATA...</script>
4546
responseStream = responseStream.pipeThrough(
4647
injectRscStreamToHtml(rscStream2, {
4748
nonce: options?.nonce,
4849
}),
4950
)
51+
console.log('[responseStream.pipeThrough:after]')
5052
}
5153

5254
return responseStream

packages/plugin-rsc/examples/starter/src/root.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import viteLogo from '/vite.svg'
33
import { getServerCounter, updateServerCounter } from './action.tsx'
44
import reactLogo from './assets/react.svg'
55
import { ClientCounter } from './client.tsx'
6+
import React from 'react'
67

78
export function Root() {
89
return (
@@ -43,6 +44,7 @@ function App() {
4344
<button>Server Counter: {getServerCounter()}</button>
4445
</form>
4546
</div>
47+
<TestSuspence />
4648
<ul className="read-the-docs">
4749
<li>
4850
Edit <code>src/client.tsx</code> to test client HMR.
@@ -68,3 +70,16 @@ function App() {
6870
</div>
6971
)
7072
}
73+
74+
function TestSuspence() {
75+
async function Inner() {
76+
await new Promise((resolve) => setTimeout(resolve, 1000))
77+
return <div>[suspense-resolved]</div>
78+
}
79+
80+
return (
81+
<React.Suspense fallback={<div>[suspense-fallback]</div>}>
82+
<Inner />
83+
</React.Suspense>
84+
)
85+
}
Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,130 @@
1-
import * as rscHtmlStreamServer from 'rsc-html-stream/server'
1+
// @ts-nocheck
2+
3+
// import * as rscHtmlStreamServer from 'rsc-html-stream/server'
4+
5+
// export const injectRscStreamToHtml = (
6+
// stream: ReadableStream<Uint8Array>,
7+
// options?: { nonce?: string },
8+
// ): TransformStream<Uint8Array, Uint8Array> =>
9+
// rscHtmlStreamServer.injectRSCPayload(stream, options)
210

311
export const injectRscStreamToHtml = (
412
stream: ReadableStream<Uint8Array>,
513
options?: { nonce?: string },
6-
): TransformStream<Uint8Array, Uint8Array> =>
7-
rscHtmlStreamServer.injectRSCPayload(stream, options)
14+
): TransformStream<Uint8Array, Uint8Array> => injectRSCPayload(stream, options)
15+
16+
const encoder = new TextEncoder()
17+
const trailer = '</body></html>'
18+
19+
function injectRSCPayload(rscStream, options) {
20+
let decoder = new TextDecoder()
21+
let resolveFlightDataPromise
22+
let flightDataPromise = new Promise(
23+
(resolve) => (resolveFlightDataPromise = resolve),
24+
)
25+
let startedRSC = false
26+
let nonce =
27+
options && typeof options.nonce === 'string' ? options.nonce : undefined
28+
29+
// Buffer all HTML chunks enqueued during the current tick of the event loop (roughly)
30+
// and write them to the output stream all at once. This ensures that we don't generate
31+
// invalid HTML by injecting RSC in between two partial chunks of HTML.
32+
let buffered = []
33+
let timeout = null
34+
function flushBufferedChunks(controller) {
35+
console.log('[flushBufferedChunks]', buffered.length)
36+
for (let chunk of buffered) {
37+
let buf = decoder.decode(chunk, { stream: true })
38+
if (buf.endsWith(trailer)) {
39+
buf = buf.slice(0, -trailer.length)
40+
}
41+
controller.enqueue(encoder.encode(buf))
42+
}
43+
44+
let remaining = decoder.decode()
45+
if (remaining.length) {
46+
if (remaining.endsWith(trailer)) {
47+
remaining = remaining.slice(0, -trailer.length)
48+
}
49+
controller.enqueue(encoder.encode(remaining))
50+
}
51+
52+
buffered.length = 0
53+
timeout = null
54+
}
55+
56+
return new TransformStream({
57+
transform(chunk, controller) {
58+
console.log('[TransformStream.transform]')
59+
60+
buffered.push(chunk)
61+
if (timeout) {
62+
return
63+
}
64+
65+
timeout = setTimeout(async () => {
66+
console.log('[setTimeout]')
67+
flushBufferedChunks(controller)
68+
if (!startedRSC) {
69+
startedRSC = true
70+
writeRSCStream(rscStream, controller, nonce)
71+
.catch((err) => controller.error(err))
72+
.then(resolveFlightDataPromise)
73+
}
74+
}, 0)
75+
},
76+
async flush(controller) {
77+
console.log('[TransformStream.flush]')
78+
await flightDataPromise
79+
console.log('[flightDataPromise.resolved]')
80+
if (timeout) {
81+
clearTimeout(timeout)
82+
flushBufferedChunks(controller)
83+
}
84+
controller.enqueue(encoder.encode(trailer))
85+
},
86+
})
87+
}
88+
89+
async function writeRSCStream(rscStream, controller, nonce) {
90+
let decoder = new TextDecoder('utf-8', { fatal: true })
91+
for await (let chunk of rscStream) {
92+
// Try decoding the chunk to send as a string.
93+
// If that fails (e.g. binary data that is invalid unicode), write as base64.
94+
try {
95+
writeChunk(
96+
JSON.stringify(decoder.decode(chunk, { stream: true })),
97+
controller,
98+
nonce,
99+
)
100+
} catch (err) {
101+
let base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)))
102+
writeChunk(
103+
`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`,
104+
controller,
105+
nonce,
106+
)
107+
}
108+
}
109+
110+
let remaining = decoder.decode()
111+
if (remaining.length) {
112+
writeChunk(JSON.stringify(remaining), controller, nonce)
113+
}
114+
}
115+
116+
function writeChunk(chunk, controller, nonce) {
117+
controller.enqueue(
118+
encoder.encode(
119+
`<script${nonce ? ` nonce="${nonce}"` : ''}>${escapeScript(`(self.__FLIGHT_DATA||=[]).push(${chunk})`)}</script>`,
120+
),
121+
)
122+
}
123+
124+
// Escape closing script tags and HTML comments in JS content.
125+
// https://www.w3.org/TR/html52/semantics-scripting.html#restrictions-for-contents-of-script-elements
126+
// Avoid replacing </script with <\/script as it would break the following valid JS: 0</script/ (i.e. regexp literal).
127+
// Instead, escape the s character.
128+
function escapeScript(script) {
129+
return script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1')
130+
}

0 commit comments

Comments
 (0)