Deno KV + server-sent events is heaps fun

I played with Deno KV's watch method today to see how easy it is to implement real-time behaviour with server-sent events (SSE), and it was super easy and actually a lot of fun.

It's pretty easy to get a live counter up and running (example with Fresh appended). It is worth noting that EventSource is a Web API, so you can consume those SSEs without a library.

The bit that I was stuck on, which is super silly, is a bit in the EventSource documentation that I missed:

... Messages in the event stream are separated by a pair of newline characters.

I realised what was happening eventually after debugging along with a working example for half an hour. 😓

// routes/api/subscribe.ts

import { Handlers } from "$fresh/server.ts";

const kv = await Deno.openKv(":memory:");

let i = 0;

setInterval(() => kv.set(["likes", "video_0001"], ++i), 10);

export const handler: Handlers = {
    GET(_req) {
        const stream = kv.watch([["score", "team-rocket"]]);
        const cleanup = () => stream.getReader().cancel();

        const body = new ReadableStream({
            async start(controller) {
                for await (const [entry] of stream) {
                    controller.enqueue(`data: ${entry.value}\n\n`);
                }
            },
            cancel() {
                cleanup();
            },
        });

        return new Response(body.pipeThrough(new TextEncoderStream()), {
            headers: {
                "Content-Type": "text/event-stream",
                "Cache-Control": "no-cache",
            },
        });
    },
};
// routes/index.tsx

import { useSignal, useSignalEffect } from "@preact/signals";

export default function Home() {
    const message = useSignal("");

    useSignalEffect(() => {
        const eventSource = new EventSource(
            `/api/subscribe`
        );

        eventSource.onmessage = (event) => {
            message.value = JSON.parse(event.data);
        };

        return () => {
            eventSource.close();
        };
    });

    return (
        <div>{message.value}</div>
    );
}