coil-runtime 0.1.1

HTTP runtime and request handling for the Coil framework.
Documentation
mod auth;
mod data;
mod render;
mod services;

use super::auth_backend::RuntimeAuthBackend;
use super::host::RuntimeWasmHostServices;
use super::support::{
    runtime_auth_backend_error, runtime_data_backend_error, runtime_executor_error,
    runtime_host_service_error, storage_class_from_grant, trace_id,
};
use super::*;
use std::sync::OnceLock;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

#[derive(Debug)]
pub(super) struct RuntimeHostServiceExecutor {
    plan: RuntimePlan,
    auth_backend: OnceLock<Result<RuntimeAuthBackend, String>>,
    data_backend: OnceLock<Result<RuntimeDataBackend, String>>,
    services: RuntimeWasmHostServices,
}

impl RuntimeHostServiceExecutor {
    pub(super) fn with_services(plan: RuntimePlan, services: RuntimeWasmHostServices) -> Self {
        Self {
            services,
            plan,
            auth_backend: OnceLock::new(),
            data_backend: OnceLock::new(),
        }
    }

    fn host_service_execution(
        &self,
        call: &HostServiceCall,
        result: HostServiceResult,
    ) -> HostServiceExecution {
        HostServiceExecution {
            call: call.clone(),
            result,
        }
    }

    fn record_observability(
        &self,
        span: &str,
        trace_id: &str,
        outcome: &str,
        duration_ms: u64,
    ) {
        let telemetry = &self.plan.observability.telemetry;
        let _ = telemetry.record_histogram("coil.extension.runtime_ms", duration_ms);
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        let _ = telemetry.record_trace(
            coil_observability::TraceRecord::new(trace_id.to_string(), span, outcome, now)
                .with_field("customer_app", self.plan.config.app.name.clone())
                .with_field("duration_ms", duration_ms.to_string()),
        );
    }
}

impl HostServiceExecutor for RuntimeHostServiceExecutor {
    fn execute(
        &self,
        call: &HostServiceCall,
        context: &InvocationContext,
    ) -> Result<HostServiceExecution, WasmModelError> {
        let span = match &call.request {
            HostServiceRequest::Auth(_) => "wasm.host.auth",
            HostServiceRequest::Data(_) => "wasm.host.data",
            HostServiceRequest::Storage(_) => "wasm.host.storage",
            HostServiceRequest::Render(_) => "wasm.host.render",
            HostServiceRequest::CacheIntent(_) => "wasm.host.cache_intent",
            HostServiceRequest::OutboundHttp { .. } => "wasm.host.outbound_http",
            HostServiceRequest::SecretRead { .. } => "wasm.host.secret_read",
            HostServiceRequest::EnqueueJob { .. } => "wasm.host.enqueue_job",
            HostServiceRequest::MetadataWrite { .. } => "wasm.host.metadata_write",
        };
        let started_at = Instant::now();
        let result = match &call.request {
            HostServiceRequest::Auth(request) => self.execute_auth(call, context, request),
            HostServiceRequest::Data(request) => self.execute_data(call, context, request),
            HostServiceRequest::Storage(request) => self.execute_storage(call, context, request),
            HostServiceRequest::Render(request) => self.execute_render(call, context, request),
            HostServiceRequest::CacheIntent(request) => {
                self.execute_cache_intent(call, context, request)
            }
            HostServiceRequest::OutboundHttp {
                integration,
                response_bytes,
            } => self.dispatch_outbound_http_to_blocking_pool(
                call,
                context,
                integration,
                *response_bytes,
            ),
            HostServiceRequest::SecretRead { secret } => self.execute_secret(call, context, secret),
            HostServiceRequest::EnqueueJob { queue } => self.execute_job(call, context, queue),
            HostServiceRequest::MetadataWrite { kind } => {
                self.execute_metadata(call, context, *kind)
            }
        };
        let elapsed_ms = started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
        self.record_observability(
            span,
            trace_id(context),
            if result.is_ok() { "ok" } else { "error" },
            elapsed_ms,
        );
        result
    }
}

#[cfg(test)]
mod tests;