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