mdb-engine 0.1.6__py3-none-any.whl
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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# WebSocket Support in MDB_ENGINE
|
|
2
|
+
|
|
3
|
+
MDB_ENGINE provides built-in WebSocket support with app-level isolation and automatic route registration via `manifest.json`.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Add WebSocket to manifest.json
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"slug": "my_app",
|
|
12
|
+
"websockets": {
|
|
13
|
+
"realtime": {
|
|
14
|
+
"path": "/ws"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it! The engine automatically:
|
|
21
|
+
- ✅ Registers the WebSocket route with FastAPI
|
|
22
|
+
- ✅ Creates an isolated connection manager for your app
|
|
23
|
+
- ✅ Handles authentication (uses app's `auth_policy` by default)
|
|
24
|
+
- ✅ Manages connection lifecycle
|
|
25
|
+
- ✅ Provides ping/pong keepalive
|
|
26
|
+
|
|
27
|
+
### 2. Broadcast messages (Server → Clients)
|
|
28
|
+
|
|
29
|
+
Send messages from your server code to connected clients:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from mdb_engine.routing.websockets import broadcast_to_app
|
|
33
|
+
|
|
34
|
+
# Broadcast to all connected clients for your app
|
|
35
|
+
await broadcast_to_app("my_app", {
|
|
36
|
+
"type": "update",
|
|
37
|
+
"data": {"status": "completed"}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
# Broadcast to specific user only
|
|
41
|
+
await broadcast_to_app("my_app", {
|
|
42
|
+
"type": "notification",
|
|
43
|
+
"data": {"message": "Hello"}
|
|
44
|
+
}, user_id="user123")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Listen to client messages (Clients → Server)
|
|
48
|
+
|
|
49
|
+
Handle messages sent from clients to your server:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from mdb_engine.routing.websockets import register_message_handler, broadcast_to_app
|
|
53
|
+
|
|
54
|
+
async def handle_client_message(websocket, message):
|
|
55
|
+
"""Handle incoming messages from WebSocket clients."""
|
|
56
|
+
message_type = message.get("type")
|
|
57
|
+
|
|
58
|
+
if message_type == "subscribe":
|
|
59
|
+
# Client wants to subscribe to a channel
|
|
60
|
+
channel = message.get("channel")
|
|
61
|
+
await broadcast_to_app("my_app", {
|
|
62
|
+
"type": "subscribed",
|
|
63
|
+
"channel": channel,
|
|
64
|
+
"user_id": message.get("user_id")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
elif message_type == "ping":
|
|
68
|
+
# Respond to custom ping
|
|
69
|
+
await broadcast_to_app("my_app", {
|
|
70
|
+
"type": "pong",
|
|
71
|
+
"timestamp": message.get("timestamp")
|
|
72
|
+
}, user_id=message.get("user_id"))
|
|
73
|
+
|
|
74
|
+
# Register handler for the "realtime" endpoint
|
|
75
|
+
register_message_handler("my_app", "realtime", handle_client_message)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Note:** Register handlers **before** calling `engine.register_app()` or `engine.register_websocket_routes()`.
|
|
79
|
+
|
|
80
|
+
## Configuration Options
|
|
81
|
+
|
|
82
|
+
### Minimal Configuration (Recommended)
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"websockets": {
|
|
87
|
+
"realtime": {
|
|
88
|
+
"path": "/ws"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Defaults:**
|
|
95
|
+
- ✅ Authentication: Uses app's `auth_policy` (respects `auth_required` and `auth_policy.required`)
|
|
96
|
+
- ✅ Ping interval: 30 seconds
|
|
97
|
+
- ✅ Automatic route registration
|
|
98
|
+
- ✅ App-level isolation (secure by default)
|
|
99
|
+
|
|
100
|
+
### Advanced Configuration
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"websockets": {
|
|
105
|
+
"realtime": {
|
|
106
|
+
"path": "/ws",
|
|
107
|
+
"description": "Real-time updates",
|
|
108
|
+
"auth": {
|
|
109
|
+
"required": true,
|
|
110
|
+
"allow_anonymous": false
|
|
111
|
+
},
|
|
112
|
+
"ping_interval": 30
|
|
113
|
+
},
|
|
114
|
+
"events": {
|
|
115
|
+
"path": "/events",
|
|
116
|
+
"auth": {
|
|
117
|
+
"required": false
|
|
118
|
+
},
|
|
119
|
+
"description": "Public event stream"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Security Features
|
|
126
|
+
|
|
127
|
+
### App-Level Isolation
|
|
128
|
+
|
|
129
|
+
- Each app has its own `WebSocketConnectionManager` instance
|
|
130
|
+
- Connections are automatically scoped to the app's `slug`
|
|
131
|
+
- Messages include `app_slug` in metadata to prevent cross-app leakage
|
|
132
|
+
- Broadcasts only reach clients connected to that specific app
|
|
133
|
+
|
|
134
|
+
### Authentication
|
|
135
|
+
|
|
136
|
+
- Integrates with mdb_engine's JWT authentication system
|
|
137
|
+
- Supports token via query parameter or cookie
|
|
138
|
+
- Respects app's `auth_policy` configuration
|
|
139
|
+
- Can be overridden per endpoint
|
|
140
|
+
|
|
141
|
+
### Connection Metadata
|
|
142
|
+
|
|
143
|
+
Each connection tracks:
|
|
144
|
+
- `app_slug`: Ensures isolation
|
|
145
|
+
- `user_id`: For user-specific filtering
|
|
146
|
+
- `user_email`: For logging and debugging
|
|
147
|
+
- `connected_at`: Connection timestamp
|
|
148
|
+
|
|
149
|
+
## Usage Examples
|
|
150
|
+
|
|
151
|
+
### Broadcasting (Server → Clients)
|
|
152
|
+
|
|
153
|
+
#### Basic Broadcast
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from mdb_engine.routing.websockets import broadcast_to_app
|
|
157
|
+
|
|
158
|
+
# After a database operation
|
|
159
|
+
await db.collection.insert_one(document)
|
|
160
|
+
await broadcast_to_app("my_app", {
|
|
161
|
+
"type": "document_created",
|
|
162
|
+
"data": {"id": str(document["_id"])}
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### User-Specific Broadcast
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# Send notification to specific user
|
|
170
|
+
await broadcast_to_app("my_app", {
|
|
171
|
+
"type": "notification",
|
|
172
|
+
"data": {"message": "Task completed"}
|
|
173
|
+
}, user_id=current_user_id)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Broadcast After CRUD Operations
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# After creating a document
|
|
180
|
+
result = await db.items.insert_one(item)
|
|
181
|
+
await broadcast_to_app("my_app", {
|
|
182
|
+
"type": "item_created",
|
|
183
|
+
"data": {"id": str(result.inserted_id), "item": item}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
# After updating
|
|
187
|
+
await db.items.update_one({"_id": item_id}, {"$set": updates})
|
|
188
|
+
await broadcast_to_app("my_app", {
|
|
189
|
+
"type": "item_updated",
|
|
190
|
+
"data": {"id": str(item_id), "updates": updates}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
# After deleting
|
|
194
|
+
await db.items.delete_one({"_id": item_id})
|
|
195
|
+
await broadcast_to_app("my_app", {
|
|
196
|
+
"type": "item_deleted",
|
|
197
|
+
"data": {"id": str(item_id)}
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Listening (Clients → Server)
|
|
202
|
+
|
|
203
|
+
#### Basic Message Handler
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from mdb_engine.routing.websockets import register_message_handler, broadcast_to_app
|
|
207
|
+
|
|
208
|
+
async def handle_realtime_messages(websocket, message):
|
|
209
|
+
"""Handle messages from WebSocket clients."""
|
|
210
|
+
msg_type = message.get("type")
|
|
211
|
+
|
|
212
|
+
if msg_type == "subscribe":
|
|
213
|
+
# Client subscribes to updates
|
|
214
|
+
channel = message.get("channel", "default")
|
|
215
|
+
await broadcast_to_app("my_app", {
|
|
216
|
+
"type": "subscribed",
|
|
217
|
+
"channel": channel
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
elif msg_type == "unsubscribe":
|
|
221
|
+
# Client unsubscribes
|
|
222
|
+
await broadcast_to_app("my_app", {
|
|
223
|
+
"type": "unsubscribed"
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
elif msg_type == "request_data":
|
|
227
|
+
# Client requests specific data
|
|
228
|
+
data_type = message.get("data_type")
|
|
229
|
+
# Fetch and send data
|
|
230
|
+
data = await fetch_data(data_type)
|
|
231
|
+
await broadcast_to_app("my_app", {
|
|
232
|
+
"type": "data_response",
|
|
233
|
+
"data_type": data_type,
|
|
234
|
+
"data": data
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
# Register before routes are registered
|
|
238
|
+
register_message_handler("my_app", "realtime", handle_realtime_messages)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Advanced: User-Aware Handler
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from mdb_engine.routing.websockets import register_message_handler, get_websocket_manager
|
|
245
|
+
|
|
246
|
+
async def handle_user_actions(websocket, message):
|
|
247
|
+
"""Handle user actions with authentication context."""
|
|
248
|
+
# Get connection metadata to identify user
|
|
249
|
+
manager = await get_websocket_manager("my_app")
|
|
250
|
+
connection = next(
|
|
251
|
+
(conn for conn in manager.active_connections if conn.websocket is websocket),
|
|
252
|
+
None
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if connection and connection.user_id:
|
|
256
|
+
user_id = connection.user_id
|
|
257
|
+
msg_type = message.get("type")
|
|
258
|
+
|
|
259
|
+
if msg_type == "update_preferences":
|
|
260
|
+
# Update user preferences
|
|
261
|
+
prefs = message.get("preferences")
|
|
262
|
+
await db.users.update_one(
|
|
263
|
+
{"_id": user_id},
|
|
264
|
+
{"$set": {"preferences": prefs}}
|
|
265
|
+
)
|
|
266
|
+
# Notify user of success
|
|
267
|
+
await broadcast_to_app("my_app", {
|
|
268
|
+
"type": "preferences_updated",
|
|
269
|
+
"user_id": user_id
|
|
270
|
+
}, user_id=user_id)
|
|
271
|
+
|
|
272
|
+
register_message_handler("my_app", "realtime", handle_user_actions)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Error Handling in Handlers
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
async def safe_message_handler(websocket, message):
|
|
279
|
+
"""Handler with error handling."""
|
|
280
|
+
try:
|
|
281
|
+
msg_type = message.get("type")
|
|
282
|
+
if msg_type == "action":
|
|
283
|
+
# Process action
|
|
284
|
+
result = await process_action(message.get("action"))
|
|
285
|
+
await broadcast_to_app("my_app", {
|
|
286
|
+
"type": "action_result",
|
|
287
|
+
"result": result
|
|
288
|
+
})
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Error handling message: {e}")
|
|
291
|
+
# Send error to client
|
|
292
|
+
manager = await get_websocket_manager("my_app")
|
|
293
|
+
await manager.send_to_connection(websocket, {
|
|
294
|
+
"type": "error",
|
|
295
|
+
"message": str(e)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
register_message_handler("my_app", "realtime", safe_message_handler)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Connection Management
|
|
302
|
+
|
|
303
|
+
#### Get Connection Manager
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from mdb_engine.routing.websockets import get_websocket_manager
|
|
307
|
+
|
|
308
|
+
manager = await get_websocket_manager("my_app")
|
|
309
|
+
connection_count = manager.get_connection_count()
|
|
310
|
+
user_connections = manager.get_connections_by_user("user123")
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### Check Connection Status
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
manager = await get_websocket_manager("my_app")
|
|
317
|
+
|
|
318
|
+
# Total connections
|
|
319
|
+
total = manager.get_connection_count()
|
|
320
|
+
|
|
321
|
+
# Connections for specific user
|
|
322
|
+
user_conns = manager.get_connections_by_user("user123")
|
|
323
|
+
user_count = manager.get_connection_count_by_user("user123")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Send to Specific Connection
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
manager = await get_websocket_manager("my_app")
|
|
330
|
+
|
|
331
|
+
# Find user's connection
|
|
332
|
+
user_connections = manager.get_connections_by_user("user123")
|
|
333
|
+
if user_connections:
|
|
334
|
+
await manager.send_to_connection(
|
|
335
|
+
user_connections[0].websocket,
|
|
336
|
+
{"type": "personal_message", "data": "Hello!"}
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Automatic Route Registration
|
|
341
|
+
|
|
342
|
+
When you call `engine.register_app(manifest)`, WebSocket routes are automatically registered:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from mdb_engine import MongoDBEngine
|
|
346
|
+
from mdb_engine.routing.websockets import register_message_handler
|
|
347
|
+
|
|
348
|
+
# 1. Register message handlers FIRST (before route registration)
|
|
349
|
+
async def handle_messages(websocket, message):
|
|
350
|
+
# Your handler logic
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
register_message_handler("my_app", "realtime", handle_messages)
|
|
354
|
+
|
|
355
|
+
# 2. Initialize engine and register app
|
|
356
|
+
engine = MongoDBEngine(mongo_uri="...", db_name="...")
|
|
357
|
+
await engine.initialize()
|
|
358
|
+
|
|
359
|
+
manifest = await engine.load_manifest("manifest.json")
|
|
360
|
+
await engine.register_app(manifest) # WebSocket config loaded here
|
|
361
|
+
|
|
362
|
+
# 3. Register routes with FastAPI app (routes are created here)
|
|
363
|
+
engine.register_websocket_routes(app, manifest["slug"])
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Important:** Register message handlers **before** calling `register_websocket_routes()` so handlers are available when routes are created.
|
|
367
|
+
|
|
368
|
+
## Two-Way Communication
|
|
369
|
+
|
|
370
|
+
MDB_ENGINE WebSockets support **both directions**:
|
|
371
|
+
|
|
372
|
+
### Server → Clients (Broadcasting)
|
|
373
|
+
- Use `broadcast_to_app()` to send messages to clients
|
|
374
|
+
- Messages are automatically scoped to the app
|
|
375
|
+
- Can filter by user_id for targeted messages
|
|
376
|
+
- Perfect for: real-time updates, notifications, data changes
|
|
377
|
+
|
|
378
|
+
### Clients → Server (Listening)
|
|
379
|
+
- Register handlers with `register_message_handler()`
|
|
380
|
+
- Handlers process incoming client messages
|
|
381
|
+
- Can respond with broadcasts or direct messages
|
|
382
|
+
- Perfect for: subscriptions, user actions, data requests
|
|
383
|
+
|
|
384
|
+
### Complete Example
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from mdb_engine.routing.websockets import (
|
|
388
|
+
register_message_handler,
|
|
389
|
+
broadcast_to_app
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# 1. Register handler to LISTEN to client messages
|
|
393
|
+
async def handle_client_requests(websocket, message):
|
|
394
|
+
msg_type = message.get("type")
|
|
395
|
+
|
|
396
|
+
if msg_type == "get_status":
|
|
397
|
+
# Client requests status - respond with broadcast
|
|
398
|
+
status = await get_current_status()
|
|
399
|
+
await broadcast_to_app("my_app", {
|
|
400
|
+
"type": "status_update",
|
|
401
|
+
"data": status
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
elif msg_type == "subscribe_channel":
|
|
405
|
+
# Client subscribes to channel
|
|
406
|
+
channel = message.get("channel")
|
|
407
|
+
await subscribe_user_to_channel(message.get("user_id"), channel)
|
|
408
|
+
|
|
409
|
+
register_message_handler("my_app", "realtime", handle_client_requests)
|
|
410
|
+
|
|
411
|
+
# 2. BROADCAST messages from your app code
|
|
412
|
+
async def on_document_created(document):
|
|
413
|
+
await broadcast_to_app("my_app", {
|
|
414
|
+
"type": "document_created",
|
|
415
|
+
"data": {"id": str(document["_id"])}
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Best Practices
|
|
420
|
+
|
|
421
|
+
1. **Keep manifest simple**: Only specify `path` in manifest.json - defaults are secure
|
|
422
|
+
2. **Use `broadcast_to_app()`**: Simplest way to send messages to clients
|
|
423
|
+
3. **Register handlers early**: Register message handlers before route registration
|
|
424
|
+
4. **Respect auth_policy**: Let the engine handle authentication automatically
|
|
425
|
+
5. **Scope messages**: Always include `app_slug` (automatically added by engine)
|
|
426
|
+
6. **Filter by user**: Use `user_id` parameter for user-specific messages
|
|
427
|
+
7. **Handle errors**: Wrap handler logic in try/except and send error responses
|
|
428
|
+
8. **Use message types**: Structure messages with `type` field for easy routing
|
|
429
|
+
|
|
430
|
+
## Architecture
|
|
431
|
+
|
|
432
|
+
- **Isolation**: Each app has isolated WebSocket manager (no cross-app access)
|
|
433
|
+
- **Security**: Messages automatically scoped to app, authentication integrated
|
|
434
|
+
- **Simplicity**: Just declare in manifest.json, register handlers in code
|
|
435
|
+
- **Flexibility**: Full two-way communication (broadcast + listen)
|
|
436
|
+
- **Automatic**: Routes registered automatically during app registration
|
|
437
|
+
|
|
438
|
+
## Message Flow
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
Client Server App Code
|
|
442
|
+
| | |
|
|
443
|
+
|--- WebSocket Connect -->| |
|
|
444
|
+
| |--- Authenticate ------->|
|
|
445
|
+
|<-- Connection Confirmed-| |
|
|
446
|
+
| | |
|
|
447
|
+
|--- Message: {"type": | |
|
|
448
|
+
| "subscribe"} ------>| |
|
|
449
|
+
| |--- Handler Called ------>|
|
|
450
|
+
| | |
|
|
451
|
+
| |<-- broadcast_to_app() ---|
|
|
452
|
+
|<-- Broadcast Message ---| |
|
|
453
|
+
| | |
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Security Considerations
|
|
457
|
+
|
|
458
|
+
1. **App Isolation**: Each app's WebSocket manager is completely isolated
|
|
459
|
+
2. **Authentication**: Uses app's `auth_policy` by default
|
|
460
|
+
3. **Message Scoping**: All messages include `app_slug` automatically
|
|
461
|
+
4. **User Context**: Connection metadata tracks user_id and user_email
|
|
462
|
+
5. **Error Handling**: Handler errors don't crash the connection
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Route registration and WebSocket support.
|
|
3
|
+
|
|
4
|
+
Handles FastAPI route mounting, middleware configuration, and WebSocket endpoints.
|
|
5
|
+
|
|
6
|
+
WebSocket support is OPTIONAL and only enabled when:
|
|
7
|
+
1. Apps define "websockets" in their manifest.json
|
|
8
|
+
2. WebSocket dependencies are available (FastAPI WebSocket support)
|
|
9
|
+
|
|
10
|
+
If WebSockets are not configured or dependencies are missing, the engine
|
|
11
|
+
gracefully degrades without WebSocket functionality.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# WebSocket support is optional - lazy import to avoid breaking if dependencies missing
|
|
15
|
+
_websockets_available = None
|
|
16
|
+
_websockets_module = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_websockets_available():
|
|
20
|
+
"""Check if WebSocket support is available."""
|
|
21
|
+
global _websockets_available, _websockets_module
|
|
22
|
+
if _websockets_available is None:
|
|
23
|
+
try:
|
|
24
|
+
from fastapi import WebSocket
|
|
25
|
+
|
|
26
|
+
_websockets_module = __import__(
|
|
27
|
+
".websockets", fromlist=[""], package=__name__
|
|
28
|
+
)
|
|
29
|
+
_websockets_available = True
|
|
30
|
+
except (ImportError, AttributeError):
|
|
31
|
+
_websockets_available = False
|
|
32
|
+
_websockets_module = None
|
|
33
|
+
return _websockets_available
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_websocket_attr(name):
|
|
37
|
+
"""Lazy getter for WebSocket attributes - raises ImportError if not available."""
|
|
38
|
+
if not _check_websockets_available():
|
|
39
|
+
raise ImportError(
|
|
40
|
+
"WebSocket support is not available. "
|
|
41
|
+
"WebSockets must be defined in manifest.json and FastAPI "
|
|
42
|
+
"WebSocket support must be installed."
|
|
43
|
+
)
|
|
44
|
+
return getattr(_websockets_module, name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Lazy exports - only available if WebSockets are configured and dependencies exist
|
|
48
|
+
def __getattr__(name):
|
|
49
|
+
"""Lazy attribute access for WebSocket exports."""
|
|
50
|
+
if name in [
|
|
51
|
+
"WebSocketConnectionManager",
|
|
52
|
+
"WebSocketConnection",
|
|
53
|
+
"get_websocket_manager",
|
|
54
|
+
"create_websocket_endpoint",
|
|
55
|
+
"authenticate_websocket",
|
|
56
|
+
"broadcast_to_app",
|
|
57
|
+
"register_message_handler",
|
|
58
|
+
"get_message_handler",
|
|
59
|
+
]:
|
|
60
|
+
return _get_websocket_attr(name)
|
|
61
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"WebSocketConnectionManager",
|
|
66
|
+
"WebSocketConnection",
|
|
67
|
+
"get_websocket_manager",
|
|
68
|
+
"create_websocket_endpoint",
|
|
69
|
+
"authenticate_websocket",
|
|
70
|
+
"broadcast_to_app", # Simplest way to broadcast from app code
|
|
71
|
+
"register_message_handler", # Register handlers to listen to client messages
|
|
72
|
+
"get_message_handler",
|
|
73
|
+
]
|