WebSocket Routes
Odac provides built-in WebSocket support for real-time bidirectional communication.
Route Definition
WebSocket routes are defined in your route files (e.g., route/www.js or route/websocket.js) using Odac.Route.ws():
// route/websocket.js
Odac.Route.ws('/chat', Odac => {
Odac.ws.send({type: 'welcome', message: 'Connected!'})
Odac.ws.on('message', data => {
console.log('Received:', data)
Odac.ws.send({type: 'echo', data})
})
Odac.ws.on('close', () => {
console.log('Client disconnected')
})
})
Handler Signature:
The handler receives the Odac instance as the only parameter. The WebSocket client is accessible via Odac.ws, providing a consistent API with HTTP routes where everything is accessed through the Odac object.
CSRF Token Protection:
By default, WebSocket routes require a valid CSRF token (like Route.get() and Route.post()). The token is sent via the Sec-WebSocket-Protocol header during the initial handshake.
Disable token requirement:
Odac.Route.ws('/public', Odac => {
Odac.ws.send({type: 'public'})
}, {token: false})
Route File Structure:
web/
├── route/
│ ├── www.js # HTTP routes
│ └── websocket.js # WebSocket routes (recommended)
Using Controllers
You can also specify a connector file as a string. Odac will look for the file in controller/ws/.
// route/websocket.js
Odac.Route.ws('/chat', 'ChatController')
File Structure:
web/
├── controller/
│ └── ws/
│ └── ChatController.js
Controller File:
// controller/ws/ChatController.js
module.exports = Odac => {
Odac.ws.send({type: 'welcome'})
Odac.ws.on('message', data => {
// ...
})
}
WebSocket Client API (Odac.ws)
The WebSocket client is accessible via Odac.ws in your handler, providing a consistent API pattern with HTTP routes.
Sending Messages
Odac.ws.send({type: 'message', text: 'Hello'}) // JSON object
Odac.ws.send('Plain text message') // String
Odac.ws.sendBinary(buffer) // Binary data
Event Handlers
Odac.ws.on('message', data => {}) // Incoming message
Odac.ws.on('close', () => {}) // Connection closed
Odac.ws.on('error', err => {}) // Error occurred
Connection Management
Odac.ws.close() // Close connection
Odac.ws.ping() // Send ping frame
Odac.ws.id // Unique client ID
Rooms
Group clients into rooms for targeted broadcasting:
Odac.Route.ws('/game', Odac => {
const roomId = Odac.Request.data.url.room || 'lobby'
Odac.ws.join(roomId)
Odac.ws.on('message', data => {
Odac.ws.to(roomId).send({
type: 'chat',
message: data.message
})
})
Odac.ws.on('close', () => {
Odac.ws.leave(roomId)
})
})
Broadcasting
// Send to all clients except sender
Odac.ws.broadcast({type: 'notification', text: 'New user joined'})
// Send to all clients in a room
Odac.ws.to('room-name').send({type: 'update', data: {}})
URL Parameters
WebSocket routes support dynamic parameters:
Odac.Route.ws('/room/{roomId}/user/{userId}', Odac => {
const {roomId, userId} = Odac.Request.data.url
Odac.ws.join(roomId)
Odac.ws.data.userId = userId
})
Authentication
Manual Authentication Check
Odac.Route.ws('/secure', async Odac => {
const isAuthenticated = await Odac.Auth.check()
if (!isAuthenticated) {
Odac.ws.close(4001, 'Unauthorized')
return
}
const user = await Odac.Auth.user()
Odac.ws.data.user = user
})
Using auth.ws() (Recommended)
Automatically requires authentication (also requires token by default):
Odac.Route.auth.ws('/secure', async Odac => {
const user = await Odac.Auth.user()
Odac.ws.data.user = user
Odac.ws.send({
type: 'welcome',
user: user.name
})
})
If the user is not authenticated, the connection is automatically closed with code 4001.
Options
Odac.Route.ws('/path', handler, {
token: true, // Require CSRF token (default: true)
maxPayload: 10 * 1024 * 1024, // Max payload size in bytes (default: 10MB)
rateLimit: { // Message rate limiting
max: 50, // Max messages allowed per window (default: 50)
window: 1000 // Time window in ms (default: 1000ms)
}
})
Examples:
// Public WebSocket (no token, no auth)
Odac.Route.ws('/public', handler, {token: false})
// Large file upload support (50MB limit)
Odac.Route.ws('/upload', handler, {maxPayload: 50 * 1024 * 1024})
// High-speed game socket (100 msgs/sec)
Odac.Route.ws('/game', handler, {
rateLimit: {max: 100, window: 1000}
})
// Token required, no auth (default)
Odac.Route.ws('/chat', handler)
// Both token and auth required
Odac.Route.auth.ws('/secure', handler)
Middleware
WebSocket routes support middleware just like HTTP routes:
// Define middleware
Odac.Route.use('auth-check', 'rate-limit').ws('/chat', Odac => {
Odac.ws.send({type: 'welcome'})
})
Middleware behavior:
- If middleware returns
false, connection closes with code4003(Forbidden) - If middleware returns anything other than
trueorundefined, connection closes with code4000 - Middleware runs before the WebSocket handler
Example with custom middleware:
// middleware/websocket-auth.js
module.exports = async Odac => {
const token = Odac.Request.header('Authorization')
if (!token) return false
const user = await validateToken(token)
if (!user) return false
Odac.Auth.setUser(user)
return true
}
// route/websocket.js
Odac.Route.use('websocket-auth').ws('/secure', Odac => {
Odac.ws.send({type: 'authenticated'})
})
Client Data Storage
Store per-connection data:
Odac.ws.data.username = 'john'
Odac.ws.data.joinedAt = Date.now()
Intervals and Timeouts
Use Odac.setInterval() and Odac.setTimeout() instead of global functions. They are automatically cleaned up when the WebSocket connection closes:
Odac.Route.ws('/live-updates', Odac => {
Odac.setInterval(() => {
Odac.ws.send({
type: 'update',
timestamp: Date.now()
})
}, 1000)
Odac.setTimeout(() => {
Odac.ws.send({type: 'delayed-message'})
}, 5000)
})
Why use Odac.setInterval/setTimeout?
- Prevents memory leaks by auto-cleanup on disconnect
- No need to manually track and clear intervals
- Works seamlessly with WebSocket lifecycle
Manual cleanup (if needed):
const intervalId = Odac.setInterval(() => {}, 1000)
Odac.clearInterval(intervalId)
const timeoutId = Odac.setTimeout(() => {}, 5000)
Odac.clearTimeout(timeoutId)
Real-Time Notifications Example
Odac.Route.ws('/notifications', async Odac => {
const user = await Odac.Auth.user()
if (!user) {
Odac.ws.close(4001, 'Unauthorized')
return
}
Odac.ws.data.userId = user.id
Odac.ws.join(`user-${user.id}`)
Odac.ws.on('close', () => {
console.log(`User ${user.id} disconnected`)
})
})
// Send notification to specific user from anywhere in your app
function notifyUser(userId, message) {
const wsServer = Odac.Route.wsServer
wsServer.toRoom(`user-${userId}`, {
type: 'notification',
message
})
}
Client-Side Usage
Frontend clients can use shared connections across tabs:
// All browser tabs share one connection
const ws = Odac.ws('/notifications', {shared: true})
ws.on('message', data => {
console.log('Notification:', data)
})