fix(channel): use per-recipient typing handles in Discord (#1005)

Replace the single shared typing_handle with a HashMap keyed by
recipient channel ID. Previously, concurrent messages would fight
over one handle — starting typing for message B would cancel message
A's indicator, and stopping one would kill the other's.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Edvard Schøyen 2026-02-20 05:02:39 -05:00 committed by GitHub
parent e2c507664c
commit 2ae12578f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,6 +3,7 @@ use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde_json::json; use serde_json::json;
use std::collections::HashMap;
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
use uuid::Uuid; use uuid::Uuid;
@ -13,7 +14,7 @@ pub struct DiscordChannel {
allowed_users: Vec<String>, allowed_users: Vec<String>,
listen_to_bots: bool, listen_to_bots: bool,
mention_only: bool, mention_only: bool,
typing_handle: Mutex<Option<tokio::task::JoinHandle<()>>>, typing_handles: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
} }
impl DiscordChannel { impl DiscordChannel {
@ -30,7 +31,7 @@ impl DiscordChannel {
allowed_users, allowed_users,
listen_to_bots, listen_to_bots,
mention_only, mention_only,
typing_handle: Mutex::new(None), typing_handles: Mutex::new(HashMap::new()),
} }
} }
@ -457,15 +458,15 @@ impl Channel for DiscordChannel {
} }
}); });
let mut guard = self.typing_handle.lock(); let mut guard = self.typing_handles.lock();
*guard = Some(handle); guard.insert(recipient.to_string(), handle);
Ok(()) Ok(())
} }
async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> { async fn stop_typing(&self, recipient: &str) -> anyhow::Result<()> {
let mut guard = self.typing_handle.lock(); let mut guard = self.typing_handles.lock();
if let Some(handle) = guard.take() { if let Some(handle) = guard.remove(recipient) {
handle.abort(); handle.abort();
} }
Ok(()) Ok(())
@ -754,18 +755,18 @@ mod tests {
} }
#[test] #[test]
fn typing_handle_starts_as_none() { fn typing_handles_start_empty() {
let ch = DiscordChannel::new("fake".into(), None, vec![], false, false); let ch = DiscordChannel::new("fake".into(), None, vec![], false, false);
let guard = ch.typing_handle.lock(); let guard = ch.typing_handles.lock();
assert!(guard.is_none()); assert!(guard.is_empty());
} }
#[tokio::test] #[tokio::test]
async fn start_typing_sets_handle() { async fn start_typing_sets_handle() {
let ch = DiscordChannel::new("fake".into(), None, vec![], false, false); let ch = DiscordChannel::new("fake".into(), None, vec![], false, false);
let _ = ch.start_typing("123456").await; let _ = ch.start_typing("123456").await;
let guard = ch.typing_handle.lock(); let guard = ch.typing_handles.lock();
assert!(guard.is_some()); assert!(guard.contains_key("123456"));
} }
#[tokio::test] #[tokio::test]
@ -773,8 +774,8 @@ mod tests {
let ch = DiscordChannel::new("fake".into(), None, vec![], false, false); let ch = DiscordChannel::new("fake".into(), None, vec![], false, false);
let _ = ch.start_typing("123456").await; let _ = ch.start_typing("123456").await;
let _ = ch.stop_typing("123456").await; let _ = ch.stop_typing("123456").await;
let guard = ch.typing_handle.lock(); let guard = ch.typing_handles.lock();
assert!(guard.is_none()); assert!(!guard.contains_key("123456"));
} }
#[tokio::test] #[tokio::test]
@ -785,12 +786,21 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn start_typing_replaces_existing_task() { async fn concurrent_typing_handles_are_independent() {
let ch = DiscordChannel::new("fake".into(), None, vec![], false, false); let ch = DiscordChannel::new("fake".into(), None, vec![], false, false);
let _ = ch.start_typing("111").await; let _ = ch.start_typing("111").await;
let _ = ch.start_typing("222").await; let _ = ch.start_typing("222").await;
let guard = ch.typing_handle.lock(); {
assert!(guard.is_some()); let guard = ch.typing_handles.lock();
assert_eq!(guard.len(), 2);
assert!(guard.contains_key("111"));
assert!(guard.contains_key("222"));
}
// Stopping one does not affect the other
let _ = ch.stop_typing("111").await;
let guard = ch.typing_handles.lock();
assert_eq!(guard.len(), 1);
assert!(guard.contains_key("222"));
} }
// ── Message ID edge cases ───────────────────────────────────── // ── Message ID edge cases ─────────────────────────────────────