From 398c90fc8b978a3ac59fd5ce639927fc97b781b8 Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Sun, 26 Jul 2026 19:28:12 -0400 Subject: [PATCH] django: a context bundle over the socket The socket carried rpc but not ctx, so a socket transport could not fetch a context and every app needed an HTTP connection beside it. Dispatches through the same execute_context the HTTP view calls; that view's origin-side cache is a CDN concern with nothing in front of a socket, so it is not on this path. Both backends now answer subscribe/unsubscribe/message/rpc/ctx on one envelope. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/mizan/channels/connection.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/backends/mizan-django/src/mizan/channels/connection.py b/backends/mizan-django/src/mizan/channels/connection.py index 29f7bc4..6ffeec1 100644 --- a/backends/mizan-django/src/mizan/channels/connection.py +++ b/backends/mizan-django/src/mizan/channels/connection.py @@ -12,6 +12,7 @@ Protocol: # RPC calls (server functions) {"action": "rpc", "id": "request-id", "fn": "function_name", "args": {...}} + {"action": "ctx", "id": "request-id", "context": "name", "params": {...}} Server sends: # Channel messages @@ -167,6 +168,8 @@ class DjangoReactConsumer(AsyncJsonWebsocketConsumer): await self._handle_message(content) elif action == "rpc": await self._handle_rpc(content) + elif action == "ctx": + await self._handle_ctx(content) else: await self.send_json( { @@ -503,6 +506,61 @@ class DjangoReactConsumer(AsyncJsonWebsocketConsumer): await self.send_json({"id": request_id, "ok": True, "data": data}) + async def _handle_ctx(self, content: dict): + """ + Handle a context-bundle fetch. + + Protocol: + Request: {"action": "ctx", "id": "request-id", "context": "name", "params": {...}} + Response: {"id": "request-id", "ok": true, "data": {fn_name: result, ...}} + or: {"id": "request-id", "ok": false, "error": {...}} + + Dispatches through the same execute_context the HTTP view calls. That view's + origin-side cache is a CDN concern with nothing in front of a socket, so it is not + part of this path. + """ + from mizan.client.executor import execute_context, FunctionError + + request_id = content.get("id") + context_name = content.get("context") + + if not request_id: + await self.send_json({"error": "ctx request missing 'id' field"}) + return + + if not context_name: + await self.send_json( + { + "id": request_id, + "ok": False, + "error": {"code": "BAD_REQUEST", "message": "Missing 'context' field"}, + } + ) + return + + ws_request = WebSocketRequest( + self.scope, channel_name=getattr(self, "channel_name", None) + ) + result = await sync_to_async(execute_context, thread_sensitive=True)( + ws_request, context_name, content.get("params") or {} + ) + + if isinstance(result, FunctionError): + await self.send_json( + { + "id": request_id, + "ok": False, + "error": { + "code": result.code.value, + "message": result.message, + **({"details": result.details} if result.details else {}), + }, + } + ) + return + + await self.send_json({"id": request_id, "ok": True, "data": result.data}) + async def channel_message(self, event: dict): """ Handle messages broadcast to a group.