From 7e0219fc47654251cc3dd0aa86980f403c464345 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 16:15:44 +0000 Subject: [PATCH 1/5] docs: reflect write-once cell refactor from whatsapp-rust#1227 register_chatstate_handler is no longer async (breaking); update its reference signature and the chatstate example call site. Also refresh the internal noise_socket / PendingDeviceSync lock types shown in the architecture doc and the pending_device_sync.add() call in the Signal Protocol doc, which the same PR moved from async-lock to std::sync. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AHB1c3sSbAvVhju9PsQCWP --- advanced/signal-protocol.mdx | 2 +- api/chatstate.mdx | 4 ++-- api/client.mdx | 11 ++++++----- concepts/architecture.mdx | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx index 64256d2c..89f29773 100644 --- a/advanced/signal-protocol.mdx +++ b/advanced/signal-protocol.mdx @@ -661,7 +661,7 @@ This mechanism ensures that group messages from newly-paired companion devices a // src/message.rs — simplified flow async fn handle_unknown_device_sync(&self, info: &Arc) { let user_jid = info.source.sender.to_non_ad(); - if !self.pending_device_sync.add(user_jid.clone()).await { + if !self.pending_device_sync.add(&user_jid) { return; // already queued, dedup } if info.is_offline { diff --git a/api/chatstate.mdx b/api/chatstate.mdx index 8732f09c..36f2e633 100644 --- a/api/chatstate.mdx +++ b/api/chatstate.mdx @@ -143,7 +143,7 @@ client.register_chatstate_handler(Arc::new(|event: ChatStateEvent| { } _ => {} } -})).await; +})); ``` ### ReceivedChatState @@ -412,4 +412,4 @@ tokio::time::sleep(Duration::from_secs(3)).await; client.chatstate().send_paused(&recipient).await?; println!("Recording stopped (would send audio here)"); -``` \ No newline at end of file +``` diff --git a/api/client.mdx b/api/client.mdx index fc95d104..0da1d6d1 100644 --- a/api/client.mdx +++ b/api/client.mdx @@ -1808,13 +1808,14 @@ See [ChannelEventHandler](/concepts/events#channeleventhandler) for details. ### register_chatstate_handler ```rust -pub async fn register_chatstate_handler( - &self, - handler: Arc, -) +pub fn register_chatstate_handler(&self, handler: Arc) ``` -Registers a handler for chat state events (typing indicators). The handler is wrapped in `Arc` for thread-safe sharing across the event dispatching system. +Registers a handler for chat state events (typing indicators). The handler is wrapped in `Arc` for thread-safe sharing across the event dispatching system. Registration is copy-on-write, so dispatch never blocks a concurrent registration and the zero-handler default path takes no lock at all. + + +**Breaking change (as of PR #1227):** `register_chatstate_handler` is no longer `async`. Drop the `.await` at call sites: `client.register_chatstate_handler(handler)`. + ### set_raw_node_forwarding diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index c86af96f..697baf3b 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -148,14 +148,14 @@ See [custom backends](/guides/custom-backends) for implementing your own runtime **Location:** `src/client.rs` -**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. Uses `async-lock` (runtime-agnostic) for all internal synchronization instead of Tokio-specific primitives. +**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. Uses `async-lock` (runtime-agnostic) instead of Tokio-specific primitives for state whose critical section awaits; state whose critical section never awaits (a clone, a store, a set op) uses a `std::sync` lock instead, and state that is built once and never replaced uses `std::sync::OnceLock` — holding either kind across an `.await` on the send path is a compile error rather than a review question ([#1227](https://github.com/oxidezap/whatsapp-rust/pull/1227)). ```rust pub struct Client { pub(crate) core: wacore::client::CoreClient, pub(crate) persistence_manager: Arc, pub(crate) media_conn: Arc>>, - pub(crate) noise_socket: Arc>>>, + pub(crate) noise_socket: Arc>>>, // ... connection state, caches, locks } ``` @@ -513,7 +513,7 @@ When online (not during offline sync), unknown devices trigger an immediate back ```rust // src/pending_device_sync.rs pub(crate) struct PendingDeviceSync { - pending: async_lock::Mutex>, + pending: std::sync::Mutex>, } ``` From 2a5b546ed3a556d0b11225bdd9e5eb1a4f978d8f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 16:20:21 +0000 Subject: [PATCH 2/5] docs: fix noise_socket access pattern in websocket-handling.mdx The connect/read-loop/send/cleanup examples mixed ArcSwap-style .store()/.load() calls with .lock().await, none of which matched the real Client::noise_socket field (a std::sync::Mutex, accessed via get_noise_socket() / lock().unwrap_or_else(...)). Align all four snippets with the actual source. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AHB1c3sSbAvVhju9PsQCWP --- advanced/websocket-handling.mdx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/advanced/websocket-handling.mdx b/advanced/websocket-handling.mdx index 9cd53d2f..1e0b8475 100644 --- a/advanced/websocket-handling.mdx +++ b/advanced/websocket-handling.mdx @@ -777,7 +777,7 @@ impl Client { ).await?; // Store socket and start receivers - self.noise_socket.store(Some(Arc::clone(&noise_socket))); + *self.noise_socket.lock().unwrap_or_else(|p| p.into_inner()) = Some(Arc::clone(&noise_socket)); self.start_frame_receiver(events, noise_socket).await; Ok(()) @@ -795,7 +795,7 @@ async fn read_messages_loop(self: &Arc) -> Result<(), anyhow::Error> { .ok_or_else(|| anyhow!("Cannot start message loop: not connected"))?; // Noise socket is stable for the lifetime of this loop; resolve once // instead of locking the mutex on every inbound frame. - let noise_socket = self.get_noise_socket().await + let noise_socket = self.get_noise_socket() .map_err(|_| anyhow!("Cannot start message loop: no noise socket"))?; let mut frame_decoder = FrameDecoder::new(); @@ -874,8 +874,7 @@ Frame decryption is always sequential (noise protocol counter ordering), but nod ```rust impl Client { pub async fn send_node(&self, node: &Node) -> Result<()> { - let noise_socket = self.noise_socket.load() - .ok_or_else(|| anyhow!("not connected"))?; + let noise_socket = self.get_noise_socket()?; // Marshal node to binary with auto-sized buffer let plaintext_buf = marshal_auto(node)?; @@ -898,7 +897,7 @@ impl Client { async fn cleanup_connection_state(&self) { self.shutdown_notifier.notify(usize::MAX); *self.transport.lock().await = None; - *self.noise_socket.lock().await = None; + *self.noise_socket.lock().unwrap_or_else(|p| p.into_inner()) = None; self.is_connected.store(false, Ordering::Release); // Drop per-chat lane senders so workers exit via channel close. From 11eedfc4cd0451785818113b583b4cb2f22c8249 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 16:21:28 +0000 Subject: [PATCH 3/5] docs: narrow the sync-lock compile-error claim to Send futures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std::sync::MutexGuard is only rejected across an .await when the enclosing future must be Send (e.g. a spawned task) — a !Send future still compiles holding one. The prior wording overstated this as a blanket guarantee and lumped in OnceLock, which has no guard to hold across an .await in the first place. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AHB1c3sSbAvVhju9PsQCWP --- concepts/architecture.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index 697baf3b..2eac1f51 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -148,7 +148,7 @@ See [custom backends](/guides/custom-backends) for implementing your own runtime **Location:** `src/client.rs` -**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. Uses `async-lock` (runtime-agnostic) instead of Tokio-specific primitives for state whose critical section awaits; state whose critical section never awaits (a clone, a store, a set op) uses a `std::sync` lock instead, and state that is built once and never replaced uses `std::sync::OnceLock` — holding either kind across an `.await` on the send path is a compile error rather than a review question ([#1227](https://github.com/oxidezap/whatsapp-rust/pull/1227)). +**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. Uses `async-lock` (runtime-agnostic) instead of Tokio-specific primitives for state whose critical section awaits; state whose critical section never awaits (a clone, a store, a set op) uses a `std::sync` lock instead, and state that is built once and never replaced uses `std::sync::OnceLock`. A `std::sync::MutexGuard` isn't `Send`, so on a path that must produce a `Send` future (e.g. a spawned task) holding one across an `.await` is a compile error rather than something a reviewer has to catch by hand ([#1227](https://github.com/oxidezap/whatsapp-rust/pull/1227)). ```rust pub struct Client { From f8efb5fd34dc9c4d1a61d7ef876265194b4ee774 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 16:25:40 +0000 Subject: [PATCH 4/5] docs: split the synchronization overview into one idea per sentence Per the style guide (AGENTS.md), each primitive's selection rule and the Send-future caveat now get their own sentence instead of one long compound one. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AHB1c3sSbAvVhju9PsQCWP --- concepts/architecture.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index 2eac1f51..2292274f 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -148,7 +148,7 @@ See [custom backends](/guides/custom-backends) for implementing your own runtime **Location:** `src/client.rs` -**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. Uses `async-lock` (runtime-agnostic) instead of Tokio-specific primitives for state whose critical section awaits; state whose critical section never awaits (a clone, a store, a set op) uses a `std::sync` lock instead, and state that is built once and never replaced uses `std::sync::OnceLock`. A `std::sync::MutexGuard` isn't `Send`, so on a path that must produce a `Send` future (e.g. a spawned task) holding one across an `.await` is a compile error rather than something a reviewer has to catch by hand ([#1227](https://github.com/oxidezap/whatsapp-rust/pull/1227)). +**Purpose:** Orchestrates connection lifecycle, event bus, and high-level operations. The synchronization primitive follows the shape of the state, not a single default. State whose critical section awaits uses `async-lock` (runtime-agnostic, not Tokio-specific). State whose critical section never awaits — a clone, a store, a set op — uses a `std::sync` lock instead. State that is built once and never replaced uses `std::sync::OnceLock`. On a path that must produce a `Send` future (e.g. a spawned task), a `std::sync::MutexGuard` isn't `Send`, so holding one across an `.await` there is a compile error rather than something a reviewer has to catch by hand ([#1227](https://github.com/oxidezap/whatsapp-rust/pull/1227)). ```rust pub struct Client { From 89ae08efb94d0b3e7b41997933c681d12b7f2df4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 16:28:23 +0000 Subject: [PATCH 5/5] docs: use active voice and second person for register_chatstate_handler Per the style guide (AGENTS.md), address the reader directly and keep one idea per sentence. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AHB1c3sSbAvVhju9PsQCWP --- api/client.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client.mdx b/api/client.mdx index 0da1d6d1..8ba3762a 100644 --- a/api/client.mdx +++ b/api/client.mdx @@ -1811,7 +1811,7 @@ See [ChannelEventHandler](/concepts/events#channeleventhandler) for details. pub fn register_chatstate_handler(&self, handler: Arc) ``` -Registers a handler for chat state events (typing indicators). The handler is wrapped in `Arc` for thread-safe sharing across the event dispatching system. Registration is copy-on-write, so dispatch never blocks a concurrent registration and the zero-handler default path takes no lock at all. +Register a handler for chat state events (typing indicators). Pass the handler in an `Arc` so the event dispatcher can share it across threads. Copy-on-write registration lets event dispatch continue while you register another handler. When no handler is registered, the client uses a lock-free fast path. **Breaking change (as of PR #1227):** `register_chatstate_handler` is no longer `async`. Drop the `.await` at call sites: `client.register_chatstate_handler(handler)`.