feat(sdk): Context::emit for WIT 1.1 host.emit-event (T1)
Some checks failed
CI / Linux x86_64 (Forgejo) (push) Failing after 1s
Some checks failed
CI / Linux x86_64 (Forgejo) (push) Failing after 1s
Brings the module SDK to WIT 1.1 in lockstep with the platform and
exposes the new host.emit-event as an ergonomic ctx.emit(name,
payload). Modules emit ephemeral progress/stream events (progress,
token streaming) delivered live to a following client and never
persisted.
- wit/world.wit → 1.1.0 (host.emit-event + use types.{payload}).
- host.rs: an emit hook (thread-local fn pointer; guest is
single-threaded). The #[fai_module] expansion installs a closure
reaching the real WIT emit-event import at the start of each
invocation; Context::emit calls through it. Outside a hub (unit
tests) it is a silent no-op.
- examples/progress: reference module emitting progress + token events;
builds clean for wasm32-wasip2, proving the ergonomics end-to-end.
The SDK keeps shielding module authors: emit is the whole surface, no
wit_bindgen in sight.
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
98e98e9371
commit
f32dc820b9
9 changed files with 568 additions and 5 deletions
|
|
@ -95,6 +95,13 @@ pub fn fai_module(_attrs: TokenStream, input: TokenStream) -> TokenStream {
|
|||
let sdk_inputs =
|
||||
::chain_module_sdk::Inputs::__from_pairs(sdk_pairs);
|
||||
|
||||
// Wire Context::emit to the real WIT host import
|
||||
// for the duration of this invocation (WIT 1.1).
|
||||
::chain_module_sdk::__set_emit_hook(|name, payload| {
|
||||
let wit = sdk_payload_to_wit(payload);
|
||||
chain::platform::host::emit_event(name, &wit);
|
||||
});
|
||||
|
||||
match super::#user_fn_name(sdk_ctx, sdk_inputs) {
|
||||
Ok(outputs) => {
|
||||
let pairs = outputs.__into_pairs();
|
||||
|
|
|
|||
|
|
@ -11,3 +11,23 @@ pub struct Context {
|
|||
pub capability_name: String,
|
||||
pub deadline_ms: u64,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Emit an ephemeral progress/stream event during this invocation
|
||||
/// (WIT 1.1 `emit-event`). Delivered live to any client following
|
||||
/// the flow (`SubmitStream`); it is **never** written to the audit
|
||||
/// log. Use it for progress updates or token streaming.
|
||||
///
|
||||
/// `name` is a caller-defined label (e.g. `"progress"`, `"token"`);
|
||||
/// `payload` is the event body. The host enforces per-invocation
|
||||
/// size + rate limits — an over-limit event is dropped, never an
|
||||
/// error. When the module runs outside a hub that supports it (or
|
||||
/// in a unit test) `emit` is a silent no-op.
|
||||
///
|
||||
/// ```ignore
|
||||
/// ctx.emit("progress", Payload::Text("50%".into()));
|
||||
/// ```
|
||||
pub fn emit(&self, name: &str, payload: crate::Payload) {
|
||||
crate::emit(name, payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
crates/chain-module-sdk/src/host.rs
Normal file
35
crates/chain-module-sdk/src/host.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! Host-callback plumbing for `Context::emit` (WIT 1.1 `emit-event`).
|
||||
//!
|
||||
//! The WIT host imports (`emit-event`) are only reachable from the
|
||||
//! `wit_bindgen`-generated glue inside the `#[fai_module]` expansion,
|
||||
//! which lives in the module crate — not here. So the macro installs a
|
||||
//! function pointer that reaches the real host import, and
|
||||
//! [`Context::emit`](crate::Context::emit) calls it through this hook.
|
||||
//!
|
||||
//! On a non-wasm host target (unit tests) no hook is installed, so
|
||||
//! `emit` is a silent no-op. The guest is single-threaded, so a
|
||||
//! thread-local cell is sufficient and lock-free.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::Payload;
|
||||
|
||||
thread_local! {
|
||||
static EMIT_HOOK: Cell<Option<fn(&str, Payload)>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
/// Install the emit hook. Called by the `#[fai_module]` expansion at
|
||||
/// the start of each invocation. Not part of the stable public API.
|
||||
#[doc(hidden)]
|
||||
pub fn __set_emit_hook(f: fn(&str, Payload)) {
|
||||
EMIT_HOOK.with(|c| c.set(Some(f)));
|
||||
}
|
||||
|
||||
/// Forward an event to the host, if a hook is installed.
|
||||
pub(crate) fn emit(name: &str, payload: Payload) {
|
||||
EMIT_HOOK.with(|c| {
|
||||
if let Some(f) = c.get() {
|
||||
f(name, payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
mod context;
|
||||
mod error;
|
||||
mod host;
|
||||
mod inputs;
|
||||
mod outputs;
|
||||
mod payload;
|
||||
|
|
@ -37,6 +38,12 @@ pub use payload::{BytesValue, FileRef, Payload};
|
|||
|
||||
pub use chain_module_sdk_macros::fai_module;
|
||||
|
||||
// Emit-hook plumbing used by the `#[fai_module]` expansion (wasm32)
|
||||
// so `Context::emit` reaches the WIT `emit-event` host import.
|
||||
#[doc(hidden)]
|
||||
pub use host::__set_emit_hook;
|
||||
pub(crate) use host::emit;
|
||||
|
||||
/// Convenience prelude — `use chain_module_sdk::prelude::*;` brings
|
||||
/// every type a typical module needs into scope.
|
||||
pub mod prelude {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ use std::path::PathBuf;
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Identical to FROZEN_WIT_SHA256 in
|
||||
/// fai/platform :: crates/fai_runtime/tests/wit_freeze.rs.
|
||||
/// fai/chain :: crates/chain_runtime/tests/wit_freeze.rs.
|
||||
/// Update both repos in lockstep, never one alone.
|
||||
/// 2026-07-05 — WIT 1.0.0 → 1.1.0 (additive: host.emit-event). The SDK
|
||||
/// wit/world.wit is now byte-identical to the platform's, so the two
|
||||
/// frozen hashes match exactly.
|
||||
const PLATFORM_FROZEN_WIT_SHA256: &str =
|
||||
"6f4e2266a4264832a16fe5a3704622cd978b290b90f3300c3ed236d98b689b5f";
|
||||
"2ba9d6c61c4c77d081679603a9389aef663429ec80f9c386cf9b1ca9d353bf6f";
|
||||
|
||||
fn world_wit_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue