0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

wip: streaming responses

This commit is contained in:
Nate Moore 2022-05-10 12:12:58 -06:00
parent c3f411a7f2
commit 7a26687f6a
6 changed files with 153 additions and 11 deletions

View file

@ -19,5 +19,8 @@
},
"dependencies": {
"svelte": "^3.47.0"
},
"volta": {
"node": "16.15.0"
}
}

View file

@ -37,8 +37,8 @@ const server = createServer((req, res) => {
});
});
server.listen(8085);
console.log('Serving at http://localhost:8085');
server.listen(3000);
console.log('Serving at http://localhost:3000');
// Silence weird <time> warning
console.error = () => {};

View file

@ -11,6 +11,7 @@ import type {
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
import { serializeListValue } from './util.js';
import { renderToReadableStream } from './stream.js';
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
export type { Metadata } from './metadata';
@ -59,6 +60,29 @@ async function _render(child: any): Promise<any> {
}
}
export async function* _renderStream(child: any): any {
child = await child;
if (child instanceof HTMLString) {
yield child;
} else if (typeof child === 'string') {
yield markHTMLString(escapeHTML(child));
} else if (Array.isArray(child)) {
yield markHTMLString((await Promise.all(child.map((value) => _render(value)))).join(''));
} else if (typeof child === 'function') {
yield* _renderStream(child());
} else if (!child && child !== 0) {
yield '';
// do nothing, safe to ignore falsey values.
} else if (
child instanceof AstroComponent ||
Object.prototype.toString.call(child) === '[object AstroComponent]'
) {
yield* child;
} else {
yield child;
}
}
// The return value when rendering a component.
// This is the result of calling render(), should this be named to RenderResult or...?
export class AstroComponent {
@ -74,7 +98,7 @@ export class AstroComponent {
return 'AstroComponent';
}
*[Symbol.iterator]() {
*[Symbol.asyncIterator]() {
const { htmlParts, expressions } = this;
for (let i = 0; i < htmlParts.length; i++) {
@ -82,7 +106,7 @@ export class AstroComponent {
const expression = expressions[i];
yield markHTMLString(html);
yield _render(expression);
yield* _renderStream(expression)();
}
}
}
@ -507,11 +531,14 @@ export async function renderPage(
const response = await componentFactory(result, props, children);
if (isAstroComponent(response)) {
let template = await renderAstroComponent(response);
const html = await replaceHeadInjection(result, template);
const stream = renderToReadableStream(response, {})
const res = new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/html;charset=UTF-8' },
});
return {
type: 'html',
html,
type: 'response',
response: res,
};
} else {
return {

View file

@ -0,0 +1,91 @@
import { _renderStream } from './index.js';
import { Readable } from 'node:stream';
import { HTMLString } from './escape.js';
export const StreamingIterator = Symbol.for('astro:streaming-iterator');
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
interface RenderToReadableStreamOptions {
identifierPrefix?: string,
namespaceURI?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
onError?: (error: any) => void,
};
export function renderToReadableStream(
Component: any,
options?: RenderToReadableStreamOptions,
): ReadableStream {
let aborted = false;
if (options && options.signal) {
const signal = options.signal;
const listener = () => {
aborted = true;
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
const encoder = new TextEncoder();
async function push(controller: ReadableStreamController<Uint8Array>) {
for await (const value of Component) {
if (aborted) break;
if (typeof value === 'object' && value.done) {
controller.close();
break;
}
if (value || value === 0) {
controller.enqueue(encoder.encode(value.toString()));
}
}
}
const stream = new ReadableStream({
async pull(controller) {
await push(controller);
controller.close();
},
cancel(reason) {
aborted = true;
},
});
return stream;
}
export function renderToNodeStream(
Component: any,
options?: RenderToReadableStreamOptions,
): Readable {
let aborted = false;
if (options && options.signal) {
const signal = options.signal;
const listener = () => {
aborted = true;
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
const encoder = new TextEncoder();
async function* body() {
for await (const value of Component) {
if (aborted) return;
if (value || value === 0) {
// for await (const subvalue of _render(value)) {
// console.log(subvalue);
// yield encoder.encode(subvalue.toString());
// }
}
}
}
const stream = Readable.from(body());
return stream;
}

View file

@ -76,7 +76,7 @@ const plugins = [
MediaQueryList: ['./MediaQueryList', 'MediaQueryList'],
Node: ['./Node', 'Node'],
ReadableStream: [
'web-streams-polyfill/dist/ponyfill.es6.mjs',
'web-streams-polyfill/dist/ponyfill.es2018.mjs',
'ReadableStream',
],
ShadowRoot: ['./Node', 'ShadowRoot'],

View file

@ -2,12 +2,33 @@ import {
default as nodeFetch,
Headers,
Request,
Response,
Response as NodeResponse,
} from 'node-fetch/src/index.js'
import { ReadableStream } from '../ponyfill.js'
import Stream from 'node:stream'
import * as _ from './utils'
export { Headers, Request, Response }
export { Headers, Request }
export class Response extends NodeResponse {
constructor(_body: any, init: any) {
let body;
if (_body instanceof ReadableStream) {
async function* getStream() {
const reader = _body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) return;
yield value;
}
}
body = Stream.Readable.from(getStream());
} else {
body = _body;
}
super(body, init);
}
};
export const fetch = {
fetch(