Skip to content

Commit

Permalink
fix: resolveHtmlStream error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed May 29, 2024
1 parent 328aea2 commit 1b1aaf0
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 39 deletions.
6 changes: 6 additions & 0 deletions .changeset/tame-dancers-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@kitajs/fastify-html-plugin': patch
'@kitajs/html': patch
---

Fixed resolveHtmlStream error handling
13 changes: 5 additions & 8 deletions packages/fastify-html-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

const fp = require('fastify-plugin');
const { isTagHtml } = require('./lib/is-tag-html');

// Loads the suspense component if it wasn't already loaded
if (!globalThis.SUSPENSE_ROOT) {
require('@kitajs/html/suspense');
}
const { resolveHtmlStream } = require('@kitajs/html/suspense');

/** @type {import('./types/index').kAutoDoctype} */
const kAutoDoctype = Symbol.for('fastify-kita-html.autoDoctype');
Expand Down Expand Up @@ -68,11 +64,12 @@ function handleHtml(htmlStr, reply) {
.send(htmlStr);
}

requestData.stream.push(htmlStr);

// Content-length is optional as long as the connection is closed after the response is done
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3
return reply.send(requestData.stream);
return reply.send(
// htmlStr might resolve after one of its suspense components
resolveHtmlStream(htmlStr, requestData)
);
}

const fastifyKitaHtml = fp(plugin, {
Expand Down
6 changes: 3 additions & 3 deletions packages/html/suspense.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export function renderToStream(
): Readable;

/**
* Prepends the fallback into a html stream handled manually without
* {@linkcode renderToStream}.
* Joins the html base template (with possible suspense's fallbacks) with the request data
* and returns the final Readable to be piped into the response stream.
*
* **This API is meant to be used by library authors and should not be used directly.**
*
Expand All @@ -120,7 +120,7 @@ export function renderToStream(
* @returns The same stream or another one with the fallback prepended.
* @see {@linkcode renderToStream}
*/
export function writeFallback(fallback: JSX.Element, stream: Readable): Readable;
export function resolveHtmlStream(template: JSX.Element, data: RequestData): Readable;

/**
* This script needs to be loaded at the top of the page. You do not need to load it
Expand Down
48 changes: 20 additions & 28 deletions packages/html/suspense.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { contentsToString, contentToString } = require('./index');
const { Readable } = require('node:stream');
const { Readable, PassThrough } = require('node:stream');

// Avoids double initialization in case this file is not cached by
// module bundlers.
Expand Down Expand Up @@ -262,42 +262,34 @@ function renderToStream(html, rid) {
});
}

return writeFallback(html, requestData.stream);
return resolveHtmlStream(html, requestData);
}

/** @type {import('./suspense').writeFallback} */
function writeFallback(fallback, readable) {
/** @type {import('./suspense').resolveHtmlStream} */
function resolveHtmlStream(template, requestData) {
// Impossible to sync templates have their
// streams being written before the fallback
if (typeof fallback === 'string') {
readable.push(fallback);
return readable;
// streams being written (sent = true) before the fallback
if (typeof template === 'string') {
requestData.stream.push(template);
return requestData.stream;
}

// The fallback might resolve after a suspense resolves,
// so we need to ensure the fallback is written and sent
// before anything the requestData.stream might already have.

const copy = new Readable({ read: noop });

void fallback
.then(async (result) => {
copy.push(result);

for await (const chunk of readable) {
copy.push(chunk);
}
const prepended = new PassThrough();

copy.push(null);
})
.catch((error) => {
copy.emit('error', error);
});
void template.then(
(result) => {
prepended.push(result);
requestData.stream.pipe(prepended);
},
(error) => {
prepended.emit('error', error);
}
);

return copy;
return prepended;
}

exports.Suspense = Suspense;
exports.renderToStream = renderToStream;
exports.writeFallback = writeFallback;
exports.resolveHtmlStream = resolveHtmlStream;
exports.SuspenseScript = SuspenseScript;

0 comments on commit 1b1aaf0

Please sign in to comment.