web_rpc/js/mod.rs
1//! Javascript endpoints rendered from service traits at compile time.
2//!
3//! [`endpoint!`](macro@endpoint) is the Javascript counterpart of [`crate::Builder`] and reads
4//! the same way: `service =` names the trait the generated endpoint **serves**, `client =` the
5//! trait it **calls**, and the values are the same generated struct names one would pass to
6//! [`with_service`](crate::Builder::with_service) and
7//! [`with_client`](crate::Builder::with_client). At least one is required.
8//!
9//! ```rust,ignore
10//! // The Rust side of this binary.
11//! Builder::new(iface)
12//! .with_service::<CalculatorService<_>>(calculator)
13//! .with_client::<DisplayClient>();
14//! // The other end of the same connection, described as itself.
15//! web_rpc::js::endpoint!(service = DisplayService, client = CalculatorClient);
16//! ```
17//!
18//! The expansion writes a `.mjs` and a `.d.ts` into two custom sections of the wasm binary,
19//! named after the class in snake_case: `__web_rpc_calculator_client_js` and
20//! `__web_rpc_calculator_client_d_ts` for the example above. Extract them with `llvm-objcopy`
21//! (or `rust-objcopy` from `cargo-binutils`), before wasm-bindgen runs:
22//!
23//! ```text
24//! llvm-objcopy --dump-section=__web_rpc_calculator_client_js=calculator_client.mjs \
25//! --dump-section=__web_rpc_calculator_client_d_ts=calculator_client.d.ts \
26//! in.wasm out.wasm
27//! ```
28//!
29//! Add `--remove-section=...` for each to strip them from what you ship. The `.mjs` has no
30//! imports and needs no bundling.
31//!
32//! The generated module is the shell in `js/endpoint.mjs`, which is trait-independent,
33//! followed by data: a schema value per type the traits reach, a method table per trait, and
34//! a class whose methods forward to the shell. Encoding and decoding are interpreted from
35//! those values by the shell's `Codec`, which the module also exports along with `Writer` and
36//! `Reader`, for an embedder that wants to speak postcard itself.
37//!
38//! One caveat follows from how `#[link_section]` works on wasm: the macro must be invoked in
39//! the binary crate that is linked into the wasm module, because a static in an rlib that
40//! contributes no symbol to the link is dropped by wasm-ld.
41//!
42//! # What the renderers reject
43//!
44//! Rendering happens during const evaluation, which cannot format a panic message, so the
45//! compiler's const-eval backtrace is what points at the offending type. An enum whose struct
46//! variant has a field named `tag` would collide with the discriminant of the Typescript union
47//! that represents it:
48//!
49//! ```compile_fail
50//! #[derive(serde::Serialize, serde::Deserialize, postcard_schema::Schema)]
51//! pub enum Bad {
52//! Variant { tag: u32 },
53//! }
54//!
55//! #[web_rpc::service]
56//! pub trait Uses {
57//! fn take(&self, value: Bad);
58//! }
59//!
60//! web_rpc::js::endpoint!(client = UsesClient);
61//! ```
62//!
63//! So would two types that render to the same Typescript name:
64//!
65//! ```compile_fail
66//! #[derive(serde::Serialize, serde::Deserialize, postcard_schema::Schema)]
67//! pub struct Alpha { pub x: u32 }
68//!
69//! #[derive(serde::Serialize, serde::Deserialize, postcard_schema::Schema)]
70//! #[serde(rename = "Alpha")]
71//! pub struct Beta { pub y: String }
72//!
73//! #[web_rpc::service]
74//! pub trait Uses {
75//! fn one(&self, value: Alpha);
76//! fn two(&self, value: Beta);
77//! }
78//!
79//! web_rpc::js::endpoint!(client = UsesClient);
80//! ```
81//!
82//! And so would a type named `Request`, `Subscription` or `Endpoint`, which the generated
83//! declarations define themselves:
84//!
85//! ```compile_fail
86//! #[derive(serde::Serialize, serde::Deserialize, postcard_schema::Schema)]
87//! pub struct Request { pub id: u32 }
88//!
89//! #[web_rpc::service]
90//! pub trait Uses {
91//! fn one(&self, value: Request);
92//! }
93//!
94//! web_rpc::js::endpoint!(client = UsesClient);
95//! ```
96
97use crate::describe::{Method, Service};
98
99mod code;
100mod decls;
101mod dts;
102mod writer;
103
104pub use decls::MAX_DECLARATIONS;
105pub use web_rpc_macro::endpoint;
106pub use writer::Output;
107
108/// The trait-independent part of every generated endpoint, emitted ahead of the rendered
109/// schemas, method tables and class.
110pub const SHELL: &str = include_str!("../../js/endpoint.mjs");
111
112/// What the macro renders: a class name and the traits filling each half of the connection.
113pub struct Endpoint {
114 /// The name of the generated Javascript class.
115 pub class: &'static str,
116 /// The trait this endpoint implements, if any.
117 pub service: Option<&'static Service>,
118 /// The trait this endpoint calls, if any.
119 pub client: Option<&'static Service>,
120}
121
122/// The number of methods of a service that survived cfg evaluation.
123pub const fn method_count(service: &Service) -> usize {
124 let mut count = 0;
125 let mut group = 0;
126 while group < service.methods.len() {
127 count += service.methods[group].len();
128 group += 1;
129 }
130 count
131}
132
133/// The `index`th enabled method of a service. Its position here is its index on the wire.
134pub const fn method_at(service: &Service, index: usize) -> &'static Method {
135 let mut seen = 0;
136 let mut group = 0;
137 while group < service.methods.len() {
138 let methods = service.methods[group];
139 if index < seen + methods.len() {
140 return &methods[index - seen];
141 }
142 seen += methods.len();
143 group += 1;
144 }
145 panic!("web_rpc: method index out of range")
146}
147
148/// Render the Javascript module for one endpoint.
149///
150/// Call once with `CAPACITY = 0` to measure, then again with `CAPACITY` set to the measured
151/// length.
152pub const fn render_js<const CAPACITY: usize>(endpoint: &Endpoint) -> Output<CAPACITY> {
153 code::render(endpoint)
154}
155
156/// Render the Typescript declarations for one endpoint.
157///
158/// Call once with `CAPACITY = 0` to measure, then again with `CAPACITY` set to the measured
159/// length.
160pub const fn render_dts<const CAPACITY: usize>(endpoint: &Endpoint) -> Output<CAPACITY> {
161 dts::render(endpoint)
162}