07. streaming clock (never-ending response)
A single HTTP response that stays open forever. Every second the server flushes a fresh <template for="clock">. The marker is re-emitted inside each template so the next tick has somewhere to land. A CSS rule hides every <span> except the last, so the clock updates in place. Zero client JS.
Open DevTools' Network panel and look at this response. The status will stay "pending" forever (or until you close the tab); each tick is a chunk on the wire.
Inspired by Phil Hawksworth's legendary Netlify demo that deployed the site once a second to prove the build pipeline could keep up. Here it is the HTTP response itself that stays open, not the build pipeline.
learn more
- Chrome blog: Declarative Partial Updates
- WICG explainer: patching — Why re-emitting
<?start>markers lets you patch the same hole repeatedly. - template-for polyfill
source
Everything the server runs for this example. Each file links to GitHub so you can copy or fork it.
handler.ts/app/src/examples/07-streaming-clock/handler.ts
import { sleep, streamingResponse } from "../../lib/streaming.ts";
import { loadShell, tryStatic } from "../../lib/files.ts";
const SHELL = loadShell(import.meta.url);
export default function handle(_req: Request, path: string): Response | Promise<Response> {
if (path === "/" || path === "/index.html") {
return streamingResponse(async (write) => {
await write(SHELL);
// Never-ending loop. When the client closes the tab, the next write()
// throws StreamAborted and the loop exits silently.
while (true) {
const time = new Date().toLocaleTimeString();
await write(
`<template for="clock">\n <?start name="clock"><span>${time}</span><?end>\n</template>\n`,
);
await sleep(1000);
}
});
}
return tryStatic(import.meta.url, path) ?? new Response("Not found", { status: 404 });
}
index.html/app/src/examples/07-streaming-clock/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>07 · streaming clock</title>
<link rel="stylesheet" href="/public/styles.css">
<link rel="stylesheet" href="/07/styles.css">
</head>
<body>
<main>
<p class="crumbs"><a href="/">← back to index</a></p>
<h1>07. streaming clock (never-ending response)</h1>
<p class="lede">A single HTTP response that stays open forever. Every second the server flushes a fresh <code><template for="clock"></code>. The marker is re-emitted inside each template so the next tick has somewhere to land. A CSS rule hides every <code><span></code> except the last, so the clock updates in place. Zero client JS.</p>
<div class="clock-face">
<section>
<?start name="clock"><span>loading...</span><?end>
</section>
</div>
<p style="color:var(--muted);font-size:.85rem;">Open DevTools' Network panel and look at this response. The status will stay "pending" forever (or until you close the tab); each tick is a chunk on the wire.</p>
<p style="color:var(--muted);font-size:.85rem;">Inspired by <a href="https://philhawksworth.dev/" target="_blank" rel="noopener">Phil Hawksworth</a>'s legendary Netlify demo that deployed the site once a second to prove the build pipeline could keep up. Here it is the HTTP response itself that stays open, not the build pipeline.</p>
<section class="refs">
<h3>learn more</h3>
<ul>
<li><a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" target="_blank" rel="noopener">Chrome blog: Declarative Partial Updates</a></li>
<li><a href="https://github.com/WICG/declarative-partial-updates/blob/main/patching-explainer.md" target="_blank" rel="noopener">WICG explainer: patching</a> <span class="note">— Why re-emitting <code><?start></code> markers lets you patch the same hole repeatedly.</span></li>
<li><a href="https://github.com/GoogleChromeLabs/template-for-polyfill" target="_blank" rel="noopener">template-for polyfill</a></li>
</ul>
</section>
<!-- source-viewer goes here -->
<footer class="byline">made by <a href="https://paul.kinlan.me/" target="_blank" rel="noopener">Paul Kinlan</a></footer>
</main>
styles.css/app/src/examples/07-streaming-clock/styles.css
.clock-face {
font-size: 3.5rem;
font-weight: 700;
text-align: center;
padding: 2.5rem 1rem;
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
margin: 1.5rem 0;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
/* The connection stays open. Every tick appends a fresh <span>.
We hide all spans and only show the most recent one. */
.clock-face section span { display: none; }
.clock-face section span:last-of-type { display: inline; color: var(--accent); }
lib/streaming.ts/app/src/lib/streaming.ts
export class StreamAborted extends Error {
constructor() {
super("stream aborted by client");
this.name = "StreamAborted";
}
}
export type ChunkSource = (write: (chunk: string) => Promise<void>) => Promise<void>;
const encoder = new TextEncoder();
export function streamingResponse(
source: ChunkSource,
contentType = "text/html; charset=utf-8",
): Response {
let aborted = false;
const body = new ReadableStream<Uint8Array>({
async start(controller) {
const write = async (chunk: string) => {
if (aborted) throw new StreamAborted();
try {
controller.enqueue(encoder.encode(chunk));
} catch {
aborted = true;
throw new StreamAborted();
}
};
try {
await source(write);
} catch (err) {
if (!(err instanceof StreamAborted)) console.error("stream source error", err);
} finally {
if (!aborted) {
try {
controller.close();
} catch {
// already closed
}
}
}
},
cancel() {
aborted = true;
},
});
return new Response(body, {
headers: {
"content-type": contentType,
"cache-control": "no-store",
"x-content-type-options": "nosniff",
},
});
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
lib/files.ts/app/src/lib/files.ts
// File helpers for examples: read sibling files, render the page shell with the
// source viewer injected, and serve sibling static assets (CSS/JS/HTML) on demand.
import { sourceBlock } from "./source.ts";
const MIME: Record<string, string> = {
css: "text/css; charset=utf-8",
js: "application/javascript; charset=utf-8",
html: "text/html; charset=utf-8",
svg: "image/svg+xml",
json: "application/json; charset=utf-8",
};
function dirFor(handlerUrl: string): string {
return new URL(".", handlerUrl).pathname;
}
export function readSibling(handlerUrl: string, name: string): string {
return Deno.readTextFileSync(dirFor(handlerUrl) + name);
}
// Reads a sibling HTML file. The marker `<!-- source-viewer goes here -->` is replaced
// with the source-viewer block. Extra named placeholders ({{name}}) can be supplied via `vars`.
const SOURCE_MARKER = "<!-- source-viewer goes here -->";
export function loadShell(
handlerUrl: string,
name = "index.html",
vars: Record<string, string> = {},
): string {
let raw = readSibling(handlerUrl, name);
raw = raw.replace(SOURCE_MARKER, sourceBlock(handlerUrl));
for (const [k, v] of Object.entries(vars)) {
raw = raw.replaceAll(`{{${k}}}`, v);
}
return raw;
}
// Try to serve a sibling static file from the example folder. Returns null when the
// file is missing, the path escapes the folder, or the path is empty.
export function tryStatic(handlerUrl: string, requestPath: string): Response | null {
const safe = requestPath.replace(/^\/+/, "");
if (!safe || safe.includes("..")) return null;
// Never let the browser fetch handler.ts or README.md through this fallback.
if (safe === "handler.ts" || safe === "README.md") return null;
try {
const content = Deno.readFileSync(dirFor(handlerUrl) + safe);
const ext = safe.split(".").pop() ?? "";
return new Response(content, {
headers: {
"content-type": MIME[ext] ?? "application/octet-stream",
"cache-control": "no-store",
},
});
} catch {
return null;
}
}