Skip to main content

topcoat_runtime/
layer.rs

1mod rerun;
2#[cfg(not(target_family = "wasm"))]
3mod socket;
4
5pub use rerun::*;
6use topcoat_core::context::Cx;
7use topcoat_router::{Body, Layer, LayerFuture, Next, Path};
8
9use crate::RuntimeSetup;
10
11/// The WebSocket subprotocol for runtime connections.
12///
13/// Request this subprotocol at a page's URL to open a connection through
14/// [`RuntimeLayer`]. The browser can then request renders and receive the
15/// content as frames. WebSocket connections require a native server.
16pub const RUNTIME_PROTOCOL: &str = "topcoat-runtime";
17
18/// A [`Layer`] that handles runtime requests at each page's URL.
19///
20/// The browser can request a render in two ways:
21///
22/// - Send a `POST` with `X-Topcoat-Runtime: true` and the document's signal values as JSON. The
23///   layer rewrites this to a `GET` at the same path and query. The page, layouts, and guards run
24///   with the supplied signal values. The rewritten request has an empty body and no
25///   [`RUNTIME_HEADER`], `Content-Type`, or `Content-Length` headers. To read the original method,
26///   use [`original_method`](topcoat_router::request::original_method).
27/// - Open a WebSocket with the [`RUNTIME_PROTOCOL`] subprotocol. Each render request on this
28///   connection describes an HTTP request: a method, a path, a body, and a few runtime headers,
29///   such as [`RUNTIME_HEADER`] for a page rerun. The layer dispatches it with the handshake's
30///   other headers as a connected render. Renders run side by side, and each message sent back
31///   names the render it belongs to. A connection may have at most
32///   [`max_runs_per_connection`](Self::max_runs_per_connection) renders at once.
33///
34/// WebSocket connections are supported on native servers. HTTP page reruns
35/// are also available on WebAssembly.
36///
37/// Other requests pass through unchanged, including form submissions and
38/// WebSocket requests that do not use the runtime subprotocol. The layer adds
39/// [`RuntimeSetup`] to their context.
40///
41/// Add it after your application's pathless layers so they receive the
42/// rewritten `GET`.
43///
44/// ```rust
45/// use topcoat_router::Router;
46/// use topcoat_runtime::RuntimeLayer;
47///
48/// let router = Router::builder().layer(RuntimeLayer::new()).build();
49/// ```
50#[derive(Debug, Clone, Copy)]
51pub struct RuntimeLayer {
52    // Read only by the WebSocket handler, which native servers compile.
53    #[cfg_attr(target_family = "wasm", allow(dead_code))]
54    max_runs_per_connection: usize,
55}
56
57impl RuntimeLayer {
58    /// Creates a runtime layer that allows 64 renders at once on each
59    /// connection.
60    #[must_use]
61    pub const fn new() -> Self {
62        Self {
63            max_runs_per_connection: 64,
64        }
65    }
66
67    /// Limits the number of renders running at once on a runtime connection.
68    /// The default limit is 64.
69    ///
70    /// Each connected page or shard has a render, unless it is inside another
71    /// connected page or shard. At the limit, new render requests receive
72    /// `429 Too Many Requests`. They can be accepted again once a render
73    /// finishes or stops.
74    ///
75    /// ```rust
76    /// use topcoat_router::Router;
77    /// use topcoat_runtime::RuntimeLayer;
78    ///
79    /// let router = Router::builder()
80    ///     .layer(RuntimeLayer::new().max_runs_per_connection(128))
81    ///     .build();
82    /// ```
83    #[must_use]
84    pub const fn max_runs_per_connection(mut self, max: usize) -> Self {
85        self.max_runs_per_connection = max;
86        self
87    }
88}
89
90impl Default for RuntimeLayer {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl Layer for RuntimeLayer {
97    fn path(&self) -> Option<&Path> {
98        None
99    }
100
101    fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
102        #[cfg(not(target_family = "wasm"))]
103        if socket::requested(cx) {
104            return Box::pin(socket::accept(cx, body, self.max_runs_per_connection));
105        }
106        if rerun::requested(cx) {
107            return Box::pin(rerun::dispatch(cx, body));
108        }
109        let cx = cx.with(RuntimeSetup);
110        Box::pin(async move { next.run(&cx, body).await })
111    }
112}