Skip to content
This repository has been archived by the owner on Mar 11, 2024. It is now read-only.

Commit

Permalink
feat: setupHtmlStream()
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed Jan 21, 2024
1 parent 12a7c91 commit 87fc8a5
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 49 deletions.
25 changes: 17 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,15 @@ function fastifyKitaHtml(fastify, opts, next) {

// The normal .html handler is much simpler than the streamHtml one
fastify.decorateReply('html', html);
fastify.decorateReply('setupHtmlStream', setupHtmlStream)

// As JSX is evaluated from the inside out, renderToStream() method requires
// a function to be able to execute some code before the JSX calls gets to
// render, it can be avoided by simply executing the code in the
// streamHtml getter method.
fastify.decorateReply('streamHtml', {
getter() {
SUSPENSE_ROOT.requests.set(this.request.id, {
// As reply.raw is a instance of Writable, we can use it instead of
// creating a our own new stream.
stream: new WeakRef(this.raw),
running: 0,
sent: false
});

this.setupHtmlStream();
return streamHtml;
}
});
Expand All @@ -55,6 +49,21 @@ function fastifyKitaHtml(fastify, opts, next) {

return next();

/**
* @type {import('fastify').FastifyReply['setupHtmlStream']}
*/
function setupHtmlStream() {
SUSPENSE_ROOT.requests.set(this.request.id, {
// As reply.raw is a instance of Writable, we can use it instead of
// creating a our own new stream.
stream: new WeakRef(this.raw),
running: 0,
sent: false
});

return this;
}

/**
* @type {import('fastify').FastifyReply['html']}
*/
Expand Down
123 changes: 82 additions & 41 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,115 @@
import { type FastifyPluginCallback } from 'fastify';
import { FastifyPluginCallback } from 'fastify';

declare module 'fastify' {
interface FastifyReply {
/**
* Returns a response with the given HTML.
* **Synchronously** waits for the component tree to resolve and sends it at
* once to the browser.
*
* **This is a sync operation, all async components will be resolved before
* sending the response, please use `Suspense` components and
* `reply.streamHtml` if you want to stream the response.**
* This method does not support the usage of `<Suspense />`, please use
* {@linkcode streamHtml} instead.
*
* If the HTML does not start with a doctype and `opts.autoDoctype` is
* enabled, it will be added automatically.
* If the HTML does not start with a doctype and `opts.autoDoctype` is enabled, it
* will be added automatically.
*
* The correct `Content-Type` header will also be defined.
*
* @example
*
* ```tsx
* app.get('/', (req, reply) => {
* app.get('/', (req, reply) =>
* reply.html(
* <html lang="en">
* <body>
* <h1>Hello, world!</h1>
* </body>
* </html>
* );
* });
* )
* );
* ```
*
* @param html The HTML to send.
* @returns The response.
*/
html(
this: this,
html: JSX.Element
): ReturnType<this['send']> | Promise<ReturnType<this['send']>>;
html(this:this,html: JSX.Element): this | Promise<this>;

/**
* Sends a HTML stream to the client, fully supporting `@kitajs/html`
* `Suspense` components.
* Sends the html to the browser as a single stream, the entire component
* tree will be waited synchronously. When using any `Suspense`, its
* fallback will be synchronously waited and sent to the browser in the
* original stream, as its children are resolved, new pieces of html will be
* sent to the browser. When all `Suspense`s pending promises are resolved,
* the connection is closed normally.
*
* **You must use `request.id` as the `Suspense`'s `rid` parameter.**
* ### `request.id` must be used as the `Suspense`'s `rid` parameter
*
* This method hijacks the response, as the html stream is just a single
* continuous stream in the http body, you cannot add/change the status
* code or headers after calling this method.
* This method hijacks the response, as the html stream is just a single continuous
* stream in the http body, any changes to the status code or headers after
* calling this method **will not have effect**.
*
* If the HTML does not start with a doctype and `opts.autoDoctype` is
* enabled, it will be added automatically.
* If the HTML does not start with a doctype and `opts.autoDoctype` is enabled, it
* will be added automatically. The correct `Content-Type` header will also be defined.
*
* The correct `Content-Type` header will also be defined.
* **Http trailers are not yet supported when using `streamHtml`**
*
* @example
*
* ```tsx
* app.get('/', (req, reply) => {
* app.get('/', (req, reply) =>
* reply.streamHtml(
* <Suspense rid={req.id} fallback={<div>Loading...</div>}>
* <MyAsyncComponent />
* </Suspense>
* );
* });
* )
* );
* ```
*
* **Http trailers are not yet supported when using `streamHtml`**
*
* @param html The HTML to send.
* @returns The response.
*/
streamHtml(
this: this,
html: JSX.Element
): ReturnType<this['send']> | Promise<ReturnType<this['send']>>;
streamHtml(this:this,html: JSX.Element): this | Promise<this>;

/**
* This function is called internally by the `streamHtml` getter.
*
* ### Executing code before sending the response and after creating your
* html is a bad pattern and should be avoided!
*
* This function must be called **manually** at the top of the route handler
* when you have to execute some code **after** your root layout and
* **before** the `streamHtml call.
*
* If `setupHtmlStream` is executed and no call to `streamHtml` happens
* before the request finishes, a memory leak will be created. Make sure
* that `setupHtmlStream` will never be executed without being followed
* by `streamHtml`.
*
* @example
*
* ```tsx
* app.get('/bad', (_, reply) => {
* const html = <Layout /> // Error: Request data was deleted before all
* // suspense components were resolved.
*
* // code that must be executed after the template
* foo();
*
* return reply.streamHtml(html);
* })
*
* app.get('/good', (_, reply) => {
* reply.setupHtmlStream();
*
* const html = <Layout /> // works!
*
* // code that must be executed after the template
* foo();
*
* return reply.streamHtml(html);
* })
* ```
*/
setupHtmlStream(this:this): this;
}
}

Expand All @@ -79,26 +118,26 @@ type FastifyKitaHtmlPlugin = FastifyPluginCallback<
>;

declare namespace fastifyKitaHtml {
/**
* Options for @kitajs/fastify-html-plugin plugin.
*/
/** Options for @kitajs/fastify-html-plugin plugin. */
export interface FastifyKitaHtmlOptions {
/**
* The content-type of the response.
* The value of the `Content-Type` header.
*
* @default 'text/html; charset=utf8'
*/
contentType: string;

/**
* Whether to automatically detect HTML content and set the content-type.
* Whether to automatically detect HTML content and set the content-type
* when `.html()` is not used.
*
* @default true
*/
autoDetect: boolean;

/**
* Whether to automatically add `<!doctype html>` to a response starting with <html>, if not found.
* Whether to automatically add `<!doctype html>` to a response starting
* with <html>, if not found.
*
* ```tsx
* // With autoDoctype: true you can just return the html
Expand All @@ -113,9 +152,11 @@ declare namespace fastifyKitaHtml {
autoDoctype: boolean;

/**
* The function used to detect if a string is a html or not when `autoDetect`
* is enabled. Default implementation if length is greater than 3, starts
* with `<` and ends with `>`.
* The function used to detect if a string is a html or not when
* `autoDetect` is enabled.
*
* Default implementation if length is greater than 3, starts with `<` and
* ends with `>`.
*
* There's no real way to validate HTML, so this is a best guess.
*
Expand Down

0 comments on commit 87fc8a5

Please sign in to comment.