Server-Sent Events (SSE)
Server-Sent Events (SSE) let you stream real-time updates from the server to clients over HTTP. Unlike WebSockets, SSE is unidirectional (server → client) and works over standard HTTP connections.
Pikku supports SSE as a progressive enhancement for GET routes - your function returns an initial response immediately, and can optionally stream updates if the client supports SSE.
Progressive Enhancement Pattern​
The recommended approach is to use pikkuFunc or pikkuFuncSessionless with an optional channel:
import { pikkuFuncSessionless } from '#pikku/pikku-types.gen.js'
export const getStatus = pikkuFuncSessionless<
void,
{ state: 'initial' | 'pending' | 'done' }
>({
func: async (services) => {
// If SSE is enabled and client supports it, send updates
if (services?.channel) {
setTimeout(() => services.channel?.send({ state: 'pending' }), 2500)
setTimeout(() => services.channel?.send({ state: 'done' }), 5000)
}
// Always return initial response immediately
return { state: 'initial' }
},
docs: {
summary: 'Get current status with optional streaming updates',
tags: ['status']
}
})
Wire it with sse: true:
import { wireHTTP } from '#pikku/pikku-types.gen.js'
wireHTTP({
method: 'get',
route: '/status',
func: getStatus,
sse: true, // Enables SSE support
auth: false,
docs: {
summary: 'Status endpoint with SSE support',
tags: ['status']
}
})
This pattern works for both SSE and regular HTTP clients:
- Regular HTTP client: Gets immediate response
{ state: 'initial' } - SSE client: Gets initial response plus streaming updates
Requirements​
- Must be a GET route
- Set
sse: truein wireHTTP config - The function's
Outtype is used for both initial response and SSE messages - Channel is automatically injected as
services.channelwhen SSE is active
Build Progress Example​
Here's a real-world example showing build progress:
export const watchBuild = pikkuFuncSessionless<
{ projectId: string },
{ status: string; progress?: number }
>({
func: async ({ buildService, channel }, data) => {
const build = await buildService.start(data.projectId)
if (channel) {
// Stream progress updates
buildService.onProgress(build.id, (progress) => {
channel.send({
status: 'building',
progress: progress.percent
})
})
buildService.onComplete(build.id, () => {
channel.send({
status: 'complete',
progress: 100
})
channel.close()
})
}
// Initial response
return {
status: 'started',
progress: 0
}
},
docs: {
summary: 'Watch build progress with real-time updates',
tags: ['builds']
}
})
wireHTTP({
method: 'get',
route: '/builds/:projectId/watch',
func: watchBuild,
sse: true,
auth: false,
docs: {
summary: 'Watch build progress',
tags: ['builds']
}
})
Client-Side Usage​
Using EventSource API​
const eventSource = new EventSource('/status')
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data)
console.log('Received:', data)
if (data.state === 'done') {
eventSource.close()
}
}
eventSource.onerror = function(error) {
console.error('SSE error:', error)
eventSource.close()
}
Handling Both Modes​
async function getStatus() {
// Try SSE first
if (window.EventSource) {
const eventSource = new EventSource('/status')
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data)
updateUI(data)
}
eventSource.onerror = function() {
// Fallback to regular HTTP
eventSource.close()
pollStatus()
}
} else {
// Browser doesn't support SSE - use polling
pollStatus()
}
}
async function pollStatus() {
const response = await fetch('/status')
const data = await response.json()
updateUI(data)
}
Live Metrics Example​
Stream live metrics with automatic updates:
export const getLiveMetrics = pikkuFuncSessionless<
void,
{ cpu: number; memory: number; requests: number }
>({
func: async ({ metricsService, channel }) => {
const current = await metricsService.getCurrent()
if (channel) {
// Send updates every second
const interval = setInterval(async () => {
const metrics = await metricsService.getCurrent()
channel.send(metrics)
}, 1000)
// Cleanup after 5 minutes
setTimeout(() => {
clearInterval(interval)
channel.close()
}, 5 * 60 * 1000)
}
return current
},
docs: {
summary: 'Get live system metrics',
tags: ['metrics']
}
})
wireHTTP({
method: 'get',
route: '/metrics/live',
func: getLiveMetrics,
sse: true,
docs: {
summary: 'Live metrics endpoint',
tags: ['metrics']
}
})
When to Use SSE​
SSE works well for:
- Live dashboards and metrics
- Progress indicators for long-running operations
- Real-time notifications
- Activity feeds
- Stock tickers or live scores
Consider WebSockets (channels) instead if you need:
- Bidirectional communication
- Client → server messaging
- Lower latency requirements
- Binary data streaming
Best Practices​
Always provide an initial response - Don't make clients wait for the first SSE message:
// âś… Good - immediate response
func: async ({ channel }) => {
const initial = computeInitialState()
if (channel) {
// Stream updates
}
return initial
}
// ❌ Bad - no initial response
func: async ({ channel }) => {
if (channel) {
// Only streams, no return
}
}
Close the channel when done - Clean up resources:
if (channel) {
// Send updates...
channel.send(finalUpdate)
channel.close() // Important!
}
Use cleanup for long-running streams:
if (channel) {
const interval = setInterval(() => {
channel.send(data)
}, 1000)
// Always clean up
setTimeout(() => {
clearInterval(interval)
channel.close()
}, maxDuration)
}