beck_core/
edge.rs

1//! The three values a host hands a pure program at the edge.
2//!
3//! A Beck program never constructs an `Envelope`, a `Session` or a `Proposal` — it receives them.
4//! §3.7's replay rule is the reason: "`env.at` and `env.actor` are read as *data* and never from a
5//! clock", which is only true if something outside the program supplies them.
6//!
7//! They are built here rather than in the runtime because a Mode B client builds them too. A
8//! browser that applies a command speculatively folds it under an envelope of its own, and an
9//! envelope with a differently-spelled field is a fold that fails in the browser and succeeds on
10//! the server — the exact class of divergence Mode B has to be free of, since its whole claim is
11//! that the client runs *the same* fold ([`crate::render`]).
12
13use std::sync::Arc;
14
15use crate::core::{Fields, Value};
16
17/// `Envelope[Event]` — the record a fold sees.
18pub fn envelope(seq: u64, at: i64, actor: &str, event: Value) -> Value {
19    Value::data(
20        Arc::from("Envelope"),
21        None,
22        Fields::from_iter([
23            (Arc::from("seq"), Value::Int(seq as i64)),
24            (Arc::from("at"), Value::Int(at)),
25            (Arc::from("actor"), Value::str_(actor)),
26            (Arc::from("body"), event),
27        ]),
28    )
29}
30
31/// `Session` — who is asking, what the identity provider said about them, and where they are.
32///
33/// The claims are a `Map[Str, Str]` and not a record, because the set is the provider's rather
34/// than the program's: a tenant claim one deployment issues is one another has never heard of.
35/// They are copied in at the edge for the same reason the actor is — a fold that read them from a
36/// token would be a fold that could not replay ([`crate::render`], §3.7).
37///
38/// `path` is the route, and it is built here for the same reason the other two are: a Mode B
39/// client renders the same view against a `Session` of its own, and a route the browser spelled
40/// differently than the server is a page that differs from the one it is hydrating.
41pub fn session<'a>(
42    actor: &str,
43    claims: impl IntoIterator<Item = (&'a str, &'a str)>,
44    path: &str,
45) -> Value {
46    Value::data(
47        Arc::from("Session"),
48        None,
49        Fields::from_iter([
50            (Arc::from("actor"), Value::str_(actor)),
51            (
52                Arc::from("claims"),
53                Value::Map(
54                    claims
55                        .into_iter()
56                        .map(|(k, v)| (Value::str_(k), Value::str_(v)))
57                        .collect(),
58                ),
59            ),
60            (Arc::from("path"), Value::str_(path)),
61        ]),
62    )
63}
64
65/// The route a connection has not stated: the application's own root.
66///
67/// One constant rather than an empty string, because "the client did not say" and "the client is
68/// at the root" are the same page and a program matching on `session.path` should not have to
69/// spell both. Every caller that has no route — `beck test`, the differential harness, a
70/// benchmark, a read model — passes through here rather than writing `"/"` again.
71pub const ROOT: &str = "/";
72
73/// The roster `presence()` produces: actor to how many connections that actor holds.
74///
75/// Built here beside the other three because it is the same kind of value — something the host
76/// hands a pure program — and because the *shape* has to be one definition. A page reads it with
77/// `map_len`, `map_keys` and `map_contains`, and a second constructor spelling the pairs
78/// differently would be a page that renders one way under `beck test` and another under `beck run`.
79pub fn presence<'a>(here: impl IntoIterator<Item = (&'a str, i64)>) -> Value {
80    Value::Map(
81        here.into_iter()
82            .map(|(actor, n)| (Value::str_(actor), Value::Int(n)))
83            .collect(),
84    )
85}
86
87/// The roster of a world with one connection: the viewer's own.
88///
89/// What `beck test` renders a page in, and what a caller with no connection registry gets. A test
90/// asserting on the page one actor sees is asking what that actor sees while looking at it, so a
91/// roster that did not contain them would be describing a page nobody is reading.
92pub fn presence_of(actor: &str) -> Value {
93    presence([(actor, 1)])
94}
95
96/// An awareness roster: each actor's contribution, keyed by actor.
97///
98/// [`presence`] with a payload. The keys are the same names and carry the same warning — they are
99/// what the client said it was, bounded by the registry rather than trusted (`docs/82` §82.5).
100pub fn awareness<'a>(here: impl IntoIterator<Item = (&'a str, Value)>) -> Value {
101    Value::Map(
102        here.into_iter()
103            .map(|(actor, v)| (Value::str_(actor), v))
104            .collect(),
105    )
106}
107
108/// The roster of a world with one connection, for [`presence_of`]'s reason: a caller with no
109/// registry is rendering the page one actor sees while looking at it, and a roster without them in
110/// it would describe a page nobody is reading.
111pub fn awareness_of(actor: &str, mine: Value) -> Value {
112    awareness([(actor, mine)])
113}
114
115/// No roster at all — what a page that reads none is handed.
116pub fn no_awareness() -> Value {
117    Value::Map(Default::default())
118}
119
120/// `Freshness` — whether the page about to be rendered is of the confirmed state or of a guess.
121///
122/// §3.7: "`Signal[T]` carries a freshness dimension (`confirmed | pending(n)`) that UI code can
123/// render (\"saving…\") — staleness is typed, not pretended away."
124///
125/// `n` is how many of this client's own commands are folded into the state being rendered and not
126/// yet reflected in what the server has confirmed. Zero is `Confirmed` rather than `Pending(0)`,
127/// which is the one decision in this function: a page asking "is this a guess" would otherwise
128/// have to know that one of the two variants sometimes means the other.
129///
130/// Built here, beside the other four, because a Mode B client is the only thing that ever produces
131/// a non-`Confirmed` value and the server produces the confirmed one — two constructors would be
132/// two spellings of a union the same `view` matches on.
133pub fn freshness(pending: usize) -> Value {
134    match pending {
135        0 => Value::data(
136            Arc::from("Freshness"),
137            Some(Arc::from("Confirmed")),
138            Fields::new(),
139        ),
140        n => Value::data(
141            Arc::from("Freshness"),
142            Some(Arc::from("Pending")),
143            Fields::from_iter([(Arc::from("n"), Value::Int(n as i64))]),
144        ),
145    }
146}
147
148/// The freshness of a page nothing is in flight for.
149///
150/// Every renderer that is not a Mode B client passes through here: the server (which holds the
151/// confirmed state by definition — a guess is the client's, and the log is the server's), `beck
152/// test`, a read model, a benchmark. It is a function rather than a constant so that the *reason*
153/// has one place to be written down.
154pub fn confirmed() -> Value {
155    freshness(0)
156}
157
158/// `Proposal` — a command and who proposed it, which is what `validate` is given and the only
159/// place a `Session` reaches (§3.5).
160pub fn proposal<'a>(
161    actor: &str,
162    claims: impl IntoIterator<Item = (&'a str, &'a str)>,
163    path: &str,
164    command: Value,
165) -> Value {
166    Value::data(
167        Arc::from("Proposal"),
168        None,
169        Fields::from_iter([
170            (Arc::from("session"), session(actor, claims, path)),
171            (Arc::from("command"), command),
172        ]),
173    )
174}