-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathrenderRouterToStream.tsx
More file actions
191 lines (177 loc) · 5.66 KB
/
Copy pathrenderRouterToStream.tsx
File metadata and controls
191 lines (177 loc) · 5.66 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import * as Solid from 'solid-js/web'
import { isbot } from 'isbot'
import {
createSsrStreamResponse,
transformReadableStreamWithRouter,
} from '@tanstack/router-core/ssr/server'
import { makeSsrSerovalPlugin } from '@tanstack/router-core'
import type { JSXElement } from 'solid-js'
import type { ReadableStream } from 'node:stream/web'
import type { AnyRouter } from '@tanstack/router-core'
const noop = () => {}
// Bot responses wait for the server renderer before streaming. If the request
// disconnects during that wait, unblock so the pipe can abort and clean up.
async function waitForReadyOrAbort(
ready: Promise<unknown>,
signal: AbortSignal,
) {
let cleanup = noop
try {
await Promise.race([
ready,
new Promise<void>((resolve) => {
const onAbort = () => resolve()
cleanup = () => signal.removeEventListener('abort', onAbort)
signal.addEventListener('abort', onAbort, { once: true })
if (signal.aborted) resolve()
}),
])
} finally {
cleanup()
}
}
export const renderRouterToStream = async ({
request,
router,
responseHeaders,
children,
isBot,
}: {
request: Request
router: AnyRouter
responseHeaders: Headers
children: () => JSXElement
/**
* Whether to treat the request as a bot/crawler. Bots wait for the full
* server render before streaming. Defaults to the built-in `isbot`
* User-Agent check.
*/
isBot?: boolean | ((request: Request) => boolean)
}) => {
const isBotRequest =
typeof isBot === 'function'
? isBot(request)
: (isBot ?? isbot(request.headers.get('User-Agent')))
const { writable, readable } = new TransformStream()
const docType = Solid.ssr('<!DOCTYPE html>')
const serializationAdapters =
(router.options as any)?.serializationAdapters ||
(router.options.ssr as any)?.serializationAdapters
const serovalPlugins = serializationAdapters?.map((adapter: any) => {
const plugin = makeSsrSerovalPlugin(adapter, { didRun: false })
return plugin
})
const stream = Solid.renderToStream(
() => (
<>
{docType}
{children()}
</>
),
{
nonce: router.options.ssr?.nonce,
plugins: serovalPlugins,
} as any,
)
// Solid's `pipeTo(w)` takes a single arg (no signal overload) and locks
// `w` via `w.getWriter()`. To still own the lifecycle we hand Solid a
// proxy WritableStream that forwards into an inner writer we control on
// the real TransformStream writable. Aborting the inner writer errors
// the underlying readable (which our router transform reads from),
// surfacing the cancel through the response pipeline.
//
// RESIDUAL RISK: solid-js@1.x does NOT expose a disposal hook on
// `renderToStream`, and its internal write loop swallows writer
// rejections (`writer.write(...).catch(() => {})` in
// solid-js/web/dist/server.js). So aborting the inner writer stops
// outbound bytes but does not terminate Solid's render continuation
// if a Suspense/resource never resolves — those pending promise
// continuations remain scheduled and can retain children/context/
// request references via captured closures until natural completion
// or process exit. The request-scoped router graph itself is released
// by ServerSsr.cleanup() through the router stream lifecycle, so the leak
// is bounded to whatever the user's Suspense/resource closures capture.
// A hard upstream-abort guarantee would require a disposal API in solid-js.
const innerWriter = writable.getWriter()
let writerDone = false
const releaseWriter = () => {
try {
innerWriter.releaseLock()
} catch {
// already released / errored
}
}
const abortSolidPipe = (reason?: unknown) => {
if (writerDone) return
writerDone = true
void innerWriter
.abort(reason)
.catch(() => {})
.finally(releaseWriter)
}
const onRequestAbort = () => {
abortSolidPipe(request.signal.reason)
}
// Wire request abort before the bot all-ready wait. Otherwise a disconnect
// during `await stream` can leave this callback pending forever.
if (request.signal.aborted) {
onRequestAbort()
} else {
request.signal.addEventListener('abort', onRequestAbort, { once: true })
router.serverSsr?.onCleanup(() => {
request.signal.removeEventListener('abort', onRequestAbort)
})
}
if (isBotRequest) {
await waitForReadyOrAbort(
Promise.resolve(stream as unknown),
request.signal,
)
}
const solidWritable = new WritableStream({
write(chunk) {
return innerWriter.write(chunk)
},
close() {
writerDone = true
return innerWriter.close().finally(releaseWriter)
},
abort(reason) {
writerDone = true
return innerWriter.abort(reason).finally(releaseWriter)
},
})
if (!request.signal.aborted) {
try {
void Promise.resolve(stream.pipeTo(solidWritable) as unknown).catch(
(err: any) => {
if (
writerDone ||
err?.name === 'AbortError' ||
err?.code === 'ABORT_ERR'
)
return
console.error('Error in Solid render stream:', err)
abortSolidPipe(err)
},
)
} catch (err: any) {
if (err?.name !== 'AbortError' && err?.code !== 'ABORT_ERR') {
console.error('Error in Solid render stream:', err)
}
abortSolidPipe(err)
}
}
const responseStream = transformReadableStreamWithRouter(
router,
readable as unknown as ReadableStream,
{ onAbort: abortSolidPipe },
)
return createSsrStreamResponse(
router,
new Response(responseStream as any, {
status: router.stores.statusCode.get(),
headers: responseHeaders,
}),
)
}