Streaming API
Odac provides a unified streaming API that automatically handles Server-Sent Events (SSE), with future support for WebSocket and HTTP/3.
Quick Start
Inline Route (Simple)
// route/www.js
Odac.Route.get('/hello', async (Odac) => {
Odac.stream('Hello World')
})
Controller (Recommended)
// route/www.js
Odac.Route.get('/hello', 'hello')
// controller/hello/get/index.js
module.exports = async (Odac) => {
Odac.stream('Hello World')
}
JSON Message
// controller/get/index.js
module.exports = async (Odac) => {
Odac.stream({ message: 'Hello', time: Date.now() })
}
Multiple Messages
Callback Pattern with Auto-Cleanup
// controller/get/index.js
module.exports = async (Odac) => {
Odac.stream((send) => {
send({ type: 'connected' })
// Use Odac.setInterval for automatic cleanup
Odac.setInterval(() => {
send({ time: Date.now() })
}, 1000)
})
}
Important: Always use Odac.setInterval() and Odac.setTimeout() instead of global functions. They are automatically cleaned up when the connection closes.
Manual Cleanup (Alternative)
// controller/get/index.js
module.exports = async (Odac) => {
Odac.stream((send, close) => {
send({ type: 'connected' })
const interval = setInterval(() => {
send({ time: Date.now() })
}, 1000)
// Return cleanup function
return () => {
clearInterval(interval)
}
})
}
Automatic Piping
Array
// controller/get/index.js
module.exports = async (Odac) => {
Odac.stream([1, 2, 3, 4, 5])
}
Async Generator
// controller/users/get/index.js
module.exports = async (Odac) => {
Odac.stream(async function* () {
const users = await Odac.DB.users.get()
for (const user of users) {
yield user
}
})
}
Promise
// controller/app/get/index.js
module.exports = async (Odac) => {
Odac.stream(
fetch('https://api.example.com/data')
.then(r => r.json())
)
}
Node.js Stream
// controller/file/get/index.js
module.exports = async (Odac) => {
const fs = require('fs')
Odac.stream(fs.createReadStream('large-file.txt'))
}
Advanced Usage
Full Control
// controller/monitor/get/index.js
module.exports = async (Odac) => {
return Odac.stream((send) => {
send({ type: 'connected' })
// Use Odac.setInterval for automatic cleanup
Odac.setInterval(() => {
send({ time: Date.now() })
}, 1000)
})
}
Note: When using Odac.setInterval() or Odac.setTimeout(), cleanup is automatic. No need for manual clearInterval() or clearTimeout().
Error Handling
// controller/data/get/fetch.js
module.exports = async (Odac) => {
const stream = Odac.stream()
try {
const data = await fetchData()
stream.send(data)
} catch (error) {
stream.error(error.message)
}
}
Real-World Examples
Real-time Logs
// route/www.js
Odac.Route.get('/logs', 'logs')
// controller/logs/get/index.js
module.exports = async (Odac) => {
return Odac.stream(async function* () {
const logStream = await getDeploymentLogs()
for await (const log of logStream) {
yield {
timestamp: Date.now(),
message: log
}
}
})
}
Database Pagination
// route/www.js
Odac.Route.get('/posts', 'posts')
// controller/posts/get/index.js
module.exports = async (Odac) => {
return Odac.stream(async function* () {
let page = 1
let hasMore = true
while (hasMore) {
const posts = await Odac.DB.posts
.limit(10)
.offset((page - 1) * 10)
.get()
if (posts.length === 0) {
hasMore = false
} else {
for (const post of posts) {
yield post
}
page++
}
}
})
}
Client-Side Usage
JavaScript
const eventSource = new EventSource('/events')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log(data)
}
eventSource.onerror = (error) => {
console.error('Connection error:', error)
}
// Close connection
eventSource.close()
React Hook
import { useEffect, useState } from 'react'
function useStream(url) {
const [data, setData] = useState(null)
useEffect(() => {
const eventSource = new EventSource(url)
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data))
}
return () => eventSource.close()
}, [url])
return data
}
// Usage
function Dashboard() {
const status = useStream('/auth/listen')
return <div>{status?.message}</div>
}
Protocol
Odac uses Server-Sent Events (SSE) for streaming:
- ✅ One-way communication (server → client)
- ✅ Automatic reconnection
- ✅ Works over HTTP/2
- ✅ No extra ports needed
Technical Details
- Protocol: HTTP/2 compatible
- Port: Standard HTTPS (443)
- Heartbeat: Automatic (every 30 seconds)
- Reconnection: Automatic (browser handles)
- Compression: Supported via HTTP/2
Memory Management
Odac automatically manages timers and intervals in streaming contexts:
module.exports = async (Odac) => {
return Odac.stream((send) => {
// ✅ Automatically cleaned up when connection closes
Odac.setInterval(() => {
send({ time: Date.now() })
}, 1000)
Odac.setTimeout(() => {
send({ type: 'delayed' })
}, 5000)
})
}
Why this matters:
- Prevents memory leaks
- No orphaned intervals after disconnect
- Automatic cleanup on connection close
Manual cleanup (if needed):
const intervalId = Odac.setInterval(() => { ... }, 1000)
Odac.clearInterval(intervalId)
const timeoutId = Odac.setTimeout(() => { ... }, 5000)
Odac.clearTimeout(timeoutId)
Best Practices
- Use Odac timers: Always use
Odac.setInterval()andOdac.setTimeout()instead of global functions - Return the stream: Always
return Odac.stream(...)from your controller - Throttle messages: Don't send too frequently (use intervals)
- Handle errors: Use try-catch for async operations
- Test reconnection: Ensure your app handles connection drops
Troubleshooting
Connection drops immediately:
- Check if you're calling
Odac.return()orres.end() - Don't use both streaming and regular responses
- Make sure to
return Odac.stream(...)
Messages not received:
- Verify JSON format
- Check browser console for errors
- Ensure CORS headers if cross-origin
High memory usage / Memory leaks:
- Use
Odac.setInterval()instead of globalsetInterval() - Use
Odac.setTimeout()instead of globalsetTimeout() - Avoid creating intervals outside the stream callback
- Check for other resource leaks (database connections, file handles)
Intervals keep running after disconnect:
- Replace
setInterval()withOdac.setInterval() - Replace
setTimeout()withOdac.setTimeout() - These are automatically cleaned up when the connection closes