Skip to content

Commit 81d5822

Browse files
committed
feat(middleware): add websocket middleware support 🔌
- Add WebSocket middleware documentation - Add WebSocket middleware implementation with event handlers - Export WebSocket middleware in middleware registry - Rename CORS middleware function for consistency - Update documentation navigation and cross-references - Update README with WebSocket middleware link
1 parent 31f35d7 commit 81d5822

9 files changed

Lines changed: 291 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Follow our [installing guide](https://docs-deserve.neabyte.com/getting-started/i
3232
- [Global Middleware](https://docs-deserve.neabyte.com/middleware/global) - Cross-cutting functionality
3333
- [Route-Specific Middleware](https://docs-deserve.neabyte.com/middleware/route-specific) - Targeted middleware for specific routes
3434
- [CORS Middleware](https://docs-deserve.neabyte.com/middleware/cors) - Cross-origin request handling
35+
- [WebSocket Middleware](https://docs-deserve.neabyte.com/middleware/websocket) - Real-time WebSocket communication
3536

3637
- **Response Utilities**
3738
- [Data Downloads](https://docs-deserve.neabyte.com/response/data) - Download in-memory content

docs/.vitepress/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export default defineConfig({
3333
items: [
3434
{ text: 'Global Middleware', link: '/middleware/global' },
3535
{ text: 'Route-Specific Middleware', link: '/middleware/route-specific' },
36-
{ text: 'CORS Middleware', link: '/middleware/cors' }
36+
{ text: 'CORS Middleware', link: '/middleware/cors' },
37+
{ text: 'WebSocket Middleware', link: '/middleware/websocket' }
3738
]
3839
},
3940
{

docs/middleware/cors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,4 @@ maxAge: 3600 // Cache preflight requests for 1 hour
123123

124124
- [Global Middleware](/middleware/global) - Cross-cutting functionality
125125
- [Route-Specific Middleware](/middleware/route-specific) - Targeted middleware
126-
- [Error Handling](/error-handling/object-details) - Custom error responses
126+
- [WebSocket Middleware](/middleware/websocket) - Real-time communication

docs/middleware/global.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ router.use(async (req: Request) => {
119119

120120
- [Route-Specific Middleware](/middleware/route-specific) - Targeted middleware
121121
- [CORS Middleware](/middleware/cors) - Cross-origin request handling
122-
- [Error Handling](/error-handling/object-details) - Custom error responses
122+
- [WebSocket Middleware](/middleware/websocket) - Real-time communication

docs/middleware/route-specific.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,4 @@ router.use('/api', (req: Request) => {
191191

192192
- [Global Middleware](/middleware/global) - Cross-cutting functionality
193193
- [CORS Middleware](/middleware/cors) - Cross-origin request handling
194-
- [Error Handling](/error-handling/object-details) - Custom error responses
194+
- [WebSocket Middleware](/middleware/websocket) - Real-time communication

docs/middleware/websocket.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# WebSocket Middleware
2+
3+
> **Reference**: [Deno.upgradeWebSocket API Documentation](https://docs.deno.com/api/deno/~/Deno.upgradeWebSocket)
4+
5+
This middleware handles WebSocket upgrades and provides event handling for real-time communication with explicit path listeners and error management.
6+
7+
## Basic Usage
8+
9+
Apply WebSocket middleware using Deserve's built-in middleware:
10+
11+
```typescript
12+
import { Router } from '@neabyte/deserve'
13+
14+
const router = new Router()
15+
16+
// Apply WebSocket with explicit listener path
17+
router.apply([['websocket', {
18+
listener: '/ws', // Matches /ws/* prefixes
19+
onConnect: (socket, req) => console.log('Client connected'),
20+
onMessage: (socket, event, req) => {
21+
if (event.data === 'ping') {
22+
socket.send('pong')
23+
}
24+
}
25+
}]])
26+
27+
router.serve(8000)
28+
```
29+
30+
## Multiple WebSocket Endpoints
31+
32+
**Important:** Deserve only processes the last WebSocket middleware pipeline. To support multiple endpoints, combine logic into a single middleware with path-based routing:
33+
34+
```typescript
35+
router.apply([['websocket', {
36+
listener: '/', // Matches /* prefixes, i.e. /ws/chat, /api/ws, etc
37+
onConnect: (socket, req) => {
38+
const url = new URL(req.url)
39+
console.log(`WebSocket connected to ${url.pathname}!`)
40+
if (url.pathname === '/ws/chat') {
41+
socket.send(JSON.stringify({ type: 'connected', message: 'Welcome to chat!' }))
42+
} else if (url.pathname === '/ws/notifications') {
43+
socket.send(JSON.stringify({ type: 'connected', message: 'Notification service ready' }))
44+
} else if (url.pathname === '/api/ws') {
45+
socket.send(JSON.stringify({ type: 'api_ready' }))
46+
}
47+
},
48+
onMessage: (socket, event, req) => {
49+
const url = new URL(req.url)
50+
const data = JSON.parse(event.data)
51+
if (url.pathname === '/ws/chat') {
52+
// Handle chat messages
53+
if (data.type === 'ping') {
54+
socket.send(JSON.stringify({ type: 'pong' }))
55+
} else if (data.type === 'message') {
56+
socket.send(JSON.stringify({
57+
type: 'message',
58+
content: data.content,
59+
timestamp: new Date().toISOString(),
60+
user: data.user || 'anonymous'
61+
}))
62+
}
63+
} else if (url.pathname === '/ws/notifications') {
64+
// Handle notification subscriptions
65+
if (data.type === 'subscribe') {
66+
socket.send(JSON.stringify({
67+
type: 'subscribed',
68+
topic: data.topic,
69+
timestamp: new Date().toISOString()
70+
}))
71+
}
72+
} else if (url.pathname === '/api/ws') {
73+
// Handle API messages
74+
socket.send(JSON.stringify({
75+
type: 'api_response',
76+
data: data,
77+
timestamp: new Date().toISOString()
78+
}))
79+
}
80+
},
81+
onDisconnect: (socket, req) => {
82+
const url = new URL(req.url)
83+
console.log(`WebSocket disconnected from ${url.pathname}!`)
84+
}
85+
}]])
86+
```
87+
88+
## WebSocket Options
89+
90+
### `listener` (Required)
91+
Specify the path to listen for WebSocket upgrades:
92+
93+
```typescript
94+
listener: '/ws' // Matches /ws and /ws/anything
95+
listener: '/api/ws' // Matches /api/ws and /api/ws/anything
96+
listener: '/ws/chat' // Matches /ws/chat and /ws/chat/anything
97+
```
98+
99+
### `onConnect`
100+
Handle new WebSocket connections:
101+
102+
```typescript
103+
onConnect: (socket, req) => {
104+
console.log('New client connected')
105+
socket.send('Welcome to the server!')
106+
}
107+
```
108+
109+
### `onMessage`
110+
Handle incoming WebSocket messages:
111+
112+
```typescript
113+
onMessage: (socket, event, req) => {
114+
const data = JSON.parse(event.data)
115+
switch (data.type) {
116+
case 'ping':
117+
socket.send(JSON.stringify({ type: 'pong' }))
118+
break
119+
case 'broadcast':
120+
// Broadcast to all connected clients
121+
break
122+
}
123+
}
124+
```
125+
126+
### `onDisconnect`
127+
Handle WebSocket disconnections:
128+
129+
```typescript
130+
onDisconnect: (socket, req) => {
131+
console.log('Client disconnected')
132+
// Clean up resources
133+
}
134+
```
135+
136+
### `onError`
137+
Handle WebSocket errors:
138+
139+
```typescript
140+
onError: (socket, event, req) => {
141+
console.error('WebSocket error:', event)
142+
// Handle error gracefully
143+
}
144+
```
145+
146+
## Client-Side Usage
147+
148+
Connect to WebSocket endpoints from JavaScript:
149+
150+
```javascript
151+
// Connect to chat WebSocket
152+
const chatSocket = new WebSocket('ws://localhost:8000/ws/chat')
153+
154+
// Handle connection open
155+
chatSocket.onopen = () => {
156+
console.log('Connected to chat')
157+
}
158+
159+
// Handle incoming messages
160+
chatSocket.onmessage = (event) => {
161+
const data = JSON.parse(event.data)
162+
console.log('Received:', data)
163+
}
164+
165+
// Handle connection close
166+
chatSocket.onclose = () => {
167+
console.log('Disconnected from chat')
168+
}
169+
170+
// Send a message
171+
chatSocket.send(JSON.stringify({
172+
type: 'chat',
173+
content: 'Hello, world!'
174+
}))
175+
```
176+
177+
## Best Practices
178+
179+
1. **Use explicit listeners** - Always specify the `listener` path for clarity
180+
2. **Handle JSON parsing** - Wrap JSON.parse in try-catch blocks
181+
3. **Validate message types** - Check message structure before processing
182+
4. **Clean up resources** - Use `onDisconnect` to clean up connections
183+
5. **Error handling** - Implement `onError` for graceful error handling
184+
6. **Message validation** - Validate incoming message structure
185+
7. **Connection limits** - Consider implementing connection limits for production
186+
187+
## Error Handling
188+
189+
WebSocket upgrade failures throw errors handled by Deserve's error middleware:
190+
191+
```typescript
192+
// If WebSocket upgrade fails, the middleware throws an error
193+
throw new Error(`WebSocket upgrade failed: ${errorMessage}`)
194+
```
195+
196+
## Troubleshooting
197+
198+
### Common Issues
199+
200+
**WebSocket connection fails:**
201+
- Check if `listener` path matches client connection URL
202+
- Verify WebSocket upgrade headers are present
203+
- Ensure server is running and accessible
204+
205+
**Messages not received:**
206+
- Check `onMessage` handler implementation
207+
- Verify JSON message format
208+
- Check for errors in `onError` handler
209+
210+
**Connection drops unexpectedly:**
211+
- Check network connectivity
212+
- Implement proper `onError` handling
213+
- Verify WebSocket protocol compatibility
214+
215+
**Multiple endpoints not working:**
216+
- **Deserve WebSocket middleware** - combine all endpoints into one middleware
217+
- Use path-based routing with `url.pathname` to handle different endpoints
218+
- Check for path conflicts in your routing logic
219+
220+
## Next Steps
221+
222+
- [Global Middleware](/middleware/global) - Cross-cutting functionality
223+
- [Route-Specific Middleware](/middleware/route-specific) - Targeted middleware
224+
- [CORS Middleware](/middleware/cors) - Cross-origin request handling

src/middlewares/CORS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface CorsOptions {
2222
* @param options - CORS configuration options
2323
* @returns Middleware function that handles CORS headers
2424
*/
25-
export default function cors(options?: CorsOptions): RouterMiddleware {
25+
export default function corsMiddleware(options?: CorsOptions): RouterMiddleware {
2626
return (req: Request, res?: Response) => {
2727
const origin = options?.origin ?? '*'
2828
const methods = options?.methods ?? httpMethods

src/middlewares/WebSocket.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { RouterMiddleware } from '@app/Types.ts'
2+
3+
/**
4+
* WebSocket middleware configuration options.
5+
*/
6+
export interface WebSocketOptions {
7+
/** Specific path to listen for WebSocket upgrades */
8+
listener?: string
9+
/** WebSocket connection handler */
10+
onConnect?: (socket: WebSocket, req: Request) => void
11+
/** WebSocket message handler */
12+
onMessage?: (socket: WebSocket, event: MessageEvent, req: Request) => void
13+
/** WebSocket disconnect handler */
14+
onDisconnect?: (socket: WebSocket, req: Request) => void
15+
/** WebSocket error handler */
16+
onError?: (socket: WebSocket, event: Event, req: Request) => void
17+
}
18+
19+
/**
20+
* WebSocket middleware factory function.
21+
* @param options - WebSocket configuration options
22+
* @returns Middleware function that handles WebSocket upgrades
23+
*/
24+
export default function websocketMiddleware(options?: WebSocketOptions): RouterMiddleware {
25+
return (req: Request, _res?: Response) => {
26+
if (!options?.listener) {
27+
return null
28+
}
29+
if (req.headers.get('upgrade') !== 'websocket') {
30+
return null
31+
}
32+
const url = new URL(req.url)
33+
if (!url.pathname.startsWith(options.listener)) {
34+
return null
35+
}
36+
try {
37+
const { socket, response } = Deno.upgradeWebSocket(req)
38+
socket.addEventListener('open', () => {
39+
options.onConnect?.(socket, req)
40+
})
41+
socket.addEventListener('message', (event) => {
42+
options.onMessage?.(socket, event, req)
43+
})
44+
socket.addEventListener('close', () => {
45+
options.onDisconnect?.(socket, req)
46+
})
47+
socket.addEventListener('error', (event) => {
48+
options.onError?.(socket, event, req)
49+
})
50+
return response
51+
} catch (error) {
52+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
53+
throw new Error(`WebSocket upgrade failed: ${errorMessage}`)
54+
}
55+
}
56+
}

src/middlewares/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import corsMiddleware from '@middlewares/CORS.ts'
2+
import websocketMiddleware from '@middlewares/WebSocket.ts'
23

34
/**
45
* Built-in middleware registry.
56
*/
67
export const middlewares = {
78
/** CORS middleware for handling cross-origin requests */
8-
cors: corsMiddleware
9+
cors: corsMiddleware,
10+
/** WebSocket middleware for handling WebSocket upgrades */
11+
websocket: websocketMiddleware
912
}

0 commit comments

Comments
 (0)