From d7e2ad126403b885d0be6caf43893bd36e16bd0d Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 16:42:45 +0000 Subject: [PATCH 1/8] Add renderToStream --- packages/next/server/render.tsx | 151 ++++++++++++++++++++++++++------ packages/next/server/utils.ts | 9 +- 2 files changed, 131 insertions(+), 29 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 1f98e28c862b492..fab604a5c12d5a3 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -63,7 +63,12 @@ import { Redirect, } from '../lib/load-custom-routes' import { DomainLocale } from './config' -import { RenderResult, resultFromChunks, resultToChunks } from './utils' +import { + Observer, + RenderResult, + resultFromChunks, + resultToChunks, +} from './utils' function noRouter() { const message = @@ -1002,32 +1007,48 @@ export async function renderToHTML( } } - // TODO: Support SSR streaming of Suspense. - const renderToString = concurrentFeatures - ? (element: React.ReactElement) => - new Promise((resolve, reject) => { - const stream = new PassThrough() - const buffers: Buffer[] = [] - stream.on('data', (chunk) => { - buffers.push(chunk) - }) - stream.once('end', () => { - resolve(Buffer.concat(buffers).toString('utf-8')) - }) + const renderToStream = (element: React.ReactElement) => + new Promise((resolve, reject) => { + const stream = new PassThrough() + let resolved = false + + const { + abort, + startWriting, + } = (ReactDOMServer as any).pipeToNodeWritable(element, stream, { + onError(error: Error) { + if (!resolved) { + resolved = true + reject(error) + } + abort() + }, + onCompleteAll() { + if (!resolved) { + resolved = true + resolve(({ complete, next }) => { + stream.on('data', (chunk) => { + next(chunk.toString('utf-8')) + }) + stream.once('end', () => { + complete() + }) - const { - abort, - startWriting, - } = (ReactDOMServer as any).pipeToNodeWritable(element, stream, { - onError(error: Error) { - abort() - reject(error) - }, - onCompleteAll() { startWriting() - }, - }) - }) + return () => { + abort() + } + }) + } + }, + }) + }).then(multiplexResult) + + const renderToString = concurrentFeatures + ? async (element: React.ReactElement) => { + const result = await renderToStream(element) + return await resultsToString([result]) + } : ReactDOMServer.renderToString const renderPage: RenderPage = ( @@ -1285,6 +1306,86 @@ function mergeResults(chunks: Array): RenderResult { } } +function multiplexResult(result: RenderResult): RenderResult { + const chunks: Array = [] + const subscribers: Set> = new Set() + let streamResult: + | { + kind: 'COMPLETE' + } + | { + kind: 'FAILED' + error: Error + } + | null = null + + result({ + next(chunk) { + chunks.push(chunk) + subscribers.forEach((subscriber) => { + try { + subscriber.next(chunk) + } catch {} + }) + }, + error(error) { + if (!streamResult) { + streamResult = { + kind: 'FAILED', + error, + } + subscribers.forEach((subscriber) => { + try { + subscriber.error(error) + } catch {} + }) + subscribers.clear() + } + }, + complete() { + if (!streamResult) { + streamResult = { + kind: 'COMPLETE', + } + subscribers.forEach((subscriber) => { + try { + subscriber.complete() + } catch {} + }) + subscribers.clear() + } + }, + }) + + return (subscriber) => { + let completed = false + process.nextTick(() => { + for (const chunk of chunks) { + if (completed) { + return + } + subscriber.next(chunk) + } + + if (!completed) { + if (!streamResult) { + subscribers.add(subscriber) + } else { + if (streamResult.kind === 'FAILED') { + subscriber.error(streamResult.error) + } else { + subscriber.complete() + } + } + } + }) + return () => { + completed = true + subscribers.delete(subscriber) + } + } +} + function errorToJSON(err: Error): Error { const { name, message, stack } = err return { name, message, stack } diff --git a/packages/next/server/utils.ts b/packages/next/server/utils.ts index e898038ecb47d97..c2ffe1d64017994 100644 --- a/packages/next/server/utils.ts +++ b/packages/next/server/utils.ts @@ -16,12 +16,13 @@ export function cleanAmpPath(pathname: string): string { } export type Disposable = () => void -// TODO: Consider just using an actual Observable here -export type RenderResult = (observer: { - next(chunk: string): void +export type Observer = { + next(chunk: T): void error(error: Error): void complete(): void -}) => Disposable +} +// TODO: Consider just using an actual Observable here +export type RenderResult = (observer: Observer) => Disposable export function resultFromChunks(chunks: string[]): RenderResult { return ({ next, complete, error }) => { From 52048ebbb2ca76f6266d023f212ab4b1385ba835 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 17:03:05 +0000 Subject: [PATCH 2/8] Support dynamic HTML too --- packages/next/server/render.tsx | 42 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index fab604a5c12d5a3..a15fc34eec51c59 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -421,6 +421,7 @@ export async function renderToHTML( previewProps, basePath, devOnlyCacheBusterQueryString, + requireStaticHTML, concurrentFeatures, } = renderOpts @@ -1007,10 +1008,29 @@ export async function renderToHTML( } } + const generateStaticHTML = requireStaticHTML || inAmpMode const renderToStream = (element: React.ReactElement) => new Promise((resolve, reject) => { const stream = new PassThrough() let resolved = false + const doResolve = () => { + if (!resolved) { + resolved = true + resolve(({ complete, next }) => { + stream.on('data', (chunk) => { + next(chunk.toString('utf-8')) + }) + stream.once('end', () => { + complete() + }) + + startWriting() + return () => { + abort() + } + }) + } + } const { abort, @@ -1023,24 +1043,14 @@ export async function renderToHTML( } abort() }, - onCompleteAll() { - if (!resolved) { - resolved = true - resolve(({ complete, next }) => { - stream.on('data', (chunk) => { - next(chunk.toString('utf-8')) - }) - stream.once('end', () => { - complete() - }) - - startWriting() - return () => { - abort() - } - }) + onReadyToStream() { + if (!generateStaticHTML) { + doResolve() } }, + onCompleteAll() { + doResolve() + }, }) }).then(multiplexResult) From c370d0d7b82f944854e6a104f57502ffbb2c3488 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 18:28:55 +0000 Subject: [PATCH 3/8] More resiliency --- packages/next/server/render.tsx | 23 +++++++++++++++++------ packages/next/server/utils.ts | 1 - 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index a15fc34eec51c59..3697c3ae975cd2f 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1374,18 +1374,29 @@ function multiplexResult(result: RenderResult): RenderResult { if (completed) { return } - subscriber.next(chunk) + try { + subscriber.next(chunk) + } catch (err) { + if (!completed) { + completed = true + try { + subscriber.error(err) + } catch {} + } + } } if (!completed) { if (!streamResult) { subscribers.add(subscriber) } else { - if (streamResult.kind === 'FAILED') { - subscriber.error(streamResult.error) - } else { - subscriber.complete() - } + try { + if (streamResult.kind === 'FAILED') { + subscriber.error(streamResult.error) + } else { + subscriber.complete() + } + } catch {} } } }) diff --git a/packages/next/server/utils.ts b/packages/next/server/utils.ts index c2ffe1d64017994..1334915ffc56232 100644 --- a/packages/next/server/utils.ts +++ b/packages/next/server/utils.ts @@ -21,7 +21,6 @@ export type Observer = { error(error: Error): void complete(): void } -// TODO: Consider just using an actual Observable here export type RenderResult = (observer: Observer) => Disposable export function resultFromChunks(chunks: string[]): RenderResult { From 4f65cdd64c009fe6e37ee5da6cf0ac702e6d910b Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 18:39:29 +0000 Subject: [PATCH 4/8] Make it nicer --- packages/next/server/render.tsx | 65 +++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 3697c3ae975cd2f..e710d98dca4ba54 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1344,11 +1344,7 @@ function multiplexResult(result: RenderResult): RenderResult { kind: 'FAILED', error, } - subscribers.forEach((subscriber) => { - try { - subscriber.error(error) - } catch {} - }) + subscribers.forEach((subscriber) => subscriber.error(error)) subscribers.clear() } }, @@ -1357,46 +1353,59 @@ function multiplexResult(result: RenderResult): RenderResult { streamResult = { kind: 'COMPLETE', } - subscribers.forEach((subscriber) => { - try { - subscriber.complete() - } catch {} - }) + subscribers.forEach((subscriber) => subscriber.complete()) subscribers.clear() } }, }) - return (subscriber) => { + return (innerSubscriber) => { let completed = false + const subscriber: Observer = { + next(chunk) { + if (!completed) { + try { + innerSubscriber.next(chunk) + } catch (err) { + subscriber.error(err) + } + } + }, + complete() { + if (!completed) { + try { + completed = true + innerSubscriber.complete() + } catch (err) {} + } + }, + error(err) { + if (!completed) { + try { + completed = true + innerSubscriber.error(err) + } catch (err) {} + } + }, + } + process.nextTick(() => { for (const chunk of chunks) { if (completed) { return } - try { - subscriber.next(chunk) - } catch (err) { - if (!completed) { - completed = true - try { - subscriber.error(err) - } catch {} - } - } + subscriber.next(chunk) } if (!completed) { if (!streamResult) { subscribers.add(subscriber) } else { - try { - if (streamResult.kind === 'FAILED') { - subscriber.error(streamResult.error) - } else { - subscriber.complete() - } - } catch {} + if (streamResult.kind === 'FAILED') { + subscriber.error(streamResult.error) + } else { + subscriber.complete() + } } } }) From 01fd33004580378515faea05188992457af15f8f Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 18:43:28 +0000 Subject: [PATCH 5/8] More clean up --- packages/next/server/render.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index e710d98dca4ba54..b15516ffb19f6c5 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1361,6 +1361,7 @@ function multiplexResult(result: RenderResult): RenderResult { return (innerSubscriber) => { let completed = false + let cleanup = () => {} const subscriber: Observer = { next(chunk) { if (!completed) { @@ -1373,21 +1374,25 @@ function multiplexResult(result: RenderResult): RenderResult { }, complete() { if (!completed) { + cleanup() try { - completed = true innerSubscriber.complete() } catch (err) {} } }, error(err) { if (!completed) { + cleanup() try { - completed = true innerSubscriber.error(err) } catch (err) {} } }, } + cleanup = () => { + completed = true + subscribers.delete(subscriber) + } process.nextTick(() => { for (const chunk of chunks) { @@ -1409,10 +1414,7 @@ function multiplexResult(result: RenderResult): RenderResult { } } }) - return () => { - completed = true - subscribers.delete(subscriber) - } + return () => cleanup() } } From ed3d640a44a943385cc297863c2229cf37c8701b Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 18:44:58 +0000 Subject: [PATCH 6/8] More clean up --- packages/next/server/render.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index b15516ffb19f6c5..5e5074ae3568111 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1332,11 +1332,7 @@ function multiplexResult(result: RenderResult): RenderResult { result({ next(chunk) { chunks.push(chunk) - subscribers.forEach((subscriber) => { - try { - subscriber.next(chunk) - } catch {} - }) + subscribers.forEach((subscriber) => subscriber.next(chunk)) }, error(error) { if (!streamResult) { From 623924631fb31e99ff4f2a7328babbfeefb2d216 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 18:48:20 +0000 Subject: [PATCH 7/8] Clean up result --- packages/next/server/render.tsx | 35 +++++++++------------------------ 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 5e5074ae3568111..4ef8f64f1240700 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1319,15 +1319,7 @@ function mergeResults(chunks: Array): RenderResult { function multiplexResult(result: RenderResult): RenderResult { const chunks: Array = [] const subscribers: Set> = new Set() - let streamResult: - | { - kind: 'COMPLETE' - } - | { - kind: 'FAILED' - error: Error - } - | null = null + let terminator: ((subscriber: Observer) => void) | null = null result({ next(chunk) { @@ -1335,21 +1327,16 @@ function multiplexResult(result: RenderResult): RenderResult { subscribers.forEach((subscriber) => subscriber.next(chunk)) }, error(error) { - if (!streamResult) { - streamResult = { - kind: 'FAILED', - error, - } - subscribers.forEach((subscriber) => subscriber.error(error)) + if (!terminator) { + terminator = (subscriber) => subscriber.error(error) + subscribers.forEach(terminator) subscribers.clear() } }, complete() { - if (!streamResult) { - streamResult = { - kind: 'COMPLETE', - } - subscribers.forEach((subscriber) => subscriber.complete()) + if (!terminator) { + terminator = (subscriber) => subscriber.complete() + subscribers.forEach(terminator) subscribers.clear() } }, @@ -1399,14 +1386,10 @@ function multiplexResult(result: RenderResult): RenderResult { } if (!completed) { - if (!streamResult) { + if (!terminator) { subscribers.add(subscriber) } else { - if (streamResult.kind === 'FAILED') { - subscriber.error(streamResult.error) - } else { - subscriber.complete() - } + terminator(subscriber) } } }) From d17b6b5669439d34f57ba577b2a3acf915de50fb Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 13 Aug 2021 19:02:01 +0000 Subject: [PATCH 8/8] Fix lint --- packages/next/server/render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 4ef8f64f1240700..7d88ecd1a806a3b 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1360,7 +1360,7 @@ function multiplexResult(result: RenderResult): RenderResult { cleanup() try { innerSubscriber.complete() - } catch (err) {} + } catch {} } }, error(err) { @@ -1368,7 +1368,7 @@ function multiplexResult(result: RenderResult): RenderResult { cleanup() try { innerSubscriber.error(err) - } catch (err) {} + } catch {} } }, }