atproto blogging
1//! Collab coordinator types and helpers.
2//!
3//! Provides shared types for collab coordination that can be used by both
4//! Rust UI frameworks (Dioxus) and JS bindings.
5
6use smol_str::SmolStr;
7
8/// Session record TTL in minutes.
9pub const SESSION_TTL_MINUTES: u32 = 15;
10
11/// How often to refresh session record (ms).
12pub const SESSION_REFRESH_INTERVAL_MS: u32 = 5 * 60 * 1000; // 5 minutes
13
14/// How often to poll for new peers (ms).
15pub const PEER_DISCOVERY_INTERVAL_MS: u32 = 30 * 1000; // 30 seconds
16
17/// Coordinator state machine states.
18///
19/// Tracks the lifecycle of a collab session from initialization through
20/// active collaboration. UI can use this to show appropriate status indicators.
21#[derive(Debug, Clone, PartialEq)]
22pub enum CoordinatorState {
23 /// Initial state - waiting for worker to be ready.
24 Initializing,
25 /// Creating session record on PDS.
26 CreatingSession {
27 /// The iroh node ID for this session.
28 node_id: SmolStr,
29 /// Optional relay URL for NAT traversal.
30 relay_url: Option<SmolStr>,
31 },
32 /// Active collab session.
33 Active {
34 /// The AT URI of the session record on PDS.
35 session_uri: SmolStr,
36 },
37 /// Error state.
38 Error(SmolStr),
39}
40
41impl Default for CoordinatorState {
42 fn default() -> Self {
43 Self::Initializing
44 }
45}
46
47impl CoordinatorState {
48 /// Returns true if the coordinator is in an error state.
49 pub fn is_error(&self) -> bool {
50 matches!(self, Self::Error(_))
51 }
52
53 /// Returns true if the coordinator is actively collaborating.
54 pub fn is_active(&self) -> bool {
55 matches!(self, Self::Active { .. })
56 }
57
58 /// Returns the error message if in error state.
59 pub fn error_message(&self) -> Option<&str> {
60 match self {
61 Self::Error(msg) => Some(msg.as_str()),
62 _ => None,
63 }
64 }
65
66 /// Returns the session URI if active.
67 pub fn session_uri(&self) -> Option<&str> {
68 match self {
69 Self::Active { session_uri } => Some(session_uri.as_str()),
70 _ => None,
71 }
72 }
73}
74
75/// Compute the gossip topic hash for a resource URI.
76///
77/// The topic is a blake3 hash of the resource URI bytes, used to identify
78/// the gossip swarm for collaborative editing of that resource.
79pub fn compute_collab_topic(resource_uri: &str) -> [u8; 32] {
80 let hash = weaver_common::blake3::hash(resource_uri.as_bytes());
81 *hash.as_bytes()
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_coordinator_state_default() {
90 assert_eq!(CoordinatorState::default(), CoordinatorState::Initializing);
91 }
92
93 #[test]
94 fn test_coordinator_state_is_error() {
95 assert!(!CoordinatorState::Initializing.is_error());
96 assert!(CoordinatorState::Error("test".into()).is_error());
97 }
98
99 #[test]
100 fn test_coordinator_state_is_active() {
101 assert!(!CoordinatorState::Initializing.is_active());
102 assert!(CoordinatorState::Active {
103 session_uri: "at://test".into()
104 }
105 .is_active());
106 }
107
108 #[test]
109 fn test_compute_collab_topic_deterministic() {
110 let topic1 = compute_collab_topic("at://did:plc:test/app.weaver.notebook.entry/abc");
111 let topic2 = compute_collab_topic("at://did:plc:test/app.weaver.notebook.entry/abc");
112 assert_eq!(topic1, topic2);
113 }
114
115 #[test]
116 fn test_compute_collab_topic_different_uris() {
117 let topic1 = compute_collab_topic("at://did:plc:test/app.weaver.notebook.entry/abc");
118 let topic2 = compute_collab_topic("at://did:plc:test/app.weaver.notebook.entry/def");
119 assert_ne!(topic1, topic2);
120 }
121}