feat: support binary WebSocket frames in message handlers#1334
feat: support binary WebSocket frames in message handlers#1334
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds native binary WebSocket frame support throughout the stack. The Python adapter's core API shifts from text-only Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Actix as Actix-web<br/>(WebSocket)
participant Rust as Rust Layer<br/>(mod.rs)
participant Connector as WebSocketConnector<br/>(Actor)
participant PyQueue as Python<br/>Message Queue
participant Handler as Python Handler
participant Adapter as WebSocketAdapter
rect rgba(100, 150, 200, 0.5)
Note over Client,Adapter: Binary Frame Reception Flow (New)
Client->>Actix: Send binary frame
Actix->>Rust: ws::Message::Binary(bytes)
Rust->>Rust: Wrap as WsPayload::Binary
Rust->>PyQueue: Enqueue WsPayload::Binary
Rust->>Connector: (inbound complete)
end
rect rgba(100, 150, 200, 0.5)
Note over Client,Adapter: Handler Invocation & Dispatch
Handler->>Adapter: call receive()
Adapter->>PyQueue: dequeue WsPayload
PyQueue->>Adapter: return WsPayload::Binary(bytes)
Adapter->>Handler: return bytes
Handler->>Handler: isinstance(msg, bytes) → process audio
Handler->>Adapter: call send(binary_data)
end
rect rgba(100, 150, 200, 0.5)
Note over Client,Adapter: Binary Frame Transmission Flow (New)
Adapter->>Rust: send(bytes)
Rust->>Connector: SendMessage{payload: WsPayload::Binary}
Connector->>Actix: Handler processes SendMessage
Actix->>Actix: ctx.binary(bytes)
Actix->>Client: Send binary frame
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/executors/web_socket_executors.rs (1)
9-20: Silent fallback toNonefor unsupported return types may mask handler bugs.When a Python handler returns an unsupported type (e.g.,
int,dict,list),extract_ws_returnsilently returnsNone, resulting in no WebSocket frame being sent. This could mask programming errors in user handlers where they accidentally return an unexpected type.Consider logging a warning for the fallback case to aid debugging:
🔧 Proposed enhancement to add a debug warning
fn extract_ws_return(_py: Python, output: &Bound<'_, PyAny>) -> Option<WsPayload> { if output.is_none() { return None; } if let Ok(s) = output.extract::<String>() { Some(WsPayload::Text(s)) } else if let Ok(b) = output.extract::<Vec<u8>>() { Some(WsPayload::Binary(b)) } else { + log::debug!( + "WebSocket handler returned unsupported type '{}'; no frame sent", + output.get_type().name().unwrap_or_default() + ); None } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/executors/web_socket_executors.rs` around lines 9 - 20, extract_ws_return currently silently returns None for unsupported Python return types, which can hide handler bugs; update extract_ws_return to log a warning (including the Python object's type and/or repr) when neither String nor Vec<u8> can be extracted so developers can see unexpected return values from their handlers. Locate the extract_ws_return function and add a warning/emitter (e.g., using the existing logger/tracing facility or a new warn! call) inside the final else branch that reports the Python object's type and a brief repr, then still return None to preserve behavior. Ensure the log text references extract_ws_return and the handler context to make debugging straightforward.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@robyn/ws.py`:
- Around line 68-86: Update the type stubs in robyn.pyi so the websocket
send/broadcast methods accept both str and bytes: change the parameter type of
async_broadcast, async_send_to, sync_broadcast, and sync_send_to from just str
to str | bytes (or Union[str, bytes] if using typing.Union in stubs), ensuring
you import Union if needed and keep existing return types and overloads intact
so the stubs match the implementations used by ws.py (methods async_send_to,
async_broadcast, send_text, send_bytes, send_json, broadcast).
In `@src/websockets/mod.rs`:
- Around line 215-239: The async_send_to method uses Uuid::parse_str(...) and
currently calls unwrap(), which can panic on invalid UUID input; replace that
unwrap with safe parsing that returns a Python error instead of panicking:
validate the recipient_id by matching Uuid::parse_str(&recipient_id) (or using
map_err) and on Err convert the uuid parse error into a PyErr (e.g.,
PyErr::new::<pyo3::exceptions::PyValueError, _>(...)) and return Err(PyErr) from
async_send_to; keep the rest of the function (payload extraction,
registry.try_send, sender_id) unchanged so the method returns a PyResult without
panics.
- Around line 200-212: In sync_send_to, avoid panicking by replacing
Uuid::parse_str(&recipient_id).unwrap() with proper error handling: call
Uuid::parse_str(&recipient_id).map_err(|e| PyErr::new_err(format!("Invalid
recipient_id UUID: {}", e)))? to return a Python error instead of panicking;
keep the parsed Uuid in the same recipient_id variable and proceed with the
existing match that sends SendMessage via self.registry_addr.try_send. Ensure
the function returns Err(PyErr) on parse failure so malformed UUIDs are reported
to Python rather than crashing the process.
---
Nitpick comments:
In `@src/executors/web_socket_executors.rs`:
- Around line 9-20: extract_ws_return currently silently returns None for
unsupported Python return types, which can hide handler bugs; update
extract_ws_return to log a warning (including the Python object's type and/or
repr) when neither String nor Vec<u8> can be extracted so developers can see
unexpected return values from their handlers. Locate the extract_ws_return
function and add a warning/emitter (e.g., using the existing logger/tracing
facility or a new warn! call) inside the final else branch that reports the
Python object's type and a brief repr, then still return None to preserve
behavior. Ensure the log text references extract_ws_return and the handler
context to make debugging straightforward.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bdd2cff9-cfa1-47cd-90c4-e77f296c3e4c
📒 Files selected for processing (4)
robyn/ws.pysrc/executors/web_socket_executors.rssrc/websockets/mod.rssrc/websockets/registry.rs
f8d2452 to
f5dd683
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/websockets/mod.rs`:
- Around line 28-33: The WsPayload enum's Binary variant currently uses Vec<u8>,
forcing owned copies through the bridge and causing extra clones in the
broadcast path; change WsPayload::Binary to use a zero-copy buffer type (e.g.,
bytes::Bytes or Arc<[u8]>) and update the inbound frame handling (the code
around the binary frame copy at line ~193) to construct that zero-copy type
instead of allocating a Vec, and then update the broadcast/registry logic in
registry.rs (the code that currently clones the payload per recipient at lines
~87-97) to pass or cheaply clone the zero-copy buffer (Bytes::clone or
Arc::clone) so broadcasts no longer perform full buffer copies. Ensure type
changes propagate to any match arms or function signatures that pattern-match on
WsPayload::Binary.
- Line 99: The code currently creates a bounded channel (let (tx, rx) =
mpsc::channel::<Option<WsPayload>>(256)) but elsewhere uses non-blocking sends
that silently drop frames when the buffer is full (lines around the
try_send/drop logic at the send site), which loses payloads; change the
send-side to preserve backpressure or fail fast: replace non-blocking
try_send/drop logic with an awaitable send (tx.send(Some(payload)).await) so the
task pauses until space is available, or if you prefer fail-fast, detect the
full/closed condition and close the websocket/return an error (handle
mpsc::error::SendError) rather than discarding frames; use the tx, rx and
WsPayload symbols to locate the sender/receiver and update send handling and
error/close logic accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8c40f335-5016-46b6-bd94-1f3ff7f9f52f
📒 Files selected for processing (8)
.github/workflows/release-CI.ymldocs_src/src/pages/documentation/en/api_reference/websockets.mdxdocs_src/src/pages/documentation/zh/api_reference/websockets.mdxrobyn/robyn.pyirobyn/ws.pysrc/executors/web_socket_executors.rssrc/websockets/mod.rssrc/websockets/registry.rs
✅ Files skipped from review due to trivial changes (1)
- docs_src/src/pages/documentation/en/api_reference/websockets.mdx
🚧 Files skipped from review as they are similar to previous changes (3)
- .github/workflows/release-CI.yml
- src/websockets/registry.rs
- robyn/ws.py
| #[derive(Clone)] | ||
| pub enum WsPayload { | ||
| Text(String), | ||
| Binary(Vec<u8>), | ||
| Close, | ||
| } |
There was a problem hiding this comment.
Vec<u8> prevents the new binary path from being zero-copy.
WsPayload::Binary(Vec<u8>) forces owned buffers through the bridge. Line 193 already copies each inbound binary frame into a fresh Vec, and src/websockets/registry.rs Lines 87-97 clone that payload again for each broadcast recipient, so the zero-copy part of #1332 still is not met for audio/streaming workloads.
Also applies to: 190-193
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/websockets/mod.rs` around lines 28 - 33, The WsPayload enum's Binary
variant currently uses Vec<u8>, forcing owned copies through the bridge and
causing extra clones in the broadcast path; change WsPayload::Binary to use a
zero-copy buffer type (e.g., bytes::Bytes or Arc<[u8]>) and update the inbound
frame handling (the code around the binary frame copy at line ~193) to construct
that zero-copy type instead of allocating a Vec, and then update the
broadcast/registry logic in registry.rs (the code that currently clones the
payload per recipient at lines ~87-97) to pass or cheaply clone the zero-copy
buffer (Bytes::clone or Arc::clone) so broadcasts no longer perform full buffer
copies. Ensure type changes propagate to any match arms or function signatures
that pattern-match on WsPayload::Binary.
src/websockets/mod.rs
Outdated
| }); | ||
|
|
||
| let (tx, rx) = mpsc::unbounded_channel::<Option<String>>(); | ||
| let (tx, rx) = mpsc::channel::<Option<WsPayload>>(256); |
There was a problem hiding this comment.
Don't turn backpressure into silent frame loss.
Line 99 caps the Python-side queue at 256 entries, and Lines 185-194 drop frames once it fills. Slow consumers will start missing text/binary messages with no signal to the handler, which is especially dangerous for ordered binary streams like audio. Please preserve queueing semantics or close on overload instead of discarding payloads.
Also applies to: 184-195
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/websockets/mod.rs` at line 99, The code currently creates a bounded
channel (let (tx, rx) = mpsc::channel::<Option<WsPayload>>(256)) but elsewhere
uses non-blocking sends that silently drop frames when the buffer is full (lines
around the try_send/drop logic at the send site), which loses payloads; change
the send-side to preserve backpressure or fail fast: replace non-blocking
try_send/drop logic with an awaitable send (tx.send(Some(payload)).await) so the
task pauses until space is available, or if you prefer fail-fast, detect the
full/closed condition and close the websocket/return an error (handle
mpsc::error::SendError) rather than discarding frames; use the tx, rx and
WsPayload symbols to locate the sender/receiver and update send handling and
error/close logic accordingly.
- Add WsPayload enum (Text/Binary) to replace String-only messages - Update WebSocketChannel.receive() to return str for text, bytes for binary - Add receive(), receive_bytes(), send(), send_bytes() to WebSocketAdapter - Update type stubs to accept str | bytes for send/broadcast methods - Replace Uuid::parse_str().unwrap() with proper PyValueError handling - Update broadcast() to accept str | bytes for binary frame support - Update WebSocket docs with binary frame examples Made-with: Cursor
f5dd683 to
4594e66
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/websockets/mod.rs (2)
98-98: Unbounded channel trades backpressure for memory risk.Switching to
unbounded_channeleliminates the silent frame loss flagged in a previous review, but a slow Python consumer can now cause unbounded memory growth. For most WebSocket workloads this is acceptable, but consider documenting this behavior or adding optional high-water-mark warnings for streaming use cases.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/websockets/mod.rs` at line 98, The current use of mpsc::unbounded_channel::<Option<WsPayload>>() (creating tx and rx) removes earlier silent drops but opens the door to unbounded memory growth if the Python consumer is slow; either document this tradeoff in websockets/mod.rs and the public API, or replace the unbounded channel with a bounded mpsc::channel(capacity) and handle full-channel backpressure (e.g., propagate errors, await send, or drop with a metric), and optionally add a high-water-mark check tied to tx/queue length that emits warnings via the existing logger when a threshold is exceeded to alert on streaming backpressure.
154-163: Extract "Connection closed" sentinel to a shared constant.The magic string
"Connection closed"is duplicated here and inregistry.rs(line 115). A typo in either location would silently break close detection.♻️ Proposed refactor
Add to a shared location (e.g.,
src/websockets/mod.rsor a constants module):pub const WS_CLOSE_SENTINEL: &str = "Connection closed";Then update both files:
// In mod.rs Handler<SendMessage> WsPayload::Text(s) => { ctx.text(s.clone()); - if s == "Connection closed" { + if s == WS_CLOSE_SENTINEL { ctx.stop(); } }// In registry.rs Handler<Close> client.do_send(SendMessage { recipient_id: msg.id, - payload: WsPayload::Text("Connection closed".to_string()), + payload: WsPayload::Text(WS_CLOSE_SENTINEL.to_string()), sender_id: msg.id, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/websockets/mod.rs` around lines 154 - 163, Introduce a single public constant (e.g., pub const WS_CLOSE_SENTINEL: &str = "Connection closed";) in the websockets module root and replace all hard-coded occurrences of the literal with that constant; specifically, in the match over msg.payload (WsPayload::Text branch) replace the ctx.text(s.clone())/s == "Connection closed" string usage with the constant (use it for sending and for the equality check), and update the matching usage in registry.rs to import and compare against the same WS_CLOSE_SENTINEL so both close detection and emitted text come from one shared symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/websockets/mod.rs`:
- Around line 240-250: The CI failure is due to rustfmt formatting of the
closure on the registry.try_send(...) call; open the block where
runtime::future_into_py(py, async move { ... }) is created (the code that
constructs SendMessage with payload, sender_id, recipient_id) and run cargo fmt
or reformat so the .map_err(|e| { ... }) closure is placed/indented according to
rustfmt (e.g., move .map_err to its own line and format the closure body),
ensuring runtime::future_into_py, registry.try_send, SendMessage and the map_err
closure are properly aligned.
- Around line 272-278: The closure formatting on the registry.try_send call
within the runtime::future_into_py async block doesn't match rustfmt; reformat
the map_err closure for SendMessageToAll so it aligns with rustfmt style (place
the .map_err on its own line and the closure body on the next line, returning
anyhow::anyhow! with the error), i.e., adjust the awaitable block around
runtime::future_into_py, registry.try_send, SendMessageToAll, and map_err to
follow rustfmt line/indentation conventions.
---
Nitpick comments:
In `@src/websockets/mod.rs`:
- Line 98: The current use of mpsc::unbounded_channel::<Option<WsPayload>>()
(creating tx and rx) removes earlier silent drops but opens the door to
unbounded memory growth if the Python consumer is slow; either document this
tradeoff in websockets/mod.rs and the public API, or replace the unbounded
channel with a bounded mpsc::channel(capacity) and handle full-channel
backpressure (e.g., propagate errors, await send, or drop with a metric), and
optionally add a high-water-mark check tied to tx/queue length that emits
warnings via the existing logger when a threshold is exceeded to alert on
streaming backpressure.
- Around line 154-163: Introduce a single public constant (e.g., pub const
WS_CLOSE_SENTINEL: &str = "Connection closed";) in the websockets module root
and replace all hard-coded occurrences of the literal with that constant;
specifically, in the match over msg.payload (WsPayload::Text branch) replace the
ctx.text(s.clone())/s == "Connection closed" string usage with the constant (use
it for sending and for the equality check), and update the matching usage in
registry.rs to import and compare against the same WS_CLOSE_SENTINEL so both
close detection and emitted text come from one shared symbol.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: df52e6ab-f153-4a80-b22c-4df25b597887
📒 Files selected for processing (7)
docs_src/src/pages/documentation/en/api_reference/websockets.mdxdocs_src/src/pages/documentation/zh/api_reference/websockets.mdxrobyn/robyn.pyirobyn/ws.pysrc/executors/web_socket_executors.rssrc/websockets/mod.rssrc/websockets/registry.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- src/executors/web_socket_executors.rs
- docs_src/src/pages/documentation/zh/api_reference/websockets.mdx
- robyn/robyn.pyi
- src/websockets/registry.rs
- robyn/ws.py
| let awaitable = runtime::future_into_py(py, async move { | ||
| match registry.try_send(SendText { | ||
| message, | ||
| sender_id, | ||
| recipient_id, | ||
| }) { | ||
| Ok(_) => log::debug!("Message sent successfully"), | ||
| Err(e) => log::error!("Failed to send message: {}", e), | ||
| } | ||
| Ok(()) | ||
| registry | ||
| .try_send(SendMessage { | ||
| payload, | ||
| sender_id, | ||
| recipient_id, | ||
| }) | ||
| .map_err(|e| { | ||
| anyhow::anyhow!("Failed to enqueue message to registry: {e}") | ||
| }) | ||
| })?; |
There was a problem hiding this comment.
Fix formatting to pass CI.
The pipeline fails because cargo fmt --check expects the map_err closure to be formatted differently.
🔧 Proposed fix (run `cargo fmt`)
let awaitable = runtime::future_into_py(py, async move {
- registry
- .try_send(SendMessage {
- payload,
- sender_id,
- recipient_id,
- })
- .map_err(|e| {
- anyhow::anyhow!("Failed to enqueue message to registry: {e}")
- })
+ registry.try_send(SendMessage {
+ payload,
+ sender_id,
+ recipient_id,
+ }).map_err(|e| anyhow::anyhow!("Failed to enqueue message to registry: {e}"))
})?;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let awaitable = runtime::future_into_py(py, async move { | |
| match registry.try_send(SendText { | |
| message, | |
| sender_id, | |
| recipient_id, | |
| }) { | |
| Ok(_) => log::debug!("Message sent successfully"), | |
| Err(e) => log::error!("Failed to send message: {}", e), | |
| } | |
| Ok(()) | |
| registry | |
| .try_send(SendMessage { | |
| payload, | |
| sender_id, | |
| recipient_id, | |
| }) | |
| .map_err(|e| { | |
| anyhow::anyhow!("Failed to enqueue message to registry: {e}") | |
| }) | |
| })?; | |
| let awaitable = runtime::future_into_py(py, async move { | |
| registry.try_send(SendMessage { | |
| payload, | |
| sender_id, | |
| recipient_id, | |
| }).map_err(|e| anyhow::anyhow!("Failed to enqueue message to registry: {e}")) | |
| })?; |
🧰 Tools
🪛 GitHub Actions: Rust Continuous integration
[error] 244-244: cargo fmt --check failed: formatting differs from rustfmt output (diff shows map_err closure condensed to a single line).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/websockets/mod.rs` around lines 240 - 250, The CI failure is due to
rustfmt formatting of the closure on the registry.try_send(...) call; open the
block where runtime::future_into_py(py, async move { ... }) is created (the code
that constructs SendMessage with payload, sender_id, recipient_id) and run cargo
fmt or reformat so the .map_err(|e| { ... }) closure is placed/indented
according to rustfmt (e.g., move .map_err to its own line and format the closure
body), ensuring runtime::future_into_py, registry.try_send, SendMessage and the
map_err closure are properly aligned.
| let awaitable = runtime::future_into_py(py, async move { | ||
| match registry.try_send(SendMessageToAll { message, sender_id }) { | ||
| Ok(_) => log::debug!("Broadcast sent successfully"), | ||
| Err(e) => log::error!("Failed to broadcast message: {}", e), | ||
| } | ||
| Ok(()) | ||
| registry | ||
| .try_send(SendMessageToAll { payload, sender_id }) | ||
| .map_err(|e| { | ||
| anyhow::anyhow!("Failed to enqueue broadcast to registry: {e}") | ||
| }) | ||
| })?; |
There was a problem hiding this comment.
Fix formatting to pass CI.
Same formatting issue as async_send_to - the map_err closure needs to conform to rustfmt output.
🔧 Proposed fix (run `cargo fmt`)
let awaitable = runtime::future_into_py(py, async move {
- registry
- .try_send(SendMessageToAll { payload, sender_id })
- .map_err(|e| {
- anyhow::anyhow!("Failed to enqueue broadcast to registry: {e}")
- })
+ registry.try_send(SendMessageToAll { payload, sender_id })
+ .map_err(|e| anyhow::anyhow!("Failed to enqueue broadcast to registry: {e}"))
})?;🧰 Tools
🪛 GitHub Actions: Rust Continuous integration
[error] 272-272: cargo fmt --check failed: formatting differs from rustfmt output (diff shows map_err closure condensed to a single line).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/websockets/mod.rs` around lines 272 - 278, The closure formatting on the
registry.try_send call within the runtime::future_into_py async block doesn't
match rustfmt; reformat the map_err closure for SendMessageToAll so it aligns
with rustfmt style (place the .map_err on its own line and the closure body on
the next line, returning anyhow::anyhow! with the error), i.e., adjust the
awaitable block around runtime::future_into_py, registry.try_send,
SendMessageToAll, and map_err to follow rustfmt line/indentation conventions.
Summary
Closes #1332
WsPayloadenum (Text(String)/Binary(Vec<u8>)) that replacesStringthroughout the entire WebSocket pipeline: channel, registry messages, send/broadcast methodsws::Message::Binary) are now forwarded through the Rust channel to Python instead of being silently echoed backchannel.receive()returnsstrfor text frames andbytesfor binary framessend_to/broadcastmethods accept bothstrandbytes, emitting the correct WebSocket frame type (ctx.text()/ctx.binary())WebSocketAdaptergains a newreceive()method returningstr | bytes, plusreceive_text()/receive_bytes()now validate frame type instead of faking conversionUsage
Files changed
src/websockets/mod.rsWsPayloadenum, channel typeOption<String>→Option<WsPayload>,StreamHandlerforwards binary, send methods accept&Bound<PyAny>src/websockets/registry.rsSendText→SendMessagewithpayload: WsPayload,SendMessageToAlllikewisesrc/executors/web_socket_executors.rsstrandbytes, dispatches toctx.text()/ctx.binary()robyn/ws.pyreceive()→str | bytes, realreceive_text()/receive_bytes()with type validation,send_bytes()sends actual binary framesTest plan
bytessend_bytes(), verify client receives binary opcodereceive_text()raisesTypeErrorwhen binary frame arrivesreceive_bytes()raisesTypeErrorwhen text frame arrivesbroadcast()with bothstrandbytesMade with Cursor
Summary by CodeRabbit
Release Notes
New Features
receive()method returns the next frame as its native type (str or bytes)send()method sends both text and binary payloadsbroadcast()now accepts both str and bytes payloadsDocumentation
receive_text()andreceive_bytes()