Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(stream-ssr): Cancel the timeout when the react stream has finished #9317

Merged
merged 7 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/vite/src/streaming/createReactStreamingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export const createReactStreamingHandler = async (
},
{
waitForAllReady: isSeoCrawler,
onError: (err) => {
if (!isProd && viteDevServer) {
viteDevServer.ssrFixStacktrace(err)
}

console.error(err)
},
}
)

Expand Down
60 changes: 37 additions & 23 deletions packages/vite/src/streaming/streamHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import path from 'node:path'

import React from 'react'

import type {
RenderToReadableStreamOptions,
ReactDOMServerReadableStream,
} from 'react-dom/server'

import type { TagDescriptor } from '@redwoodjs/web'
// @TODO (ESM), use exports field. Cannot import from web because of index exports
import {
Expand All @@ -10,6 +15,7 @@ import {
} from '@redwoodjs/web/dist/components/ServerInject'

import { createBufferedTransformStream } from './transforms/bufferedTransform'
import { createTimeoutTransform } from './transforms/cancelTimeoutTransform'
import { createServerInjectionTransform } from './transforms/serverInjectionTransform'

interface RenderToStreamArgs {
Expand All @@ -24,6 +30,7 @@ interface RenderToStreamArgs {

interface StreamOptions {
waitForAllReady?: boolean
onError?: (err: Error) => void
}

export async function reactRenderToStreamResponse(
Expand Down Expand Up @@ -55,13 +62,22 @@ export async function reactRenderToStreamResponse(
const { injectionState, injectToPage } = createInjector()

// This makes it safe for us to inject at any point in the stream
const bufferedTransformStream = createBufferedTransformStream()
const bufferTransform = createBufferedTransformStream()

// This is a transformer stream, that will inject all things called with useServerInsertedHtml
const serverInjectionTransformer = createServerInjectionTransform({
const serverInjectionTransform = createServerInjectionTransform({
injectionState,
})

// Timeout after 10 seconds
// @TODO make this configurable
const controller = new AbortController()
const timeoutHandle = setTimeout(() => {
controller.abort()
}, 10000)

const timeoutTransform = createTimeoutTransform(timeoutHandle)

// @ts-expect-error Something in React's packages mean types dont come through
// Possible that we need to upgrade the @types/* packages
const { renderToReadableStream } = await import('react-dom/server.edge')
Expand Down Expand Up @@ -96,29 +112,27 @@ export async function reactRenderToStreamResponse(
// This gets set if there are errors inside Suspense boundaries
let didErrorOutsideShell = false

// Timeout after 10 seconds
// @TODO make this configurable
const controller = new AbortController()
setTimeout(() => {
controller.abort()
}, 10000)
// Assign here so we get types, the dynamic import messes types
const renderToStreamOptions: RenderToReadableStreamOptions = {
...bootstrapOptions,
signal: controller.signal,
onError: (err: any) => {
didErrorOutsideShell = true
console.error('🔻 Caught error outside shell')
streamOptions.onError?.(err)
},
}

const reactStream = await renderToReadableStream(
renderRoot(currentPathName),
{
...bootstrapOptions,
signal: controller.signal,
onError: (err: any) => {
didErrorOutsideShell = true
console.error('🔻 Caught error outside shell')
console.error(err)
},
}
)
const reactStream: ReactDOMServerReadableStream =
await renderToReadableStream(
renderRoot(currentPathName),
renderToStreamOptions
)

const output = reactStream
.pipeThrough(bufferedTransformStream)
.pipeThrough(serverInjectionTransformer)
.pipeThrough(bufferTransform)
.pipeThrough(serverInjectionTransform)
.pipeThrough(timeoutTransform)

if (waitForAllReady) {
await reactStream.allReady
Expand All @@ -130,7 +144,7 @@ export async function reactRenderToStreamResponse(
})
} catch (e) {
console.error('🔻 Failed to render shell')
console.error(e)
streamOptions.onError?.(e as Error)

// @TODO Asking for clarification from React team. Their documentation on this is incomplete I think.
// Having the Document (and bootstrap scripts) here allows client to recover from errors in the shell
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function createTimeoutTransform(timeoutHandle: NodeJS.Timeout) {
return new TransformStream({
flush() {
clearTimeout(timeoutHandle)
},
})
}
Loading