Wires the KV store into Rhai scripts via the handle pattern:
let widgets = kv::collection("widgets");
widgets.set("k", #{ n: 1 });
let v = widgets.get("k"); // value or () if absent
widgets.has("k") / widgets.delete("k")
let page = widgets.list(); // cursor-style pagination
`KvHandle` is a custom Rhai type holding `Arc<dyn KvService>` + the
per-call `Arc<SdkCallCx>`. Methods route async service calls through
`tokio::Handle::current().block_on(...)` — works because
`LocalExecutorClient` runs the script under `spawn_blocking` so a
runtime is reachable. The bridge surfaces `app_id` exclusively
through `cx.app_id`; no public-facing argument can spoof an app.
`TriggerEvent` lands in `picloud-shared` as the wire shape the
dispatcher will emit (KV + DeadLetter variants — KV exercised now,
DL hooks up with the dispatcher in commit 5/8). `SdkCallCx` and
`ExecRequest` grow `is_dead_letter_handler: bool` and
`event: Option<TriggerEvent>`. `engine.rs::build_ctx_map` flattens
the event into `ctx.event` for triggered handlers; direct ingress
leaves the key absent so scripts can `if "event" in ctx`.
Tests:
- 7 `sdk_kv.rs` integration tests covering the full Rhai surface
(round-trip, missing-key unit, has bool, delete was-present,
empty-collection rejection, cursor pagination, cross-app
isolation through the bridge).
- 3 new `engine.rs` tests pinning `ctx.event` shape per
design notes §4 (KV insert with value, delete with unit value,
direct invocations have no `event` key).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
3.3 KiB
Rust
70 lines
3.3 KiB
Rust
//! `SdkCallCx` — per-call context every stateful SDK service receives.
|
|
//!
|
|
//! Service trait methods (added by subsequent v1.1.x PRs starting with
|
|
//! KV) all take `&SdkCallCx` so they can:
|
|
//! * scope by `app_id` for cross-app isolation,
|
|
//! * audit `principal` when authenticated,
|
|
//! * carry `execution_id` / `request_id` into emitted events,
|
|
//! * bound trigger chains via `trigger_depth` / `root_execution_id`.
|
|
//!
|
|
//! The struct lives in `picloud-shared` (not `executor-core`) because
|
|
//! future service impls live in `manager-core` and the trait that hands
|
|
//! the cx in is shared by both sides. Pure value type — no handles, no
|
|
//! DB pool references, no allocations beyond what's in `Principal`.
|
|
|
|
use crate::{AppId, ExecutionId, Principal, RequestId, TriggerEvent};
|
|
|
|
/// Per-invocation context for every stateful SDK service call.
|
|
///
|
|
/// Constructed once at the start of an invocation by `executor-core`
|
|
/// from the incoming `ExecRequest`, then handed (by reference) to every
|
|
/// service trait method the script triggers during execution. Services
|
|
/// MUST derive `app_id` from this struct — never from script-passed
|
|
/// arguments — to preserve cross-app isolation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SdkCallCx {
|
|
/// Owning application for this invocation. Source of truth for
|
|
/// every `(app_id, …)` storage lookup the script makes.
|
|
pub app_id: AppId,
|
|
|
|
/// Caller identity, when authenticated. `None` for unauthenticated
|
|
/// data-plane HTTP requests (the common case for public endpoints);
|
|
/// `Some` when the call came in via the dashboard, an API key, or a
|
|
/// future authed surface.
|
|
pub principal: Option<Principal>,
|
|
|
|
/// Unique id for THIS execution. Matches `ExecRequest.execution_id`.
|
|
pub execution_id: ExecutionId,
|
|
|
|
/// Unique id for the ingress request that started the chain. The
|
|
/// same `request_id` is shared across every execution triggered by
|
|
/// the same request (direct + trigger fan-out).
|
|
pub request_id: RequestId,
|
|
|
|
/// `0` for direct invocations (HTTP request, manual run). Each
|
|
/// indirect invocation through the triggers framework (v1.1.1)
|
|
/// increments this; the dispatcher rejects beyond a configured
|
|
/// ceiling to prevent runaway feedback loops.
|
|
pub trigger_depth: u32,
|
|
|
|
/// `== execution_id` when `trigger_depth == 0`; otherwise the
|
|
/// `execution_id` of the original ingress execution. Lets the audit
|
|
/// log group every fan-out execution under the originating event.
|
|
pub root_execution_id: ExecutionId,
|
|
|
|
/// `true` only when this invocation is a `dead_letter` trigger
|
|
/// handler. Set by the dispatcher when it picks an outbox row
|
|
/// whose trigger has `kind = 'dead_letter'`. The retry / dead-
|
|
/// letter machinery short-circuits when this is set: handlers
|
|
/// execute once, with no retry, and a failed run can NEVER be
|
|
/// dead-lettered itself (design notes §4 recursion-stop rule).
|
|
/// `false` for every other invocation, including the script
|
|
/// being used as a non-DL trigger handler.
|
|
pub is_dead_letter_handler: bool,
|
|
|
|
/// The event that fired this script, when it's a triggered
|
|
/// invocation. `None` for direct ingress (HTTP request, manual
|
|
/// run). Surfaced to scripts as `ctx.event`.
|
|
pub event: Option<TriggerEvent>,
|
|
}
|