1use std::collections::BTreeMap;
4use std::sync::Mutex;
5
6use serde_json::json;
7
8use super::error::AgentError;
9use super::records::{AgentEvent, AgentTransition};
10use super::registry::AgentRegistry;
11use super::retained::Retained;
12
13const SUPERSEDED: &str = "superseded by a newer window of the same person";
15
16#[derive(Debug, Default)]
18pub struct Presence {
19 connections: Mutex<BTreeMap<String, Seat>>,
20}
21
22#[derive(Clone, Copy, Debug)]
25struct Seat {
26 connection: u64,
27 since_ms: u64,
28}
29
30#[derive(Clone, Copy)]
32pub struct ConnectionObservation<'a> {
33 pub name: &'a str,
35 pub holder: &'a str,
37 pub connection: u64,
39 pub at_ms: u64,
41}
42
43impl Presence {
44 pub fn agent(&self, connection: u64) -> Result<Option<String>, AgentError> {
49 Ok(self
50 .connections
51 .lock()
52 .map_err(|_| AgentError::Store("presence lock poisoned".into()))?
53 .iter()
54 .find(|(_, own)| own.connection == connection)
55 .map(|(name, _)| name.clone()))
56 }
57 pub fn connect<T>(
64 &self,
65 registry: &AgentRegistry,
66 retained: &Retained,
67 observation: ConnectionObservation<'_>,
68 attach: impl FnOnce() -> Result<T, AgentError>,
69 ) -> Result<T, AgentError> {
70 let ConnectionObservation {
71 name,
72 holder,
73 connection,
74 at_ms,
75 } = observation;
76 if connection == 0 {
77 return Err(AgentError::Invalid("connection id"));
78 }
79 if holder != name {
80 return Err(AgentError::Identity {
81 holder: holder.into(),
82 agent: name.into(),
83 });
84 }
85 let mut connections = self
86 .connections
87 .lock()
88 .map_err(|_| AgentError::Store("presence lock poisoned".into()))?;
89 if let Some(&existing) = connections.get(name) {
90 if existing.connection == connection {
91 return attach();
92 }
93 if !name.starts_with("person-") {
96 return Err(AgentError::Present {
97 agent: name.into(),
98 connection: existing.connection,
99 since_ms: existing.since_ms,
100 });
101 }
102 registry.observe(
103 name,
104 AgentTransition {
105 holder: "server".into(),
106 at_ms,
107 change: AgentEvent::Gone {
108 connection: existing.connection,
109 reason: SUPERSEDED.into(),
110 },
111 },
112 )?;
113 connections.remove(name);
114 }
115 if connections
116 .values()
117 .any(|existing| existing.connection == connection)
118 {
119 return Err(AgentError::Transition(
120 "one connection cannot claim two agents",
121 ));
122 }
123 let record = registry.record(name)?;
124 if !record.active() {
125 return Err(AgentError::Transition("agent is retired"));
126 }
127 let subscription = attach()?;
128 registry.observe(
129 name,
130 AgentTransition {
131 holder: holder.into(),
132 at_ms,
133 change: AgentEvent::Present { connection },
134 },
135 )?;
136 connections.insert(
137 name.into(),
138 Seat {
139 connection,
140 since_ms: at_ms,
141 },
142 );
143 retained.publish(&[
144 (
145 format!("agents.{name}.presence"),
146 json!({"present":true,"at_ms":at_ms,"connection":connection}),
147 ),
148 (
149 format!("agents.{name}.state"),
150 presence_state(retained, name, false)?,
151 ),
152 ])?;
153 drop(connections);
154 Ok(subscription)
155 }
156
157 pub fn close(
163 &self,
164 registry: &AgentRegistry,
165 retained: &Retained,
166 connection: u64,
167 reason: &str,
168 at_ms: u64,
169 ) -> Result<(), AgentError> {
170 let mut connections = self
171 .connections
172 .lock()
173 .map_err(|_| AgentError::Store("presence lock poisoned".into()))?;
174 let name = connections
175 .iter()
176 .find(|(_, own)| own.connection == connection)
177 .map(|(name, _)| name.clone());
178 let Some(name) = name else {
179 return Ok(());
180 };
181 registry.observe(
182 &name,
183 AgentTransition {
184 holder: "server".into(),
185 at_ms,
186 change: AgentEvent::Gone {
187 connection,
188 reason: reason.into(),
189 },
190 },
191 )?;
192 connections.remove(&name);
193 let result = retained.publish(&[
194 (
195 format!("agents.{name}.presence"),
196 json!({"present":false,"at_ms":at_ms,"reason":reason}),
197 ),
198 (
199 format!("agents.{name}.state"),
200 presence_state(retained, &name, true)?,
201 ),
202 ]);
203 drop(connections);
204 result
205 }
206
207 pub fn connection(&self, name: &str) -> Result<Option<u64>, AgentError> {
212 Ok(self
213 .connections
214 .lock()
215 .map_err(|_| AgentError::Store("presence lock poisoned".into()))?
216 .get(name)
217 .map(|seat| seat.connection))
218 }
219}
220
221fn presence_state(
222 retained: &Retained,
223 name: &str,
224 gone: bool,
225) -> Result<serde_json::Value, AgentError> {
226 let mut state = retained
227 .value(&format!("agents.{name}.state"))?
228 .unwrap_or_else(|| json!({}));
229 if state["state"] != "waiting" {
230 state["state"] = json!(if gone { "gone" } else { "present" });
231 }
232 Ok(state)
233}