| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 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 |
|
| 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 |
} |