Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: HTML streaming in Podium #507

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Conversation

digitalsadhu
Copy link
Member

@digitalsadhu digitalsadhu commented Sep 25, 2024

This PR is a first attempt at an API for HTML streaming. This is made possible by the early hints work which makes it possible to get assets from podlets before the rest of the podlet content.

podium-streaming.mov

The whole approach is non breaking with the caveat that in order to use the streaming feature, all podlets would need to be updated to the latest version of the podlet module (no code changes)

Other comments thoughts inline..

this.#view(incoming, responseStream, ...args);
// return the stream for the developer to work with
return responseStream;
};
Copy link
Member Author

@digitalsadhu digitalsadhu Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with a new function on res here. res.podiumStream which can be passed additional arguments for html template and returns a stream object to work with after setting up streaming internally. Worth noting that there will need to be a slightly different implementation for Fastify than Express here which is a good argument for implementing this as a function along side res.podiumSend I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with this approach, we can stay non breaking and any page that wants to implement streaming just needs to ensure that all podlets on the page are updated to a a version of @podium/podlet that supports early hints.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existing templates continue to work as before since we just inject a token and use it to split the template into 2 pieces. We then immediately send the first (header) and hand control to the developer to do streaming. Once the developer is finished and hands control back, we send the footer and terminate the stream.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that there will need to be a slightly different implementation for Fastify than Express

The difference are under the hood or also in the public API surface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference would be in how we pipe the streams in the fastify plugin, api surface should be the same for the end user

end() {
setTimeout(() => this.push(null), 0);
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Readable stream class probably belongs in the Podium utils package. It provides a .send() method to push chunks of html onto the response, a .done() method to be used by the developer to signal when they are finished pushing content, as well as a .end() method to indicate that the app has no more html to push and closes the stream.

Essentially, html template uses .send() to push the document head, then the application developer uses .send() to push the content and calls .done() when they are finished at which point HTML template takes over again and uses .send() to push the final bits of the template and ends the stream with .end()

lib/template.js Outdated
body.send(documentTrailers);
body.end();
});
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much difference here between this and the original built in html template. We check if body is a response stream instead of a string and if it is, we set up streaming, otherwise we do the same as before and just spit out a template

const contentFetch = content.fetch(incoming);
const footerFetch = footer.fetch(incoming);

incoming.hints.on('complete', async ({ js, css }) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we have hints in from the podlets, we can set up streaming

incoming.hints.on('complete', async ({ js, css }) => {
// set the assets on httpincoming so that they are available in the document template
incoming.js = [...incoming.js, ...js];
incoming.css = [...incoming.css, ...css];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty awkward but works for now at least

</div>
</div>
</template>
`);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case we are sending a DSD template with slots and placeholder skeleton html so that we can replace these by streaming in podlet content later.


// close out the dom and the stream
await Promise.all([headerFetch, menuFetch, contentFetch, footerFetch]);
stream.done();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indicate that we are finished streaming content so that the html template can finish things off

// stream in podlet content when available...
headerFetch.then((content) => {
stream.send(`<div slot="header">${content}</div>`);
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we decorate the podlet content in divs with slot attributes to indicate where they should be placed into the document.

@digitalsadhu digitalsadhu changed the title feat: html streaming RFC: HTML streaming in Podium Sep 25, 2024
Copy link
Contributor

@wkillerud wkillerud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very cool 🎉


// call our document template, injecting a token we can use to split the template
// into header and footer
const splitToken = `<!--split-token-->`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a tough time visualizing how this split would affect loading. Is this only here to handle the server-simple case of shipping off <head> and <body> in two chunks?

Copy link
Member Author

@digitalsadhu digitalsadhu Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We split the template on the position that would normally have the body injected by the developers so that we are left with 2 pieces that we are able to deliver. The first bit goes immediately, then we allow the user to stream whatever they want, and then we deliver the second bit at the end. In practice this looks like...

We deliver this right away..

<html>
<head>
...
</head>
<body>

Then the developer does there thing and streams the content they want in the body... podlets etc

<main>
... podlets etc...
</main>

Then we deliver the second split part of the template which is the document end...

... some scripts etc...
</body>
</html>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nice thing about using the split technique is that all existing templates should just work as they are so its nice and backwards compatible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my main concern about shipping a "finished" <head> is that podlets' early hints might give us styles and scripts that should be up there. I suppose that's a design constraint with streaming you have to deal with as an app developer, that your stuff can come in at any time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we wait for all assets before shipping the head by using incoming.hints.on('complete')

@digitalsadhu
Copy link
Member Author

Looked into browser and server support and the need for fallbacks etc. As I understand it, we are good because:

  1. HTTP 1.1 onwards supports streams (HTTP 2 is more efficient)
  2. If a browser or server didn't support streaming, content would just be buffered and displayed at once
  3. Early hints work between podlet and layout regardless because we control the client (Undici) which supports early hints
  4. Early hints won't work between the layout and the browser for older browsers or servers not using HTTP 2
  5. Again, we dont need a fallback for early hints between layout and browser, all it will mean is a perf degradation.

@trygve-lie
Copy link
Contributor

This is great stuff!

On the general side; are the loading sequential or are they out of order? In the video they look sequential. Getting it rendered out of order is a bit different problem I think.

@digitalsadhu
Copy link
Member Author

This is great stuff!

On the general side; are the loading sequential or are they out of order? In the video they look sequential. Getting it rendered out of order is a bit different problem I think.

You have full control, in my demo I just did sequential setTimeouts to show the delay. You can have it all come in when it comes in or you can use Promise.all to wait for all podlets and send them out at once or you could do the header first and the rest after or however you like.

Base automatically changed from next to main November 6, 2024 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants