add sse helper

This commit is contained in:
Chris Wanstrath 2026-01-27 22:34:12 -08:00
parent a8d3a8203e
commit 1a8d422834
5 changed files with 132 additions and 0 deletions

View File

@ -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:

View File

@ -8,6 +8,7 @@ export default () => (
<ul>
<li><a href="/about">About This Website</a></li>
<li><a href="/js-test">JS Test</a></li>
<li><a href="/sse">SSE Demo</a></li>
</ul>
</section>
)

View File

@ -0,0 +1,41 @@
import { js } from 'hype'
export default () => (
<section>
<h1>SSE Demo</h1>
<div id="status">Connecting...</div>
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
<div id="count">Events received: 0</div>
<p style="margin-top: 2em;">
<a href="/">Back to Home</a>
</p>
{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'
}
`}
</section>
)

View File

@ -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

View File

@ -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<E extends Env> = (
send: (data: unknown, event?: string) => Promise<void>,
c: Context<E>
) => void | (() => void) | Promise<void | (() => 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<E>) {
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<void>((resolve) => {
stream.onAbort(() => {
cleanup?.()
resolve()
})
})
})
})
}
registerRoutes() {
if (this.routesRegistered) return
this.routesRegistered = true