@owlmeans/server-socket 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +560 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +2 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +2 -0
- package/build/consts.js.map +1 -0
- package/build/helper.d.ts +7 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +14 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/middleware.d.ts +3 -0
- package/build/middleware.d.ts.map +1 -0
- package/build/middleware.js +25 -0
- package/build/middleware.js.map +1 -0
- package/build/service.d.ts +5 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +109 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +11 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/connection.d.ts +6 -0
- package/build/utils/connection.d.ts.map +1 -0
- package/build/utils/connection.js +104 -0
- package/build/utils/connection.js.map +1 -0
- package/build/utils/index.d.ts +3 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +3 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/server.d.ts +5 -0
- package/build/utils/server.d.ts.map +1 -0
- package/build/utils/server.js +15 -0
- package/build/utils/server.js.map +1 -0
- package/package.json +46 -0
- package/src/consts.ts +2 -0
- package/src/helper.ts +21 -0
- package/src/index.ts +6 -0
- package/src/middleware.ts +32 -0
- package/src/service.ts +133 -0
- package/src/types.ts +12 -0
- package/src/utils/connection.ts +124 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/server.ts +21 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# @owlmeans/server-socket
|
|
2
|
+
|
|
3
|
+
The **@owlmeans/server-socket** package provides WebSocket server functionality for OwlMeans Common Libraries, designed for fullstack microservices and microclients development with focus on security and proper authentication and authorization.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package serves as the server-side WebSocket implementation that:
|
|
8
|
+
|
|
9
|
+
- **Provides WebSocket server capabilities** using Fastify WebSocket integration
|
|
10
|
+
- **Supports real-time communication** between server and clients
|
|
11
|
+
- **Integrates with module system** for WebSocket route handling
|
|
12
|
+
- **Implements authentication and authorization** for WebSocket connections
|
|
13
|
+
- **Offers connection management** with lifecycle hooks and proper cleanup
|
|
14
|
+
- **Enables bidirectional messaging** with typed message handling
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
### Socket Service
|
|
19
|
+
A service that manages WebSocket server functionality and integrates with the Fastify web server to provide WebSocket endpoints.
|
|
20
|
+
|
|
21
|
+
### Connection Handling
|
|
22
|
+
Provides abstraction for WebSocket connections with proper lifecycle management, authentication, and error handling.
|
|
23
|
+
|
|
24
|
+
### Module Integration
|
|
25
|
+
WebSocket endpoints are defined using the OwlMeans module system, allowing for consistent routing, validation, and middleware application.
|
|
26
|
+
|
|
27
|
+
### Authentication & Authorization
|
|
28
|
+
WebSocket connections can be protected using the same guard and gate system used for HTTP endpoints.
|
|
29
|
+
|
|
30
|
+
## API Reference
|
|
31
|
+
|
|
32
|
+
### Services
|
|
33
|
+
|
|
34
|
+
#### `createSocketService(alias?): SocketService`
|
|
35
|
+
|
|
36
|
+
Creates a WebSocket service that can be registered with a server context.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createSocketService } from '@owlmeans/server-socket'
|
|
40
|
+
|
|
41
|
+
const socketService = createSocketService('websocket')
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
- `alias`: string (optional) - Service alias, defaults to `DEFAULT_ALIAS` ('socket-server')
|
|
46
|
+
|
|
47
|
+
**Returns:** SocketService instance
|
|
48
|
+
|
|
49
|
+
#### `SocketService`
|
|
50
|
+
|
|
51
|
+
Interface for the WebSocket service.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface SocketService extends InitializedService {
|
|
55
|
+
update: (api: ApiServer) => Promise<void>
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Methods:**
|
|
60
|
+
- `update(api)`: Configures the WebSocket server with the provided API server instance
|
|
61
|
+
|
|
62
|
+
### Helper Functions
|
|
63
|
+
|
|
64
|
+
#### `handleConnection<T>(handler): RefedModuleHandler`
|
|
65
|
+
|
|
66
|
+
Creates a WebSocket connection handler that integrates with the module system.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
70
|
+
import type { Connection } from '@owlmeans/socket'
|
|
71
|
+
|
|
72
|
+
const myConnectionHandler = handleConnection<MyConnection>(
|
|
73
|
+
async (conn, ctx, req, res) => {
|
|
74
|
+
// Handle WebSocket connection
|
|
75
|
+
console.log('New WebSocket connection')
|
|
76
|
+
|
|
77
|
+
conn.onMessage(async (message) => {
|
|
78
|
+
console.log('Received:', message)
|
|
79
|
+
await conn.send({ type: 'echo', data: message })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
conn.onClose(() => {
|
|
83
|
+
console.log('Connection closed')
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Parameters:**
|
|
90
|
+
- `handler`: Function that handles WebSocket connections with signature:
|
|
91
|
+
- `conn`: Connection instance
|
|
92
|
+
- `ctx`: Server context
|
|
93
|
+
- `req`: Abstract request
|
|
94
|
+
- `res`: Abstract response
|
|
95
|
+
|
|
96
|
+
**Returns:** Module handler compatible with OwlMeans modules
|
|
97
|
+
|
|
98
|
+
### Types
|
|
99
|
+
|
|
100
|
+
#### `Config`
|
|
101
|
+
|
|
102
|
+
Configuration interface extending ServerConfig.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
interface Config extends ServerConfig {}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `Context<C>`
|
|
109
|
+
|
|
110
|
+
Context interface that extends ServerContext with API server capabilities.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface Context<C extends Config = Config> extends ServerContext<C>, ApiServerAppend {}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Constants
|
|
117
|
+
|
|
118
|
+
#### `DEFAULT_ALIAS`
|
|
119
|
+
|
|
120
|
+
Default alias for the socket service.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const DEFAULT_ALIAS = 'socket-server'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Usage Examples
|
|
127
|
+
|
|
128
|
+
### Basic WebSocket Server Setup
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { makeServerContext, makeServerConfig } from '@owlmeans/server-context'
|
|
132
|
+
import { createSocketService } from '@owlmeans/server-socket'
|
|
133
|
+
import { createApiService } from '@owlmeans/server-api'
|
|
134
|
+
import { AppType } from '@owlmeans/context'
|
|
135
|
+
|
|
136
|
+
// Create server configuration
|
|
137
|
+
const config = makeServerConfig(AppType.Backend, 'websocket-server', {
|
|
138
|
+
host: 'localhost',
|
|
139
|
+
port: 3000
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Create context and services
|
|
143
|
+
const context = makeServerContext(config)
|
|
144
|
+
const apiService = createApiService()
|
|
145
|
+
const socketService = createSocketService()
|
|
146
|
+
|
|
147
|
+
// Register services
|
|
148
|
+
context.registerService(apiService)
|
|
149
|
+
context.registerService(socketService)
|
|
150
|
+
|
|
151
|
+
// Configure and initialize
|
|
152
|
+
context.configure()
|
|
153
|
+
await context.init()
|
|
154
|
+
|
|
155
|
+
console.log('WebSocket server running on ws://localhost:3000')
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### WebSocket Module with Authentication
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { module, guard } from '@owlmeans/module'
|
|
162
|
+
import { route } from '@owlmeans/route'
|
|
163
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
164
|
+
import type { Connection } from '@owlmeans/socket'
|
|
165
|
+
|
|
166
|
+
// Create WebSocket module with authentication
|
|
167
|
+
const chatModule = module(
|
|
168
|
+
route('chat-socket', '/ws/chat'),
|
|
169
|
+
guard('authenticated')
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// Set WebSocket handler
|
|
173
|
+
chatModule.handle = handleConnection<Connection>(
|
|
174
|
+
async (conn, ctx, req, res) => {
|
|
175
|
+
const userId = req.auth?.user?.id
|
|
176
|
+
|
|
177
|
+
if (!userId) {
|
|
178
|
+
await conn.close(1008, 'Authentication required')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`User ${userId} connected to chat`)
|
|
183
|
+
|
|
184
|
+
// Join user to chat room
|
|
185
|
+
await joinChatRoom(conn, userId)
|
|
186
|
+
|
|
187
|
+
// Handle incoming messages
|
|
188
|
+
conn.onMessage(async (message) => {
|
|
189
|
+
if (message.type === 'chat-message') {
|
|
190
|
+
await broadcastMessage(userId, message.content)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Handle disconnection
|
|
195
|
+
conn.onClose(() => {
|
|
196
|
+
console.log(`User ${userId} disconnected from chat`)
|
|
197
|
+
leaveChatRoom(userId)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Register module with context
|
|
203
|
+
context.registerModule(chatModule)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Real-time Data Streaming
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
210
|
+
import type { Connection } from '@owlmeans/socket'
|
|
211
|
+
|
|
212
|
+
const dataStreamModule = module(
|
|
213
|
+
route('data-stream', '/ws/data'),
|
|
214
|
+
guard('authenticated')
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
dataStreamModule.handle = handleConnection<Connection>(
|
|
218
|
+
async (conn, ctx, req, res) => {
|
|
219
|
+
const userId = req.auth?.user?.id
|
|
220
|
+
|
|
221
|
+
// Start streaming data to client
|
|
222
|
+
const streamInterval = setInterval(async () => {
|
|
223
|
+
const data = await fetchLatestData(userId)
|
|
224
|
+
await conn.send({
|
|
225
|
+
type: 'data-update',
|
|
226
|
+
timestamp: Date.now(),
|
|
227
|
+
data: data
|
|
228
|
+
})
|
|
229
|
+
}, 1000)
|
|
230
|
+
|
|
231
|
+
// Handle client requests
|
|
232
|
+
conn.onMessage(async (message) => {
|
|
233
|
+
switch (message.type) {
|
|
234
|
+
case 'subscribe':
|
|
235
|
+
await subscribeToChannel(userId, message.channel)
|
|
236
|
+
break
|
|
237
|
+
case 'unsubscribe':
|
|
238
|
+
await unsubscribeFromChannel(userId, message.channel)
|
|
239
|
+
break
|
|
240
|
+
case 'get-history':
|
|
241
|
+
const history = await getHistoricalData(message.from, message.to)
|
|
242
|
+
await conn.send({
|
|
243
|
+
type: 'history-data',
|
|
244
|
+
requestId: message.requestId,
|
|
245
|
+
data: history
|
|
246
|
+
})
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Cleanup on disconnect
|
|
252
|
+
conn.onClose(() => {
|
|
253
|
+
clearInterval(streamInterval)
|
|
254
|
+
unsubscribeFromAllChannels(userId)
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### WebSocket with Validation
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { module, filter, body } from '@owlmeans/module'
|
|
264
|
+
import { route } from '@owlmeans/route'
|
|
265
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
266
|
+
|
|
267
|
+
// Define message validation schema
|
|
268
|
+
const messageSchema = {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
type: { type: 'string', enum: ['join', 'leave', 'message'] },
|
|
272
|
+
content: { type: 'string', maxLength: 1000 },
|
|
273
|
+
roomId: { type: 'string' }
|
|
274
|
+
},
|
|
275
|
+
required: ['type']
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const validatedSocketModule = module(
|
|
279
|
+
route('validated-socket', '/ws/validated'),
|
|
280
|
+
filter(body(messageSchema))
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
validatedSocketModule.handle = handleConnection<Connection>(
|
|
284
|
+
async (conn, ctx, req, res) => {
|
|
285
|
+
conn.onMessage(async (message) => {
|
|
286
|
+
// Message is automatically validated against schema
|
|
287
|
+
try {
|
|
288
|
+
switch (message.type) {
|
|
289
|
+
case 'join':
|
|
290
|
+
await joinRoom(conn, message.roomId)
|
|
291
|
+
await conn.send({ type: 'joined', roomId: message.roomId })
|
|
292
|
+
break
|
|
293
|
+
case 'leave':
|
|
294
|
+
await leaveRoom(conn, message.roomId)
|
|
295
|
+
await conn.send({ type: 'left', roomId: message.roomId })
|
|
296
|
+
break
|
|
297
|
+
case 'message':
|
|
298
|
+
await broadcastToRoom(message.roomId, {
|
|
299
|
+
type: 'room-message',
|
|
300
|
+
content: message.content,
|
|
301
|
+
sender: req.auth?.user?.id
|
|
302
|
+
})
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await conn.send({
|
|
307
|
+
type: 'error',
|
|
308
|
+
message: error.message
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Connection Management
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
320
|
+
import type { Connection } from '@owlmeans/socket'
|
|
321
|
+
|
|
322
|
+
// Global connection registry
|
|
323
|
+
const connections = new Map<string, Connection>()
|
|
324
|
+
|
|
325
|
+
const managedConnectionModule = module(
|
|
326
|
+
route('managed-connection', '/ws/managed')
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
managedConnectionModule.handle = handleConnection<Connection>(
|
|
330
|
+
async (conn, ctx, req, res) => {
|
|
331
|
+
const connectionId = generateConnectionId()
|
|
332
|
+
|
|
333
|
+
// Register connection
|
|
334
|
+
connections.set(connectionId, conn)
|
|
335
|
+
|
|
336
|
+
// Send connection info
|
|
337
|
+
await conn.send({
|
|
338
|
+
type: 'connection-established',
|
|
339
|
+
connectionId: connectionId,
|
|
340
|
+
serverTime: Date.now()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// Handle ping/pong for connection health
|
|
344
|
+
conn.onMessage(async (message) => {
|
|
345
|
+
if (message.type === 'ping') {
|
|
346
|
+
await conn.send({ type: 'pong', timestamp: Date.now() })
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Cleanup on disconnect
|
|
351
|
+
conn.onClose(() => {
|
|
352
|
+
connections.delete(connectionId)
|
|
353
|
+
console.log(`Connection ${connectionId} closed`)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Send periodic server stats
|
|
357
|
+
const statsInterval = setInterval(async () => {
|
|
358
|
+
await conn.send({
|
|
359
|
+
type: 'server-stats',
|
|
360
|
+
activeConnections: connections.size,
|
|
361
|
+
serverUptime: process.uptime()
|
|
362
|
+
})
|
|
363
|
+
}, 30000)
|
|
364
|
+
|
|
365
|
+
conn.onClose(() => {
|
|
366
|
+
clearInterval(statsInterval)
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
// Function to broadcast to all connections
|
|
372
|
+
const broadcastToAll = async (message: any) => {
|
|
373
|
+
const promises = Array.from(connections.values()).map(conn =>
|
|
374
|
+
conn.send(message).catch(console.error)
|
|
375
|
+
)
|
|
376
|
+
await Promise.allSettled(promises)
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Error Handling
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { handleConnection } from '@owlmeans/server-socket'
|
|
384
|
+
import { ResilientError } from '@owlmeans/error'
|
|
385
|
+
|
|
386
|
+
const errorHandlingModule = module(
|
|
387
|
+
route('error-handling', '/ws/errors')
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
errorHandlingModule.handle = handleConnection<Connection>(
|
|
391
|
+
async (conn, ctx, req, res) => {
|
|
392
|
+
conn.onMessage(async (message) => {
|
|
393
|
+
try {
|
|
394
|
+
await processMessage(message)
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof ResilientError) {
|
|
397
|
+
// Handle known errors gracefully
|
|
398
|
+
await conn.send({
|
|
399
|
+
type: 'error',
|
|
400
|
+
code: error.code,
|
|
401
|
+
message: error.message,
|
|
402
|
+
recoverable: true
|
|
403
|
+
})
|
|
404
|
+
} else {
|
|
405
|
+
// Handle unknown errors
|
|
406
|
+
console.error('Unexpected error:', error)
|
|
407
|
+
await conn.send({
|
|
408
|
+
type: 'error',
|
|
409
|
+
message: 'Internal server error',
|
|
410
|
+
recoverable: false
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
conn.onError((error) => {
|
|
417
|
+
console.error('WebSocket connection error:', error)
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Integration Patterns
|
|
424
|
+
|
|
425
|
+
### With Chat Systems
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Real-time chat implementation
|
|
429
|
+
const chatRooms = new Map<string, Set<Connection>>()
|
|
430
|
+
|
|
431
|
+
const joinRoom = async (conn: Connection, roomId: string) => {
|
|
432
|
+
if (!chatRooms.has(roomId)) {
|
|
433
|
+
chatRooms.set(roomId, new Set())
|
|
434
|
+
}
|
|
435
|
+
chatRooms.get(roomId)!.add(conn)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const broadcastToRoom = async (roomId: string, message: any) => {
|
|
439
|
+
const room = chatRooms.get(roomId)
|
|
440
|
+
if (room) {
|
|
441
|
+
const promises = Array.from(room).map(conn => conn.send(message))
|
|
442
|
+
await Promise.allSettled(promises)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### With Gaming Applications
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// Game server with real-time updates
|
|
451
|
+
const gameSessionModule = module(
|
|
452
|
+
route('game-session', '/ws/game/:sessionId'),
|
|
453
|
+
guard('authenticated')
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
gameSessionModule.handle = handleConnection<Connection>(
|
|
457
|
+
async (conn, ctx, req, res) => {
|
|
458
|
+
const sessionId = req.params.sessionId
|
|
459
|
+
const playerId = req.auth?.user?.id
|
|
460
|
+
|
|
461
|
+
await joinGameSession(sessionId, playerId, conn)
|
|
462
|
+
|
|
463
|
+
conn.onMessage(async (message) => {
|
|
464
|
+
switch (message.type) {
|
|
465
|
+
case 'player-move':
|
|
466
|
+
await processPlayerMove(sessionId, playerId, message.move)
|
|
467
|
+
await broadcastGameState(sessionId)
|
|
468
|
+
break
|
|
469
|
+
case 'player-action':
|
|
470
|
+
await processPlayerAction(sessionId, playerId, message.action)
|
|
471
|
+
break
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### With Monitoring Systems
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Real-time monitoring dashboard
|
|
482
|
+
const monitoringModule = module(
|
|
483
|
+
route('monitoring', '/ws/monitor'),
|
|
484
|
+
guard('admin')
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
monitoringModule.handle = handleConnection<Connection>(
|
|
488
|
+
async (conn, ctx, req, res) => {
|
|
489
|
+
// Send initial system state
|
|
490
|
+
await conn.send({
|
|
491
|
+
type: 'system-state',
|
|
492
|
+
data: await getSystemMetrics()
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// Subscribe to system events
|
|
496
|
+
const unsubscribe = await subscribeToSystemEvents(async (event) => {
|
|
497
|
+
await conn.send({
|
|
498
|
+
type: 'system-event',
|
|
499
|
+
event: event
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
conn.onClose(unsubscribe)
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Security Considerations
|
|
509
|
+
|
|
510
|
+
### Authentication and Authorization
|
|
511
|
+
- **Guard Integration** - Use authentication guards to protect WebSocket endpoints
|
|
512
|
+
- **Token Validation** - Validate JWT tokens or session tokens before accepting connections
|
|
513
|
+
- **Connection Limits** - Implement connection limits per user or IP address
|
|
514
|
+
|
|
515
|
+
### Data Validation
|
|
516
|
+
- **Message Validation** - Always validate incoming WebSocket messages
|
|
517
|
+
- **Rate Limiting** - Implement rate limiting for message frequency
|
|
518
|
+
- **Sanitization** - Sanitize user input to prevent injection attacks
|
|
519
|
+
|
|
520
|
+
### Connection Security
|
|
521
|
+
- **TLS/SSL** - Use secure WebSocket connections (WSS) in production
|
|
522
|
+
- **Origin Validation** - Validate connection origins to prevent CSRF attacks
|
|
523
|
+
- **Connection Timeouts** - Implement proper timeout mechanisms
|
|
524
|
+
|
|
525
|
+
## Best Practices
|
|
526
|
+
|
|
527
|
+
1. **Implement proper error handling** - Always handle connection errors and message processing errors
|
|
528
|
+
2. **Use connection pooling** - Manage connections efficiently to prevent memory leaks
|
|
529
|
+
3. **Implement heartbeat mechanism** - Use ping/pong to detect dead connections
|
|
530
|
+
4. **Validate all messages** - Never trust incoming WebSocket messages without validation
|
|
531
|
+
5. **Handle cleanup properly** - Always clean up resources when connections close
|
|
532
|
+
6. **Monitor connection health** - Track connection metrics and performance
|
|
533
|
+
|
|
534
|
+
## Error Handling
|
|
535
|
+
|
|
536
|
+
The package integrates with OwlMeans error handling:
|
|
537
|
+
|
|
538
|
+
- **ResilientError** - For recoverable errors that clients can handle
|
|
539
|
+
- **Connection errors** - Proper handling of WebSocket connection failures
|
|
540
|
+
- **Authentication errors** - Clear error messages for authentication failures
|
|
541
|
+
- **Validation errors** - Detailed validation error reporting
|
|
542
|
+
|
|
543
|
+
## Dependencies
|
|
544
|
+
|
|
545
|
+
This package depends on:
|
|
546
|
+
- `@owlmeans/context` - For service registration and context management
|
|
547
|
+
- `@owlmeans/server-api` - For API server integration
|
|
548
|
+
- `@owlmeans/server-context` - For server-side context management
|
|
549
|
+
- `@owlmeans/server-module` - For module system integration
|
|
550
|
+
- `@owlmeans/module` - For core module functionality
|
|
551
|
+
- `@owlmeans/socket` - For connection abstractions
|
|
552
|
+
- `@owlmeans/error` - For error handling
|
|
553
|
+
- `@fastify/websocket` - For WebSocket server implementation
|
|
554
|
+
|
|
555
|
+
## Related Packages
|
|
556
|
+
|
|
557
|
+
- [`@owlmeans/socket`](../socket) - WebSocket connection abstractions
|
|
558
|
+
- [`@owlmeans/client-socket`](../client-socket) - Client-side WebSocket implementation
|
|
559
|
+
- [`@owlmeans/server-api`](../server-api) - HTTP API server functionality
|
|
560
|
+
- [`@owlmeans/server-module`](../server-module) - Server-side module system
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,kBAAkB,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AbstractRequest, AbstractResponse } from '@owlmeans/module';
|
|
2
|
+
import type { Context } from '@owlmeans/server-api';
|
|
3
|
+
import type { RefedModuleHandler } from '@owlmeans/server-module';
|
|
4
|
+
import type { Connection } from '@owlmeans/socket';
|
|
5
|
+
import type { WebSocket } from '@fastify/websocket';
|
|
6
|
+
export declare const handleConnection: <T extends Connection = Connection>(handler: (conn: T, ctx: Context, req: AbstractRequest<WebSocket>, res: AbstractResponse<any>) => Promise<void>) => RefedModuleHandler<AbstractResponse<any>>;
|
|
7
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,eAAO,MAAM,gBAAgB,EAAE,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EAC/D,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,KAC3G,kBAAkB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAU5C,CAAA"}
|
package/build/helper.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context';
|
|
2
|
+
import { makeConnection } from './utils/connection.js';
|
|
3
|
+
export const handleConnection = handler => ref => async (req, res) => {
|
|
4
|
+
const ctx = assertContext(ref.ref?.ctx);
|
|
5
|
+
try {
|
|
6
|
+
await handler(makeConnection(req, ctx), ctx, req, res);
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
res.reject(e);
|
|
10
|
+
}
|
|
11
|
+
// Actually it does nothing - just for compatibility here
|
|
12
|
+
return res.value;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAMjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,MAAM,CAAC,MAAM,gBAAgB,GAEoB,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACpF,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAY,CAAA;IAClD,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IAC/D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,MAAM,CAAC,CAAU,CAAC,CAAA;IACxB,CAAC;IAED,yDAAyD;IACzD,OAAO,GAAG,CAAC,KAAK,CAAA;AAClB,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAQnD,eAAO,MAAM,sBAAsB,SAAS,MAAM,sBAAuC,UAsBxF,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MiddlewareType, MiddlewareStage, assertContext } from '@owlmeans/context';
|
|
2
|
+
import { DEFAULT_ALIAS as WEB_ALIAS } from '@owlmeans/server-api';
|
|
3
|
+
import { DEFAULT_ALIAS } from './consts.js';
|
|
4
|
+
export const createSocketMiddleware = (web = WEB_ALIAS, socket = DEFAULT_ALIAS) => {
|
|
5
|
+
const middleware = {
|
|
6
|
+
type: MiddlewareType.Context,
|
|
7
|
+
stage: MiddlewareStage.Loading,
|
|
8
|
+
apply: async (ctx) => {
|
|
9
|
+
const context = assertContext(ctx, 'socket-middleware');
|
|
10
|
+
const webService = context.service(web);
|
|
11
|
+
const socketService = context.service(socket);
|
|
12
|
+
// @TODO There is some middlewares that are ok to work only once during context intialization
|
|
13
|
+
const marker = `__socketServiceAdded-${web}-${socket}`;
|
|
14
|
+
if (!ctx.cfg.records?.find(record => record.id === marker)) {
|
|
15
|
+
await socketService.update(webService);
|
|
16
|
+
if (ctx.cfg.records == null) {
|
|
17
|
+
ctx.cfg.records = [];
|
|
18
|
+
}
|
|
19
|
+
ctx.cfg.records.push({ id: marker });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return middleware;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAElF,OAAO,EAAE,aAAa,IAAI,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAEjE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG3C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAc,SAAS,EAAE,MAAM,GAAG,aAAa,EAAc,EAAE;IACpG,MAAM,UAAU,GAAe;QAC7B,IAAI,EAAE,cAAc,CAAC,OAAO;QAC5B,KAAK,EAAE,eAAe,CAAC,OAAO;QAC9B,KAAK,EAAE,KAAK,EAAC,GAAG,EAAC,EAAE;YACjB,MAAM,OAAO,GAAG,aAAa,CAAkB,GAAyB,EAAE,mBAAmB,CAAC,CAAA;YAC9F,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAY,GAAG,CAAC,CAAA;YAClD,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAgB,MAAM,CAAC,CAAA;YAC5D,6FAA6F;YAE7F,MAAM,MAAM,GAAG,wBAAwB,GAAG,IAAI,MAAM,EAAE,CAAA;YACtD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;gBAC3D,MAAM,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;gBACtC,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;oBAC5B,GAAG,CAAC,GAAG,CAAC,OAAO,GAAG,EAAE,CAAA;gBACtB,CAAC;gBACD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;YACtC,CAAC;QACH,CAAC;KACF,CAAA;IAED,OAAO,UAAU,CAAA;AACnB,CAAC,CAAA"}
|