Skip to main content

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}