flaron-sdk 1.1.0

Official Rust SDK for writing Flaron edge flares - WebAssembly modules that run on the Flaron CDN edge runtime.
Documentation
//! # flaron-sdk
//!
//! Rust SDK for building **flares** - Wasm functions that run on the
//! [Flaron][flaron] CDN edge. A flare receives an HTTP request (or WebSocket
//! event) at the nearest edge, runs your Rust code in a sandboxed Wasm
//! runtime, and returns a response with single-digit-millisecond latency.
//!
//! [flaron]: https://flaron.dev
//!
//! ## Quick start
//!
//! ```toml
//! # Cargo.toml
//! [package]
//! name = "my-flare"
//! version = "0.1.0"
//! edition = "2021"
//!
//! [lib]
//! crate-type = ["cdylib"]
//!
//! [dependencies]
//! flaron-sdk = "0.1"
//! ```
//!
//! ```ignore
//! // src/lib.rs
//! use flaron_sdk::{request, response, FlareAction};
//!
//! flaron_sdk::handle_request!(my_flare);
//!
//! fn my_flare() -> FlareAction {
//!     let body = format!("hello from {} {}", request::method(), request::url());
//!     response::set_status(200);
//!     response::set_header("content-type", "text/plain");
//!     response::set_body(body.as_bytes());
//!     FlareAction::Respond
//! }
//! ```
//!
//! Build with:
//!
//! ```sh
//! cargo build --release --target wasm32-unknown-unknown
//! ```
//!
//! Then deploy `target/wasm32-unknown-unknown/release/my_flare.wasm` to
//! Flaron via the dashboard or `flaronctl`.
//!
//! ## What's in the SDK
//!
//! | Module                | What it does                                              |
//! |-----------------------|-----------------------------------------------------------|
//! | [`request`]           | Read inbound request (method, URL, headers, body)         |
//! | [`response`]          | Write outbound response (status, headers, body)           |
//! | [`beam`]              | Outbound HTTP from the edge ([`beam::fetch`])             |
//! | [`spark`]             | Per-site KV with TTL, persisted to disk on the edge       |
//! | [`plasma`]            | Cross-edge CRDT KV - counters, presence, leaderboards     |
//! | [`secrets`]           | Read domain-scoped secrets allowlisted for this flare     |
//! | [`crypto`]            | Hash, HMAC, AES-GCM encrypt/decrypt, JWT signing, RNG     |
//! | [`encoding`]          | Base64, hex, URL encode/decode helpers                    |
//! | [`id`]                | UUID v4/v7, ULID, KSUID, Nanoid, Snowflake generators     |
//! | [`time`]              | Timestamps in unix / ms / ns / RFC3339 / HTTP / ISO8601   |
//! | [`logging`]           | Structured logs surfaced via the edge node's slog stream  |
//! | [`ws`]                | WebSocket: send, close, read events from open/message/close |
//!
//! ## Memory model
//!
//! Each flare invocation gets a fresh 256 KiB bump arena that the host writes
//! into via the guest's exported `alloc` function. The SDK resets the arena
//! at the top of every invocation so memory is reclaimed automatically - you
//! never need to free anything yourself.
//!
//! The recommended entrypoint is the [`handle_request!`] macro for HTTP
//! flares and [`ws_handlers!`] for WebSocket flares - both wire up the
//! `alloc` export, reset the arena on every invocation, and call your
//! handler. Use [`export_alloc!`] + [`reset_arena`] manually only when you
//! need to define the host exports yourself.

mod ffi;
pub(crate) mod mem;

pub mod beam;
pub mod crypto;
pub mod encoding;
pub mod id;
pub mod logging;
pub mod plasma;
pub mod request;
pub mod response;
pub mod secrets;
pub mod spark;
pub mod time;
pub mod ws;

pub use beam::{BeamError, FetchOptions, FetchResponse};
pub use crypto::{CryptoError, RandomBytesError};
pub use mem::{guest_alloc, reset_arena};
pub use plasma::PlasmaError;
pub use spark::{SparkEntry, SparkError, SparkPullError};
pub use ws::WsSendError;

/// Action returned by an `handle_request()` export to tell the host how to
/// proceed once the flare's body has run.
///
/// The flaron host runtime decodes this from the high 32 bits of the i64
/// return value (`(action << 32)` - produced by [`FlareAction::to_i64`]).
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlareAction {
    /// Send the response the flare just constructed (status / headers /
    /// body set via [`response`]). This is the typical case.
    Respond = 1,

    /// Forward to origin and let the flare transform the upstream response
    /// before it is sent back to the client.
    Transform = 2,

    /// Skip the flare entirely on this request - pass through to origin
    /// untouched. Useful for conditional bypass logic.
    PassThrough = 3,
}

impl FlareAction {
    /// Encode this action as the i64 return value of `handle_request`.
    ///
    /// The host expects the action enum in the upper 32 bits - anything in
    /// the lower 32 bits is reserved for future use.
    pub fn to_i64(self) -> i64 {
        ((self as u64) << 32) as i64
    }
}

