add sse helper
This commit is contained in:
parent
a8d3a8203e
commit
1a8d422834
54
README.md
54
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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
41
examples/ssr/src/pages/sse.tsx
Normal file
41
examples/ssr/src/pages/sse.tsx
Normal 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>
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user