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, and what the identity provider said about them.
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).
37pub fn session<'a>(actor: &str, claims: impl IntoIterator<Item = (&'a str, &'a str)>) -> Value {
38    Value::data(
39        Arc::from("Session"),
40        None,
41        Fields::from_iter([
42            (Arc::from("actor"), Value::str_(actor)),
43            (
44                Arc::from("claims"),
45                Value::Map(
46                    claims
47                        .into_iter()
48                        .map(|(k, v)| (Value::str_(k), Value::str_(v)))
49                        .collect(),
50                ),
51            ),
52        ]),
53    )
54}
55
56/// The roster `presence()` produces: actor to how many connections that actor holds.
57///
58/// Built here beside the other three because it is the same kind of value — something the host
59/// hands a pure program — and because the *shape* has to be one definition. A page reads it with
60/// `map_len`, `map_keys` and `map_contains`, and a second constructor spelling the pairs
61/// differently would be a page that renders one way under `beck test` and another under `beck run`.
62pub fn presence<'a>(here: impl IntoIterator<Item = (&'a str, i64)>) -> Value {
63    Value::Map(
64        here.into_iter()
65            .map(|(actor, n)| (Value::str_(actor), Value::Int(n)))
66            .collect(),
67    )
68}
69
70/// The roster of a world with one connection: the viewer's own.
71///
72/// What `beck test` renders a page in, and what a caller with no connection registry gets. A test
73/// asserting on the page one actor sees is asking what that actor sees while looking at it, so a
74/// roster that did not contain them would be describing a page nobody is reading.
75pub fn presence_of(actor: &str) -> Value {
76    presence([(actor, 1)])
77}
78
79/// `Proposal` — a command and who proposed it, which is what `validate` is given and the only
80/// place a `Session` reaches (§3.5).
81pub fn proposal<'a>(
82    actor: &str,
83    claims: impl IntoIterator<Item = (&'a str, &'a str)>,
84    command: Value,
85) -> Value {
86    Value::data(
87        Arc::from("Proposal"),
88        None,
89        Fields::from_iter([
90            (Arc::from("session"), session(actor, claims)),
91            (Arc::from("command"), command),
92        ]),
93    )
94}