Streaming (SSE)
Server-Sent Events: a live leaderboard over plain HTTP.
What it is
Server-Sent Events keep one HTTP response open and write messages into it as things happen: lines of event:, data: and id:, with a blank line between messages (text/event-stream). The browser's EventSource reads them, and if the connection drops it reconnects by itself, sending the last id it saw as Last-Event-ID so the server can carry on from there. Simpler than WebSockets (one direction, ordinary HTTP, no extra protocol), and enough whenever only the server has news.
When to use it
- Live updates the server pushes: scores, notifications, build logs, progress bars
- Streaming AI responses token by token (most AI APIs use SSE)
- Not when the client sends a stream back too: that's a WebSocket's job
1. A live leaderboard
Leaderboard
Connect to load it.
What arrived
Connect, then post a score (or open this page in a second window and post from there): the board arrives again the moment the score is stored. Each stream lasts 25 seconds; watch the browser reconnect with Last-Event-ID and pick up where it left off. The game boards are live too: they change when someone finishes a run on the Motion lab.
A serverless function has no memory shared with the function that stored the score, so the stream polls the event log once a second for anything after its last id: the same event log the webhooks are sent from. Streams end after 25 seconds and this page stops listening after two minutes, because an open stream keeps a function running.
Show code · src/api/routes/scores.ts
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { and, asc, desc, eq, gt, lt, notInArray, or, sql } from "drizzle-orm";
import { streamSSE } from "hono/streaming";
import type { AuthEnv } from "../auth";
import { db } from "../db/client";
import { events, type ScoreRow, scores } from "../db/schema";
import { etagFor, isNotModified } from "../http";
import { idempotency } from "../idempotency";
import { Problem } from "../problem";
import { rateLimit } from "../ratelimit";
import { validationHook } from "../validation";
import { processDue, recordEvent } from "../webhooks";
// Leaderboards for the Motion lab's games (https://motion.djmiskey.com). The
// games post a score when a run ends; retries carry the same Idempotency-Key,
// so a flaky connection can't put one run on the board twice.
export const GAMES = {
toppler: { name: "Tower Toppler", unit: "points", better: "higher", max: 20_000 },
marble: { name: "Marble Maze", unit: "ms", better: "lower", max: 3_600_000 }, // all three levels, in milliseconds
stacker: { name: "Stacker", unit: "slabs", better: "higher", max: 500 },
runner: { name: "Starfield Runner", unit: "points", better: "higher", max: 1_000_000 },
// For this lab's own pages, so trying the demos doesn't touch the games' boards.
demo: { name: "Lab demo", unit: "points", better: "higher", max: 1_000_000 },
} as const;
export type Game = keyof typeof GAMES;
const GAME_IDS = Object.keys(GAMES) as [Game, ...Game[]];
const KEEP_PER_GAME = 100;
const STREAM_SECONDS = Number(process.env.LAB_SSE_SECONDS ?? 25);
// A score can't be checked: the game runs in the visitor's browser, so anyone
// can post any number. The limits below only keep the board plausible; a
// game where cheating matters would run (or replay) the game on the server.
const NewScore = z
.strictObject({
game: z.enum(GAME_IDS),
initials: z.string().regex(/^[A-Z]{3}$/, "Three capital letters, A–Z"),
value: z.number().int().min(0),
})
.superRefine((s, ctx) => {
const game = GAMES[s.game];
if (game && s.value > game.max) ctx.addIssue({ code: "custom", path: ["value"], message: `At most ${game.max} for ${game.name}` });
})
.openapi("NewScore", { example: { game: "runner", initials: "ACE", value: 1234 } });
const Entry = z.object({ rank: z.number().int(), id: z.uuid(), initials: z.string(), value: z.number().int(), createdAt: z.iso.datetime() }).openapi("ScoreEntry");
const Board = z
.object({ game: z.enum(GAME_IDS), name: z.string(), unit: z.string(), better: z.enum(["higher", "lower"]), entries: z.array(Entry) })
.openapi("Leaderboard");
const Created = z.object({ id: z.uuid(), game: z.enum(GAME_IDS), initials: z.string(), value: z.number().int(), rank: z.number().int(), createdAt: z.iso.datetime() }).openapi("Score");
const GameQuery = z.object({
game: z.enum(GAME_IDS).openapi({ description: "toppler, marble, stacker, runner, or demo (this lab's pages)" }),
limit: z.coerce.number().int().min(1).max(50).default(10),
});
const json = <T extends z.ZodType>(schema: T, description: string) => ({ content: { "application/json": { schema } }, description });
const problemResponse = (description: string) => ({ content: { "application/problem+json": { schema: Problem } }, description });
// Best first. Ties go to whoever got there first.
const order = (game: Game) => GAMES[game].better === "higher" ? [desc(scores.value), asc(scores.createdAt)] : [asc(scores.value), asc(scores.createdAt)];
export async function board(game: Game, limit = 10): Promise<z.infer<typeof Board>> {
const rows = await (await db()).select().from(scores).where(eq(scores.game, game)).orderBy(...order(game)).limit(limit);
const { name, unit, better } = GAMES[game];
return { game, name, unit, better, entries: rows.map((r, i) => ({ rank: i + 1, id: r.id, initials: r.initials, value: r.value, createdAt: r.createdAt.toISOString() })) };
}
/** 1 + how many scores beat this one (better value, or the same value earlier). */
async function rankOf(row: ScoreRow) {
const game = row.game as Game;
const better = GAMES[game].better === "higher" ? gt(scores.value, row.value) : lt(scores.value, row.value);
const [{ n }] = await (await db()).select({ n: sql<number>`count(*)::int` }).from(scores)
.where(and(eq(scores.game, game), or(better, and(eq(scores.value, row.value), lt(scores.createdAt, row.createdAt)))));
return Number(n) + 1;
}
export const scoresApi = new OpenAPIHono<AuthEnv>({ defaultHook: validationHook });
scoresApi.openapi(
createRoute({
method: "get", path: "/scores", tags: ["Leaderboards"], summary: "A game's leaderboard, best first",
request: { query: GameQuery },
responses: {
200: { ...json(Board, "The top scores"), headers: z.object({ ETag: z.string() }) },
304: { description: "Not modified since the ETag you sent" },
422: problemResponse("Unknown game or bad limit"),
},
}),
async (c) => {
const { game, limit } = c.req.valid("query");
const body = await board(game, limit);
const etag = await etagFor(body);
c.header("ETag", etag);
c.header("Cache-Control", "no-cache");
return isNotModified(c, etag) ? c.body(null, 304) : c.json(body, 200);
},
);
scoresApi.openapi(
createRoute({
method: "post", path: "/scores", tags: ["Leaderboards"],
summary: "Submit a score (Idempotency-Key required: retries with the same key are replayed, not counted again)",
middleware: [
rateLimit({ name: "scores", windowSec: 60, limit: () => Number(process.env.LAB_SCORE_LIMIT ?? 10) }),
idempotency({ required: true }),
] as const,
request: {
headers: z.object({ "idempotency-key": z.string().optional().openapi({ description: "A unique value per score, e.g. a UUID. Required", example: '"5d1f6c2a-7f4e-4a59-9b8e-0d6c3a2b1e7f"' }) }),
body: { content: { "application/json": { schema: NewScore } }, required: true },
},
responses: {
201: { ...json(Created, "On the board, with its rank. A replay has Idempotent-Replayed: true") },
400: problemResponse("No Idempotency-Key, or the body isn't JSON"),
409: problemResponse("The first request with this key is still running"),
422: problemResponse("Invalid score, or the key was used for a different score"),
429: problemResponse("More than 10 scores a minute"),
},
}),
async (c) => {
const input = c.req.valid("json");
const d = await db();
const [row] = await d.insert(scores).values({ ...input, createdAt: new Date() }).returning();
const rank = await rankOf(row);
// Only the best 100 per game are kept.
const keep = d.select({ id: scores.id }).from(scores).where(eq(scores.game, input.game)).orderBy(...order(input.game)).limit(KEEP_PER_GAME);
await d.delete(scores).where(and(eq(scores.game, input.game), notInArray(scores.id, keep)));
const created = { id: row.id, game: input.game, initials: row.initials, value: row.value, rank, createdAt: row.createdAt.toISOString() };
// Tell the subscribers: the event is stored first, then each webhook gets
// its first attempt now; failures are retried later.
const { event } = await recordEvent("score.submitted", created);
await processDue({ eventId: event.id });
return c.json(created, 201);
},
);
scoresApi.openapi(
createRoute({
method: "get", path: "/scores/stream", tags: ["Leaderboards"],
summary: "Server-Sent Events: the leaderboard, again each time it changes",
request: {
query: GameQuery,
headers: z.object({ "last-event-id": z.string().optional().openapi({ description: "Sent by EventSource on reconnect: resume after this event" }) }),
},
responses: {
200: { content: { "text/event-stream": { schema: z.string() } }, description: "event: board, data: a Leaderboard, id: the event it's up to date with" },
422: problemResponse("Unknown game"),
},
}),
(c) => {
const { game, limit } = c.req.valid("query");
const resumeFrom = Number(c.req.header("last-event-id"));
return streamSSE(c, async (stream) => {
const d = await db();
let cursor: number;
if (Number.isSafeInteger(resumeFrom) && resumeFrom > 0) {
// A reconnect: only what's new since the last event the client saw.
cursor = resumeFrom;
await stream.write("retry: 3000\n\n");
} else {
// A new client: the board as it is now, tagged with the latest event id.
const [{ max }] = await d.select({ max: sql<number>`coalesce(max(${events.id}), 0)` }).from(events);
cursor = Number(max);
await stream.writeSSE({ event: "board", id: String(cursor), data: JSON.stringify(await board(game, limit)), retry: 3000 });
}
// Serverless functions have no shared memory to publish through, so the
// stream polls the event log once a second. After STREAM_SECONDS it ends;
// EventSource reconnects by itself (after `retry` ms) with Last-Event-ID,
// and continues where this one stopped.
const end = Date.now() + STREAM_SECONDS * 1000;
let quietSince = Date.now();
while (!stream.aborted && Date.now() < end) {
const fresh = await d.select().from(events).where(gt(events.id, cursor)).orderBy(asc(events.id)).limit(100);
if (fresh.length) {
cursor = fresh.at(-1)!.id;
const mine = fresh.filter((e) => e.type === "score.submitted" && e.data.game === game);
if (mine.length) {
await stream.writeSSE({ event: "board", id: String(cursor), data: JSON.stringify({ ...(await board(game, limit)), latest: mine.at(-1)!.data }) });
quietSince = Date.now();
}
}
// A comment line now and then keeps proxies from closing an idle stream.
if (Date.now() - quietSince > 10_000) {
await stream.write(": keep-alive\n\n");
quietSince = Date.now();
}
await stream.sleep(1000);
}
});
},
);