Skip to main content

flaron_sdk/
lib.rs

1//! # flaron-sdk
2//!
3//! Rust SDK for building **flares** - Wasm functions that run on the
4//! [Flaron][flaron] CDN edge. A flare receives an HTTP request (or WebSocket
5//! event) at the nearest edge, runs your Rust code in a sandboxed Wasm
6//! runtime, and returns a response with single-digit-millisecond latency.
7//!
8//! [flaron]: https://flaron.dev
9//!
10//! ## Quick start
11//!
12//! ```toml
13//! # Cargo.toml
14//! [package]
15//! name = "my-flare"
16//! version = "0.1.0"
17//! edition = "2021"
18//!
19//! [lib]
20//! crate-type = ["cdylib"]
21//!
22//! [dependencies]
23//! flaron-sdk = "0.1"
24//! ```
25//!
26//! ```ignore
27//! // src/lib.rs
28//! use flaron_sdk::{request, response, FlareAction};
29//!
30//! flaron_sdk::handle_request!(my_flare);
31//!
32//! fn my_flare() -> FlareAction {
33//!     let body = format!("hello from {} {}", request::method(), request::url());
34//!     response::set_status(200);
35//!     response::set_header("content-type", "text/plain");
36//!     response::set_body(body.as_bytes());
37//!     FlareAction::Respond
38//! }
39//! ```
40//!
41//! Build with:
42//!
43//! ```sh
44//! cargo build --release --target wasm32-unknown-unknown
45//! ```
46//!
47//! Then deploy `target/wasm32-unknown-unknown/release/my_flare.wasm` to
48//! Flaron via the dashboard or `flaronctl`.
49//!
50//! ## What's in the SDK
51//!
52//! | Module                | What it does                                              |
53//! |-----------------------|-----------------------------------------------------------|
54//! | [`request`]           | Read inbound request (method, URL, headers, body)         |
55//! | [`response`]          | Write outbound response (status, headers, body)           |
56//! | [`beam`]              | Outbound HTTP from the edge ([`beam::fetch`])             |
57//! | [`spark`]             | Per-site KV with TTL, persisted to disk on the edge       |
58//! | [`plasma`]            | Cross-edge CRDT KV - counters, presence, leaderboards     |
59//! | [`secrets`]           | Read domain-scoped secrets allowlisted for this flare     |
60//! | [`crypto`]            | Hash, HMAC, AES-GCM encrypt/decrypt, JWT signing, RNG     |
61//! | [`encoding`]          | Base64, hex, URL encode/decode helpers                    |
62//! | [`id`]                | UUID v4/v7, ULID, KSUID, Nanoid, Snowflake generators     |
63//! | [`time`]              | Timestamps in unix / ms / ns / RFC3339 / HTTP / ISO8601   |
64//! | [`logging`]           | Structured logs surfaced via the edge node's slog stream  |
65//! | [`ws`]                | WebSocket: send, close, read events from open/message/close |
66//!
67//! ## Memory model
68//!
69//! Each flare invocation gets a fresh 256 KiB bump arena that the host writes
70//! into via the guest's exported `alloc` function. The SDK resets the arena
71//! at the top of every invocation so memory is reclaimed automatically - you
72//! never need to free anything yourself.
73//!
74//! The recommended entrypoint is the [`handle_request!`] macro for HTTP
75//! flares and [`ws_handlers!`] for WebSocket flares - both wire up the
76//! `alloc` export, reset the arena on every invocation, and call your
77//! handler. Use [`export_alloc!`] + [`reset_arena`] manually only when you
78//! need to define the host exports yourself.
79
80mod ffi;
81pub(crate) mod mem;
82
83pub mod beam;
84pub mod crypto;
85pub mod encoding;
86pub mod id;
87pub mod logging;
88pub mod plasma;
89pub mod request;
90pub mod response;
91pub mod secrets;
92pub mod spark;
93pub mod time;
94pub mod ws;
95
96pub use beam::{BeamError, FetchOptions, FetchResponse};
97pub use crypto::{CryptoError, RandomBytesError};
98pub use mem::{guest_alloc, reset_arena};
99pub use plasma::PlasmaError;
100pub use spark::{SparkEntry, SparkError, SparkPullError};
101pub use ws::WsSendError;
102
103/// Action returned by an `handle_request()` export to tell the host how to
104/// proceed once the flare's body has run.
105///
106/// The flaron host runtime decodes this from the high 32 bits of the i64
107/// return value (`(action << 32)` - produced by [`FlareAction::to_i64`]).
108#[repr(u32)]
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum FlareAction {
111    /// Send the response the flare just constructed (status / headers /
112    /// body set via [`response`]). This is the typical case.
113    Respond = 1,
114
115    /// Forward to origin and let the flare transform the upstream response
116    /// before it is sent back to the client.
117    Transform = 2,
118
119    /// Skip the flare entirely on this request - pass through to origin
120    /// untouched. Useful for conditional bypass logic.
121    PassThrough = 3,
122}
123
124impl FlareAction {
125    /// Encode this action as the i64 return value of `handle_request`.
126    ///
127    /// The host expects the action enum in the upper 32 bits - anything in
128    /// the lower 32 bits is reserved for future use.
129    pub fn to_i64(self) -> i64 {
130        ((self as u64) << 32) as i64
131    }
132}
133
134/// Export the guest `alloc` function the flaron host runtime requires.
135///
136/// Call this once at the crate root of your flare:
137///
138/// ```ignore
139/// flaron_sdk::export_alloc!();
140/// ```
141///
142/// This expands to a `#[no_mangle]` `extern "C" fn alloc(size: i32) -> i32`
143/// that delegates to [`guest_alloc`]. Without it, every host function that
144/// returns data to the guest will fail.
145///
146/// Most flares should use [`handle_request!`] or [`ws_handlers!`] instead -
147/// those macros include the `alloc` export for you.
148#[macro_export]
149macro_rules! export_alloc {
150    () => {
151        #[no_mangle]
152        pub extern "C" fn alloc(size: i32) -> i32 {
153            $crate::guest_alloc(size)
154        }
155    };
156}
157
158/// Wire up an HTTP flare entrypoint with a single line.
159///
160/// Pass the name of a function that takes no arguments and returns a
161/// [`FlareAction`]. The macro expands to:
162///
163/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
164/// - a `#[no_mangle] extern "C" fn handle_request() -> i64` that resets the
165///   bump arena, calls your handler, and encodes the action for the host.
166///
167/// Read inbound request data from the [`request`] module and write your
168/// response with [`response`]. Returning the action - rather than calling a
169/// host function - lets the host decide whether to ship the response,
170/// transform an upstream response, or pass through to origin.
171///
172/// ```ignore
173/// use flaron_sdk::{request, response, FlareAction};
174///
175/// flaron_sdk::handle_request!(my_flare);
176///
177/// fn my_flare() -> FlareAction {
178///     let body = format!("hello from {} {}", request::method(), request::url());
179///     response::set_status(200);
180///     response::set_header("content-type", "text/plain");
181///     response::set_body_str(&body);
182///     FlareAction::Respond
183/// }
184/// ```
185///
186/// Use [`ws_handlers!`] for WebSocket flares instead - the two macros are
187/// mutually exclusive within one crate because they both define `alloc`.
188#[macro_export]
189macro_rules! handle_request {
190    ($handler:ident) => {
191        $crate::export_alloc!();
192
193        #[no_mangle]
194        pub extern "C" fn handle_request() -> i64 {
195            $crate::reset_arena();
196            let action: $crate::FlareAction = $handler();
197            action.to_i64()
198        }
199    };
200}
201
202/// Wire up a WebSocket flare entrypoint with a single line.
203///
204/// Pass three function names in `open, message, close` order. Each function
205/// takes no arguments and returns nothing. The macro expands to:
206///
207/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
208/// - three `#[no_mangle] extern "C"` exports - `ws_open`, `ws_message`,
209///   `ws_close` - that each reset the bump arena and dispatch to your
210///   handler.
211///
212/// Read connection state and inbound frame data from the [`ws`] module
213/// (`ws::conn_id`, `ws::event_text`, `ws::close_code`, etc.) and send
214/// outbound frames with `ws::send_text` / `ws::send_binary`.
215///
216/// ```ignore
217/// use flaron_sdk::{logging, ws};
218///
219/// flaron_sdk::ws_handlers!(on_open, on_message, on_close);
220///
221/// fn on_open() {
222///     let conn = ws::conn_id();
223///     logging::info(&format!("ws open conn={}", conn));
224///     let _ = ws::send_text(&format!("welcome {}", conn));
225/// }
226///
227/// fn on_message() {
228///     let payload = ws::event_text();
229///     let _ = ws::send_text(&format!("echo: {}", payload));
230/// }
231///
232/// fn on_close() {
233///     let code = ws::close_code();
234///     logging::info(&format!("ws close code={}", code));
235/// }
236/// ```
237///
238/// Use [`handle_request!`] for HTTP flares instead - the two macros are
239/// mutually exclusive within one crate because they both define `alloc`.
240#[macro_export]
241macro_rules! ws_handlers {
242    ($open:ident, $message:ident, $close:ident) => {
243        $crate::export_alloc!();
244
245        #[no_mangle]
246        pub extern "C" fn ws_open() -> i64 {
247            $crate::reset_arena();
248            $open();
249            0
250        }
251
252        #[no_mangle]
253        pub extern "C" fn ws_message() -> i64 {
254            $crate::reset_arena();
255            $message();
256            0
257        }
258
259        #[no_mangle]
260        pub extern "C" fn ws_close() -> i64 {
261            $crate::reset_arena();
262            $close();
263            0
264        }
265    };
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn flare_action_respond_in_high_bits() {
274        let i = FlareAction::Respond.to_i64();
275        assert_eq!(i, 1i64 << 32);
276        assert_eq!((i as u64) >> 32, 1);
277        assert_eq!((i as u64) & 0xFFFF_FFFF, 0);
278    }
279
280    #[test]
281    fn flare_action_transform_in_high_bits() {
282        let i = FlareAction::Transform.to_i64();
283        assert_eq!(i, 2i64 << 32);
284    }
285
286    #[test]
287    fn flare_action_pass_through_in_high_bits() {
288        let i = FlareAction::PassThrough.to_i64();
289        assert_eq!(i, 3i64 << 32);
290    }
291
292    #[test]
293    fn flare_action_values_distinct() {
294        assert_ne!(
295            FlareAction::Respond.to_i64(),
296            FlareAction::Transform.to_i64()
297        );
298        assert_ne!(
299            FlareAction::Transform.to_i64(),
300            FlareAction::PassThrough.to_i64()
301        );
302    }
303
304    #[test]
305    fn flare_action_repr_matches_enum_value() {
306        assert_eq!(FlareAction::Respond as u32, 1);
307        assert_eq!(FlareAction::Transform as u32, 2);
308        assert_eq!(FlareAction::PassThrough as u32, 3);
309    }
310}