actr-framework 0.3.1

Actor-RTC framework core (stub for code generation testing)
Documentation
//! Adapter glue between the user's [`Workload`] impl and the Component
//! Model `Guest` trait generated by wit-bindgen.
//!
//! The `entry!` macro emits a zero-sized `__ActrEntryAdapter` struct in
//! user-crate scope and implements [`generated::exports::actr::workload::workload::Guest`][g]
//! for it. Each trait method delegates to one of the helpers in this
//! module — keeping the macro body terse and the translation logic
//! reviewable here rather than inside a macro expansion.
//!
//! [g]: super::generated::exports::actr::workload::workload::Guest
//!
//! # Singleton workload
//!
//! The generated `Guest` trait methods are **associated functions** on a
//! ZST, not instance methods. actr models a workload as a single owned
//! struct (`impl Workload for MyActor`), so we store a user-constructed
//! instance in a module-scoped `OnceLock`. The `entry!` macro initialises
//! the cell lazily on first access — this matches the pre-Phase-1 semantics
//! where `actr_init` is called once before the first `actr_handle`.
//!
//! # Per-dispatch `WasmContext`
//!
//! Every adapter method constructs a fresh [`super::WasmContext`] by
//! calling the host's `get-self-id` / `get-caller-id` / `get-request-id`
//! imports. The context is cheap to build (three async import calls)
//! and its fields are owned `Clone`able values — no lifetimes threaded
//! through the async boundary.

use std::sync::OnceLock;

use actr_protocol::{ActrError, RpcEnvelope};
use bytes::Bytes;

use crate::workload::{BackpressureEvent, CredentialEvent, ErrorCategory, ErrorEvent, PeerEvent};
use crate::{MessageDispatcher, Workload};

use super::context::{WasmContext, proto_actr_error_to_wit, wit_actr_error_to_proto};
use super::generated::actr::workload::types as wit_types;

// ─────────────────────────────────────────────────────────────────────────────
// Singleton workload cell
// ─────────────────────────────────────────────────────────────────────────────

/// Typed cell that the `entry!` macro populates on first access.
///
/// The macro generates a module-local `OnceLock<W>` (one per compiled
/// guest) and routes every `Guest` trait method through helpers in this
/// module. We expose the cell + accessor as a generic type so the macro
/// stays small and readable.
pub struct WorkloadCell<W: 'static> {
    cell: OnceLock<W>,
}

impl<W: 'static> WorkloadCell<W> {
    /// Create an empty cell. `const`-constructible so it can live in a
    /// `static` with no runtime init cost.
    pub const fn new() -> Self {
        Self {
            cell: OnceLock::new(),
        }
    }

    /// Return the stored workload, constructing it lazily the first time
    /// this is called. `init` is a zero-argument constructor so the
    /// `entry!` macro can wrap any expression (`Default::default()`, a
    /// specific struct literal, etc.).
    pub fn get_or_init<F: FnOnce() -> W>(&self, init: F) -> &W {
        self.cell.get_or_init(init)
    }
}

impl<W: 'static> Default for WorkloadCell<W> {
    fn default() -> Self {
        Self::new()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Error translation
// ─────────────────────────────────────────────────────────────────────────────

/// Map an [`ActrError`] to the WIT `actr-error` variant the host expects
/// as the error arm of every fallible export.
pub(crate) fn actr_error_to_wit(e: ActrError) -> wit_types::ActrError {
    proto_actr_error_to_wit(e)
}

/// Map a WIT `actr-error` variant back into [`ActrError`]. Used when the
/// adapter needs to translate host-reported errors that surface through
/// an export's return value (not currently used, but pairs with
/// [`actr_error_to_wit`] and keeps the surface symmetrical).
pub(crate) fn actr_error_from_wit(e: wit_types::ActrError) -> ActrError {
    wit_actr_error_to_proto(e)
}

// ─────────────────────────────────────────────────────────────────────────────
// Event-type translation (WIT ⇄ actr_framework)
// ─────────────────────────────────────────────────────────────────────────────

fn peer_event_from_wit(e: wit_types::PeerEvent) -> PeerEvent {
    PeerEvent {
        peer: super::context_helpers::actr_id_from_wit(&e.peer),
        relayed: e.relayed,
    }
}

fn error_category_from_wit(c: wit_types::ErrorCategory) -> ErrorCategory {
    match c {
        wit_types::ErrorCategory::HandlerPanic => ErrorCategory::HandlerPanic,
        wit_types::ErrorCategory::HandlerError => ErrorCategory::HandlerError,
        wit_types::ErrorCategory::SignalingFailure => ErrorCategory::SignalingFailure,
        wit_types::ErrorCategory::TransportFailure => ErrorCategory::TransportFailure,
        wit_types::ErrorCategory::DataStreamDeliveryUncertain => {
            ErrorCategory::DataStreamDeliveryUncertain
        }
    }
}

fn timestamp_from_wit(t: wit_types::Timestamp) -> std::time::SystemTime {
    std::time::UNIX_EPOCH + std::time::Duration::new(t.seconds, t.nanoseconds)
}

fn error_event_from_wit(e: wit_types::ErrorEvent) -> ErrorEvent {
    ErrorEvent {
        source: actr_error_from_wit(e.source),
        category: error_category_from_wit(e.category),
        context: e.context,
        timestamp: timestamp_from_wit(e.timestamp),
    }
}

fn credential_event_from_wit(e: wit_types::CredentialEvent) -> CredentialEvent {
    CredentialEvent {
        new_expiry: timestamp_from_wit(e.new_expiry),
    }
}

fn backpressure_event_from_wit(e: wit_types::BackpressureEvent) -> BackpressureEvent {
    BackpressureEvent {
        queue_len: e.queue_len as usize,
        threshold: e.threshold as usize,
    }
}

fn rpc_envelope_from_wit(envelope: wit_types::RpcEnvelope) -> RpcEnvelope {
    RpcEnvelope {
        request_id: envelope.request_id,
        route_key: envelope.route_key,
        payload: if envelope.payload.is_empty() {
            None
        } else {
            Some(Bytes::from(envelope.payload))
        },
        ..Default::default()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Dispatch helpers — called from the generated Guest trait impl
// ─────────────────────────────────────────────────────────────────────────────

/// Bridge from the generated `dispatch(envelope)` export into the
/// user's [`MessageDispatcher`].
///
/// Reads the singleton workload, builds a fresh [`WasmContext`] from
/// the host's per-call context accessors, decodes the WIT envelope,
/// and invokes `<W::Dispatcher as MessageDispatcher>::dispatch`. The
/// handler's `ActorResult<Bytes>` is translated into the WIT
/// `result<list<u8>, actr-error>` shape.
pub async fn run_dispatch<W: Workload>(
    workload: &W,
    envelope: wit_types::RpcEnvelope,
) -> Result<Vec<u8>, wit_types::ActrError> {
    let ctx = WasmContext::from_host().await;
    let envelope = rpc_envelope_from_wit(envelope);
    match <W::Dispatcher as MessageDispatcher>::dispatch(workload, envelope, &ctx).await {
        Ok(bytes) => Ok(bytes.to_vec()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

// ── on_start / on_ready / on_stop / on_error (4, fallible) ───────────────
//
// Each lifecycle hook is inlined rather than funnelled through a
// closure-taking `run_lifecycle_hook` helper. Rust's async lifetimes
// would otherwise force an HRTB shape that fights with `&ctx` crossing
// await points; inlining here keeps the type-check trivial.
//
// The native host installs a synthetic `InvocationContext` while invoking
// package lifecycle exports. Read it through the same context imports as
// dispatch so wasm, dynclib, and linked workloads observe matching
// `self_id` / `caller_id` / `request_id` semantics.

pub async fn run_on_start<W: Workload>(workload: &W) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_host().await;
    match workload.on_start(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_ready<W: Workload>(workload: &W) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_host().await;
    match workload.on_ready(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_stop<W: Workload>(workload: &W) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_host().await;
    match workload.on_stop(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_error<W: Workload>(
    workload: &W,
    event: wit_types::ErrorEvent,
) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_host().await;
    let event = error_event_from_wit(event);
    match workload.on_error(&ctx, &event).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

// ── Signaling (3, infallible) ─────────────────────────────────────────────
//
// `on_signaling_connecting` / `on_signaling_connected` take `Option<&C>`;
// the hosted Component has no way to signal "no context" because the
// context accessors always consult the active invocation. For now we
// always pass `Some(&ctx)` — matches the behaviour during reconnects,
// which is where these hooks actually surface. The initial-connection
// case (where the pre-Phase-1 host passed `None`) does not fire through
// the guest runtime path.

pub async fn run_on_signaling_connecting<W: Workload>(workload: &W) {
    let ctx = WasmContext::from_host().await;
    workload.on_signaling_connecting(Some(&ctx)).await;
}

pub async fn run_on_signaling_connected<W: Workload>(workload: &W) {
    let ctx = WasmContext::from_host().await;
    workload.on_signaling_connected(Some(&ctx)).await;
}

pub async fn run_on_signaling_disconnected<W: Workload>(workload: &W) {
    let ctx = WasmContext::from_host().await;
    workload.on_signaling_disconnected(&ctx).await;
}

// ── WebSocket (3, infallible) ─────────────────────────────────────────────

pub async fn run_on_websocket_connecting<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_websocket_connecting(&ctx, &event).await;
}

pub async fn run_on_websocket_connected<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_websocket_connected(&ctx, &event).await;
}

pub async fn run_on_websocket_disconnected<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_websocket_disconnected(&ctx, &event).await;
}

// ── WebRTC P2P (3, infallible) ────────────────────────────────────────────

pub async fn run_on_webrtc_connecting<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_webrtc_connecting(&ctx, &event).await;
}

pub async fn run_on_webrtc_connected<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_webrtc_connected(&ctx, &event).await;
}

pub async fn run_on_webrtc_disconnected<W: Workload>(workload: &W, event: wit_types::PeerEvent) {
    let ctx = WasmContext::from_host().await;
    let event = peer_event_from_wit(event);
    workload.on_webrtc_disconnected(&ctx, &event).await;
}

// ── Credential (2, infallible) ────────────────────────────────────────────

pub async fn run_on_credential_renewed<W: Workload>(
    workload: &W,
    event: wit_types::CredentialEvent,
) {
    let ctx = WasmContext::from_host().await;
    let event = credential_event_from_wit(event);
    workload.on_credential_renewed(&ctx, &event).await;
}

pub async fn run_on_credential_expiring<W: Workload>(
    workload: &W,
    event: wit_types::CredentialEvent,
) {
    let ctx = WasmContext::from_host().await;
    let event = credential_event_from_wit(event);
    workload.on_credential_expiring(&ctx, &event).await;
}

// ── Mailbox (1, infallible) ───────────────────────────────────────────────

pub async fn run_on_mailbox_backpressure<W: Workload>(
    workload: &W,
    event: wit_types::BackpressureEvent,
) {
    let ctx = WasmContext::from_host().await;
    let event = backpressure_event_from_wit(event);
    workload.on_mailbox_backpressure(&ctx, &event).await;
}

pub async fn run_on_data_stream(
    chunk: wit_types::DataStream,
    sender: wit_types::ActrId,
) -> Result<(), wit_types::ActrError> {
    super::context::dispatch_registered_stream(chunk, sender)
        .await
        .map_err(actr_error_to_wit)
}