1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//! Single-flight session resolution per `(chat, topic)` (#1201).
//!
//! Resolving a chat to its session is lookup-then-create, and the two steps
//! were not atomic. Two messages landing in the same brand-new forum topic
//! ~88 ms apart both missed the lookup and both created a session, so two
//! full turns ran in parallel against one topic: two acks, two provider
//! calls, and the orphaned session's context lost when later messages bound
//! to the survivor.
//!
//! The existing "session busy, queue into the in-flight turn" guard cannot
//! help, because neither turn was streaming yet when the other resolved — the
//! race is entirely inside the window before either session exists.
//!
//! Both creations were on one thread, so this is not a cross-thread data
//! race on a shared map. It is two independent async tasks interleaving at an
//! await point between the lookup and the insert, which a `Mutex` around the
//! map alone would not close: the lock has to span BOTH steps.
//!
//! Scope matters. The guard covers resolution and the chat→session
//! registration that follows it, and is released before the turn runs.
//! Holding it for the whole turn would serialize a topic's messages, which
//! would defeat the mid-turn queueing that #302 exists to provide.
//!
//! The gate itself lives in [`crate::channels::single_flight`], which owns
//! the `OnceLock<Mutex<HashMap<_, Arc<AsyncMutex>>>>` registry keyed by
//! `String`. This module is a thin typed wrapper over it so the Telegram
//! resolver keeps a `(chat_id, topic_id)` API. There is exactly one
//! implementation; every channel maps its own key type onto the shared one.
use OwnedMutexGuard;
use cratesingle_flight;
/// Render a `(chat_id, topic_id)` pair as the shared gate's `String` key.
///
/// `None` (the General topic and non-forum groups) must map onto a distinct
/// key so it is serialized like any other, matching how the rest of the
/// Telegram state keys them.
/// Hold the resolution gate for `(chat_id, topic_id)` until the guard drops.
///
/// The second caller waits for the first to finish resolving, and then finds
/// the session the first created rather than creating its own.
///
/// A poisoned registry is recovered from rather than propagated: refusing to
/// resolve a session is a worse outcome than the duplicate this prevents.
pub async
/// How many gates are being tracked. Exposed for tests.
pub