beck_rt/signals.rs
1//! The signal graph a devtools panel draws, as JSON.
2//!
3//! [`docs/08-roadmap.md`](../../../../../docs/08-roadmap.md) Phase 3 asks for a devtools view of "the
4//! signal graph, patch traffic and pending state". Two of those three are the client's own and are
5//! measured where they happen ([`crate::PATCH_CLIENT`]); this is the third, and it is the only one
6//! the browser cannot know, because the signal graph is a fact about the *program* and a Mode A
7//! client is never sent one.
8//!
9//! # It is a projection, not a second account
10//!
11//! Everything here is read off the running program: the declared signals and their edges come from
12//! [`beck_core::graph`], the maintained/recomputed verdict from [`beck_core::incremental`] — which
13//! is what `beck explain incremental` prints — and the operator counts from the plan the engine
14//! executes. Nothing is recomputed for the panel and nothing is stored for it, so a panel cannot
15//! describe a program this process is not running. That is
16//! [`docs/23`](../../../../../docs/23-incremental-views-report.md)'s argument for the read
17//! models applied to a third kind of reader: the cheapest way for a view of a thing to be right is
18//! for it to be the thing.
19//!
20//! # What it does not carry
21//!
22//! No source, no `Core`, no types, no state. A panel says what the shape of the program is and how
23//! its view is maintained; it is not a debugger and it does not put the accumulator on the wire —
24//! which is the one thing here that would be a disclosure, since a Mode A page is precisely the
25//! part of the state its viewer is allowed to see.
26
27use std::sync::OnceLock;
28
29use beck_core::incremental;
30use beck_core::plan::Plan;
31use beck_core::Placed;
32use serde_json::{json, Value};
33
34/// The document the panel fetches, built once.
35///
36/// The program does not change while the process runs, so this is [`mod@crate::dash`]'s rule
37/// applied to a second endpoint: every answer is `O(size of the answer)` and the assessment behind
38/// it — which walks the program — happens once rather than once per panel.
39pub fn document(placed: &Placed, plan: &Plan) -> &'static str {
40 static ONCE: OnceLock<String> = OnceLock::new();
41 ONCE.get_or_init(|| of(placed, plan).to_string())
42}
43
44/// The graph, the verdicts and the plan's shape, for one program.
45pub fn of(placed: &Placed, plan: &Plan) -> Value {
46 let graph = &placed.graph;
47 let verdicts = incremental::verdicts(placed);
48 let (maintained, recomputed) = plan.counts();
49
50 let nodes: Vec<Value> = graph
51 .order()
52 .into_iter()
53 .map(|id| {
54 let node = graph.node(id);
55 json!({
56 "id": id,
57 "label": node.label.as_ref(),
58 "op": node.op.name(),
59 "tier": node.tier.name(),
60 "inputs": node.inputs,
61 "verdict": verdicts.get(&node.label).map(incremental::Verdict::name),
62 })
63 })
64 .collect();
65
66 json!({
67 "program": placed.program.name,
68 "wire": placed.wire_id,
69 "mode": placed.render.mode.letter(),
70 "page": placed.roles.page_name.as_ref(),
71 // What the page may be a function of, in the vocabulary `beck explain render` uses. A
72 // panel showing a route that does not change the page should say which of the two it is.
73 "reads": placed.render.uses.describe(),
74 "nodes": nodes,
75 "plan": {
76 "operators": plan.nodes.len(),
77 "maintained": maintained,
78 "recomputed": recomputed,
79 "per_session": plan.nodes.iter().filter(|n| n.per_session).count(),
80 },
81 })
82}