├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src ├── server.js ├── templates └── list.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .wrangler/ 3 | wrangler.toml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Out of order streaming without JavaScript 2 | 3 | A technique for streaming HTML out of order without JavaScript. [See the blog post](https://lamplightdev.com/blog/2024/01/10/streaming-html-out-of-order-without-javascript/) for more information. 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ooo", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@hono/node-server": "^1.4.0", 9 | "hono": "^3.12.0", 10 | "swtl": "^0.0.22" 11 | }, 12 | "engines": { 13 | "node": ">=18" 14 | } 15 | }, 16 | "node_modules/@hono/node-server": { 17 | "version": "1.4.0", 18 | "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.4.0.tgz", 19 | "integrity": "sha512-bhDkhldW7w9VgjrX0gG1vJ2YyvTxFWd5WG9nHjSR4UauhVECQZC3qy7mVVuQ054I5NWhKttHfKzYfoPzmUzAjw==", 20 | "engines": { 21 | "node": ">=18.14.1" 22 | } 23 | }, 24 | "node_modules/hono": { 25 | "version": "3.12.0", 26 | "resolved": "https://registry.npmjs.org/hono/-/hono-3.12.0.tgz", 27 | "integrity": "sha512-UPEtZuLY7Wo7g0mqKWSOjLFdT8t7wJ60IYEcxKl3AQNU4u+R2QqU2fJMPmSu24C+/ag20Z8mOTQOErZzK4DMvA==", 28 | "engines": { 29 | "node": ">=16.0.0" 30 | } 31 | }, 32 | "node_modules/swtl": { 33 | "version": "0.0.22", 34 | "resolved": "https://registry.npmjs.org/swtl/-/swtl-0.0.22.tgz", 35 | "integrity": "sha512-dw2QKaDCCx5yp1+UhDNi4OhwaYfgSCmrK26ckD9PeXn5tsES11+ETMgYX0d2RzTggxFAlvmTaPisrD188P2FOw==" 36 | } 37 | }, 38 | "dependencies": { 39 | "@hono/node-server": { 40 | "version": "1.4.0", 41 | "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.4.0.tgz", 42 | "integrity": "sha512-bhDkhldW7w9VgjrX0gG1vJ2YyvTxFWd5WG9nHjSR4UauhVECQZC3qy7mVVuQ054I5NWhKttHfKzYfoPzmUzAjw==" 43 | }, 44 | "hono": { 45 | "version": "3.12.0", 46 | "resolved": "https://registry.npmjs.org/hono/-/hono-3.12.0.tgz", 47 | "integrity": "sha512-UPEtZuLY7Wo7g0mqKWSOjLFdT8t7wJ60IYEcxKl3AQNU4u+R2QqU2fJMPmSu24C+/ag20Z8mOTQOErZzK4DMvA==" 48 | }, 49 | "swtl": { 50 | "version": "0.0.22", 51 | "resolved": "https://registry.npmjs.org/swtl/-/swtl-0.0.22.tgz", 52 | "integrity": "sha512-dw2QKaDCCx5yp1+UhDNi4OhwaYfgSCmrK26ckD9PeXn5tsES11+ETMgYX0d2RzTggxFAlvmTaPisrD188P2FOw==" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">=18" 4 | }, 5 | "type": "module", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "@hono/node-server": "^1.4.0", 11 | "hono": "^3.12.0", 12 | "swtl": "^0.0.22" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { stream } from 'hono/streaming'; 3 | import { render } from 'swtl'; 4 | 5 | import { createReadableStreamFromAsyncGenerator } from './utils.js'; 6 | import { template as listTemplate } from './templates/list.js'; 7 | 8 | const app = new Hono(); 9 | 10 | app.get('/', (ctx) => { 11 | return stream(ctx, async (stream) => { 12 | ctx.res.headers.set('Content-Type', 'text/html'); 13 | await stream.pipe( 14 | createReadableStreamFromAsyncGenerator(render(listTemplate())) 15 | ); 16 | }); 17 | }); 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /src/templates/list.js: -------------------------------------------------------------------------------- 1 | import { html } from 'swtl'; 2 | import { 3 | renderInResolvedOrder, 4 | delayed, 5 | globalStyles, 6 | appStyles, 7 | } from '../utils.js'; 8 | 9 | export const template = () => html` 10 | 11 | 12 | 13 | 14 | OOO 15 | ${globalStyles()} 16 | 17 | 18 |
19 | 40 | 41 | ${delayed( 42 | 1000, 43 | html`
44 | 53 | ${renderInResolvedOrder( 54 | [5, 9, 2, 1, 4, 3, 8, 6, 7, 0].map((num, index) => { 55 | return delayed( 56 | 1000 * (index + 2), 57 | html` Item number ${num} ` 58 | ); 59 | }) 60 | )} 61 |
` 62 | )} 63 |
64 | 65 | `; 66 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { render, html } from 'swtl'; 2 | 3 | export const globalStyles = () => html` 4 | 20 | `; 21 | 22 | export const appStyles = () => html` 23 | 59 | `; 60 | 61 | const encoder = new TextEncoder(); 62 | 63 | export function createReadableStreamFromAsyncGenerator(output) { 64 | return new ReadableStream({ 65 | async start(controller) { 66 | while (true) { 67 | const { done, value } = await output.next(); 68 | 69 | if (done) { 70 | controller.close(); 71 | break; 72 | } 73 | 74 | controller.enqueue(encoder.encode(value)); 75 | } 76 | }, 77 | }); 78 | } 79 | 80 | export const delayed = (ms, data) => 81 | new Promise((resolve) => { 82 | setTimeout(() => { 83 | resolve(data); 84 | }, ms); 85 | }); 86 | 87 | export async function* renderInResolvedOrder(promises) { 88 | let promisesWithIndexes = promises.map((promise, index) => { 89 | return { 90 | index, 91 | promise: promise.then((value) => { 92 | return { index, value }; 93 | }), 94 | }; 95 | }); 96 | 97 | while (promisesWithIndexes.length > 0) { 98 | const result = await Promise.race( 99 | promisesWithIndexes.map((p) => p.promise) 100 | ); 101 | yield* render(result.value); 102 | 103 | promisesWithIndexes = promisesWithIndexes.filter((promise) => { 104 | return promise.index !== result.index; 105 | }); 106 | } 107 | } 108 | --------------------------------------------------------------------------------