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:
parent
c3f411a7f2
commit
7a26687f6a
6 changed files with 153 additions and 11 deletions
|
@ -19,5 +19,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"svelte": "^3.47.0"
|
"svelte": "^3.47.0"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "16.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@ const server = createServer((req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(8085);
|
server.listen(3000);
|
||||||
console.log('Serving at http://localhost:8085');
|
console.log('Serving at http://localhost:3000');
|
||||||
|
|
||||||
// Silence weird <time> warning
|
// Silence weird <time> warning
|
||||||
console.error = () => {};
|
console.error = () => {};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
||||||
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
|
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
|
import { renderToReadableStream } from './stream.js';
|
||||||
|
|
||||||
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
||||||
export type { Metadata } from './metadata';
|
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.
|
// The return value when rendering a component.
|
||||||
// This is the result of calling render(), should this be named to RenderResult or...?
|
// This is the result of calling render(), should this be named to RenderResult or...?
|
||||||
export class AstroComponent {
|
export class AstroComponent {
|
||||||
|
@ -74,7 +98,7 @@ export class AstroComponent {
|
||||||
return 'AstroComponent';
|
return 'AstroComponent';
|
||||||
}
|
}
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
*[Symbol.asyncIterator]() {
|
||||||
const { htmlParts, expressions } = this;
|
const { htmlParts, expressions } = this;
|
||||||
|
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
for (let i = 0; i < htmlParts.length; i++) {
|
||||||
|
@ -82,7 +106,7 @@ export class AstroComponent {
|
||||||
const expression = expressions[i];
|
const expression = expressions[i];
|
||||||
|
|
||||||
yield markHTMLString(html);
|
yield markHTMLString(html);
|
||||||
yield _render(expression);
|
yield* _renderStream(expression)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,11 +531,14 @@ export async function renderPage(
|
||||||
const response = await componentFactory(result, props, children);
|
const response = await componentFactory(result, props, children);
|
||||||
|
|
||||||
if (isAstroComponent(response)) {
|
if (isAstroComponent(response)) {
|
||||||
let template = await renderAstroComponent(response);
|
const stream = renderToReadableStream(response, {})
|
||||||
const html = await replaceHeadInjection(result, template);
|
const res = new Response(stream, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html;charset=UTF-8' },
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
type: 'html',
|
type: 'response',
|
||||||
html,
|
response: res,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|
91
packages/astro/src/runtime/server/stream.ts
Normal file
91
packages/astro/src/runtime/server/stream.ts
Normal 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;
|
||||||
|
}
|
|
@ -76,7 +76,7 @@ const plugins = [
|
||||||
MediaQueryList: ['./MediaQueryList', 'MediaQueryList'],
|
MediaQueryList: ['./MediaQueryList', 'MediaQueryList'],
|
||||||
Node: ['./Node', 'Node'],
|
Node: ['./Node', 'Node'],
|
||||||
ReadableStream: [
|
ReadableStream: [
|
||||||
'web-streams-polyfill/dist/ponyfill.es6.mjs',
|
'web-streams-polyfill/dist/ponyfill.es2018.mjs',
|
||||||
'ReadableStream',
|
'ReadableStream',
|
||||||
],
|
],
|
||||||
ShadowRoot: ['./Node', 'ShadowRoot'],
|
ShadowRoot: ['./Node', 'ShadowRoot'],
|
||||||
|
|
|
@ -2,12 +2,33 @@ import {
|
||||||
default as nodeFetch,
|
default as nodeFetch,
|
||||||
Headers,
|
Headers,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response as NodeResponse,
|
||||||
} from 'node-fetch/src/index.js'
|
} from 'node-fetch/src/index.js'
|
||||||
|
import { ReadableStream } from '../ponyfill.js'
|
||||||
import Stream from 'node:stream'
|
import Stream from 'node:stream'
|
||||||
import * as _ from './utils'
|
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 = {
|
export const fetch = {
|
||||||
fetch(
|
fetch(
|
||||||
|
|
Loading…
Reference in a new issue