Skip to main content

conversation_api/
control.rs

1//! Authoritative owner control state. History and live cursors never order this projection.
2use crate::{ActiveRun, ConversationQueue, ConversationSurface, PendingInteraction, ThreadSummary};
3use serde::{Deserialize, Serialize};
4
5/// Fixed-width decimal pair (catalog epoch, state revision), lexicographically ordered on every client.
6#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
7#[serde(transparent)]
8pub struct ControlVersion(String);
9impl ControlVersion {
10    pub fn new(catalog: u64, state: u64) -> Self {
11        Self(format!("{catalog:020}:{state:020}"))
12    }
13    pub fn as_str(&self) -> &str {
14        &self.0
15    }
16}
17impl<'de> Deserialize<'de> for ControlVersion {
18    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
19        let value = String::deserialize(deserializer)?;
20        let bytes = value.as_bytes();
21        if bytes.len() != 41
22            || bytes[20] != b':'
23            || !bytes[..20]
24                .iter()
25                .chain(&bytes[21..])
26                .all(u8::is_ascii_digit)
27            || value[..20].parse::<u64>().is_err()
28            || value[21..].parse::<u64>().is_err()
29        {
30            return Err(serde::de::Error::custom("invalid control version"));
31        }
32        Ok(Self(value))
33    }
34}
35#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct ControlGetRequest {}
38
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct ConversationControl {
42    pub surface_id: ConversationSurface,
43    pub version: ControlVersion,
44    pub active_thread: Option<ThreadSummary>,
45    pub active_run: Option<ActiveRun>,
46    pub pending_interaction: Option<PendingInteraction>,
47    pub queue: ConversationQueue,
48    /// History cursor for the active thread. Fetch history when behind; do not use it to order control.
49    pub history_version: u64,
50}
51
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct ControlledResponse<T> {
55    pub receipt: T,
56    /// Null means the command committed but its projection could not be read; refetch control.
57    pub control: Option<ConversationControl>,
58}
59
60#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
61pub enum ControlUpdatedEventType {
62    #[default]
63    #[serde(rename = "control.updated")]
64    ControlUpdated,
65}
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct ControlUpdatedEvent {
69    #[serde(rename = "type")]
70    pub event_type: ControlUpdatedEventType,
71    pub surface_id: ConversationSurface,
72    pub data: ConversationControl,
73}
74#[cfg(test)]
75mod tests {
76    use super::*;
77    #[test]
78    fn catalog_epoch_dominates_thread_revision() {
79        assert!(ControlVersion::new(2, 0) > ControlVersion::new(1, u64::MAX));
80        assert!(ControlVersion::new(2, 10) > ControlVersion::new(2, 9));
81    }
82    #[test]
83    fn invalid_versions_never_enter_ordering() {
84        for value in [
85            "1:2",
86            "00000000000000000000:18446744073709551616",
87            "0000000000000000000é:00000000000000000000",
88        ] {
89            assert!(serde_json::from_value::<ControlVersion>(serde_json::json!(value)).is_err());
90        }
91        let max = ControlVersion::new(u64::MAX, u64::MAX);
92        assert_eq!(
93            serde_json::from_str::<ControlVersion>(&serde_json::to_string(&max).unwrap()).unwrap(),
94            max
95        );
96    }
97}