/// Export the guest `alloc` function the flaron host runtime requires.
///
/// Call this once at the crate root of your flare:
///
/// ```ignore
/// flaron_sdk::export_alloc!();
/// ```
///
/// This expands to a `#[no_mangle]` `extern "C" fn alloc(size: i32) -> i32`
/// that delegates to [`guest_alloc`]. Without it, every host function that
/// returns data to the guest will fail.
///
/// Most flares should use [`handle_request!`] or [`ws_handlers!`] instead -
/// those macros include the `alloc` export for you.
#[macro_export]
macro_rules! export_alloc {
    () => {
        #[no_mangle]
        pub extern "C" fn alloc(size: i32) -> i32 {
            $crate::guest_alloc(size)
        }
    };
}

/// Wire up an HTTP flare entrypoint with a single line.
///
/// Pass the name of a function that takes no arguments and returns a
/// [`FlareAction`]. The macro expands to:
///
/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
/// - a `#[no_mangle] extern "C" fn handle_request() -> i64` that resets the
///   bump arena, calls your handler, and encodes the action for the host.
///
/// Read inbound request data from the [`request`] module and write your
/// response with [`response`]. Returning the action - rather than calling a
/// host function - lets the host decide whether to ship the response,
/// transform an upstream response, or pass through to origin.
///
/// ```ignore
/// use flaron_sdk::{request, response, FlareAction};
///
/// flaron_sdk::handle_request!(my_flare);
///
/// fn my_flare() -> FlareAction {
///     let body = format!("hello from {} {}", request::method(), request::url());
///     response::set_status(200);
///     response::set_header("content-type", "text/plain");
///     response::set_body_str(&body);
///     FlareAction::Respond
/// }
/// ```
///
/// Use [`ws_handlers!`] for WebSocket flares instead - the two macros are
/// mutually exclusive within one crate because they both define `alloc`.
#[macro_export]
macro_rules! handle_request {
    ($handler:ident) => {
        $crate::export_alloc!();

        #[no_mangle]
        pub extern "C" fn handle_request() -> i64 {
            $crate::reset_arena();
            let action: $crate::FlareAction = $handler();
            action.to_i64()
        }
    };
}

/// Wire up a WebSocket flare entrypoint with a single line.
///
/// Pass three function names in `open, message, close` order. Each function
/// takes no arguments and returns nothing. The macro expands to:
///
/// - the `alloc` export the host runtime requires (via [`export_alloc!`]),
/// - three `#[no_mangle] extern "C"` exports - `ws_open`, `ws_message`,
///   `ws_close` - that each reset the bump arena and dispatch to your
///   handler.
///
/// Read connection state and inbound frame data from the [`ws`] module
/// (`ws::conn_id`, `ws::event_text`, `ws::close_code`, etc.) and send
/// outbound frames with `ws::send_text` / `ws::send_binary`.
///
/// ```ignore
/// use flaron_sdk::{logging, ws};
///
/// flaron_sdk::ws_handlers!(on_open, on_message, on_close);
///
/// fn on_open() {
///     let conn = ws::conn_id();
///     logging::info(&format!("ws open conn={}", conn));
///     let _ = ws::send_text(&format!("welcome {}", conn));
/// }
///
/// fn on_message() {
///     let payload = ws::event_text();
///     let _ = ws::send_text(&format!("echo: {}", payload));
/// }
///
/// fn on_close() {
///     let code = ws::close_code();
///     logging::info(&format!("ws close code={}", code));
/// }
/// ```
///
/// Use [`handle_request!`] for HTTP flares instead - the two macros are
/// mutually exclusive within one crate because they both define `alloc`.
#[macro_export]
macro_rules! ws_handlers {
    ($open:ident, $message:ident, $close:ident) => {
        $crate::export_alloc!();

        #[no_mangle]
        pub extern "C" fn ws_open() -> i64 {
            $crate::reset_arena();
            $open();
            0
        }

        #[no_mangle]
        pub extern "C" fn ws_message() -> i64 {
            $crate::reset_arena();
            $message();
            0
        }

        #[no_mangle]
        pub extern "C" fn ws_close() -> i64 {
            $crate::reset_arena();
            $close();
            0
        }
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn flare_action_respond_in_high_bits() {
        let i = FlareAction::Respond.to_i64();
        assert_eq!(i, 1i64 << 32);
        assert_eq!((i as u64) >> 32, 1);
        assert_eq!((i as u64) & 0xFFFF_FFFF, 0);
    }

    #[test]
    fn flare_action_transform_in_high_bits() {
        let i = FlareAction::Transform.to_i64();
        assert_eq!(i, 2i64 << 32);
    }

    #[test]
    fn flare_action_pass_through_in_high_bits() {
        let i = FlareAction::PassThrough.to_i64();
        assert_eq!(i, 3i64 << 32);
    }

    #[test]
    fn flare_action_values_distinct() {
        assert_ne!(
            FlareAction::Respond.to_i64(),
            FlareAction::Transform.to_i64()
        );
        assert_ne!(
            FlareAction::Transform.to_i64(),
            FlareAction::PassThrough.to_i64()
        );
    }

    #[test]
    fn flare_action_repr_matches_enum_value() {
        assert_eq!(FlareAction::Respond as u32, 1);
        assert_eq!(FlareAction::Transform as u32, 2);
        assert_eq!(FlareAction::PassThrough as u32, 3);
    }
}