//! `InvokeService` — the v1.1.9 function-composition contract. //! //! Backs the Rhai `invoke(target, args)` and `invoke_async(target, args)` //! SDK calls. Resolves a target (by route path, by script name, or by //! script id) within the **caller's app** to a `ResolvedScript`. Same- //! app only; cross-app calls are rejected at the service entry point //! (v1.1.x maintains strict isolation; cross-app sharing arrives in //! v1.3+). //! //! The re-entrant Rhai pattern lives in `executor-core`: the bridge //! captures `Arc`, resolves the target via this trait, builds a //! fresh `ExecRequest` with `trigger_depth + 1`, and calls //! `Engine::execute(&source, req)` synchronously inside the caller's //! `spawn_blocking` thread. Engine + AST cache are shared; only the //! `SdkCallCx` changes per call. use async_trait::async_trait; use chrono::{DateTime, Utc}; use thiserror::Error; use crate::{AppId, ExecutionId, ScriptId, SdkCallCx}; /// Identifies the script to invoke. Accepted by `invoke()` and /// `invoke_async()` script-side. /// /// - `Path("/users/:id")` → resolve via the orchestrator's route trie. /// - `Name("payments_worker")` → `(cx.app_id, name) → script_id`. /// - `Id(...)` → direct lookup, same-app verification afterward. #[derive(Debug, Clone)] pub enum InvokeTarget { Path(String), Name(String), Id(ScriptId), } impl InvokeTarget { /// Best-effort summary for error messages. #[must_use] pub fn describe(&self) -> String { match self { Self::Path(p) => format!("path {p:?}"), Self::Name(n) => format!("name {n:?}"), Self::Id(i) => format!("id {i}"), } } } /// The script the invoke service resolved. `app_id` MUST match the /// caller's `cx.app_id` — the service enforces this and returns /// `InvokeError::CrossApp` otherwise. #[derive(Debug, Clone)] pub struct ResolvedScript { pub script_id: ScriptId, pub app_id: AppId, pub source: String, pub updated_at: DateTime, /// Script.name — surfaced to the callee as `ctx.script_name`. pub name: String, } #[async_trait] pub trait InvokeService: Send + Sync { /// Resolve a target to its script + verify same-app. Used by both /// `invoke()` (sync) and `invoke_async()` (fire-and-forget). async fn resolve( &self, cx: &SdkCallCx, target: InvokeTarget, ) -> Result; /// Fire-and-forget: write a v1.1.9 `OutboxSourceKind::Invoke` row /// the dispatcher consumes; return the new `ExecutionId`. No retry /// (one shot); the script wraps in `retry::with` if it wants more. async fn enqueue_async( &self, cx: &SdkCallCx, target: InvokeTarget, args: serde_json::Value, ) -> Result; } #[derive(Debug, Error)] pub enum InvokeError { /// Target script not found in `cx.app_id`. #[error("invoke: target not found ({0})")] NotFound(String), /// Target exists but belongs to a different app. v1.1.x rejects /// cross-app invokes outright. #[error("invoke: target script belongs to a different app")] CrossApp, /// Depth limit exceeded — `cx.trigger_depth >= limits.trigger_depth_max`. /// Caller's `invoke()` would push the callee past the bound. #[error("invoke: depth limit exceeded (max {0})")] DepthExceeded(u32), /// Caller principal lacks the required capability. Anonymous public- /// HTTP scripts skip this check; authenticated callers gate. #[error("forbidden")] Forbidden, /// Payload could not be serialized (e.g. a Rhai FnPtr passed across /// the invoke boundary — closures are explicitly rejected). #[error("invoke rejected: {0}")] Rejected(String), /// Backend / database error. Named `Backend` to align with KvError / /// DocsError / FilesError / SecretsError / QueueError / PubsubError. #[error("invoke backend error: {0}")] Backend(String), } /// Test-only stub: every call errors. Lets harnesses build a `Services` /// bundle without wiring an `InvokeService`. #[derive(Debug, Default, Clone, Copy)] pub struct NoopInvokeService; #[async_trait] impl InvokeService for NoopInvokeService { async fn resolve( &self, _cx: &SdkCallCx, _target: InvokeTarget, ) -> Result { Err(InvokeError::Backend("invoke is not wired in".into())) } async fn enqueue_async( &self, _cx: &SdkCallCx, _target: InvokeTarget, _args: serde_json::Value, ) -> Result { Err(InvokeError::Backend("invoke is not wired in".into())) } }