0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00
This commit is contained in:
Matt Kane 2024-10-03 09:12:20 +01:00
parent 8f064fa5b9
commit a1dc4948fc
3 changed files with 55 additions and 10 deletions

View file

@ -125,7 +125,7 @@ export async function renderToReadableStream(
} }
// Queue error on next microtask to flush the remaining chunks written synchronously // 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 // The `next` is an object `{ promise, resolve, reject }` that we use to wait
// for chunks to be pushed into the buffer. // for chunks to be pushed into the buffer.
let next: ReturnType<typeof promiseWithResolvers<void>> | null = null; let next: ReturnType<typeof promiseWithResolvers<void>> | null = null;
const buffer: Uint8Array[] = []; // []Uint8Array const buffer: Uint8Array[] = [];
let renderingComplete = false; let renderingComplete = false;
const BATCH_SIZE = 1024 * 1;
let currentBatchSize = 0;
const iterator: AsyncIterator<Uint8Array> = { const iterator: AsyncIterator<Uint8Array> = {
async next() { async next() {
if (result.cancelled) return { done: true, value: undefined }; if (result.cancelled) return { done: true, value: undefined };
@ -271,8 +274,9 @@ export async function renderToAsyncIterable(
offset += item.length; 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; buffer.length = 0;
currentBatchSize = 0; // **Reset batch size after sending**
const returnValue = { const returnValue = {
// The iterator is done when rendering has finished // The iterator is done when rendering has finished
@ -297,7 +301,9 @@ export async function renderToAsyncIterable(
renderedFirstPageChunk = true; renderedFirstPageChunk = true;
if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) { if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) {
const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n'; const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n';
buffer.push(encoder.encode(doctype)); const doctypeBytes = encoder.encode(doctype);
buffer.push(doctypeBytes);
currentBatchSize += doctypeBytes.length;
} }
} }
if (chunk instanceof Response) { 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() // Push the chunks into the buffer and resolve the promise so that next()
// will run. // will run.
buffer.push(bytes); 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) { } else if (buffer.length > 0) {
next?.resolve(); next?.resolve();
} }

View file

@ -7,6 +7,28 @@ import { renderToAsyncIterable, renderToReadableStream, renderToString } from '.
import { encoder } from './common.js'; import { encoder } from './common.js';
import { isDeno, isNode } from './util.js'; import { isDeno, isNode } from './util.js';
function readableStreamFromAsyncIterable(
asyncIterable: AsyncIterable<Uint8Array>,
) {
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( export async function renderPage(
result: SSRResult, result: SSRResult,
componentFactory: AstroComponentFactory | NonAstroPageComponent, componentFactory: AstroComponentFactory | NonAstroPageComponent,
@ -59,9 +81,7 @@ export async function renderPage(
true, true,
route, route,
); );
// Node.js allows passing in an AsyncIterable to the Response constructor. body = nodeBody instanceof Response ? nodeBody : readableStreamFromAsyncIterable(nodeBody);
// This is non-standard so using `any` here to preserve types everywhere else.
body = nodeBody as any;
} else { } else {
body = await renderToReadableStream(result, componentFactory, props, children, true, route); body = await renderToReadableStream(result, componentFactory, props, children, true, route);
} }

View file

@ -232,9 +232,22 @@ export type PromiseWithResolvers<T> = {
reject: (reason?: any) => void; reject: (reason?: any) => void;
}; };
// This is an implementation of Promise.withResolvers(), which we can't yet rely on. interface PromiseConstructorWithResolvers<T> extends PromiseConstructor {
// We can remove this once the native function is available in Node.js withResolvers: () => PromiseWithResolvers<T>;
}
function hasWithResolvers<T>(
promiseConstructor: PromiseConstructor | PromiseConstructorWithResolvers<T>,
): promiseConstructor is PromiseConstructorWithResolvers<T> {
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<T = any>(): PromiseWithResolvers<T> { export function promiseWithResolvers<T = any>(): PromiseWithResolvers<T> {
if (hasWithResolvers<T>(Promise)) {
return Promise.withResolvers();
}
let resolve: any, reject: any; let resolve: any, reject: any;
const promise = new Promise<T>((_resolve, _reject) => { const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve; resolve = _resolve;