beck_rt/
awareness.rs

1//! What everybody is doing now — the roster `awareness(f)` reads.
2//!
3//! [`crate::presence`] holds who is connected; this holds what each of them contributes. The two
4//! are separate registries because they move at different rates and for different reasons: a
5//! connection joins and leaves, and between those two moments a client may change its
6//! contribution any number of times by navigating.
7//!
8//! The compiler's half is a source in the signal graph ([`beck_core::signal::Op::Awareness`]) and a
9//! role beside the view ([`beck_core::split::Roles::awareness`]). The role is a function
10//! `Session -> T`; **this** is what applies it, once per connection per change, because the
11//! subscribers are the runtime's fact and not the graph's — the signal graph of one program has no
12//! way to name another connection's session.
13//!
14//! # Everything here follows from presence's one sentence
15//!
16//! Nothing is appended, snapshotted or replayed; a process that restarts comes back empty; the
17//! checker keeps `awareness` away from the chokepoint (`B0515`) so no event's existence depended on
18//! it. The bound is [`crate::presence`]'s bound for [`crate::presence`]'s reason — the table is
19//! keyed by a string the client may choose ([`docs/82`](../../../../../docs/82-the-edge-report.md)
20//! §82.5) — and past [`Config::capacity`] distinct actors a new one is **not recorded**, so the
21//! roster under-reports rather than growing.
22//!
23//! # What is different: a value, and therefore a size
24//!
25//! A roster of counts is bounded by its capacity alone. A roster of *values* is bounded by the
26//! capacity times whatever `f` returns, and `f` is the program's — a session's path is a few
27//! dozen bytes, and nothing in the type system says it has to be. [`Config::each`] is the second
28//! bound this needs and presence does not: a contribution whose rendered size exceeds it is
29//! **refused**, the actor keeps whatever it contributed before, and [`Registry::oversized`] counts
30//! it. Refusing one client's update is the failure this direction should have, and holding an
31//! unbounded value per connection is not.
32
33use std::collections::BTreeMap;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::{Arc, Mutex};
36
37use beck_core::Value;
38use tokio::sync::watch;
39
40/// How large a roster this process will hold, and how large one contribution may be.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct Config {
43    /// How many distinct actors may be in the roster at once.
44    pub capacity: usize,
45    /// How many bytes one actor's contribution may render to.
46    pub each: usize,
47}
48
49impl Default for Config {
50    /// 4,096 actors of 4 KiB each — sixteen megabytes at worst, and that is the number to read this
51    /// as, because the product is what the process pays.
52    ///
53    /// The capacity is [`crate::presence::Config::default`]'s and for its reason. The per-actor
54    /// bound is a cursor, a selection or a route with room to spare, and far short of a document.
55    fn default() -> Config {
56        Config {
57            capacity: 4096,
58            each: 4096,
59        }
60    }
61}
62
63/// The awareness roster of one application.
64pub struct Registry {
65    /// Each actor's contribution, and how many connections that actor has.
66    ///
67    /// The count is here for the same reason presence keeps one: an actor with two tabs leaves the
68    /// roster when the second closes, not the first. The *value* is whichever of that actor's
69    /// connections published last, which is the only answer available when the roster is keyed by
70    /// actor and a person may open two tabs.
71    inner: Mutex<BTreeMap<Arc<str>, Entry>>,
72    value: watch::Sender<Value>,
73    config: Config,
74    refused: AtomicU64,
75    oversized: AtomicU64,
76}
77
78struct Entry {
79    connections: u32,
80    contribution: Value,
81}
82
83impl Registry {
84    pub fn new(config: Config) -> Arc<Registry> {
85        Arc::new(Registry {
86            inner: Mutex::new(BTreeMap::new()),
87            value: watch::channel(beck_core::edge::no_awareness()).0,
88            config,
89            refused: AtomicU64::new(0),
90            oversized: AtomicU64::new(0),
91        })
92    }
93
94    /// The roster as a Beck value: `Map[Str, T]`, actor to contribution.
95    pub fn value(&self) -> Value {
96        self.value.borrow().clone()
97    }
98
99    /// Wake on every change to it. A subscription watches this **only** when the program's page
100    /// reads `awareness` — a program that never asks must not re-render when somebody navigates.
101    pub fn watch(&self) -> watch::Receiver<Value> {
102        self.value.subscribe()
103    }
104
105    /// How many actors are in the roster.
106    pub fn here(&self) -> usize {
107        self.inner.lock().expect("awareness").len()
108    }
109
110    /// How many joins the capacity refused, for the life of this process.
111    pub fn refused(&self) -> u64 {
112        self.refused.load(Ordering::Relaxed)
113    }
114
115    /// How many contributions the per-actor bound refused, for the life of this process.
116    pub fn oversized(&self) -> u64 {
117        self.oversized.load(Ordering::Relaxed)
118    }
119
120    /// Record a connection's first contribution. The roster holds it until the guard is dropped.
121    ///
122    /// A guard for [`crate::presence::Guard`]'s reason: a subscription ends by returning, by
123    /// erroring or by its socket dying, and a roster that only removed an actor on the happy path
124    /// would fill up with people who left hours ago.
125    pub fn join(self: &Arc<Self>, actor: &str, contribution: Value) -> Guard {
126        let held = {
127            let mut rows = self.inner.lock().expect("awareness");
128            let room = rows.len() < self.config.capacity;
129            match rows.get_mut(actor) {
130                Some(entry) => {
131                    entry.connections += 1;
132                    true
133                }
134                None if room => {
135                    rows.insert(
136                        Arc::from(actor),
137                        Entry {
138                            connections: 1,
139                            contribution: Value::Unit,
140                        },
141                    );
142                    true
143                }
144                None => {
145                    self.refused.fetch_add(1, Ordering::Relaxed);
146                    false
147                }
148            }
149        };
150        let guard = Guard {
151            registry: held.then(|| self.clone()),
152            actor: Arc::from(actor),
153        };
154        if held {
155            // Through `update` rather than written above, so the size bound applies to the first
156            // contribution as it does to every later one. A joining connection whose contribution
157            // is too large is in the roster with nothing in it, and says so.
158            self.update(actor, contribution);
159        }
160        guard
161    }
162
163    /// Publish a new contribution for an actor already in the roster.
164    ///
165    /// Silently does nothing for an actor that is not — the capacity refused them, and a client
166    /// navigating should not be a way to get in through a different door.
167    ///
168    /// Returns whether the roster changed, which is what saves a re-render: the common navigation
169    /// republishes the same route.
170    pub fn update(&self, actor: &str, contribution: Value) -> bool {
171        let changed = {
172            let mut rows = self.inner.lock().expect("awareness");
173            let Some(entry) = rows.get_mut(actor) else {
174                return false;
175            };
176            // The bound, measured on what the value renders to rather than on a shallow field
177            // count: a list of a million empty strings is one field and eight megabytes.
178            if contribution.display().len() > self.config.each {
179                self.oversized.fetch_add(1, Ordering::Relaxed);
180                return false;
181            }
182            // Structural equality, not the engine's conservative pointer test: a client that
183            // navigates to the route it is already on rebuilds an equal value, and answering
184            // "changed" there would wake every subscriber for nothing.
185            let changed = entry.contribution != contribution;
186            entry.contribution = contribution;
187            changed
188        };
189        if changed {
190            self.publish();
191        }
192        changed
193    }
194
195    fn leave(&self, actor: &str) {
196        {
197            let mut rows = self.inner.lock().expect("awareness");
198            match rows.get_mut(actor) {
199                Some(entry) if entry.connections > 1 => {
200                    entry.connections -= 1;
201                    // The value stays: the actor is still here through another tab. Which of that
202                    // actor's tabs it came from is not something this roster distinguishes.
203                    return;
204                }
205                Some(_) => {
206                    rows.remove(actor);
207                }
208                None => return,
209            }
210        }
211        self.publish();
212    }
213
214    /// Rebuild the published value from the rows.
215    ///
216    /// `O(actors)` per change, and unlike presence's the change here is per *navigation* rather
217    /// than per connection. That is the cost this trades knowingly: a roster is read on every
218    /// render of every subscriber, so rebuilding it once per change beats rebuilding it once per
219    /// reader. If a program ever moves a cursor through here, this is the line that becomes a
220    /// delta stream rather than a rebuild.
221    fn publish(&self) {
222        let rows = self.inner.lock().expect("awareness");
223        let value = beck_core::edge::awareness(
224            rows.iter()
225                .map(|(actor, entry)| (actor.as_ref(), entry.contribution.clone())),
226        );
227        // `send_replace` for [`crate::presence::Registry::publish`]'s reason: `send` fails when
228        // there is no receiver and leaves the value unpublished, so every join before the first
229        // subscription would be lost — including the first client's own.
230        self.value.send_replace(value);
231    }
232}
233
234/// One connection's membership of the awareness roster.
235pub struct Guard {
236    /// `None` when the capacity refused this join.
237    registry: Option<Arc<Registry>>,
238    actor: Arc<str>,
239}
240
241impl Guard {
242    /// Whether this connection is in the roster. False only when the capacity refused it.
243    pub fn recorded(&self) -> bool {
244        self.registry.is_some()
245    }
246
247    /// Publish this connection's new contribution. Returns whether the roster changed.
248    pub fn publish(&self, contribution: Value) -> bool {
249        match &self.registry {
250            Some(registry) => registry.update(&self.actor, contribution),
251            None => false,
252        }
253    }
254}
255
256impl Drop for Guard {
257    fn drop(&mut self) {
258        if let Some(registry) = &self.registry {
259            registry.leave(&self.actor);
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    fn text(s: &str) -> Value {
269        Value::str_(s)
270    }
271
272    fn roster(r: &Registry) -> Vec<(String, String)> {
273        let value = r.value();
274        let map = value.as_map().expect("a map");
275        map.iter()
276            .map(|(k, v)| {
277                (
278                    k.as_str().expect("actor").to_string(),
279                    v.as_str().expect("contribution").to_string(),
280                )
281            })
282            .collect()
283    }
284
285    #[test]
286    fn a_connection_contributes_and_its_guard_removes_it() {
287        let r = Registry::new(Config::default());
288        assert_eq!(roster(&r), Vec::new());
289        {
290            let _ana = r.join("ana", text("/todos"));
291            assert_eq!(roster(&r), vec![("ana".into(), "/todos".to_string())]);
292            let _bo = r.join("bo", text("/done"));
293            assert_eq!(
294                roster(&r),
295                vec![
296                    ("ana".into(), "/todos".to_string()),
297                    ("bo".into(), "/done".to_string())
298                ]
299            );
300        }
301        assert_eq!(roster(&r), Vec::new());
302    }
303
304    #[test]
305    fn navigating_republishes_and_says_whether_anything_moved() {
306        let r = Registry::new(Config::default());
307        let ana = r.join("ana", text("/todos"));
308        assert!(!ana.publish(text("/todos")), "the same route is no change");
309        assert!(ana.publish(text("/done")));
310        assert_eq!(roster(&r), vec![("ana".into(), "/done".to_string())]);
311    }
312
313    #[test]
314    fn one_actor_with_two_tabs_leaves_when_the_second_closes() {
315        let r = Registry::new(Config::default());
316        let first = r.join("ana", text("/todos"));
317        let second = r.join("ana", text("/done"));
318        assert_eq!(roster(&r), vec![("ana".into(), "/done".to_string())]);
319        drop(second);
320        assert_eq!(
321            roster(&r),
322            vec![("ana".into(), "/done".to_string())],
323            "still here through the other tab"
324        );
325        drop(first);
326        assert_eq!(roster(&r), Vec::new());
327    }
328
329    /// §82.5's finding, keyed by a string the client chooses.
330    #[test]
331    fn the_capacity_refuses_rather_than_growing() {
332        let r = Registry::new(Config {
333            capacity: 2,
334            each: 4096,
335        });
336        let _a = r.join("a", text("/a"));
337        let _b = r.join("b", text("/b"));
338        let c = r.join("c", text("/c"));
339        assert!(!c.recorded());
340        assert_eq!(r.here(), 2);
341        assert_eq!(r.refused(), 1);
342        // And a refused actor cannot get in by publishing.
343        assert!(!c.publish(text("/c2")));
344        assert_eq!(r.here(), 2);
345        drop(c);
346        assert_eq!(r.here(), 2);
347    }
348
349    /// The bound presence does not need: a roster of values is the capacity times the value.
350    #[test]
351    fn a_contribution_past_the_size_bound_is_refused_and_the_last_one_stands() {
352        let r = Registry::new(Config {
353            capacity: 8,
354            each: 16,
355        });
356        let ana = r.join("ana", text("/todos"));
357        assert_eq!(roster(&r), vec![("ana".into(), "/todos".to_string())]);
358        assert!(!ana.publish(text(&"x".repeat(64))));
359        assert_eq!(
360            roster(&r),
361            vec![("ana".into(), "/todos".to_string())],
362            "the actor keeps what it last contributed"
363        );
364        assert_eq!(r.oversized(), 1);
365        // A joining connection whose first contribution is too large is in the roster, empty.
366        let _bo = r.join("bo", text(&"y".repeat(64)));
367        assert_eq!(r.oversized(), 2);
368        assert_eq!(r.here(), 2);
369    }
370
371    #[test]
372    fn a_watcher_wakes_on_a_join_a_change_and_a_leave() {
373        let r = Registry::new(Config::default());
374        let mut w = r.watch();
375        assert!(!w.has_changed().expect("live"));
376        let ana = r.join("ana", text("/todos"));
377        assert!(w.has_changed().expect("live"));
378        w.mark_unchanged();
379        ana.publish(text("/done"));
380        assert!(w.has_changed().expect("live"));
381        w.mark_unchanged();
382        drop(ana);
383        assert!(w.has_changed().expect("live"));
384    }
385}