Skip to content

perf!: yoke zero-copy node decoding and Jid Server enum - #513

Merged
jlucaso1 merged 2 commits into
mainfrom
perf/yoke-zero-copy-and-server-enum
Apr 12, 2026
Merged

perf!: yoke zero-copy node decoding and Jid Server enum#513
jlucaso1 merged 2 commits into
mainfrom
perf/yoke-zero-copy-and-server-enum

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three major performance optimizations that eliminate allocation overhead in the node decode path:

  1. Jid Server enum: Replace Jid { server: Cow<'static, str> } with a Server u8 enum. Shrinks Jid from 56→32 bytes. Server comparisons become u8==u8 instead of string comparison.

  2. Yoke zero-copy decoding: Replace Arc<Node> with Arc<OwnedNodeRef> throughout the entire handler chain. OwnedNodeRef wraps Yoke<NodeRef<'static>, Vec<u8>>, keeping the decoded NodeRef borrowing directly from the decompressed buffer — zero allocation for attribute keys, values, and byte content.

  3. Full NodeRef migration + API ergonomics: Flipped the ProtocolNode trait so try_from_node_ref(&NodeRef) is the required method. Unified API naming — NodeRef::attrs() matches Node::attrs(), ValueRef::as_str() returns Cow<str> like NodeValue::as_str(). Added delegation methods to OwnedNodeRef so handlers can call node.attrs() directly without .get().


Breaking Changes

1. Jid.server: Cow<'static, str>Server enum

Type change: Jid { server: Cow<'static, str> }Jid { server: Server }
Same for: JidRef { server: Cow<'a, str> }JidRef { server: Server }

// Before
Jid::new("user", "s.whatsapp.net")
if jid.server == "s.whatsapp.net" { ... }

// After
Jid::new("user", Server::Pn)
if jid.server == Server::Pn { ... }
// Or use helpers:
if jid.is_pn() { ... }
  • cow_server_from_str() deleted — use Server::try_from(s) instead
  • Unknown servers rejected at parse time (JidError)
  • Server serializes as string ("s.whatsapp.net") for backward-compatible JSON
  • Variants: Pn, Lid, Group, Broadcast, Newsletter, Hosted, HostedLid, Messenger, Interop, Bot, Legacy

2. Handler trait: Arc<Node>Arc<OwnedNodeRef>

// Before
async fn handle(&self, client: Arc<Client>, node: Arc<Node>, ...) -> bool {
    let from = node.attrs().optional_jid("from");
}

// After — OwnedNodeRef has delegation methods, no .get() needed for common ops
async fn handle(&self, client: Arc<Client>, node: Arc<OwnedNodeRef>, ...) -> bool {
    let from = node.attrs().optional_jid("from");  // same ergonomics!
}

OwnedNodeRef delegates: tag(), attrs(), get_attr(), children(), get_optional_child(), get_optional_child_by_tag(), get_children_by_tag(), content_bytes(), content_str(), content_nodes().

Use .get() only when you need the full &NodeRef (e.g., passing to functions).

3. Unified API — NodeRef matches Node

Operation Node NodeRef OwnedNodeRef
Attr parser node.attrs() node.attrs() node.attrs()
Get attribute node.attrs.get("k") node.get_attr("k") node.get_attr("k")
Children node.children() node.children() node.children()
Find child node.get_optional_child("t") same same
Byte content node.content → NodeContent::Bytes node.content_bytes() node.content_bytes()
String content node.content_as_string() node.content_str() node.content_str()

4. ValueRef::as_str() now matches NodeValue::as_str()

// Before (footgun — returned None for JID attributes):
let from = node.get_attr("from").and_then(|v| v.as_str()); // None for JIDs!

// After — returns Cow<str> for both String AND JID variants:
let from = node.get_attr("from").map(|v| v.as_str()); // Always works

to_string_cow() deleted — as_str() does the same thing now.

5. Event types

// Before
Event::Notification(Node)          // deep-cloned on dispatch
Event::RawNode(Arc<Node>)

// After
Event::Notification(Arc<OwnedNodeRef>)  // cheap Arc clone, #[serde(skip)]
Event::RawNode(Arc<OwnedNodeRef>)       // #[serde(skip)]

6. ProtocolNode trait flipped

// Before — try_from_node was required
fn try_from_node(node: &Node) -> Result<Self>;

// After — try_from_node_ref is required
fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self>;
fn try_from_node(node: &Node) -> Result<Self> { /* default: delegates */ }

7. IqSpec::parse_response

// Before
fn parse_response(&self, response: &Node) -> Result<Self::Response>;
// After
fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response>;

8. Helper functions renamed (dropped _ref suffix)

All iq::node helpers take &NodeRef as canonical signature:
required_child, optional_child, required_attr, optional_attr, collect_children

Stanza parsers:
DeviceNotification::try_parse, BusinessNotification::try_parse, parse_lid_mappings_from_response, parse_prekeys_response

9. New public re-exports

whatsapp_rust::{Server, OwnedNodeRef, CompactString, Jid}

wacore_binary::{Server, Jid, JidRef, JidExt, Node, NodeRef, NodeValue, OwnedNodeRef, NodeContent, NodeContentRef, Attrs, CompactString, AttrParser, AttrParserRef, DeviceKey, ...}


What's zero-copy and what's not

Fully zero-copy (no allocation on decode path):

  • decrypt_frameOwnedNodeRef (yoke wraps decompressed buffer)
  • Handler dispatch via Arc<OwnedNodeRef> (cheap refcount)
  • All 33 IQ spec parse_response implementations
  • Message processing (parse_message_info, encryption, routing)
  • Receipt, notification, IB handling
  • Node waiters and event dispatch

Structurally required to_owned() (7 calls):

  • GroupNotificationAction::Create/Link/Unlink { raw: Node } — struct field
  • StreamError.raw / ConnectFailure.raw — event fields
  • IqError::Disconnected(Node)'static error type
  • enc_node clone crossing async task boundary

Test plan

  • cargo fmt --all
  • cargo clippy --all --tests — zero errors, zero warnings
  • cargo test -p wacore-binary -p wacore -p whatsapp-rust — all passing
  • cargo check -p wacore-binary --all-features — serde verified
  • CI / e2e tests

Summary by CodeRabbit

  • New Features

    • More efficient, zero-copy message/node handling and a new owned node-ref type for faster processing and lower memory use.
    • Exposed typed server identifier alongside JID for clearer address handling.
  • Bug Fixes

    • Stricter JID/server validation to reject invalid server identifiers.
  • Refactor

    • Large migration from owned payloads to reference-based parsing and unified server/address handling across the codebase.
  • Tests

    • Updated tests and benchmarks to match new APIs and parsing behavior.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant