From 1a8d4228347a47b696b4c6918179cda94fc1b0cd Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 27 Jan 2026 22:34:12 -0800 Subject: [PATCH] add sse helper --- README.md | 54 ++++++++++++++++++++++++++++++++ examples/ssr/src/pages/index.tsx | 1 + examples/ssr/src/pages/sse.tsx | 41 ++++++++++++++++++++++++ examples/ssr/src/server/index.ts | 7 +++++ src/index.tsx | 29 +++++++++++++++++ 5 files changed, 132 insertions(+) create mode 100644 examples/ssr/src/pages/sse.tsx diff --git a/README.md b/README.md index 2a7569b..f5d3221 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,60 @@ Install the `vscode-styled-components` VSCode extension to highlight inline `js` Anything in `pub/` is served as-is. Simple stuff. +### Server-Sent Events (SSE) + +`hype` provides a simple `app.sse()` helper for streaming data to clients: + +```typescript +import { Hype } from "hype" + +const app = new Hype() + +// Stream current time every second +app.sse('/api/time', (send) => { + send({ time: Date.now() }) + const interval = setInterval(() => send({ time: Date.now() }), 1000) + return () => clearInterval(interval) // cleanup on disconnect +}) +``` + +The `send()` function: +- Automatically JSON.stringify's objects (strings are sent as-is) +- Accepts an optional second parameter for named events: `send(data, 'eventName')` + +The handler can return a cleanup function that's called when the client disconnects. + +You also have access to the Hono context as the second parameter: + +```typescript +app.sse('/api/user-events', (send, c) => { + const userId = c.req.query('userId') + // ... subscribe to user-specific events +}) +``` + +**Client-side usage:** + +```typescript +const events = new EventSource('/api/time') + +events.onmessage = (e) => { + const data = JSON.parse(e.data) + console.log('Time:', data.time) +} + +// For named events: +events.addEventListener('eventName', (e) => { + console.log('Got event:', e.data) +}) +``` + +Test with curl: + +```sh +curl -N http://localhost:3000/api/time +``` + ### utils `hype` includes helpful utils for your webapp: diff --git a/examples/ssr/src/pages/index.tsx b/examples/ssr/src/pages/index.tsx index 8fd45e3..4067b18 100644 --- a/examples/ssr/src/pages/index.tsx +++ b/examples/ssr/src/pages/index.tsx @@ -8,6 +8,7 @@ export default () => ( ) diff --git a/examples/ssr/src/pages/sse.tsx b/examples/ssr/src/pages/sse.tsx new file mode 100644 index 0000000..121134c --- /dev/null +++ b/examples/ssr/src/pages/sse.tsx @@ -0,0 +1,41 @@ +import { js } from 'hype' + +export default () => ( +
+

SSE Demo

+ +
Connecting...
+
+
Events received: 0
+ +

+ Back to Home +

+ + {js` + const statusEl = document.getElementById('status') + const timeEl = document.getElementById('time') + const countEl = document.getElementById('count') + let count = 0 + + const events = new EventSource('/api/time') + + events.onopen = () => { + statusEl.textContent = 'Connected' + statusEl.style.color = 'green' + } + + events.onmessage = (e) => { + const data = JSON.parse(e.data) + timeEl.textContent = new Date(data.time).toLocaleTimeString() + count++ + countEl.textContent = 'Events received: ' + count + } + + events.onerror = () => { + statusEl.textContent = 'Disconnected' + statusEl.style.color = 'red' + } + `} +
+) diff --git a/examples/ssr/src/server/index.ts b/examples/ssr/src/server/index.ts index b6cfadb..4f33d34 100644 --- a/examples/ssr/src/server/index.ts +++ b/examples/ssr/src/server/index.ts @@ -5,4 +5,11 @@ const app = new Hype({}) // custom routes go here // app.get("/my-custom-routes", (c) => c.text("wild, wild stuff")) +// SSE example: streams current time every second +app.sse('/api/time', (send) => { + send({ time: Date.now() }) + const interval = setInterval(() => send({ time: Date.now() }), 1000) + return () => clearInterval(interval) +}) + export default app.defaults diff --git a/src/index.tsx b/src/index.tsx index 753d244..f5c5947 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import { join } from 'path' import { render as formatHTML } from './lib/html-formatter' import { type Context, Hono, type Schema, type Env } from 'hono' import { serveStatic } from 'hono/bun' +import { streamSSE } from 'hono/streaming' import color from 'kleur' import { transpile } from './utils' @@ -25,6 +26,11 @@ export type HypeProps = { layout?: boolean } +export type SSEHandler = ( + send: (data: unknown, event?: string) => Promise, + c: Context +) => void | (() => void) | Promise void)> + export class Hype< E extends Env = Env, S extends Schema = {}, @@ -39,6 +45,29 @@ export class Hype< this.props = props ?? {} } + sse(path: string, handler: SSEHandler) { + return this.get(path, (c) => { + return streamSSE(c, async (stream) => { + const send = async (data: unknown, event?: string) => { + await stream.writeSSE({ + data: typeof data === 'string' ? data : JSON.stringify(data), + event, + }) + } + + const cleanup = await handler(send, c) + + // Keep stream open until client disconnects + await new Promise((resolve) => { + stream.onAbort(() => { + cleanup?.() + resolve() + }) + }) + }) + }) + } + registerRoutes() { if (this.routesRegistered) return this.routesRegistered = true