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
45 changes: 45 additions & 0 deletions example/streaming/podlets/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'content',
version: Date.now().toString(),
pathname: '/',
});

podlet.css({ value: 'http://localhost:6103/css' });

const app = express();

app.use(podlet.middleware());

app.get('/manifest.json', (req, res) => {
res.send(podlet);
});

app.get('/css', (req, res) => {
res.set('Content-Type', 'text/css');
res.send(`
.content {
border: 1px solid black;
border-radius: 5px;
width: 100%;
padding: 20px;
margin: 0;
margin-bottom: 20px;
box-sizing: border-box;
}
`);
});

app.get('/', (req, res) => {
res.send(`
<section class="content">
main content goes here
</section>
`);
});

app.listen(6103, () => {
console.log(`content podlet server running at http://localhost:6103`);
});
45 changes: 45 additions & 0 deletions example/streaming/podlets/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'footer',
version: Date.now().toString(),
pathname: '/',
});

podlet.css({ value: 'http://localhost:6104/css' });

const app = express();

app.use(podlet.middleware());

app.get('/manifest.json', (req, res) => {
res.send(podlet);
});

app.get('/css', (req, res) => {
res.set('Content-Type', 'text/css');
res.send(`
footer {
border: 1px solid black;
border-radius: 5px;
width: 100%;
padding: 20px;
margin: 0;
margin-bottom: 20px;
box-sizing: border-box;
}
`);
});

app.get('/', (req, res) => {
res.send(`
<footer>
footer content
</footer>
`);
});

app.listen(6104, () => {
console.log(`footer podlet server running at http://localhost:6104`);
});
50 changes: 50 additions & 0 deletions example/streaming/podlets/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'header',
version: Date.now().toString(),
pathname: '/',
});

podlet.css({ value: 'http://localhost:6101/css' });

const app = express();

app.use(podlet.middleware());

app.get('/manifest.json', (req, res) => {
res.send(podlet);
});

app.get('/css', (req, res) => {
res.set('Content-Type', 'text/css');
res.send(`
header {
border: 1px solid black;
border-radius: 5px;
width: 100%;
padding: 20px;
margin: 0;
margin-bottom: 20px;
box-sizing: border-box;
}
header h1 {
text-align: center;
margin: 0;
padding: 0;
}
`);
});

app.get('/', (req, res) => {
res.send(`
<header>
<h1>Header</h1>
</header>
`);
});

app.listen(6101, () => {
console.log(`header podlet server running at http://localhost:6101`);
});
62 changes: 62 additions & 0 deletions example/streaming/podlets/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'menu',
version: Date.now().toString(),
pathname: '/',
});

podlet.css({ value: 'http://localhost:6102/css' });

const app = express();

app.use(podlet.middleware());

app.get('/manifest.json', (req, res) => {
res.send(podlet);
});

app.get('/css', (req, res) => {
res.set('Content-Type', 'text/css');
res.send(`
menu {
border: 1px solid black;
border-radius: 5px;
width: 100%;
padding: 10px;
margin: 0;
margin-bottom: 20px;
box-sizing: border-box;
}
menu ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
justify-content: space-evenly;
align-items: center;
}
menu ul li {
margin: 0;
padding: 0;
}
`);
});

app.get('/', (req, res) => {
res.send(`
<menu>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/things">Things</a></li>
<li><a href="/stuff">Stuff</a></li>
</ul>
</menu>
`);
});

app.listen(6102, () => {
console.log(`menu podlet server running at http://localhost:6102`);
});
165 changes: 165 additions & 0 deletions example/streaming/server-advanced.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import express from 'express';
import Layout from '../../lib/layout.js';

const layout = new Layout({
pathname: '/foo',
logger: console,
name: 'demo',
});

const content = layout.client.register({
name: 'content',
uri: 'http://localhost:6103/manifest.json',
});

const header = layout.client.register({
name: 'header',
uri: 'http://localhost:6101/manifest.json',
});

const menu = layout.client.register({
name: 'menu',
uri: 'http://localhost:6102/manifest.json',
});

const footer = layout.client.register({
name: 'footer',
uri: 'http://localhost:6104/manifest.json',
});

layout.css({ value: '/css' });

const app = express();

app.use(layout.pathname(), layout.middleware());

app.get('/foo/css', (req, res) => {
res.set('Content-Type', 'text/css');
res.send(`
@keyframes pulse {
0% {
background-color: #e0e0e0;
}
50% {
background-color: #f0f0f0;
}
100% {
background-color: #e0e0e0;
}
}
.skeleton {
width: 100%;
background-color: #e0e0e0;
border-radius: 5px;
animation: pulse 1.5s infinite ease-in-out;
margin: 0;
margin-bottom: 20px;
box-sizing: border-box;
}
.skeleton.header {
height:79px;
}
.skeleton.menu {
height:40px;
}
.skeleton.content {
height:60px;
}
.skeleton.footer {
height:60px;
}
`);
});

app.get(layout.pathname(), async (req, res) => {
const incoming = res.locals.podium;

incoming.view = {
title: 'Example streaming application',
};

const headerFetch = header.fetch(incoming);
const menuFetch = menu.fetch(incoming);
const contentFetch = content.fetch(incoming);
const footerFetch = footer.fetch(incoming);

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];

// set up the stream which will send the document template head
const stream = res.podiumStream();

// stream in the document body with slot placeholders for podlets
stream.send(`
<template shadowrootmode="open">
<link href="/foo/css" type="text/css" rel="stylesheet">
<div class="container">
<div>
<div>
<slot name="header"><div class="skeleton header"></div></slot>
</div>
</div>
<div>
<div>
<slot name="menu"><div class="skeleton menu"></div></slot>
</div>
<div>
<slot name="content"><div class="skeleton content"></div></slot>
</div>
</div>
<div>
<div>
<slot name="footer"><div class="skeleton footer"></div></slot>
</div>
</div>
</div>
</template>
`);

// fake 1 second delay
await new Promise((res) => setTimeout(res, 1000));

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

await new Promise((res) => setTimeout(res, 1000));

menuFetch.then((content) => {
stream.send(`<div slot="menu">${content}</div>`);
});

await new Promise((res) => setTimeout(res, 1000));

contentFetch.then((content) => {
stream.send(`<div slot="content">${content}</div>`);
});

await new Promise((res) => setTimeout(res, 1000));

footerFetch.then((content) => {
stream.send(`<div slot="footer">${content}</div>`);
});

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

app.use(`${layout.pathname()}/assets`, express.static('assets'));

// eslint-disable-next-line no-unused-vars
app.use((error, req, res, next) => {
console.error(error);
res.status(500).send(
'<html><body><h1>Internal server error</h1></body></html>',
);
});

app.listen(6123, () => {
console.log(`layout server running at http://localhost:6123`);
});
Loading