wren/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files acd87b6b6146da11e0c9272c92a02a8e707adf4a
81 lines · 2.4 KB · ~708 tok Raw History
1
/**
2
 * Realtime without a framework. Actions `emit` what changed; `GET /events`
3
 * streams it to every open page as server-sent events, numbered, with the
4
 * last hundred kept so a client that reconnects with `Last-Event-ID` (or
5
 * `?since=`) hears what it missed. `static/live.js` turns the stream into
6
 * DOM events. The bus is this process's: the terminal is another process,
7
 * and its writes show on the next load rather than live.
8
 */
9
10
export interface LiveEvent {
11
  readonly id: number;
12
  readonly type: string;
13
  readonly data: unknown;
14
}
15
16
const KEEP = 100;
17
const HEARTBEAT_MS = 25_000;
18
const kept: LiveEvent[] = [];
19
const listeners = new Set<(event: LiveEvent) => void>();
20
let last = 0;
21
22
export function emit(type: string, data: unknown): LiveEvent {
23
  const event: LiveEvent = { id: ++last, type, data };
24
  kept.push(event);
25
  if (kept.length > KEEP) kept.shift();
26
  for (const listen of listeners) listen(event);
27
  return event;
28
}
29
30
export function subscribe(listen: (event: LiveEvent) => void): () => void {
31
  listeners.add(listen);
32
  return () => listeners.delete(listen);
33
}
34
35
/** `GET /events`: a stream that replays what `since` missed, then follows. */
36
export function events(request: Request): Response {
37
  const since = Number(
38
    request.headers.get("last-event-id") ??
39
      new URL(request.url).searchParams.get("since") ??
40
      0,
41
  );
42
  const encoder = new TextEncoder();
43
  let stop = (): void => {};
44
  const stream = new ReadableStream<Uint8Array>({
45
    start(controller) {
46
      const write = (text: string): void => {
47
        try {
48
          controller.enqueue(encoder.encode(text));
49
        } catch {
50
          stop();
51
        }
52
      };
53
      const send = (event: LiveEvent): void =>
54
        write(
55
          `id: ${event.id}\ndata: ${JSON.stringify({ type: event.type, data: event.data })}\n\n`,
56
        );
57
      const heartbeat = setInterval(() => write(":\n\n"), HEARTBEAT_MS);
58
      const unsubscribe = subscribe(send);
59
      stop = () => {
60
        clearInterval(heartbeat);
61
        unsubscribe();
62
        try {
63
          controller.close();
64
        } catch {}
65
      };
66
      request.signal.addEventListener("abort", stop);
67
      write("retry: 2000\n\n");
68
      for (const event of kept) if (event.id > since) send(event);
69
    },
70
    cancel() {
71
      stop();
72
    },
73
  });
74
  return new Response(stream, {
75
    headers: {
76
      "content-type": "text/event-stream",
77
      "cache-control": "no-store",
78
      "x-accel-buffering": "no",
79
    },
80
  });
81
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts