flaron-sdk 0.99.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::export_alloc!();
//!
//! #[no_mangle]
//! pub extern "C" fn handle_request() -> i64 {
//!     flaron_sdk::reset_arena();
//!
//!     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.to_i64()
//! }
//! ```
//!
//! 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 [`export_alloc!`] macro wires
//! the export up; the [`handle_request!`] / [`ws_handlers!`] macros (TODO)
//! will reset the arena for you. Until those exist, call [`reset_arena`] at
//! the top of your exports.

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.
#[macro_export]
macro_rules! export_alloc {
    () => {
        #[no_mangle]
        pub extern "C" fn alloc(size: i32) -> i32 {
            $crate::guest_alloc(size)
        }
    };
}

#[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);
    }
}