-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: main
Are you sure you want to change the base?
Conversation
this.#view(incoming, responseStream, ...args); | ||
// return the stream for the developer to work with | ||
return responseStream; | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); | ||
} | ||
} |
There was a problem hiding this comment.
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(); | ||
}); | ||
}; |
There was a problem hiding this comment.
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
example/streaming/server.js
Outdated
const contentFetch = content.fetch(incoming); | ||
const footerFetch = footer.fetch(incoming); | ||
|
||
incoming.hints.on('complete', async ({ js, css }) => { |
There was a problem hiding this comment.
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
example/streaming/server.js
Outdated
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]; |
There was a problem hiding this comment.
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
example/streaming/server.js
Outdated
</div> | ||
</div> | ||
</template> | ||
`); |
There was a problem hiding this comment.
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.
example/streaming/server.js
Outdated
|
||
// close out the dom and the stream | ||
await Promise.all([headerFetch, menuFetch, contentFetch, footerFetch]); | ||
stream.done(); |
There was a problem hiding this comment.
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
example/streaming/server.js
Outdated
// stream in podlet content when available... | ||
headerFetch.then((content) => { | ||
stream.send(`<div slot="header">${content}</div>`); | ||
}); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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-->`; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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')
Looked into browser and server support and the need for fallbacks etc. As I understand it, we are good because:
|
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. |
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..