Overview
WhatsApp-Rust uses an event-driven architecture where the client emits events for all WhatsApp protocol interactions. Your application subscribes to these events to handle messages, connection changes, and notifications.Event system architecture
CoreEventBus
Location:wacore/src/types/events.rs
- Thread-safe event dispatching via
Arc<Event>— each event is wrapped once and shared across all handlers, eliminating deep clones - Multiple handlers supported
- Clone-cheap with
Arc
EventHandler Trait
Arc<Event> — a shared reference-counted pointer to the event. Since Arc<Event> implements Deref<Target = Event>, you can pattern-match on it directly.
Implementation:
Typed event interest (skip boxing unwanted events)
By default a handler receives every event. If you only care about a few kinds, overrideinterest() so the event bus skips building and dispatching the kinds nobody wants — for high-throughput events (presence, receipts) this avoids the per-event Arc allocation entirely.
EventKindis a#[repr(u8)]discriminant — one variant perEventvariant (Message,Connected,Receipt, …). The enum is#[non_exhaustive], somatchblocks onEventKindmust include a wildcard arm (_ => …); new kinds may be added in minor releases as the library tracks new server events.EventKind::CAPACITYis a publicu8constant (currently64) that bounds the number of kinds. It exists because each discriminant is packed as a bit inEventInterest’su64mask, and a future variant that would overflow it fails compilation rather than silently corrupting the mask at runtime. Treat it as a read-only ceiling — you don’t need to check it at runtime.EventInterestis a 64-bit set of kinds. Build it withEventInterest::of(&[…]),EventInterest::ALL(the default),EventInterest::none(), or chain.with(kind). Query it with.wants(kind).- The bus exposes
has_handler_for(kind)and only produces an event when at least one registered handler wants its kind.
Bot builder, the same narrowing is available via on_event_for:
on_event (without kinds) keeps subscribing to everything.
Event Enum
Location:wacore/src/types/events.rs
The
Event enum is #[non_exhaustive], so your match statements must include a wildcard arm (_ => {}). New variants may be added in minor releases without a breaking change.Connection Events
Connected
Emitted: After successful connection and authenticationDisconnected
Emitted: When connection is lostConnectFailure
Emitted: When connection fails with a specific reasonThe 403 variant was renamed
MainDeviceGone → AccountLocked in v0.6 to match WA Web’s REASON_LOCKED semantics (the account/device is locked server-side; a manual unlink arrives as a different reason). It still maps from wire code 403 and reports is_logged_out() == true with no auto-reconnect. Update any match arms referencing the old name.TemporaryBan
Emitted: When account is temporarily bannedStreamReplaced
Emitted: When another device connects with the same credentials (stream error code 409 or<conflict type="replaced">)
LoggedOut
Emitted: When the session is invalidated by the server (stream error code 401 or 516) or whenclient.logout() is called
on_connect—trueif the logout happened during a connection attempt (server-initiated),falseif triggered byclient.logout()or a stream error while connectedreason— The reason for the logout (e.g.,ConnectFailureReason::LoggedOut)
StreamError
Emitted: For unrecognized stream error codes (codes not matching 401, 409, 429, 503, 515, or 516)Recognized stream error codes emit specific events instead of
StreamError:- 401 →
LoggedOut(session invalidated) - 409 →
StreamReplaced(another client connected) - 429 → No event emitted (reconnects with extended backoff)
- 503 → No event emitted (reconnects with normal backoff)
- 515 → No event emitted (immediate reconnect, e.g., after pairing)
- 516 →
LoggedOut(device removed)
Pairing Events
PairingQrCode
Emitted: For each QR code in rotationPairingCode
Emitted: When pair code is generatedPairSuccess
Emitted: When pairing completes successfullyPairError
Emitted: When pairing failsQrScannedWithoutMultidevice
Emitted: When a QR code is scanned by a device that does not support multi-deviceClientOutdated
Emitted: When the server rejects the connection because the client version is too old (connect failure code 405)Message Events
Message
Emitted: For all incoming messages (text, media, etc.)Both the message body and
MessageInfo are Arc-wrapped. The bus dispatches the same Arc<wa::Message> to every handler — no deep clone on fan-out — and Event::as_message() returns Option<(&Arc<wa::Message>, &MessageInfo)> so you can cheaply share the payload with spawned tasks or downstream channels. Before v0.6 the body was Box<wa::Message>; the public guarantee changed from “owned, freely mutable” to “shared, immutable read access” — call Arc::make_mut (or clone the inner wa::Message) only if you genuinely need to mutate.sender_alt carries the LID/PN counterpart of sender whenever the stanza exposes one — including status@broadcast messages, which always include participant_lid (or participant_pn for LID-addressed status). The library reads it unconditionally so the LID-PN cache can re-warm from the message itself, matching WA Web’s WAWebMsgParser.
MessageCategory:
edit attribute on message stanzas.
| Field | Description |
|---|---|
target_id | ID of the message being targeted (for edits/revokes) |
target_sender | Sender of the targeted message |
deprecated_lid_session | Legacy LID session flag |
thread_message_id | ID of the thread parent message |
thread_message_sender_jid | Sender of the thread parent message |
content_type | High-level content classification carried by the server (e.g. "image", "document") so consumers can filter without decoding the message body |
appdata | Opaque per-message app data used by the abuse-report flow |
reporting_tag | Tag the server attaches so the recipient can submit an abuse report tied to this message |
reporting_token | Raw bytes of the abuse-report token paired with reporting_tag |
reporting_token_version | Version of the reporting-token scheme used by the server |
server_timestamp_us, verified_level, verified_name_serial, peer_recipient_pn, plus all five MsgMetaInfo additions above were added in v0.6 as part of aligning inbound parsing with WA Web. They are populated only when the server includes the corresponding stanza attribute, so existing consumers that ignore them keep working.Receipt
Emitted: For delivery/read/played receiptsReceiptType is #[non_exhaustive]. Server-driven sets like this grow over time (recent additions include EncRekeyRetry, ReadSelf, PlayedSelf, PeerMsg, and HistorySync), so your match arms must always include a wildcard (_ => …). New variants can be added in minor releases without a breaking change.UndecryptableMessage
Emitted: When a message cannot be decrypted or is unavailable. This includes:- Decryption failures (no session, invalid keys, MAC errors)
- Group messages that fail with
NoSenderKeyState(missing sender key) — dispatched before the retry receipt is sent - Messages with an
<unavailable>node (view-once already viewed, server-side unavailability) — the client automatically requests content from your primary phone via PDO
is_unavailable is true, the message had no encrypted content in the stanza. The client sends a PDO request to your primary phone, and if the phone responds successfully, a follow-up Event::Message is dispatched with the recovered content.
Notification
Emitted: For raw notification stanzas that are not handled by a more specific event typeOwnedNodeRef provides zero-copy access to the decoded stanza — call .get() to obtain a NodeRef for inspecting the tag, attributes, and children.
Example:
Most notifications are already parsed into specific event types (e.g.,
GroupUpdate, DeviceListUpdate, ContactUpdated). This event only fires for unhandled notification types.DecryptFailMode is determined by the decrypt-fail attribute on incoming <enc> nodes. If any <enc> node has decrypt-fail="hide", the entire message uses Hide mode.
Show— Default. The application should display a “waiting for this message” placeholder. Used for regular user-visible messages.Hide— The application should silently discard the failure. Used for infrastructure messages (reactions, poll votes, pin changes, edit messages, event responses, message history notices, secret encrypted event/poll edits, certain protocol messages, and SKDM stanzas) that don’t need user-visible placeholders.
Presence Events
ChatPresence
Emitted: For typing indicators and recording statesPresence
Emitted: For online/offline status and last seenUser update events
PictureUpdate
Emitted: When a user changes their profile picturejid- The JID whose picture changed (user or group)author- The user who made the change. Present for group picture changes (the admin who changed it).Nonefor personal picture updates.removed- Whether the picture was removed (true) or set/updated (false)picture_id- The server-assigned picture ID.Nonefor deletions.
UserAboutUpdate
Emitted: When a user changes their status/aboutPushNameUpdate
Emitted: When a contact changes their display nameSelfPushNameUpdated
Emitted: When your own push name is updatedGroup Events
GroupUpdate
Emitted: For each action in a group notification (subject changes, participant changes, settings updates, etc.). A single notification may produce multipleGroupUpdate events.
group_jid- The group this update applies toparticipant- The admin/user who triggered the changeparticipant_pn- Phone number JID of the participant (for LID-addressed groups)is_lid_addressing_mode- Whether the group uses LID addressing modeaction- The specific group notification action (subject change, participant add/remove/promote/demote, description change, etc.)
GroupNotificationAction
Theaction field on GroupUpdate is a GroupNotificationAction enum with the following variants:
| Variant | Wire tag | Description |
|---|---|---|
Add { participants, reason } | <add> | Members added to group |
Remove { participants, reason } | <remove> | Members removed from group |
Promote { participants } | <promote> | Members promoted to admin |
Demote { participants } | <demote> | Members demoted from admin |
Modify { participants } | <modify> | Member changed phone number |
ChangeNumber { new_owner, sub_group_suggestions } | <change_number> | A participant changed their phone number. new_owner is read from the jid attribute of <change_number> and sub_group_suggestions is collected from any <sub_group_suggestion> children. The previous JID is carried by GroupUpdate::participant. |
Subject { subject, subject_owner, subject_time } | <subject> | Group name changed |
Description { id, description } | <description> | Group description changed or deleted |
Locked { threshold } | <locked> | Only admins can edit group info |
Unlocked | <unlocked> | All members can edit group info |
Announce | <announcement> | Only admins can send messages |
NotAnnounce | <not_announcement> | All members can send messages |
Ephemeral { expiration, trigger } | <ephemeral> | Disappearing messages setting changed |
MembershipApprovalMode { enabled } | <membership_approval_mode> | Join approval toggled |
MembershipApprovalRequest { request_method, parent_group_jid } | <membership_approval_request> | A user requested to join the group |
CreatedMembershipRequests { request_method, parent_group_jid, requests } | <created_membership_requests> | Admin-side: new join requests appeared |
RevokedMembershipRequests { participants } | <revoked_membership_requests> | Membership requests rejected or cancelled |
MemberAddMode { mode } | <member_add_mode> | Who can add members (admin_add or all_member_add) |
NoFrequentlyForwarded | <no_frequently_forwarded> | Forwarding restricted |
FrequentlyForwardedOk | <frequently_forwarded_ok> | Forwarding allowed |
Invite { code } | <invite> | Member joined via invite link |
RevokeInvite | <revoke> | Invite link revoked |
GrowthLocked { expiration, lock_type } | <growth_locked> | Invite links unavailable |
GrowthUnlocked | <growth_unlocked> | Invite links available again |
Create { raw } | <create> | Group created |
Delete { reason } | <delete> | Group deleted |
Link { link_type, raw } | <link> | Subgroup linked (community) |
Unlink { unlink_type, unlink_reason, raw } | <unlink> | Subgroup unlinked (community) |
Unknown { tag } | varies | Unknown notification tag (forward compatibility) |
participants field of type Vec<GroupParticipantInfo>:
display_name carries the server-provided label for a participant — for non-contacts this is typically the masked phone number ("+55•••••••••79"). It is populated only when the participant appears as a <participant> child of a group notification; entries that arrive via <requested_user> (membership requests) leave it None.
Membership request variants include a request_method field of type MembershipRequestMethod:
MembershipApprovalRequest— emitted when a user requests to join a group. The requester is identified by the parentGroupUpdate::participantfield.CreatedMembershipRequests— admin-side notification: new join requests appeared. Therequestsfield contains the requesting users (asVec<GroupParticipantInfo>).RevokedMembershipRequests— emitted when membership requests are rejected by an admin or cancelled by the requester. Theparticipantsfield contains the affected JIDs.
MembershipApprovalRequest and CreatedMembershipRequests include an optional parent_group_jid field for community-linked joins.
Example: handling specific group actions:
A single group notification from the server can contain multiple actions. The library dispatches a separate
GroupUpdate event for each action, so your handler may receive multiple events from one notification.Contact notification events
These events are emitted from<notification type="contacts"> stanzas sent by the server. They are distinct from ContactUpdate, which comes from app-state sync mutations.
ContactUpdated
Emitted: When a contact’s profile changes (server notification)<notification type="contacts"><update jid="..."/>
When you receive this event, you should invalidate any cached presence or profile picture data for the contact. WhatsApp Web resets its PresenceCollection and refreshes the profile picture thumbnail on this event.
Example:
ContactNumberChanged
Emitted: When a contact changes their phone number<notification type="contacts"><modify old="..." new="..." old_lid="..." new_lid="..."/>
The library automatically creates LID-PN mappings when LID attributes are present (old_lid→old_jid and new_lid→new_jid). WhatsApp Web generates a system notification message in both the old and new chats.
Example:
ContactSyncRequested
Emitted: When the server requests a full contact re-sync<notification type="contacts"><sync after="..."/>
Example:
ContactUpdate
Emitted: When a contact’s information changes via app-state sync (e.g., first name, last name set in your address book)jid- The contact whose information changedtimestamp- When the change occurredaction- The contact action from app-state sync, containing fields likefull_nameandfirst_namefrom_full_sync- Whether this came from a full app-state sync (initial load) or an incremental update
ContactUpdate comes from app-state sync mutations and is distinct from ContactUpdated, which comes from server-side <notification type="contacts"> stanzas.The server may also send
<add/> and <remove/> child actions in contacts notifications for lightweight roster changes. These are acknowledged automatically and do not emit events.Chat state events
PinUpdate
Emitted: When a chat is pinned/unpinnedMuteUpdate
Emitted: When a chat is muted/unmutedArchiveUpdate
Emitted: When a chat is archived/unarchivedStarUpdate
Emitted: When a message is starred or unstarredchat_jid- The chat containing the starred messageparticipant_jid- The sender of the message (only for group messages from others;Nonefor self-authored or 1-on-1 messages)message_id- The ID of the starred/unstarred messagefrom_me- Whether the starred message was sent by you
MarkChatAsReadUpdate
Emitted: When a chat is marked as read or unread across linked devicesDeleteChatUpdate
Emitted: When a chat is deleted across linked devicesjid- The JID of the deleted chatdelete_media- Whether media files were also deletedaction- The underlying protobuf action containing the optionalmessage_range
ClearChatUpdate
Emitted: When a chat’s messages are cleared (but the chat is kept) on a linked devicejid- The chat that was cleareddelete_starred- Whether starred messages were also removeddelete_media- Whether downloaded media was also removedfrom_full_sync-truewhile replaying the initial app state full sync
clear_chat for the outbound API that emits this on other devices.
UserStatusMuteUpdate
Emitted: When a contact/group/channel’s status updates are muted or unmuted on a linked devicejid- The entity whose status updates were (un)mutedmuted-truewhen status was muted,falsewhen unmutedfrom_full_sync-truewhile replaying the initial app state full sync
set_user_status_mute for the outbound API.
DeleteMessageForMeUpdate
Emitted: When a message is deleted locally (not for everyone) across linked deviceschat_jid- The chat containing the deleted messageparticipant_jid- The sender of the message (only for group messages from others;Nonefor self-authored or 1-on-1 messages)message_id- The ID of the deleted messagefrom_me- Whether the deleted message was sent by youaction- The underlying protobuf action containingdelete_mediaand optionalmessage_timestamp
LabelEditUpdate
Emitted: When a chat label is created, renamed, recolored, or deleted on a linked devicelabel_id— Stable label identifieraction.name— New display name (Nonewhen only the deleted flag changes)action.color— WhatsApp color index for the swatchaction.deleted—Some(true)when the label was removedfrom_full_sync—truewhile replaying the initial app state full sync
LabelAssociationUpdate
Emitted: When a label is added to or removed from a chat on a linked devicelabel_id— Identifier of the label being attached or detachedchat_jid— Chat whose label set changedaction.labeled—Some(true)when the label was added,Some(false)when removedfrom_full_sync—truewhile replaying the initial app state full sync
History sync events
HistorySync
Emitted: For chat history synchronizationwa::HistorySync proto on demand. Cheap metadata (sync_type, chunk_order, progress) is available without decoding, so you can filter or route events without paying the decode cost.
- Metadata without decoding —
sync_type(),chunk_order(),progress(), andpeer_data_request_session_id()are extracted during the streaming phase and available immediately - Parse-once semantics —
get()decodes the full proto on first call and caches the result viaOnceLock. WithArc<Event>dispatch, all handlers share the sameLazyHistorySyncinstance - Raw bytes access —
raw_bytes()provides the decompressed protobuf bytes for custom or partial decoding without triggering the full decode - Cheap clone — Cloning shares the underlying
Bytesbuffer (reference-counted) but creates a freshOnceLock, so each clone decodes independently - Serialization — Only metadata (sync_type, chunk_order, progress, peer_data_request_session_id) is serialized, not the blob
- On-demand correlation —
peer_data_request_session_id()is set only on syncs the server pushes in response tofetchMessageHistory/requestPlaceholderResend. Server-initiated syncs (initial bootstrap, recent, push-name) returnNone. Use it to route the blob back to the request that triggered it.
The blob is only retained in memory if event handlers are registered. If no handlers are listening, the history sync pipeline extracts internal data (pushname, NCT salt, TC tokens) and discards the blob without allocating it for event dispatch.
OfflineSyncPreview
Emitted: Preview of pending offline sync data when reconnectingOfflineSyncCompleted
Emitted: When offline sync completes after reconnectionOffline sync happens automatically when the client reconnects after being disconnected. The client tracks progress internally and emits these events to notify your application of sync status.If the server does not complete offline sync within 60 seconds, the client forces completion via a timeout fallback —
OfflineSyncCompleted is still emitted with the count of items processed so far. This prevents startup from blocking indefinitely.Device Events
DeviceListUpdate
Emitted: When a user’s device list changes (a companion device is added, removed, or updated)| Field | Description |
|---|---|
user | The user whose device list changed (PN JID) |
lid_user | The user’s LID JID, if known from the notification |
update_type | Whether a device was added, removed, or updated |
devices | List of affected devices with their IDs and key indexes |
key_index | ADV key index info for device identity verification (add operations only) |
contact_hash | Server-side contact hash for update operations |
IdentityChange
Emitted: When a contact reinstalls WhatsApp (their identity key changed). The event fires from two paths: an explicit server<identity/> notification, or a locally-detected change discovered while decrypting an incoming message. The implicit field distinguishes them.
user— The phone number JID of the user whose identity changedlid_user— The user’s LID JID, if provided in the notificationimplicit—falsefor server-pushed<identity/>notifications (full cleanup performed);truefor locally-detected changes during decrypt (lighter cleanup, see below)
WAWebHandleIdentityChange flow. When the server sends an <identity/> notification inside a type="encrypt" stanza, the client:
- Clears the device record for the user (deletes Signal sessions for all non-primary devices). Per-device sender key tracking is not wiped here — matching WhatsApp Web’s
WAWebUpdateLocalSignalSession, SKDM redistribution is driven per-group/per-device by retry receipts (markForgetSenderKey), so a global wipe would empty the tracker too aggressively. - Deletes the primary device session and identity key so a fresh session can be established (matching WhatsApp Web’s
deleteRemoteInfo) - Deletes the
status@broadcastsender key for forward secrecy on the next status send (matching WhatsApp Web’smarkStatusSenderKeyRotate) - Invalidates the device registry cache so the next send triggers a fresh device list sync
- Dispatches this event so your application can show a “security code changed” notice
- Spawns a background
ensure_e2e_sessionstask to proactively re-establish the session (self-defers when the client is offline)
UntrustedIdentity error during decryption (indicating the sender reinstalled WhatsApp), the client:
- Clears the old identity key and retries decryption with the new identity, preserving the old session for in-flight messages
- Handles
InvalidPreKeyIderrors in the retry path by sending a retry receipt so the sender can establish a new session - Re-issues TC tokens for the sender in the background (matching WhatsApp Web’s
sendTcTokenWhenDeviceIdentityChangebehavior) so the contact retains a valid privacy token
wait_for_offline_delivery_end when the client is offline.
Implicit (locally-detected) identity changes
The client also firesIdentityChange with implicit: true when decrypting a peer’s message replaces an existing identity key with a different one — for example, when a contact’s reinstall reaches you through an incoming message before the server <identity/> push arrives. This mirrors WhatsApp Web’s saveIdentity → handleNewIdentity flow.
The implicit path is deliberately lighter than the server push:
- Clears the device record (non-primary sessions + per-device sender key tracking)
- Invalidates the device registry cache so the next send re-runs usync
- Re-issues an active TC token if one exists
status@broadcast sender key, or proactively re-establish sessions — the in-flight message is already establishing a new session, and the heavier reset is handled when the server <identity/> push reliably follows.
Identity change notifications from companion devices (device ID != 0) and from your own JID are ignored on both paths — only primary device identity changes for other users are processed.
BusinessStatusUpdate
Emitted: When a business account status changesNewsletter Events
NewsletterLiveUpdate
Emitted: When reaction counts change or messages are updated on a newsletter you’re subscribed to (viasubscribe_live_updates).
newsletter_jid— The newsletter channel this update is formessages— List of messages with updated reaction countsserver_id— Server-assigned message IDreactions— Current reaction counts (emoji code and count)
You must call
client.newsletter().subscribe_live_updates(&jid) to receive these events. The subscription has a limited duration (typically 300 seconds) and must be renewed periodically.Call Events
IncomingCall
Emitted: When the server delivers a<call> stanza — voice or video, 1-on-1 or group. Mirrors WhatsApp Web’s inbound call signaling.
action field is a tagged enum that mirrors the inner stanza child (<offer>, <offer_notice>, <preaccept>, <accept>, <reject>, <terminate>):
- The router automatically acks every
<call>stanza. ForOfferit additionally sends an<receipt><offer/></receipt>so the caller’s UI advances past “ringing”. OfferNoticeis the server’s fan-out to other group members when a group call starts. No offer-receipt is sent — only the generic ack.- Use
action.call_id()andaction.call_creator()to access the common identifiers without matching every variant.
group_jid on CallAction::Offer is the primary signal for distinguishing a group call from a 1-on-1 call (matches WhatsApp Web’s WAWebVoipGatingUtils). OfferNotice is the secondary signal for members who were not directly offered the call — for example, when you are a passive group member receiving the announcement that a call started.Notification Events
DisappearingModeChanged
Emitted: When a contact changes their default disappearing messages setting. Sent by the server as a<notification type="disappearing_mode"> stanza.
from- The contact whose disappearing messages setting changedduration- New duration in seconds (0= disabled,86400= 24 hours,604800= 7 days, etc.)setting_timestamp-DateTime<Utc>indicating when the setting was changed (serialized as Unix timestamp in seconds)
You should only apply this update if
setting_timestamp is newer than your previously stored value for this contact. This prevents out-of-order updates from overwriting newer settings.Raw stanza events
RawNode
Emitted: For every decoded stanza before router dispatch. This is an opt-in event gated byClient::set_raw_node_forwarding(true) — it is not emitted by default to avoid overhead.
OwnedNodeRef uses yoke-based zero-copy decoding, so string and byte payloads are borrowed directly from the network buffer without allocation.
Example:
RawNode is skipped during serialization (#[serde(skip)]). Enable it only when debugging or building protocol-level tooling, as it dispatches for every incoming stanza.Event handler patterns
Bot builder pattern
Multiple Handlers
ChannelEventHandler
ChannelEventHandler is a built-in event handler that forwards events to an async_channel for async consumption. It uses async-channel (runtime-agnostic) instead of Tokio channels, so it works with any async executor — including WASM targets.
Events are buffered in an unbounded channel, so events fired before the receiver starts listening are not lost.
- Testing — assert on specific event sequences without closures
- Custom event loops — process events in your own async task with full control over ordering
- Runtime-agnostic code — no dependency on Tokio’s
mpscchannels
ChannelEventHandler::new() returns (Arc<ChannelEventHandler>, async_channel::Receiver<Arc<Event>>). The handler is already wrapped in Arc for direct use with client.register_handler(). The receiver yields Arc<Event>, so use &*event or event.as_ref() to pattern-match.Custom async event handlers
For custom channel-based patterns, you can implementEventHandler directly:
Arc<Event>, you can forward them to channels without any cloning overhead.
Performance Optimization
LazyHistorySync
Purpose: Avoid parsing large protobuf blobs unless needed. The raw decompressed bytes flow through the pipeline as reference-countedBytes, and full protobuf decoding only happens if your code calls get().
Bytes is reference-counted so cloning the raw data is O(1). However, parsed uses a plain OnceLock (not Arc<OnceLock>), so each clone gets its own parse cache and parses independently. This is acceptable because parsing is idempotent and the common case is a single handler.
Usage:
Arc<Event> dispatch
WithArc<Event> dispatch, each event is wrapped in a single Arc by the CoreEventBus and shared across all handlers. This eliminates deep clones of large event payloads like LazyHistorySync blobs and Message(Arc<wa::Message>, Arc<MessageInfo>). Both the wa::Message body and the MessageInfo are Arc-wrapped, enabling zero-cost sharing across the message dispatch, retry receipt, and PDO recovery paths without cloning the full struct.
Combined with LazyHistorySync’s OnceLock, all handlers sharing the same Arc<Event> get parse-once semantics for free — the first handler to call lazy_sync.get() triggers the decode, and subsequent handlers reuse the cached result.
Best Practices
Event Filtering
Error Handling
Spawning Tasks
Related Sections
Architecture
Understand the event bus system
Authentication
Learn about pairing events
Sending messages
Sending and receiving messages
Client API
Complete client API reference