bynk_check/websocket.rs
1//! v0.104 (real-time track slice 3b): shared analysis of a `from websocket`
2//! `on open` handler for the Workers wire path.
3//!
4//! On Workers the upgrade is authenticated at the edge, then forwarded to the
5//! **Durable Object that hosts the connection** — the agent the `on open`
6//! transfers it to. For that routing to be static, the handler must transfer the
7//! connection to exactly one agent, by a key derivable from the request (D2).
8//! This module finds that single transfer; both the checker (to diagnose a bad
9//! shape) and the emitter (to route the upgrade) use it.
10//!
11//! Lives in `bynk-check` (moved here in the compiler-pipeline-review's Wave
12//! 5, batch 5.1) rather than `bynk-emit`: it depends only on `bynk-syntax`'s
13//! AST and is conceptually checker-side analysis, misfiled under the emitter.
14
15use std::collections::HashSet;
16
17use bynk_syntax::ast::{Block, Expr, ExprKind, Statement};
18
19/// The single connection-transfer target a `from websocket` `on open` resolves
20/// to: the agent that will host the connection, and the key expression
21/// addressing the instance (`Room(roomId)` → agent `Room`, key `roomId`).
22pub struct WsOpenTarget<'a> {
23 pub agent: &'a str,
24 pub key: &'a Expr,
25}
26
27/// The shape of an `on open` body's connection handling (D2).
28pub enum WsOpenShape<'a> {
29 /// Exactly one top-level agent transfer — the routable case.
30 One(WsOpenTarget<'a>),
31 /// No top-level agent transfer (e.g. the connection is only closed, or
32 /// transferred inside a conditional — not statically routable).
33 None,
34 /// More than one agent transfer — ambiguous routing.
35 Multiple,
36}
37
38/// The synthetic name of the held connection an `on open` handler receives.
39pub const CONNECTION_BINDING: &str = "connection";
40
41/// Analyse a `from websocket` `on open` body: find the **top-level** agent
42/// transfers of the `connection` binding (a `let _ <- Agent(key).m(…, connection)`
43/// statement). A transfer nested in a conditional is deliberately *not* counted —
44/// the host DO must be statically resolvable.
45pub fn analyse_open_shape<'a>(body: &'a Block, local_agents: &HashSet<String>) -> WsOpenShape<'a> {
46 let mut targets: Vec<WsOpenTarget<'a>> = Vec::new();
47 for stmt in &body.statements {
48 let value = match stmt {
49 Statement::Let(l) | Statement::EffectLet(l) => &l.value,
50 _ => continue,
51 };
52 if let Some(t) = transfer_target(value, local_agents) {
53 targets.push(t);
54 }
55 }
56 // The tail is an expression too (rare, but a transfer could be the tail).
57 if let Some(t) = transfer_target(&body.tail, local_agents) {
58 targets.push(t);
59 }
60 match targets.len() {
61 0 => WsOpenShape::None,
62 1 => WsOpenShape::One(targets.pop().unwrap()),
63 _ => WsOpenShape::Multiple,
64 }
65}
66
67/// If `e` is `Agent(key).method(… connection …)` for a known agent and the
68/// `connection` binding is one of the call arguments, return the (agent, key).
69fn transfer_target<'a>(e: &'a Expr, local_agents: &HashSet<String>) -> Option<WsOpenTarget<'a>> {
70 let ExprKind::MethodCall { receiver, args, .. } = &e.kind else {
71 return None;
72 };
73 let ExprKind::Call {
74 name,
75 args: ctor_args,
76 ..
77 } = &receiver.kind
78 else {
79 return None;
80 };
81 if !local_agents.contains(&name.name) {
82 return None;
83 }
84 let transfers_connection = args
85 .iter()
86 .any(|a| matches!(&a.kind, ExprKind::Ident(id) if id.name == CONNECTION_BINDING));
87 if !transfers_connection {
88 return None;
89 }
90 let key = ctor_args.first()?;
91 Some(WsOpenTarget {
92 agent: name.name.as_str(),
93 key,
94 })
95}