Stream server-sent events

View as Markdown

This tutorial builds a server-sent events (SSE) endpoint on the JavaScript lane and consumes it from a browser page with EventSource. You will stream a sequence of events over one long-lived HTTP response and see them arrive incrementally in the client. It takes about fifteen minutes.

Afterburner streams response bodies natively, so SSE works without special configuration. Long-lived duplex sockets are not the model; SSE plus POST covers the push cases. See Streaming and realtime for the full picture.

Prerequisites

  • The hyjal CLI installed and logged in.
  • Node.js installed locally, for the JS lane build.
  • A Project. This tutorial uses acme with an api Resource.

1. Write the SSE handler

The JavaScript lane contract is a fetch handler: export default { fetch(request) }. Return a Response whose body is a ReadableStream and whose Content-Type is text/event-stream. Each SSE message is a data: line terminated by a blank line.

1// src/index.js
2export default {
3 async fetch(request) {
4 const url = new URL(request.url);
5 if (url.pathname !== "/events") {
6 return new Response("not found", { status: 404 });
7 }
8
9 const encoder = new TextEncoder();
10 const stream = new ReadableStream({
11 start(controller) {
12 let n = 0;
13 const send = () => {
14 const payload = JSON.stringify({ n, at: Date.now() });
15 controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
16 n += 1;
17 if (n >= 5) {
18 controller.close();
19 return;
20 }
21 setTimeout(send, 1000);
22 };
23 send();
24 },
25 });
26
27 return new Response(stream, {
28 headers: {
29 "content-type": "text/event-stream",
30 "cache-control": "no-cache",
31 },
32 });
33 },
34};

This handler emits five events, one per second, then closes the stream. Keep each streamed run within the Resource’s run limit (runLimitSecs, default 30 seconds).

2. Configure the Resource

Set the api Resource to the JS lane and mark it public so the browser can reach the endpoint.

1[project]
2name = "acme"
3
4[resource.api]
5lane = "js"
6public = true

3. Test locally with hyjal dev

Run the local dev server. It serves your component under the same wasi:http contract as production, with hot rebuild and request logging in the terminal.

$$ hyjal dev --port 3000
$Building resource api (lane: js)...
$ componentize-js
$Serving acme/api on http://localhost:3000
$Watching for changes...

In another terminal, stream the endpoint with curl. The -N flag disables buffering so events print as they arrive.

$$ curl -N http://localhost:3000/events
$data: {"n":0,"at":1752412920000}
$
$data: {"n":1,"at":1752412921000}
$
$data: {"n":2,"at":1752412922000}
$
$data: {"n":3,"at":1752412923000}
$
$data: {"n":4,"at":1752412924000}

The one-second gap between lines confirms the response is streaming, not buffered and flushed at the end. See hyjal dev for more on the local server.

4. Consume from a browser page

Add a static web Resource that serves an HTML page. The page opens an EventSource against the api endpoint and appends each event to the DOM.

1<!-- public/index.html -->
2<!doctype html>
3<meta charset="utf-8" />
4<title>SSE demo</title>
5<ul id="log"></ul>
6<script>
7 const log = document.getElementById("log");
8 const es = new EventSource("https://api--acme.hyjal.cloud/events");
9 es.onmessage = (event) => {
10 const item = document.createElement("li");
11 item.textContent = event.data;
12 log.appendChild(item);
13 };
14 es.onerror = () => es.close();
15</script>

Add the front-end Resource to the manifest and mount the API behind it so the page and endpoint share an origin. Same-origin dispatch removes the need for any CORS configuration.

1[resource.web]
2lane = "static"
3output = "public"
4mounts = { "/events" = "api" }

With the mount in place, point EventSource at the same-origin path instead of the absolute URL:

1const es = new EventSource("/events");

5. Deploy and verify

Deploy both Resources.

$$ hyjal deploy
$Building resource api (lane: js)... ✓
$Building resource web (lane: static)... ✓
$Uploading artifacts...
$Applying policy...
$Flipping routes...
$
$Deployed acme/web https://web--acme.hyjal.cloud
$Deployed acme/api https://api--acme.hyjal.cloud

Open https://web--acme.hyjal.cloud. The list fills in one row per second as events arrive over the streamed response.

What you built

  • A JavaScript-lane SSE endpoint that streams events over one HTTP response.
  • A browser page that consumes the stream with EventSource.
  • A same-origin mounts wiring that removes CORS configuration from the client.

Next steps