From dd55f982915cb441ea98dc4b987309b7da084eb2 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Wed, 8 Sep 2021 09:56:31 -0700 Subject: [PATCH] Simplify `RenderResult` (#28900) We're no longer currently planning on supporting caching for dynamic responses, so we can do some cleaning & simplification: * Multiplexing can be removed since we only ever subscribe once (via `RenderResult.pipe`, described below) * `RenderResult.toUnchunkedString` can become synchronous since static responses are never chunked * `RenderResult.forEach` can become `RenderResult.pipe` which helps encapsulate some of the details of `RenderResult` --- .../next-serverless-loader/page-handler.ts | 2 +- packages/next/export/worker.ts | 4 +- packages/next/server/render-result.ts | 41 ++++++++------ packages/next/server/render.tsx | 55 ++----------------- packages/next/server/response-cache.ts | 2 +- packages/next/server/send-payload.ts | 10 +--- 6 files changed, 32 insertions(+), 82 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 50fdcc093cbf..954281fba74b 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -400,7 +400,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { } if (renderMode) return { html: result, renderOpts } - return result ? await result.toUnchunkedString() : null + return result ? result.toUnchunkedString() : null } catch (err) { if (!parsedUrl!) { parsedUrl = parseUrl(req.url!, true) diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index f4e40cde8f6d..9422a414563f 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -417,7 +417,7 @@ export default async function exportPage({ } } - const html = renderResult ? await renderResult.toUnchunkedString() : '' + const html = renderResult ? renderResult.toUnchunkedString() : '' if (inAmpMode && !curRenderOpts.ampSkipValidation) { if (!results.ssgNotFound) { await validateAmp(html, path, curRenderOpts.ampValidatorPath) @@ -460,7 +460,7 @@ export default async function exportPage({ } const ampHtml = ampRenderResult - ? await ampRenderResult.toUnchunkedString() + ? ampRenderResult.toUnchunkedString() : '' if (!curRenderOpts.ampSkipValidation) { await validateAmp(ampHtml, page + '?amp=1') diff --git a/packages/next/server/render-result.ts b/packages/next/server/render-result.ts index ef68878c23d3..b7cf3c1f4815 100644 --- a/packages/next/server/render-result.ts +++ b/packages/next/server/render-result.ts @@ -1,40 +1,45 @@ +import { ServerResponse } from 'http' import Observable from 'next/dist/compiled/zen-observable' export default class RenderResult { _response: string | Observable - _dynamic: boolean - constructor(response: string | Observable, dynamic: boolean) { + constructor(response: string | Observable) { this._response = response - this._dynamic = dynamic } - async toUnchunkedString(): Promise { - if (typeof this._response === 'string') { - return this._response + toUnchunkedString(): string { + if (typeof this._response !== 'string') { + throw new Error( + 'invariant: dynamic responses cannot be unchunked. This is a bug in Next.js' + ) } - const chunks: string[] = [] - await this._response.forEach((chunk) => chunks.push(chunk)) - return chunks.join('') + return this._response } - forEach(fn: (chunk: string) => void): Promise { + async pipe(res: ServerResponse): Promise { if (typeof this._response === 'string') { - const value = this._response - return new Promise((resolve) => { - fn(value) - resolve() - }) + throw new Error( + 'invariant: static responses cannot be piped. This is a bug in Next.js' + ) } - return this._response.forEach(fn) + const maybeFlush = + typeof (res as any).flush === 'function' + ? () => (res as any).flush() + : () => {} + await this._response.forEach((chunk) => { + res.write(chunk) + maybeFlush() + }) + res.end() } isDynamic(): boolean { - return this._dynamic + return typeof this._response !== 'string' } static fromStatic(value: string): RenderResult { - return new RenderResult(value, false) + return new RenderResult(value) } static empty = RenderResult.fromStatic('') diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index c641d8a0109b..fb39125f6428 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -942,7 +942,7 @@ export async function renderToHTML( doResolve() }, }) - }).then(multiplexObservable) + }) const renderDocument = async () => { if (Document.getInitialProps) { @@ -1214,8 +1214,9 @@ export async function renderToHTML( } return new RenderResult( - resultsToObservable(results), - generateStaticHTML === false + generateStaticHTML + ? (await observableToChunks(resultsToObservable(results))).join('') + : resultsToObservable(results) ) } @@ -1252,54 +1253,6 @@ async function observableToChunks( return chunks } -function multiplexObservable(result: Observable): Observable { - const chunks: Array = [] - const subscribers: Set> = new Set() - let terminator: - | ((subscriber: ZenObservable.SubscriptionObserver) => void) - | null = null - - result.subscribe({ - next(chunk) { - chunks.push(chunk) - subscribers.forEach((subscriber) => subscriber.next(chunk)) - }, - error(error) { - if (!terminator) { - terminator = (subscriber) => subscriber.error(error) - subscribers.forEach(terminator) - subscribers.clear() - } - }, - complete() { - if (!terminator) { - terminator = (subscriber) => subscriber.complete() - subscribers.forEach(terminator) - subscribers.clear() - } - }, - }) - - return new Observable((observer) => { - for (const chunk of chunks) { - if (observer.closed) { - return - } - observer.next(chunk) - } - - if (terminator) { - terminator(observer) - return - } - - subscribers.add(observer) - return () => { - subscribers.delete(observer) - } - }) -} - function errorToJSON(err: Error): Error { const { name, message, stack } = err return { name, message, stack } diff --git a/packages/next/server/response-cache.ts b/packages/next/server/response-cache.ts index 9393b4d13f1d..144a3b491f60 100644 --- a/packages/next/server/response-cache.ts +++ b/packages/next/server/response-cache.ts @@ -99,7 +99,7 @@ export default class ResponseCache { cacheEntry.value?.kind === 'PAGE' ? { kind: 'PAGE', - html: await cacheEntry.value.html.toUnchunkedString(), + html: cacheEntry.value.html.toUnchunkedString(), pageData: cacheEntry.value.pageData, } : cacheEntry.value, diff --git a/packages/next/server/send-payload.ts b/packages/next/server/send-payload.ts index 46e27db60854..9d9b9dc7a4c9 100644 --- a/packages/next/server/send-payload.ts +++ b/packages/next/server/send-payload.ts @@ -90,15 +90,7 @@ export async function sendRenderResult({ } else if (payload) { res.end(payload) } else { - const maybeFlush = - typeof (res as any).flush === 'function' - ? () => (res as any).flush() - : () => {} - await result.forEach((chunk) => { - res.write(chunk) - maybeFlush() - }) - res.end() + await result.pipe(res) } }