Context
After #88 drops the ASGI bridge, OxyRoute has no WebSocket support. RSGI exposes a first-class WebSocket protocol (RSGIWebsocketProtocol with accept / close and RSGIWebsocketTransport with receive / send_str / send_bytes); we want to dispatch WS connections through the same Rust router as HTTP.
Goal
- Register WebSocket routes via
@app.websocket(path).
- When
scope.proto == "websocket" the native dispatcher resolves the path against a separate router and either runs the user handler with a thin WebSocket helper or closes 1000.
- Keep the helper minimal (
accept / receive / receive_text / receive_bytes / send_text / send_bytes / close); subprotocols and close codes follow Granian's RSGI semantics.
Scope
src/state.rs: add WS route table (ws_routes, ws_router plus compiled snapshot).
src/lib.rs: add add_websocket_route(path, handler, is_async).
src/dispatch.rs: handle proto == "websocket" (route match → call handler with WebSocket(protocol) or close 1000).
oxyroute/websocket.py: WebSocket helper class wrapping the RSGI protocol/transport.
oxyroute/app.py: @app.websocket(path) decorator delegating to native registration.
oxyroute/__init__.py: export WebSocket.
tests/test_websocket_native.py: connect / accept / echo / close / disconnect-before-accept (Granian e2e).
docs/websocket.md: native RSGI guide.
Acceptance criteria
- Granian e2e test passes against
--interface rsgi over ws:// .
- Helper covers the common cases without leaking RSGI internals.
- README/feature.md updated to mark WebSocket as supported.
Depends on
Context
After #88 drops the ASGI bridge, OxyRoute has no WebSocket support. RSGI exposes a first-class WebSocket protocol (
RSGIWebsocketProtocolwithaccept/closeandRSGIWebsocketTransportwithreceive/send_str/send_bytes); we want to dispatch WS connections through the same Rust router as HTTP.Goal
@app.websocket(path).scope.proto == "websocket"the native dispatcher resolves the path against a separate router and either runs the user handler with a thinWebSockethelper or closes 1000.accept/receive/receive_text/receive_bytes/send_text/send_bytes/close); subprotocols and close codes follow Granian's RSGI semantics.Scope
src/state.rs: add WS route table (ws_routes,ws_routerplus compiled snapshot).src/lib.rs: addadd_websocket_route(path, handler, is_async).src/dispatch.rs: handleproto == "websocket"(route match → call handler withWebSocket(protocol)or close 1000).oxyroute/websocket.py:WebSockethelper class wrapping the RSGI protocol/transport.oxyroute/app.py:@app.websocket(path)decorator delegating to native registration.oxyroute/__init__.py: exportWebSocket.tests/test_websocket_native.py: connect / accept / echo / close / disconnect-before-accept (Granian e2e).docs/websocket.md: native RSGI guide.Acceptance criteria
--interface rsgiover ws:// .Depends on