From a1dc4948fc058d9f1aa6f73df9030208a2ec4d38 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 3 Oct 2024 09:12:20 +0100 Subject: [PATCH] wip --- .../src/runtime/server/render/astro/render.ts | 22 ++++++++++++---- .../astro/src/runtime/server/render/page.ts | 26 ++++++++++++++++--- .../astro/src/runtime/server/render/util.ts | 17 ++++++++++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts index adc335495d..d608e5de08 100644 --- a/packages/astro/src/runtime/server/render/astro/render.ts +++ b/packages/astro/src/runtime/server/render/astro/render.ts @@ -125,7 +125,7 @@ export async function renderToReadableStream( } // Queue error on next microtask to flush the remaining chunks written synchronously - setTimeout(() => controller.error(e), 0); + queueMicrotask(() => controller.error(e)); } })(); }, @@ -229,9 +229,12 @@ export async function renderToAsyncIterable( // The `next` is an object `{ promise, resolve, reject }` that we use to wait // for chunks to be pushed into the buffer. let next: ReturnType> | null = null; - const buffer: Uint8Array[] = []; // []Uint8Array + const buffer: Uint8Array[] = []; let renderingComplete = false; + const BATCH_SIZE = 1024 * 1; + let currentBatchSize = 0; + const iterator: AsyncIterator = { async next() { if (result.cancelled) return { done: true, value: undefined }; @@ -271,8 +274,9 @@ export async function renderToAsyncIterable( offset += item.length; } - // Empty the array. We do this so that we can reuse the same array. + // Empty the buffer and reset the batch size buffer.length = 0; + currentBatchSize = 0; // **Reset batch size after sending** const returnValue = { // The iterator is done when rendering has finished @@ -297,7 +301,9 @@ export async function renderToAsyncIterable( renderedFirstPageChunk = true; if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) { const doctype = result.compressHTML ? '' : '\n'; - buffer.push(encoder.encode(doctype)); + const doctypeBytes = encoder.encode(doctype); + buffer.push(doctypeBytes); + currentBatchSize += doctypeBytes.length; } } if (chunk instanceof Response) { @@ -310,7 +316,13 @@ export async function renderToAsyncIterable( // Push the chunks into the buffer and resolve the promise so that next() // will run. buffer.push(bytes); - next?.resolve(); + currentBatchSize += bytes.length; + + // Check if batch size threshold is reached + if (currentBatchSize >= BATCH_SIZE) { + next?.resolve(); + // Note: Do not reset currentBatchSize here; it will be reset in `next()` + } } else if (buffer.length > 0) { next?.resolve(); } diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts index 0e0bcf295a..94d27dfd88 100644 --- a/packages/astro/src/runtime/server/render/page.ts +++ b/packages/astro/src/runtime/server/render/page.ts @@ -7,6 +7,28 @@ import { renderToAsyncIterable, renderToReadableStream, renderToString } from '. import { encoder } from './common.js'; import { isDeno, isNode } from './util.js'; +function readableStreamFromAsyncIterable( + asyncIterable: AsyncIterable, +) { + const iterator = asyncIterable[Symbol.asyncIterator](); + return new ReadableStream( + { + async pull(controller) { + try { + const { value, done } = await iterator.next(); + if (done) { + controller.close(); + } else { + controller.enqueue(value); + } + } catch (err) { + controller.error(err); + } + } + }, + ); +} + export async function renderPage( result: SSRResult, componentFactory: AstroComponentFactory | NonAstroPageComponent, @@ -59,9 +81,7 @@ export async function renderPage( true, route, ); - // Node.js allows passing in an AsyncIterable to the Response constructor. - // This is non-standard so using `any` here to preserve types everywhere else. - body = nodeBody as any; + body = nodeBody instanceof Response ? nodeBody : readableStreamFromAsyncIterable(nodeBody); } else { body = await renderToReadableStream(result, componentFactory, props, children, true, route); } diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 953488a839..63f53aa915 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -232,9 +232,22 @@ export type PromiseWithResolvers = { reject: (reason?: any) => void; }; -// This is an implementation of Promise.withResolvers(), which we can't yet rely on. -// We can remove this once the native function is available in Node.js +interface PromiseConstructorWithResolvers extends PromiseConstructor { + withResolvers: () => PromiseWithResolvers; +} + +function hasWithResolvers( + promiseConstructor: PromiseConstructor | PromiseConstructorWithResolvers, +): promiseConstructor is PromiseConstructorWithResolvers { + return 'withResolvers' in promiseConstructor; +} + +// This is an implementation of Promise.withResolvers(), which was added in Node v22 +// We can remove this once we drop support for Node <22.0.0 export function promiseWithResolvers(): PromiseWithResolvers { + if (hasWithResolvers(Promise)) { + return Promise.withResolvers(); + } let resolve: any, reject: any; const promise = new Promise((_resolve, _reject) => { resolve = _resolve;