diff --git a/README.md b/README.md index 5f91e20d..bd98607b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The engine uses the name as the state namespace, and it refuses a missing, empty cargo run -p nexum-cli -- target/wasm32-wasip2/release/example.wasm modules/example/component.toml ``` -A module that subscribes to `block` or `chain-log` events needs its chain declared in `engine.toml`, or the engine refuses to boot. +A module that declares a `block` or `event` trigger needs its chain declared in `engine.toml`, or the engine refuses to boot. The smallest working stanza is: ```toml @@ -57,7 +57,7 @@ rpc_url = "http://localhost:8545" ``` `http(s)://` URLs are not dialled at boot; `ws(s)://` URLs are. -The example module declares no subscriptions, so `just run` needs no `engine.toml`; the modules under `modules/examples/` and `modules/fixtures/` do. +The example module declares no triggers, so `just run` needs no `engine.toml`; the modules under `modules/examples/` and `modules/fixtures/` do. ## Component integrity diff --git a/crates/nexum-module-macros/Cargo.toml b/crates/nexum-module-macros/Cargo.toml index bb5df461..6a24849c 100644 --- a/crates/nexum-module-macros/Cargo.toml +++ b/crates/nexum-module-macros/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Proc-macro glue for nexum runtime modules: #[module] emits the per-cdylib wit-bindgen, host adapter, event dispatch, and export." +description = "Proc-macro glue for nexum runtime modules: #[module] emits the per-cdylib wit-bindgen, host adapter, trigger dispatch, and export." [lib] proc-macro = true diff --git a/crates/nexum-module-macros/src/lib.rs b/crates/nexum-module-macros/src/lib.rs index eaf10ae1..00941bcf 100644 --- a/crates/nexum-module-macros/src/lib.rs +++ b/crates/nexum-module-macros/src/lib.rs @@ -14,13 +14,19 @@ use syn::{ImplItem, ItemImpl}; /// The handler names recognized on a `#[module]` impl. An `on_`-prefixed /// method outside this set is a compile error; an absent handler /// dispatches as a no-op. -const HANDLERS: [&str; 5] = ["init", "on_block", "on_chain_logs", "on_tick", "on_custom"]; +const HANDLERS: [&str; 5] = [ + "init", + "on_block", + "on_event", + "on_schedule", + "on_extension", +]; /// Generate the per-cdylib glue for a nexum module. /// -/// Apply to an `impl` block whose associated functions are the event -/// handlers (`init`, `on_block`, `on_chain_logs`, `on_tick`, -/// `on_custom`); each takes its event's wit-bindgen +/// Apply to an `impl` block whose associated functions are the trigger +/// handlers (`init`, `on_block`, `on_event`, `on_schedule`, +/// `on_extension`); each takes its trigger's wit-bindgen /// payload and returns `Result<(), Fault>`, and `init` takes the config /// table. Undefined handlers dispatch as no-ops. Emits /// `wit_bindgen::generate!`, the host adapter, the `Guest` impl, and @@ -36,8 +42,8 @@ const HANDLERS: [&str; 5] = ["init", "on_block", "on_chain_logs", "on_tick", "on /// std prelude names `Result`, `Vec`, or `Ok` (the generated `Guest` /// trait refers to them unqualified). /// -/// `subscribes(EventType, ...)` fails the build unless the named events' -/// `SolEvent::SIGNATURE_HASH` values and the manifest's chain-log +/// `sol_events(EventType, ...)` fails the build unless the named Solidity +/// events' `SolEvent::SIGNATURE_HASH` values and the manifest's event trigger /// `event_signature` values match as sets; the manifest stays authoritative. #[proc_macro_attribute] pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -72,8 +78,8 @@ fn expand(args: ModuleArgs, input: ItemImpl) -> Result Result Result Result ::core::result::Result<(), Fault> { - match event { + fn on_trigger( + trigger: nexum::host::types::Trigger, + ) -> ::core::result::Result<(), Fault> { + match trigger { #block_arm - #logs_arm - #tick_arm - #custom_arm + #event_arm + #schedule_arm + #extension_arm } } } @@ -167,14 +175,14 @@ fn expand(args: ModuleArgs, input: ItemImpl) -> Result syn::Error::new(span, message), + | Self::EmptySolEvents { span } => syn::Error::new(span, message), Self::UnnamedSelfType { self_ty: tokens } | Self::TraitImpl { trait_path: tokens } | Self::GenericImpl { generics: tokens } @@ -252,16 +260,16 @@ impl MacroError { | Self::Manifest { .. } | Self::RegistryUnreadable { .. } | Self::WitResolution { .. } - | Self::SubscribesWithoutChainLog { .. } => { + | Self::SolEventsWithoutEventTrigger { .. } => { syn::Error::new(proc_macro2::Span::call_site(), message) } } } } -/// The macro's arguments: bare, or `subscribes(EventType, ...)`. +/// The macro's arguments: bare, or `sol_events(EventType, ...)`. struct ModuleArgs { - subscribes: Vec, + sol_events: Vec, } /// Syn's own grammar failures pass through as @@ -280,14 +288,14 @@ fn parse_args_inner( ) -> syn::Result> { if input.is_empty() { return Ok(Ok(ModuleArgs { - subscribes: Vec::new(), + sol_events: Vec::new(), })); } let span = input.span(); let Ok(ident) = input.parse::() else { return refuse(input, MacroError::NonIdentArgument { span }); }; - if ident != "subscribes" { + if ident != "sol_events" { return refuse(input, MacroError::UnknownArgument { span: ident.span() }); } let inner; @@ -300,10 +308,10 @@ fn parse_args_inner( let paths = inner.call(syn::punctuated::Punctuated::::parse_terminated)?; if paths.is_empty() { - return refuse(input, MacroError::EmptySubscribes { span: ident.span() }); + return refuse(input, MacroError::EmptySolEvents { span: ident.span() }); } Ok(Ok(ModuleArgs { - subscribes: paths.into_iter().collect(), + sol_events: paths.into_iter().collect(), })) } @@ -321,15 +329,15 @@ struct ManifestFacts { /// Rebuild anchor paths: the manifests the emitted world depends on. anchors: Vec, world: nexum_world::ModuleWorld, - /// Distinct chain-log `event_signature` topics, in declaration order. - chain_log_topics: Vec, + /// Distinct event trigger `event_signature` topics, in declaration order. + event_topics: Vec, } /// Synthesize the per-module world from the crate's `component.toml` /// `[dependencies]` plus the nearest ancestor `extensions.toml`. /// Topics are read only for `want_topics`, so a manifest field no /// opted-in module names can never fail a build; a `want_topics` manifest -/// with no chain-log subscription refuses. +/// with no event trigger refuses. fn derive_manifest_facts( crate_dir: &std::path::Path, want_topics: bool, @@ -345,8 +353,8 @@ fn derive_manifest_facts( path: manifest_path.clone(), source: Box::new(source), })?; - let chain_log_topics = if want_topics { - nexum_world::manifest_chain_log_topics(&text).map_err(|source| MacroError::Manifest { + let event_topics = if want_topics { + nexum_world::manifest_event_topics(&text).map_err(|source| MacroError::Manifest { path: manifest_path.clone(), source: Box::new(source), })? @@ -380,20 +388,20 @@ fn derive_manifest_facts( source: Box::new(source), })?; // Last, so a manifest rule refusal above keeps surfacing first. - if want_topics && chain_log_topics.is_empty() { - return Err(MacroError::SubscribesWithoutChainLog { + if want_topics && event_topics.is_empty() { + return Err(MacroError::SolEventsWithoutEventTrigger { path: manifest_path, }); } Ok(ManifestFacts { anchors, world, - chain_log_topics, + event_topics, }) } -/// Const assertions pinning set equality between the `subscribes(...)` -/// events' topic-0 hashes and the manifest's chain-log topics. Const +/// Const assertions pinning set equality between the `sol_events(...)` +/// events' topic-0 hashes and the manifest's event topics. Const /// eval stops at the first failure, so every message names both sides: /// a `SIGNATURE_HASH` cannot be formatted into one. fn topic_parity_check(events: &[syn::Path], topics: &[B256]) -> proc_macro2::TokenStream { @@ -421,25 +429,25 @@ fn topic_parity_check(events: &[syn::Path], topics: &[B256]) -> proc_macro2::Tok }); let declared_checks = events.iter().enumerate().map(|(i, path)| { let msg = format!( - "topic drift: `{}`'s topic-0 is not among the component.toml chain-log event_signature \ - values [{manifest_list}]", + "topic drift: `{}`'s topic-0 is not among the component.toml event trigger \ + event_signature values [{manifest_list}]", path_string(path), ); quote! { ::core::assert!( - ::nexum_sdk::events::contains_topic(&DECLARED[#i], &MANIFEST), + ::nexum_sdk::sol_events::contains_topic(&DECLARED[#i], &MANIFEST), #msg, ); } }); let manifest_checks = topics.iter().enumerate().map(|(j, topic)| { let msg = format!( - "topic drift: component.toml chain-log event_signature {topic} is not the topic-0 of any \ - of subscribes({declared_list})", + "topic drift: component.toml event trigger event_signature {topic} is not the topic-0 \ + of any of sol_events({declared_list})", ); quote! { ::core::assert!( - ::nexum_sdk::events::contains_topic(&MANIFEST[#j], &DECLARED), + ::nexum_sdk::sol_events::contains_topic(&MANIFEST[#j], &DECLARED), #msg, ); } @@ -486,7 +494,7 @@ fn init_export(self_ty: &syn::Type, has_init: bool) -> proc_macro2::TokenStream } } -/// One `on_event` arm; an absent handler dispatches as a no-op. +/// One `on_trigger` arm; an absent handler dispatches as a no-op. fn dispatch_arm( self_ty: &syn::Type, present: &[&str], @@ -496,9 +504,9 @@ fn dispatch_arm( let variant = syn::Ident::new(variant, proc_macro2::Span::call_site()); if present.contains(&handler) { let call = syn::Ident::new(handler, proc_macro2::Span::call_site()); - quote! { nexum::host::types::Event::#variant(payload) => <#self_ty>::#call(payload), } + quote! { nexum::host::types::Trigger::#variant(payload) => <#self_ty>::#call(payload), } } else { - quote! { nexum::host::types::Event::#variant(_) => ::core::result::Result::Ok(()), } + quote! { nexum::host::types::Trigger::#variant(_) => ::core::result::Result::Ok(()), } } } @@ -519,20 +527,20 @@ mod tests { #[test] fn bare_attribute_names_no_events() { - assert!(parse_args(quote! {}).unwrap().subscribes.is_empty()); + assert!(parse_args(quote! {}).unwrap().sol_events.is_empty()); } #[test] - fn subscribes_parses_paths_in_order() { - let args = parse_args(quote! { subscribes(OrderPlacement, events::Refund) }).unwrap(); - let names: Vec = args.subscribes.iter().map(path_string).collect(); + fn sol_events_parses_paths_in_order() { + let args = parse_args(quote! { sol_events(OrderPlacement, events::Refund) }).unwrap(); + let names: Vec = args.sol_events.iter().map(path_string).collect(); assert_eq!(names, ["OrderPlacement", "events::Refund"]); } #[test] - fn empty_subscribes_is_rejected() { - let err = parse_args(quote! { subscribes() }).err().unwrap(); - assert!(matches!(err, MacroError::EmptySubscribes { .. }), "{err:?}"); + fn empty_sol_events_is_rejected() { + let err = parse_args(quote! { sol_events() }).err().unwrap(); + assert!(matches!(err, MacroError::EmptySolEvents { .. }), "{err:?}"); } #[test] @@ -552,14 +560,14 @@ mod tests { #[test] fn trailing_tokens_are_rejected() { - let err = parse_args(quote! { subscribes(Foo), extra }).err().unwrap(); + let err = parse_args(quote! { sol_events(Foo), extra }).err().unwrap(); assert!(matches!(err, MacroError::TrailingTokens { .. }), "{err:?}"); } /// A failure of syn's own grammar keeps syn's diagnostic. #[test] fn malformed_args_pass_through_syn_diagnostics() { - let err = parse_args(quote! { subscribes(1) }).err().unwrap(); + let err = parse_args(quote! { sol_events(1) }).err().unwrap(); assert!(matches!(err, MacroError::MalformedArgs(_)), "{err:?}"); } @@ -570,41 +578,44 @@ mod tests { #[test] fn present_handler_arm_calls_the_impl() { - let ty: syn::Type = syn::parse_quote!(Watcher); + let ty: syn::Type = syn::parse_quote!(Alerts); let arm = flat(dispatch_arm(&ty, &["on_block"], "on_block", "Block")); assert_eq!( arm, - "nexum::host::types::Event::Block(payload)=>::on_block(payload),", + "nexum::host::types::Trigger::Block(payload)=>::on_block(payload),", ); } #[test] fn absent_handler_arm_is_a_no_op() { - let ty: syn::Type = syn::parse_quote!(Watcher); - let arm = flat(dispatch_arm(&ty, &["on_block"], "on_tick", "Tick")); + let ty: syn::Type = syn::parse_quote!(Alerts); + let arm = flat(dispatch_arm(&ty, &["on_block"], "on_schedule", "Schedule")); assert_eq!( arm, - "nexum::host::types::Event::Tick(_)=>::core::result::Result::Ok(()),", + "nexum::host::types::Trigger::Schedule(_)=>::core::result::Result::Ok(()),", ); } - /// Every handler dispatches on its own event variant: a payload can + /// Every handler dispatches on its own trigger variant: a payload can /// never reach another handler's arm. #[test] fn each_handler_binds_its_own_variant() { - let ty: syn::Type = syn::parse_quote!(Watcher); + let ty: syn::Type = syn::parse_quote!(Alerts); let pairs = [ ("on_block", "Block"), - ("on_chain_logs", "ChainLogs"), - ("on_tick", "Tick"), - ("on_custom", "Custom"), + ("on_event", "Event"), + ("on_schedule", "Schedule"), + ("on_extension", "Extension"), ]; let all: Vec<&str> = pairs.iter().map(|(h, _)| *h).collect(); for (handler, variant) in pairs { let arm = flat(dispatch_arm(&ty, &all, handler, variant)); - assert!(arm.contains(&format!("Event::{variant}(payload)")), "{arm}"); assert!( - arm.contains(&format!("::{handler}(payload)")), + arm.contains(&format!("Trigger::{variant}(payload)")), + "{arm}" + ); + assert!( + arm.contains(&format!("::{handler}(payload)")), "{arm}" ); } @@ -612,21 +623,21 @@ mod tests { #[test] fn defined_init_export_forwards_the_config() { - let ty: syn::Type = syn::parse_quote!(Watcher); + let ty: syn::Type = syn::parse_quote!(Alerts); let emitted = flat(init_export(&ty, true)); - assert!(emitted.contains("::init(config)"), "{emitted}"); + assert!(emitted.contains("::init(config)"), "{emitted}"); } #[test] fn absent_init_export_is_a_no_op() { - let ty: syn::Type = syn::parse_quote!(Watcher); + let ty: syn::Type = syn::parse_quote!(Alerts); let emitted = flat(init_export(&ty, false)); assert!(emitted.contains("_config"), "{emitted}"); assert!( emitted.contains("::core::result::Result::Ok(())"), "{emitted}", ); - assert!(!emitted.contains("Watcher>::init"), "{emitted}"); + assert!(!emitted.contains("Alerts>::init"), "{emitted}"); } /// An accepted manifest's declared capabilities come out as the caps @@ -662,7 +673,7 @@ mod tests { "{emitted}", ); assert!( - emitted.contains("is not the topic-0 of any of subscribes(OrderPlacement)"), + emitted.contains("is not the topic-0 of any of sol_events(OrderPlacement)"), "{emitted}", ); // Topic bytes are embedded, not re-parsed at build time. @@ -694,7 +705,7 @@ mod tests { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write( dir.path().join("component.toml"), - format!("{MANIFEST}{SUBSCRIPTION}"), + format!("{MANIFEST}{TRIGGER}"), ) .expect("write manifest"); std::fs::write( @@ -718,13 +729,13 @@ mod tests { assert_eq!(emitted.matches("include_bytes").count(), 2, "{emitted}"); } - /// A manifest field only `subscribes(...)` reads must not fail the build + /// A manifest field only `sol_events(...)` reads must not fail the build /// of a module that does not name it. #[test] fn topics_are_read_only_when_the_attribute_names_events() { let dir = tempfile::tempdir().expect("tempdir"); let manifest = format!( - "{MANIFEST}\n[[subscription]]\nkind = \"chain-log\"\nchain_id = 1\n\ + "{MANIFEST}\n[[trigger]]\non = \"event\"\nchain_id = 1\n\ event_signature = \"not-a-topic\"\n" ); std::fs::write(dir.path().join("component.toml"), manifest).expect("write manifest"); @@ -770,21 +781,21 @@ mod tests { ); } - /// `subscribes(...)` needs a manifest chain-log subscription; one with + /// `sol_events(...)` needs a manifest event trigger; one with /// an `event_signature` satisfies it. #[test] - fn subscribes_without_chain_log_subscription_is_refused() { + fn sol_events_without_event_trigger_is_refused() { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write(dir.path().join("component.toml"), MANIFEST).expect("write manifest"); let err = derive_manifest_facts(dir.path(), true).err().unwrap(); assert!( - matches!(err, MacroError::SubscribesWithoutChainLog { .. }), + matches!(err, MacroError::SolEventsWithoutEventTrigger { .. }), "{err:?}" ); std::fs::write( dir.path().join("component.toml"), - format!("{MANIFEST}{SUBSCRIPTION}"), + format!("{MANIFEST}{TRIGGER}"), ) .expect("write manifest"); assert!(derive_manifest_facts(dir.path(), true).is_ok()); @@ -792,9 +803,9 @@ mod tests { const MANIFEST: &str = "[component]\nname = \"t\"\n\n[dependencies]\nlogging = {}\n"; - /// A chain-log subscription with a valid `event_signature`, appended + /// An event trigger with a valid `event_signature`, appended /// to [`MANIFEST`] where a test wants topics. - const SUBSCRIPTION: &str = "\n[[subscription]]\nkind = \"chain-log\"\nchain_id = 1\n\ + const TRIGGER: &str = "\n[[trigger]]\non = \"event\"\nchain_id = 1\n\ event_signature = \"0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9\"\n"; /// The string literals the emitted `assert!`s carry. @@ -819,7 +830,7 @@ mod tests { fn non_ident_argument() { assert_eq!( MacroError::NonIdentArgument { span: site() }.to_string(), - "expected `subscribes(EventType, ...)` or no arguments", + "expected `sol_events(EventType, ...)` or no arguments", ); } @@ -827,7 +838,7 @@ mod tests { fn unknown_argument() { assert_eq!( MacroError::UnknownArgument { span: site() }.to_string(), - "#[nexum_sdk::module] takes no arguments except `subscribes(EventType, ...)`", + "#[nexum_sdk::module] takes no arguments except `sol_events(EventType, ...)`", ); } @@ -835,15 +846,15 @@ mod tests { fn trailing_tokens() { assert_eq!( MacroError::TrailingTokens { span: site() }.to_string(), - "unexpected tokens after `subscribes(...)`", + "unexpected tokens after `sol_events(...)`", ); } #[test] - fn empty_subscribes() { + fn empty_sol_events() { assert_eq!( - MacroError::EmptySubscribes { span: site() }.to_string(), - "`subscribes(...)` must name at least one event type", + MacroError::EmptySolEvents { span: site() }.to_string(), + "`sol_events(...)` must name at least one event type", ); } @@ -897,8 +908,8 @@ mod tests { } .to_string(), "`on_blocks` is not a recognized #[nexum_sdk::module] handler; expected one of \ - [\"init\", \"on_block\", \"on_chain_logs\", \"on_tick\", \"on_custom\"] (rename \ - helpers so they do not start with `on_`)", + [\"init\", \"on_block\", \"on_event\", \"on_schedule\", \"on_extension\"] \ + (rename helpers so they do not start with `on_`)", ); } @@ -910,7 +921,7 @@ mod tests { } .to_string(), "#[nexum_sdk::module] found no recognized handlers on this impl; define at least \ - one of `init`, `on_block`, `on_chain_logs`, `on_tick`, `on_custom`", + one of `init`, `on_block`, `on_event`, `on_schedule`, `on_extension`", ); } @@ -972,14 +983,14 @@ mod tests { } #[test] - fn subscribes_without_chain_log() { - let err = MacroError::SubscribesWithoutChainLog { + fn sol_events_without_event_trigger() { + let err = MacroError::SolEventsWithoutEventTrigger { path: "/m/component.toml".into(), }; assert_eq!( err.to_string(), - "`subscribes(...)` names events, but /m/component.toml declares no chain-log \ - subscription with an `event_signature`; add the subscription or drop the argument", + "`sol_events(...)` names events, but /m/component.toml declares no event \ + trigger with an `event_signature`; add the trigger or drop the argument", ); } } diff --git a/crates/nexum-module-macros/tests/ui/empty_subscribes.rs b/crates/nexum-module-macros/tests/ui/empty_sol_events.rs similarity index 50% rename from crates/nexum-module-macros/tests/ui/empty_subscribes.rs rename to crates/nexum-module-macros/tests/ui/empty_sol_events.rs index b6b9682a..1827a583 100644 --- a/crates/nexum-module-macros/tests/ui/empty_subscribes.rs +++ b/crates/nexum-module-macros/tests/ui/empty_sol_events.rs @@ -1,13 +1,13 @@ -//! `subscribes()` with no events is rejected: an empty list would pin +//! `sol_events()` with no events is rejected: an empty list would pin //! nothing while looking like it does. use nexum_module_macros::module; struct Alerts; -#[module(subscribes())] +#[module(sol_events())] impl Alerts { - fn on_chain_logs(_payload: u64) -> Result<(), ()> { + fn on_event(_payload: u64) -> Result<(), ()> { Ok(()) } } diff --git a/crates/nexum-module-macros/tests/ui/empty_sol_events.stderr b/crates/nexum-module-macros/tests/ui/empty_sol_events.stderr new file mode 100644 index 00000000..0cbd5946 --- /dev/null +++ b/crates/nexum-module-macros/tests/ui/empty_sol_events.stderr @@ -0,0 +1,5 @@ +error: `sol_events(...)` must name at least one event type + --> tests/ui/empty_sol_events.rs:8:10 + | +8 | #[module(sol_events())] + | ^^^^^^^^^^ diff --git a/crates/nexum-module-macros/tests/ui/empty_subscribes.stderr b/crates/nexum-module-macros/tests/ui/empty_subscribes.stderr deleted file mode 100644 index ad33bde4..00000000 --- a/crates/nexum-module-macros/tests/ui/empty_subscribes.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `subscribes(...)` must name at least one event type - --> tests/ui/empty_subscribes.rs:8:10 - | -8 | #[module(subscribes())] - | ^^^^^^^^^^ diff --git a/crates/nexum-module-macros/tests/ui/generic_impl.rs b/crates/nexum-module-macros/tests/ui/generic_impl.rs index 84dd02d5..c9e27102 100644 --- a/crates/nexum-module-macros/tests/ui/generic_impl.rs +++ b/crates/nexum-module-macros/tests/ui/generic_impl.rs @@ -7,7 +7,7 @@ struct Alerts(T); #[module] impl Alerts { - fn on_tick(_payload: u64) -> Result<(), ()> { + fn on_schedule(_payload: u64) -> Result<(), ()> { Ok(()) } } diff --git a/crates/nexum-module-macros/tests/ui/no_handlers.stderr b/crates/nexum-module-macros/tests/ui/no_handlers.stderr index 41ec5a2e..be2492a3 100644 --- a/crates/nexum-module-macros/tests/ui/no_handlers.stderr +++ b/crates/nexum-module-macros/tests/ui/no_handlers.stderr @@ -1,4 +1,4 @@ -error: #[nexum_sdk::module] found no recognized handlers on this impl; define at least one of `init`, `on_block`, `on_chain_logs`, `on_tick`, `on_custom` +error: #[nexum_sdk::module] found no recognized handlers on this impl; define at least one of `init`, `on_block`, `on_event`, `on_schedule`, `on_extension` --> tests/ui/no_handlers.rs:9:6 | 9 | impl Alerts { diff --git a/crates/nexum-module-macros/tests/ui/non_ident_argument.rs b/crates/nexum-module-macros/tests/ui/non_ident_argument.rs index d889886b..20b18eb3 100644 --- a/crates/nexum-module-macros/tests/ui/non_ident_argument.rs +++ b/crates/nexum-module-macros/tests/ui/non_ident_argument.rs @@ -6,7 +6,7 @@ struct Alerts; #[module(42)] impl Alerts { - fn on_tick(_payload: u64) -> Result<(), ()> { + fn on_schedule(_payload: u64) -> Result<(), ()> { Ok(()) } } diff --git a/crates/nexum-module-macros/tests/ui/non_ident_argument.stderr b/crates/nexum-module-macros/tests/ui/non_ident_argument.stderr index 05a24156..09e29efa 100644 --- a/crates/nexum-module-macros/tests/ui/non_ident_argument.stderr +++ b/crates/nexum-module-macros/tests/ui/non_ident_argument.stderr @@ -1,4 +1,4 @@ -error: expected `subscribes(EventType, ...)` or no arguments +error: expected `sol_events(EventType, ...)` or no arguments --> tests/ui/non_ident_argument.rs:7:10 | 7 | #[module(42)] diff --git a/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.rs b/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.rs new file mode 100644 index 00000000..8cf56fb7 --- /dev/null +++ b/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.rs @@ -0,0 +1,14 @@ +//! Nothing may follow the `sol_events(...)` list. + +use nexum_module_macros::module; + +struct Alerts; + +#[module(sol_events(Transfer), extra)] +impl Alerts { + fn on_event(_payload: u64) -> Result<(), ()> { + Ok(()) + } +} + +fn main() {} diff --git a/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.stderr b/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.stderr new file mode 100644 index 00000000..ab78d48e --- /dev/null +++ b/crates/nexum-module-macros/tests/ui/tokens_after_sol_events.stderr @@ -0,0 +1,5 @@ +error: unexpected tokens after `sol_events(...)` + --> tests/ui/tokens_after_sol_events.rs:7:30 + | +7 | #[module(sol_events(Transfer), extra)] + | ^ diff --git a/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.rs b/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.rs deleted file mode 100644 index b9d3c327..00000000 --- a/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Nothing may follow the `subscribes(...)` list. - -use nexum_module_macros::module; - -struct Alerts; - -#[module(subscribes(Transfer), extra)] -impl Alerts { - fn on_chain_logs(_payload: u64) -> Result<(), ()> { - Ok(()) - } -} - -fn main() {} diff --git a/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.stderr b/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.stderr deleted file mode 100644 index 9074f792..00000000 --- a/crates/nexum-module-macros/tests/ui/tokens_after_subscribes.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: unexpected tokens after `subscribes(...)` - --> tests/ui/tokens_after_subscribes.rs:7:30 - | -7 | #[module(subscribes(Transfer), extra)] - | ^ diff --git a/crates/nexum-module-macros/tests/ui/unknown_argument.rs b/crates/nexum-module-macros/tests/ui/unknown_argument.rs index 36f1a0af..0f506521 100644 --- a/crates/nexum-module-macros/tests/ui/unknown_argument.rs +++ b/crates/nexum-module-macros/tests/ui/unknown_argument.rs @@ -1,4 +1,4 @@ -//! The only recognized attribute argument is `subscribes(...)`. +//! The only recognized attribute argument is `sol_events(...)`. use nexum_module_macros::module; @@ -6,7 +6,7 @@ struct Alerts; #[module(emits(Transfer))] impl Alerts { - fn on_tick(_payload: u64) -> Result<(), ()> { + fn on_schedule(_payload: u64) -> Result<(), ()> { Ok(()) } } diff --git a/crates/nexum-module-macros/tests/ui/unknown_argument.stderr b/crates/nexum-module-macros/tests/ui/unknown_argument.stderr index 32d83d3e..a2971351 100644 --- a/crates/nexum-module-macros/tests/ui/unknown_argument.stderr +++ b/crates/nexum-module-macros/tests/ui/unknown_argument.stderr @@ -1,4 +1,4 @@ -error: #[nexum_sdk::module] takes no arguments except `subscribes(EventType, ...)` +error: #[nexum_sdk::module] takes no arguments except `sol_events(EventType, ...)` --> tests/ui/unknown_argument.rs:7:10 | 7 | #[module(emits(Transfer))] diff --git a/crates/nexum-module-macros/tests/ui/unknown_handler.stderr b/crates/nexum-module-macros/tests/ui/unknown_handler.stderr index f9a77205..ac8bc74d 100644 --- a/crates/nexum-module-macros/tests/ui/unknown_handler.stderr +++ b/crates/nexum-module-macros/tests/ui/unknown_handler.stderr @@ -1,4 +1,4 @@ -error: `on_blocks` is not a recognized #[nexum_sdk::module] handler; expected one of ["init", "on_block", "on_chain_logs", "on_tick", "on_custom"] (rename helpers so they do not start with `on_`) +error: `on_blocks` is not a recognized #[nexum_sdk::module] handler; expected one of ["init", "on_block", "on_event", "on_schedule", "on_extension"] (rename helpers so they do not start with `on_`) --> tests/ui/unknown_handler.rs:10:8 | 10 | fn on_blocks(_payload: u64) -> Result<(), ()> { diff --git a/crates/nexum-module-macros/tests/ui/unnamed_self_type.rs b/crates/nexum-module-macros/tests/ui/unnamed_self_type.rs index 41250c81..73d4586d 100644 --- a/crates/nexum-module-macros/tests/ui/unnamed_self_type.rs +++ b/crates/nexum-module-macros/tests/ui/unnamed_self_type.rs @@ -6,7 +6,7 @@ struct Alerts; #[module] impl &Alerts { - fn on_tick(_payload: u64) -> Result<(), ()> { + fn on_schedule(_payload: u64) -> Result<(), ()> { Ok(()) } } diff --git a/crates/nexum-runtime/src/bindings.rs b/crates/nexum-runtime/src/bindings.rs index 6aa4090e..84d8640d 100644 --- a/crates/nexum-runtime/src/bindings.rs +++ b/crates/nexum-runtime/src/bindings.rs @@ -1,12 +1,12 @@ //! WIT bindings generated by `wasmtime::component::bindgen!`. //! -//! Binds the `nexum:host/event-module` world (the six core primitives). +//! Binds the `nexum:host/trigger-module` world (the six core primitives). //! Outbound HTTP is wasi:http, linked separately; clocks are ambient -//! wasi:clocks. `nexum:host` is a leaf package: the host `event` variant +//! wasi:clocks. `nexum:host` is a leaf package: the host `trigger` variant //! carries a status transition as opaque bytes. An extension remaps onto these //! shared interfaces with `with`, so the `Host` impls and `fault` type its //! components see are the ones the core host constructs. `PartialEq` is derived -//! so extension services can compare event payloads. +//! so extension services can compare trigger payloads. // Every item in this module is macro output, so a doc comment has nowhere // to attach. The module doc above stands for the world it binds. @@ -14,7 +14,7 @@ wasmtime::component::bindgen!({ path: ["../../wit/nexum-host"], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", imports: { default: async }, exports: { default: async }, additional_derives: [PartialEq], diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs index 30c05bff..4c5e38f3 100644 --- a/crates/nexum-runtime/src/builder.rs +++ b/crates/nexum-runtime/src/builder.rs @@ -4,8 +4,8 @@ //! component builders, add-ons) through a type-state chain; //! [`ReadyBuilder::launch`] opens the backends and hands off to //! [`AssembledRuntime::launch`], which installs add-ons, builds the engine and -//! linker, boots the supervisor, opens subscriptions, spawns the event loop, -//! and returns a [`RuntimeHandle`]. [`RuntimeBuilder::runtime`] binds a +//! linker, boots the supervisor, opens the trigger sources, spawns the event +//! loop, and returns a [`RuntimeHandle`]. [`RuntimeBuilder::runtime`] binds a //! [`Runtime`] preset for the common case. use std::future::IntoFuture; @@ -24,7 +24,7 @@ use crate::engine_config::{EngineConfig, ModuleEntry, PolicySection}; use crate::host::component::{ BuilderContext, ComponentBuilder, Components, ComponentsBuilder, RuntimeTypes, }; -use crate::host::extension::{self, EventSources, Extension}; +use crate::host::extension::{self, Extension, SourceContext}; use crate::host::logs::LogPipeline; use crate::host::provider_pool::ProviderPool; use crate::preset::Runtime; @@ -69,14 +69,14 @@ pub enum LaunchRefusal { /// How many were tried. modules: usize, }, - /// Some modules survived `init`, but no surviving one holds a - /// subscription, so the engine would run and never be woken. + /// Some modules survived `init`, but no surviving one declares a + /// trigger, so the engine would run and never be woken. #[error( - "every declared [[subscription]] belongs to an init-failed module - \ + "every declared [[trigger]] belongs to an init-failed module - \ the engine would idle with nothing to run; fix or remove the \ failing module(s)" )] - DeadHoldSubs, + DeadHoldTriggers, } /// Ambient inputs the launcher reads. @@ -296,7 +296,7 @@ impl AssembledRuntime { }; let alive = supervisor.alive_count(); - let plan = supervisor.subscription_plan(); + let plan = supervisor.trigger_plan(); info!( modules = supervisor.module_count(), alive, @@ -339,29 +339,31 @@ impl AssembledRuntime { // The handle keeps the log read side reachable after launch consumes // the components. let logs = components.logs.clone(); - // Extension event sources open only for subscription kinds some - // live module declares; an extension returns no stream when it has - // nothing to observe. + // Extension sources open only for trigger kinds some live module + // declares; an extension returns no stream when it has nothing to + // observe. let mut reconnect_tasks = TaskSet::new(); let mut extension_streams = Vec::new(); { - let mut sources = EventSources::new( + let mut sources = SourceContext::new( engine_cfg, &plan.extension_kinds, &executor, &mut reconnect_tasks, ); for ext in &extensions { - extension_streams.extend(ext.events(&mut sources)?); + extension_streams.extend(ext.open_sources(&mut sources)?); } } match plan.viability(extension_streams.len()) { - Viability::DeadHoldSubs => return Err(refuse_launch(LaunchRefusal::DeadHoldSubs)), + Viability::DeadHoldTriggers => { + return Err(refuse_launch(LaunchRefusal::DeadHoldTriggers)); + } Viability::Nothing => { // Nothing to drive: return a handle whose event loop is // already complete so `wait` resolves immediately. - info!("no [[subscription]] entries - engine has nothing to run; exiting"); + info!("no [[trigger]] entries - engine has nothing to run; exiting"); let event_loop = executor.spawn(async { TaskExit::ReceiverGone }); return Ok(RuntimeHandle { event_loop, @@ -374,9 +376,9 @@ impl AssembledRuntime { Viability::Live => {} } - // Open per-chain block subscriptions + per-module chain-log - // subscriptions through the executor, then drive them in the event - // loop until shutdown. + // Open per-chain block streams + per-module chain-log streams + // through the executor, then drive them in the event loop until + // shutdown. let block_streams = event_loop::open_block_streams( &components.chain, &plan.block_chains, @@ -385,7 +387,7 @@ impl AssembledRuntime { ); let chain_log_streams = event_loop::open_chain_log_streams( &components.chain, - plan.chain_log_subs, + plan.event_triggers, &executor, &mut reconnect_tasks, ); @@ -1175,7 +1177,7 @@ mod tests { let manifest = TestManifest::new("price-alert") .cap("logging") .cap("chain") - .block_sub(11_155_111) + .block_trigger(11_155_111) .config( "oracle_address", "0x694AA1769357215DE4FAC081bf1f309aDC325306", @@ -1210,14 +1212,14 @@ mod tests { } #[tokio::test] - async fn launch_bails_on_an_unconfigured_chain_subscription() { + async fn launch_bails_on_an_unconfigured_chain_trigger() { let dir = tempfile::tempdir().expect("tempdir"); let wasm = dir.path().join("missing.wasm"); let manifest = dir.path().join("component.toml"); std::fs::write( &manifest, "[component]\nname = \"example\"\n\n[dependencies]\nlogging = {}\n\n\ - [[subscription]]\nkind = \"block\"\nchain_id = 424242\n", + [[trigger]]\non = \"block\"\nchain_id = 424242\n", ) .expect("write manifest"); @@ -1235,7 +1237,7 @@ mod tests { .launch() .await { - Ok(_) => panic!("an unconfigured chain subscription must abort launch"), + Ok(_) => panic!("an unconfigured chain trigger must abort launch"), Err(err) => err, }; Refusal::from(err).variant::(|e| { diff --git a/crates/nexum-runtime/src/host/extension.rs b/crates/nexum-runtime/src/host/extension.rs index 6e804d5a..fcd043c5 100644 --- a/crates/nexum-runtime/src/host/extension.rs +++ b/crates/nexum-runtime/src/host/extension.rs @@ -1,5 +1,5 @@ //! Extension seam: what one extension contributes to the host (namespace, -//! capabilities, linker hook, event sources, and manifest-section install +//! capabilities, linker hook, trigger sources, and manifest-section install //! predicates). use std::collections::BTreeSet; @@ -11,7 +11,7 @@ use nexum_tasks::{TaskExecutor, TaskExit, TaskSet}; use wasmtime::component::Linker; pub use wasmtime_wasi::HostWallClock; -use crate::bindings::nexum::host::types::Event; +use crate::bindings::nexum::host::types::Trigger; use crate::engine_config::EngineConfig; use crate::host::component::RuntimeTypes; use crate::host::state::HostState; @@ -50,15 +50,18 @@ pub trait Extension: Send + Sync + 'static { Ok(()) } - /// Subscription kinds this extension's event sources emit; an unknown - /// non-core kind is refused at boot. - fn subscriptions(&self) -> &'static [&'static str] { + /// Trigger kinds this extension's sources emit; an unknown non-core + /// kind is refused at boot. + fn emits_trigger_kinds(&self) -> &'static [&'static str] { &[] } - /// Open the extension's event sources after boot; the event loop merges + /// Open the extension's sources after boot; the event loop merges /// and dispatches them. - fn events(&self, sources: &mut EventSources<'_>) -> anyhow::Result> { + fn open_sources( + &self, + sources: &mut SourceContext<'_>, + ) -> anyhow::Result> { let _ = sources; Ok(Vec::new()) } @@ -76,47 +79,47 @@ pub(crate) fn attach_wall_clock( } } -/// Event dispatched to every module with a `[[subscription]]` of `kind` whose -/// filters match `attrs`. -pub struct ExtensionEvent { - /// Manifest subscription kind that routes this event. - pub kind: &'static str, - /// Routing attributes a subscription's filters match against. +/// Delivered to every module with a `[[trigger]]` of `extension_kind` +/// whose filters match `attrs`. +pub struct ExtensionDelivery { + /// Manifest trigger kind that routes this delivery. + pub extension_kind: &'static str, + /// Routing attributes a trigger's filters match against. pub attrs: Vec<(&'static str, String)>, - /// The host event delivered to each matching module. - pub event: Event, + /// The host trigger delivered to each matching module. + pub trigger: Trigger, } -/// A stream of extension events the event loop merges and drives. -pub type ExtensionEventStream = Pin + Send>>; +/// A stream of deliveries the event loop merges and drives. +pub type ExtensionSource = Pin + Send>>; -/// Launch inputs for [`Extension::events`]. -pub struct EventSources<'a> { +/// Launch inputs for [`Extension::open_sources`]. +pub struct SourceContext<'a> { /// The loaded engine config. pub config: &'a EngineConfig, - /// Extension subscription kinds declared by at least one module. - pub subscribed: &'a BTreeSet, + /// Extension trigger kinds declared by at least one module. + pub demanded_extension_kinds: &'a BTreeSet, executor: &'a TaskExecutor, tasks: &'a mut TaskSet, } -impl<'a> EventSources<'a> { - /// Bundle the launch inputs for one [`Extension::events`] pass. +impl<'a> SourceContext<'a> { + /// Bundle the launch inputs for one [`Extension::open_sources`] pass. pub fn new( config: &'a EngineConfig, - subscribed: &'a BTreeSet, + demanded_extension_kinds: &'a BTreeSet, executor: &'a TaskExecutor, tasks: &'a mut TaskSet, ) -> Self { Self { config, - subscribed, + demanded_extension_kinds, executor, tasks, } } - /// Spawn an event-source task; it must end when its stream's receiver + /// Spawn a source task; it must end when its stream's receiver /// drops. pub fn spawn(&mut self, task: impl Future + Send + 'static) { self.tasks.push(self.executor.spawn(async move { diff --git a/crates/nexum-runtime/src/manifest/capabilities.rs b/crates/nexum-runtime/src/manifest/capabilities.rs index 21867815..30dfeccf 100644 --- a/crates/nexum-runtime/src/manifest/capabilities.rs +++ b/crates/nexum-runtime/src/manifest/capabilities.rs @@ -24,7 +24,7 @@ pub struct NamespaceCaps { pub ifaces: &'static [&'static str], } -/// The core namespace: the interfaces the `event-module` world links. +/// The core namespace: the interfaces the `trigger-module` world links. pub const CORE_NAMESPACE: NamespaceCaps = NamespaceCaps { prefix: "nexum:host/", ifaces: CORE_CAPABILITIES, @@ -255,7 +255,7 @@ mod tests { .collect(), http_allowlist: vec![], config: vec![], - subscriptions: vec![], + triggers: vec![], extensions: Default::default(), } } diff --git a/crates/nexum-runtime/src/manifest/error.rs b/crates/nexum-runtime/src/manifest/error.rs index acbd049e..502e44a4 100644 --- a/crates/nexum-runtime/src/manifest/error.rs +++ b/crates/nexum-runtime/src/manifest/error.rs @@ -58,42 +58,42 @@ pub enum ParseError { #[source] source: crate::digest::DigestParseError, }, - /// A `[[subscription]]` table without a string `kind`. - #[error("manifest: [[subscription]] table {index} must declare a string `kind`")] - MissingSubscriptionKind { - /// 1-based position among the `[[subscription]]` tables. + /// A `[[trigger]]` table without a string `on`. + #[error("manifest: [[trigger]] table {index} must declare a string `on`")] + MissingTriggerKind { + /// 1-based position among the `[[trigger]]` tables. index: usize, }, - /// A core-kind `[[subscription]]` table whose shape does not match - /// its kind. - #[error("manifest: invalid {kind:?} subscription ([[subscription]] table {index}): {source}")] - InvalidSubscription { - /// 1-based position among the `[[subscription]]` tables. + /// A core-kind `[[trigger]]` table whose shape does not match its + /// kind. + #[error("manifest: invalid {kind:?} trigger ([[trigger]] table {index}): {source}")] + InvalidTrigger { + /// 1-based position among the `[[trigger]]` tables. index: usize, /// The declared core kind. kind: String, #[source] source: toml::de::Error, }, - /// A chain-log `address` that is not 20-byte hex. - #[error("manifest: invalid chain-log address {value:?}: {source}")] - InvalidChainLogAddress { + /// An event trigger `address` that is not 20-byte hex. + #[error("manifest: invalid event address {value:?}: {source}")] + InvalidEventAddress { /// The address as written. value: String, #[source] source: alloy_primitives::hex::FromHexError, }, - /// A chain-log `event_signature` that is not 32-byte hex. + /// An event trigger `event_signature` that is not 32-byte hex. #[error("manifest: invalid topic {value:?}: {source}")] - InvalidChainLogTopic { + InvalidEventTopic { /// The topic as written. value: String, #[source] source: alloy_primitives::hex::FromHexError, }, - /// An extension-kind subscription filter with a non-string value. - #[error("manifest: subscription filter `{key}` must be a string")] - NonStringSubscriptionFilter { + /// An extension-kind trigger filter with a non-string value. + #[error("manifest: trigger filter `{key}` must be a string")] + NonStringTriggerFilter { /// The filter key. key: String, }, diff --git a/crates/nexum-runtime/src/manifest/load.rs b/crates/nexum-runtime/src/manifest/load.rs index 699d00e1..1014766f 100644 --- a/crates/nexum-runtime/src/manifest/load.rs +++ b/crates/nexum-runtime/src/manifest/load.rs @@ -53,7 +53,7 @@ pub fn load(path: &Path, registry: &CapabilityRegistry) -> Result String { + fn load_refuses_malformed_event_hex_at_parse() { + fn event_trigger(field: &str) -> String { format!( - "[component]\nname = \"bad\"\n\n[[subscription]]\nkind = \"chain-log\"\n\ + "[component]\nname = \"bad\"\n\n[[trigger]]\non = \"event\"\n\ chain_id = 1\n{field}\n" ) } - let err = validate(&chain_log("address = \"0xabc\"")).expect_err("malformed address"); + let err = validate(&event_trigger("address = \"0xabc\"")).expect_err("malformed address"); assert!( - matches!(err, ParseError::InvalidChainLogAddress { ref value, .. } if value == "0xabc"), + matches!(err, ParseError::InvalidEventAddress { ref value, .. } if value == "0xabc"), "{err:?}", ); // Operator wording pin. assert!( - err.to_string() - .contains("invalid chain-log address \"0xabc\""), + err.to_string().contains("invalid event address \"0xabc\""), "{err}" ); - let err = - validate(&chain_log("event_signature = \"not-a-topic\"")).expect_err("malformed topic"); + let err = validate(&event_trigger("event_signature = \"not-a-topic\"")) + .expect_err("malformed topic"); assert!( - matches!(err, ParseError::InvalidChainLogTopic { ref value, .. } if value == "not-a-topic"), + matches!(err, ParseError::InvalidEventTopic { ref value, .. } if value == "not-a-topic"), "{err:?}", ); // Operator wording pin. @@ -139,28 +138,28 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000dea /// A core-kind table whose shape does not match its kind carries the /// declared kind and the table's position in the refusal. #[test] - fn load_refuses_a_core_subscription_missing_its_shape() { - let toml = "[component]\nname = \"bad\"\n\n[[subscription]]\nkind = \"chain-log\"\n"; - let err = validate(toml).expect_err("chain-log without chain_id"); + fn load_refuses_a_core_trigger_missing_its_shape() { + let toml = "[component]\nname = \"bad\"\n\n[[trigger]]\non = \"event\"\n"; + let err = validate(toml).expect_err("event trigger without chain_id"); assert!( matches!( err, - ParseError::InvalidSubscription { index: 1, ref kind, .. } if kind == "chain-log" + ParseError::InvalidTrigger { index: 1, ref kind, .. } if kind == "event" ), "{err:?}", ); } - /// A subscription table without a `kind` cannot dispatch; the refusal + /// A trigger table without an `on` cannot dispatch; the refusal /// carries the table's 1-based position, the only locator left once /// validation runs after the TOML parse. #[test] - fn load_refuses_a_subscription_without_a_kind() { - let toml = "[component]\nname = \"bad\"\n\n[[subscription]]\nkind = \"block\"\n\ - chain_id = 1\n\n[[subscription]]\nchain_id = 1\n"; - let err = validate(toml).expect_err("kindless subscription"); + fn load_refuses_a_trigger_without_an_on() { + let toml = "[component]\nname = \"bad\"\n\n[[trigger]]\non = \"block\"\n\ + chain_id = 1\n\n[[trigger]]\nchain_id = 1\n"; + let err = validate(toml).expect_err("kindless trigger"); assert!( - matches!(err, ParseError::MissingSubscriptionKind { index: 2 }), + matches!(err, ParseError::MissingTriggerKind { index: 2 }), "{err:?}" ); // The position reaches the operator. @@ -170,7 +169,7 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000dea /// Typing the field must neither widen nor narrow the accepted spelling: /// `0x`-prefixed or bare, any case, no checksum requirement. #[test] - fn load_accepts_every_hex_spelling_of_a_chain_log_address() { + fn load_accepts_every_hex_spelling_of_an_event_address() { let expected: alloy_primitives::Address = "0xc92e8bdf79f0507f65a392b0ab4667716bfe0110" .parse() .expect("canonical address"); @@ -181,14 +180,14 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000dea "c92e8bdf79f0507f65a392b0ab4667716bfe0110", ] { let toml = format!( - "[component]\nname = \"ok\"\n\n[dependencies]\n\n[[subscription]]\n\ - kind = \"chain-log\"\nchain_id = 1\naddress = \"{spelling}\"\n" + "[component]\nname = \"ok\"\n\n[dependencies]\n\n[[trigger]]\n\ + on = \"event\"\nchain_id = 1\naddress = \"{spelling}\"\n" ); let loaded = validate(&toml).expect(spelling); assert!( matches!( - &loaded.subscriptions[0], - Subscription::ChainLog { address: Some(a), .. } if *a == expected + &loaded.triggers[0], + Trigger::Event { address: Some(a), .. } if *a == expected ), "{spelling} must parse to the canonical address", ); @@ -202,37 +201,37 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000dea fn world_topic_extraction_agrees_with_load() { let toml = r#" [component] -name = "watcher" +name = "alerts" [dependencies] -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 event_signature = "0xCF5F9DE2984132265203B5C335B25727702CA77262FF622E136BAA7362BF1DA9" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 event_signature = "0x0000000000000000000000000000000000000000000000000000000000000001" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 100 event_signature = "cf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9" "#; let loaded_manifest = validate(toml).expect("parse"); // Distinct, not `dedup`: the repeat is non-adjacent, as it is on chain. let mut loaded: Vec = Vec::new(); - for sub in &loaded_manifest.subscriptions { - if let Subscription::ChainLog { + for trigger in &loaded_manifest.triggers { + if let Trigger::Event { event_signature: Some(topic), .. - } = sub + } = trigger && !loaded.contains(topic) { loaded.push(*topic); @@ -244,66 +243,66 @@ event_signature = "cf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1 "the fixture repeats a topic non-adjacently" ); assert_eq!( - nexum_world::manifest_chain_log_topics(toml).expect("extract"), + nexum_world::manifest_event_topics(toml).expect("extract"), loaded, ); - let bad = "[component]\nname = \"bad\"\n\n[[subscription]]\nkind = \"chain-log\"\n\ + let bad = "[component]\nname = \"bad\"\n\n[[trigger]]\non = \"event\"\n\ chain_id = 1\nevent_signature = \"not-a-topic\"\n"; assert!(matches!( validate(bad), - Err(ParseError::InvalidChainLogTopic { .. }) + Err(ParseError::InvalidEventTopic { .. }) )); - assert!(nexum_world::manifest_chain_log_topics(bad).is_err()); + assert!(nexum_world::manifest_event_topics(bad).is_err()); } #[test] - fn load_parses_the_retired_log_kind_as_an_extension_kind() { - // The chain-event kind is `chain-log`; a stale `kind = "log"` - // parses as an extension kind and boot refuses it against the - // extension vocabulary, so a not-yet-migrated manifest still - // surfaces clearly rather than silently dropping events. + fn load_refuses_the_retired_chain_log_kind() { + // A not-yet-migrated manifest must refuse rather than silently + // drop deliveries. `chain-log` parses as an extension kind, so + // its integer `chain_id` fails the string-filter rule first. let toml = r#" [component] name = "stale" [dependencies] -[[subscription]] -kind = "log" -chain_id = "1" +[[trigger]] +on = "chain-log" +chain_id = 1 "#; - let loaded = validate(toml).expect("parse"); assert!(matches!( - &loaded.subscriptions[0], - Subscription::Extension { kind, .. } if kind == "log" + validate(toml), + Err(ParseError::NonStringTriggerFilter { key }) if key == "chain_id" )); } #[test] - fn load_parses_extension_subscriptions_with_string_filters() { + fn load_parses_extension_triggers_with_string_filters() { let toml = r#" [component] -name = "watcher" +name = "alerts" [dependencies] -[[subscription]] -kind = "acme-status" +[[trigger]] +on = "acme-status" -[[subscription]] -kind = "acme-status" +[[trigger]] +on = "acme-status" scope = "primary" "#; let loaded = validate(toml).expect("parse"); assert!(matches!( - &loaded.subscriptions[0], - Subscription::Extension { kind, filters } if kind == "acme-status" && filters.is_empty() + &loaded.triggers[0], + Trigger::Extension { extension_kind, filters } + if extension_kind == "acme-status" && filters.is_empty() )); assert!(matches!( - &loaded.subscriptions[1], - Subscription::Extension { kind, filters } - if kind == "acme-status" && filters.get("scope").is_some_and(|v| v == "primary") + &loaded.triggers[1], + Trigger::Extension { extension_kind, filters } + if extension_kind == "acme-status" + && filters.get("scope").is_some_and(|v| v == "primary") )); } @@ -313,15 +312,15 @@ scope = "primary" fn load_rejects_a_non_string_extension_filter() { let toml = r#" [component] -name = "watcher" +name = "alerts" -[[subscription]] -kind = "acme-status" +[[trigger]] +on = "acme-status" scope = 7 "#; let err = validate(toml).expect_err("non-string filter"); assert!( - matches!(err, ParseError::NonStringSubscriptionFilter { ref key } if key == "scope"), + matches!(err, ParseError::NonStringTriggerFilter { ref key } if key == "scope"), "{err:?}", ); // Operator wording pin. @@ -340,13 +339,13 @@ name = "keeper" [venue] body_version = 2 -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 "#; let loaded = validate(toml).expect("parse"); assert_eq!(loaded.name.as_str(), "keeper"); - assert_eq!(loaded.subscriptions.len(), 1); + assert_eq!(loaded.triggers.len(), 1); assert_eq!(loaded.extensions.len(), 1); let venue = loaded.extensions.get("venue").expect("venue section"); assert_eq!( @@ -369,22 +368,19 @@ name = "plain" } #[test] - fn load_parses_cron_subscription() { + fn load_parses_schedule_trigger() { let toml = r#" [component] name = "scheduler" [dependencies] -[[subscription]] -kind = "cron" -schedule = "*/5 * * * *" +[[trigger]] +on = "schedule" +cron = "*/5 * * * *" "#; let loaded = validate(toml).expect("parse"); - assert!(matches!( - &loaded.subscriptions[0], - Subscription::Cron { .. } - )); + assert!(matches!(&loaded.triggers[0], Trigger::Schedule { .. })); } #[test] diff --git a/crates/nexum-runtime/src/manifest/mod.rs b/crates/nexum-runtime/src/manifest/mod.rs index 9eabe2fc..c9565130 100644 --- a/crates/nexum-runtime/src/manifest/mod.rs +++ b/crates/nexum-runtime/src/manifest/mod.rs @@ -15,7 +15,7 @@ pub use capabilities::{CapabilityRegistry, NamespaceCaps}; pub(crate) use error::{CapabilityError, ParseError}; pub(crate) use load::load; pub use types::ExtensionSections; -pub(crate) use types::{LoadedManifest, ResourceSection, Subscription}; +pub(crate) use types::{LoadedManifest, ResourceSection, Trigger}; // CapabilityViolation and the *Section structs are // reachable through these functions' return / argument types; // consumers that need to name them directly do so via diff --git a/crates/nexum-runtime/src/manifest/types.rs b/crates/nexum-runtime/src/manifest/types.rs index c8172547..f4c54eda 100644 --- a/crates/nexum-runtime/src/manifest/types.rs +++ b/crates/nexum-runtime/src/manifest/types.rs @@ -26,10 +26,9 @@ pub(crate) struct Manifest { pub dependencies: Option, #[serde(default)] pub config: toml::Table, - /// `[[subscription]]` tables as written; parsed by the validation - /// pass. - #[serde(default, rename = "subscription")] - pub subscriptions: Vec, + /// `[[trigger]]` tables as written; parsed by the validation pass. + #[serde(default, rename = "trigger")] + pub triggers: Vec, /// Extension-owned sections (every non-core top-level key), parsed /// opaquely and routed to the wired extensions; a section no extension /// claims is refused at boot. @@ -41,22 +40,22 @@ pub(crate) struct Manifest { /// to the runtime; each claiming extension parses its own. pub type ExtensionSections = BTreeMap; -/// One `[[subscription]]` table. The `kind` field discriminates; an -/// unknown kind parses as [`Subscription::Extension`] and is validated at -/// boot against the wired extensions' declared kinds. +/// One `[[trigger]]` table. The `on` field discriminates; an unknown +/// kind parses as [`Trigger::Extension`] and is validated at boot +/// against the wired extensions' declared kinds. #[derive(Debug, Clone)] -pub enum Subscription { - /// New-block events; one subscription per chain id, fanned out to every - /// module watching that chain. +pub enum Trigger { + /// A new block; one stream per chain id, fanned out to every module + /// watching that chain. Block { /// EVM chain id. chain_id: u64, }, - /// Chain-log events matching `address` + topic-0; one subscription per - /// entry, tagged with the owning module. A re-open replays its start - /// height; `removed` retraction covers only the last delivered + /// A contract event's log matching `address` + topic-0; one stream + /// per entry, tagged with the owning module. A re-open replays its + /// start height; `removed` retraction covers only the last delivered /// log-bearing height. - ChainLog { + Event { /// EVM chain id. chain_id: u64, /// Contract address filter, declared as 20-byte hex. @@ -67,37 +66,37 @@ pub enum Subscription { /// Persist a durable cursor; a restart re-opens AT the cursor block /// and replays it. resume: bool, - /// Backfill cap in blocks for a `resume` subscription; `None` + /// Backfill cap in blocks for a `resume` trigger; `None` /// backfills the whole gap, a cap drops the oldest missed blocks. max_lookback: Option, }, - /// Cron-scheduled tick; parsed but not dispatched (the supervisor - /// warns). - Cron { + /// A cron expression's time arriving; parsed but not dispatched (the + /// supervisor warns). + Schedule { /// Standard 5-field cron expression. #[allow(dead_code)] - schedule: String, + cron: String, }, - /// An extension-owned event kind. Delivered when the kind matches and - /// every filter pair is present in the event's attributes. + /// An extension-owned kind. Delivered when the kind matches and + /// every filter pair is present in the delivery's attributes. Extension { - /// The extension-declared subscription kind. - kind: String, - /// Attribute filters; empty admits every event of the kind. + /// The manifest's `on` value, verbatim. + extension_kind: String, + /// Attribute filters; empty admits every delivery of the kind. filters: BTreeMap, }, } -/// Core subscription kinds shaped by serde; the hex fields stay raw -/// strings until the [`Subscription`] conversion validates them. -// `kebab-case` reproduces `nexum_world::SubscriptionKind`, which gates this. +/// Core trigger kinds shaped by serde; the hex fields stay raw +/// strings until the [`Trigger`] conversion validates them. +// `kebab-case` reproduces `nexum_world::TriggerKind`, which gates this. #[derive(Deserialize)] -#[serde(tag = "kind", rename_all = "kebab-case")] -enum CoreSubscription { +#[serde(tag = "on", rename_all = "kebab-case")] +enum CoreTrigger { Block { chain_id: u64, }, - ChainLog { + Event { chain_id: u64, #[serde(default)] address: Option, @@ -108,77 +107,79 @@ enum CoreSubscription { #[serde(default)] max_lookback: Option, }, - Cron { - schedule: String, + Schedule { + cron: String, }, } -impl TryFrom for Subscription { +impl TryFrom for Trigger { type Error = ParseError; /// Validates the hex filters; the wording is operator-pinned. - fn try_from(sub: CoreSubscription) -> Result { - Ok(match sub { - CoreSubscription::Block { chain_id } => Self::Block { chain_id }, - CoreSubscription::ChainLog { + fn try_from(trigger: CoreTrigger) -> Result { + Ok(match trigger { + CoreTrigger::Block { chain_id } => Self::Block { chain_id }, + CoreTrigger::Event { chain_id, address, event_signature, resume, max_lookback, - } => Self::ChainLog { + } => Self::Event { chain_id, address: address .map(|raw| { - raw.parse::
().map_err(|source| { - ParseError::InvalidChainLogAddress { value: raw, source } - }) + raw.parse::
() + .map_err(|source| ParseError::InvalidEventAddress { + value: raw, + source, + }) }) .transpose()?, event_signature: event_signature .map(|raw| { raw.parse::() - .map_err(|source| ParseError::InvalidChainLogTopic { - value: raw, - source, - }) + .map_err(|source| ParseError::InvalidEventTopic { value: raw, source }) }) .transpose()?, resume, max_lookback, }, - CoreSubscription::Cron { schedule } => Self::Cron { schedule }, + CoreTrigger::Schedule { cron } => Self::Schedule { cron }, }) } } -impl Subscription { +impl Trigger { /// The kind dispatch: a core kind must match its shape, an unknown - /// kind becomes [`Subscription::Extension`] with string filters. + /// kind becomes [`Trigger::Extension`] with string filters. /// `index` is the table's 1-based position; a refusal carries it /// because the parsed tables have no source spans. fn from_table(index: usize, table: toml::Table) -> Result { - let Some(kind) = table.get("kind").and_then(toml::Value::as_str) else { - return Err(ParseError::MissingSubscriptionKind { index }); + let Some(kind) = table.get("on").and_then(toml::Value::as_str) else { + return Err(ParseError::MissingTriggerKind { index }); }; - if kind.parse::().is_err() { - let kind = kind.to_owned(); + if kind.parse::().is_err() { + let extension_kind = kind.to_owned(); let mut filters = BTreeMap::new(); for (key, value) in table { - if key == "kind" { + if key == "on" { continue; } let toml::Value::String(value) = value else { - return Err(ParseError::NonStringSubscriptionFilter { key }); + return Err(ParseError::NonStringTriggerFilter { key }); }; filters.insert(key, value); } - return Ok(Self::Extension { kind, filters }); + return Ok(Self::Extension { + extension_kind, + filters, + }); } let kind = kind.to_owned(); toml::Value::Table(table) - .try_into::() - .map_err(|source| ParseError::InvalidSubscription { + .try_into::() + .map_err(|source| ParseError::InvalidTrigger { index, kind, source, @@ -262,8 +263,8 @@ pub struct LoadedManifest { /// module's `init`. Scalars become their text form; arrays and tables /// their TOML representation. pub config: Vec<(String, String)>, - /// Parsed `[[subscription]]` tables. - pub subscriptions: Vec, + /// Parsed `[[trigger]]` tables. + pub triggers: Vec, /// Extension-owned sections. pub extensions: ExtensionSections, } @@ -272,7 +273,7 @@ impl TryFrom for LoadedManifest { type Error = ParseError; /// Every context-free value check, in order: name, digest, - /// subscriptions, then `[dependencies]` presence. The registry + /// triggers, then `[dependencies]` presence. The registry /// cross-check and the `hosts` placement check stay in `load`, which /// holds the registry and refuses an unknown name first. fn try_from(manifest: Manifest) -> Result { @@ -288,11 +289,11 @@ impl TryFrom for LoadedManifest { value: manifest.component.digest.clone().unwrap_or_default(), source, })?; - let subscriptions = manifest - .subscriptions + let triggers = manifest + .triggers .into_iter() .zip(1..) - .map(|(table, index)| Subscription::from_table(index, table)) + .map(|(table, index)| Trigger::from_table(index, table)) .collect::, _>>()?; let dependencies = manifest .dependencies @@ -318,7 +319,7 @@ impl TryFrom for LoadedManifest { dependencies, http_allowlist, config, - subscriptions, + triggers, extensions: manifest.extensions, }) } diff --git a/crates/nexum-runtime/src/refusal.rs b/crates/nexum-runtime/src/refusal.rs index f5b111f8..304df2ca 100644 --- a/crates/nexum-runtime/src/refusal.rs +++ b/crates/nexum-runtime/src/refusal.rs @@ -184,26 +184,26 @@ mod tests { "missing_capabilities", "misplaced_dependency_attribute", "invalid_component_digest", - "missing_subscription_kind", - "invalid_subscription", - "invalid_chain_log_address", - "invalid_chain_log_topic", - "non_string_subscription_filter", + "missing_trigger_kind", + "invalid_trigger", + "invalid_event_address", + "invalid_event_topic", + "non_string_trigger_filter", // LoadRefusal. "section_unclaimed", "extension_namespace_claimed", - "subscription_kind_claimed", + "trigger_kind_claimed", "section_claimed", - "unknown_event_kind", + "unknown_trigger_kind", "digest_unpinned", "capability_not_permitted", - "chain_subscription_not_permitted", + "chain_trigger_not_permitted", // LaunchRefusal, less the wait-time `event_loop_gone`, which is // raised after a successful boot and never counted. "nothing_to_run", "all_dead_override", "all_dead_configured", - "dead_hold_subs", + "dead_hold_triggers", // CapabilityError. "undeclared", "unknown_wasi", diff --git a/crates/nexum-runtime/src/runtime/dispatch_rate.rs b/crates/nexum-runtime/src/runtime/dispatch_rate.rs index c4273ee0..969cf579 100644 --- a/crates/nexum-runtime/src/runtime/dispatch_rate.rs +++ b/crates/nexum-runtime/src/runtime/dispatch_rate.rs @@ -1,5 +1,5 @@ //! Per-module dispatch rate limiter: one token bucket per module, checked -//! before `on_event`, drops over-rate events. Caps how often a dispatch +//! before `on_trigger`, drops over-rate triggers. Caps how often a dispatch //! starts (fuel/memory/poison cap what one costs); per-module, so a flood //! cannot starve other modules. Pure with injected time. diff --git a/crates/nexum-runtime/src/runtime/event_loop.rs b/crates/nexum-runtime/src/runtime/event_loop.rs index b20e5df0..80088984 100644 --- a/crates/nexum-runtime/src/runtime/event_loop.rs +++ b/crates/nexum-runtime/src/runtime/event_loop.rs @@ -4,7 +4,7 @@ //! and retracts a reorged delivered tail. //! //! `open_block_streams` and `open_chain_log_streams` each spawn one -//! reconnect-aware task per subscription: it opens the stream, pumps items to +//! reconnect-aware task per trigger or chain: it opens the stream, pumps items to //! an mpsc channel, and on drop waits `restart_policy::backoff_for` before //! reopening, resetting the backoff once the stream has been healthy for //! `HEALTHY_WINDOW`. The tasks exit with [`TaskExit::ReceiverGone`] when `run` @@ -25,11 +25,11 @@ use tracing::{info, warn}; use crate::bindings::nexum; use crate::host::component::RuntimeTypes; -use crate::host::extension::{ExtensionEvent, ExtensionEventStream}; +use crate::host::extension::{ExtensionDelivery, ExtensionSource}; use crate::host::provider_pool::ProviderPool; use crate::module_id::ModuleId; use crate::runtime::restart_policy::{backoff_for, jitter_seed}; -use crate::supervisor::{ChainLogSub, Supervisor}; +use crate::supervisor::{EventTrigger, Supervisor}; use nexum_tasks::{TaskExecutor, TaskExit, TaskSet}; /// Uninterrupted-event duration before the backoff counter resets to 0. @@ -67,27 +67,32 @@ pub fn open_block_streams( streams } -/// Open one reconnect-aware chain-log task per subscription; see +/// Open one reconnect-aware chain-log task per event trigger; see /// [`open_block_streams`]. pub fn open_chain_log_streams( pool: &ProviderPool, - subs: Vec, + triggers: Vec, executor: &TaskExecutor, tasks: &mut TaskSet, ) -> Vec { let mut streams = Vec::new(); - for sub in subs { + for trigger in triggers { let (tx, rx) = mpsc::channel::(RECONNECT_CHANNEL_BUF); let pool = pool.clone(); let resume = ChainLogResume { - // The cursor key is constant per subscription and cloned onto every + // The cursor key is constant per trigger and cloned onto every // log; `Arc` keeps that clone cheap. - cursor_key: sub.cursor_key.map(Arc::from), - initial_cursor: sub.initial_cursor, - max_lookback: sub.max_lookback, + cursor_key: trigger.cursor_key.map(Arc::from), + initial_cursor: trigger.initial_cursor, + max_lookback: trigger.max_lookback, }; tasks.push(executor.spawn(reconnecting_chain_log_task( - pool, sub.module, sub.chain, sub.filter, resume, tx, + pool, + trigger.module, + trigger.chain, + trigger.filter, + resume, + tx, ))); let tagged: TaggedChainLogStream = Box::pin(receiver_stream(rx)); streams.push(tagged); @@ -192,9 +197,9 @@ async fn reconnecting_block_task( } } -/// Per-subscription resume and backfill knobs for a chain-log task. +/// Per-trigger resume and backfill knobs for a chain-log task. struct ChainLogResume { - /// Durable cursor key; `Some` for a `resume` subscription. + /// Durable cursor key; `Some` for a `resume` trigger. cursor_key: Option>, /// Persisted resume block read at boot; the first successful open starts here. initial_cursor: Option, @@ -210,7 +215,7 @@ struct DeliveredTail { logs: Vec, } -/// Poller-backed loop for one (module, chain) chain-log subscription; a +/// Poller-backed loop for one (module, chain) event trigger; a /// re-open resumes past the scanned range and retracts a reorged tail. async fn reconnecting_chain_log_task( pool: ProviderPool, @@ -293,7 +298,7 @@ async fn reconnecting_chain_log_task( } let mut start_block = poller_start_block(boot_resume, resume_from, invalidated_tail, head); // Opt-in bound: `max_lookback` caps how far back a resume - // subscription backfills. The default (`None`) backfills fully; a + // trigger backfills. The default (`None`) backfills fully; a // set cap clamps the start up to `head - cap` and surfaces the // dropped oldest blocks. if let Some(cap) = max_lookback { @@ -458,26 +463,26 @@ pub type TaggedChainLog = (ModuleId, Chain, alloy_rpc_types_eth::Log, Option + Send>>; -/// Drive the supervisor with events until `shutdown` resolves. +/// Drive the supervisor with triggers until `shutdown` resolves. /// -/// `shutdown` is observed only between guest calls, never mid-`call_on_event`: -/// here between events, and through the supervisor's stop probe between the -/// per-module calls of one event. The in-flight call finishes before the loop -/// exits; the guard `shutdown` yields is held until return, so the drain -/// covers that call and its cursor commit. Returns the `(blocks, chain_logs)` -/// dispatch tally. +/// `shutdown` is observed only between guest calls, never +/// mid-`call_on_trigger`: here between triggers, and through the +/// supervisor's stop probe between the per-module calls of one trigger. The +/// in-flight call finishes before the loop exits; the guard `shutdown` +/// yields is held until return, so the drain covers that call and its +/// cursor commit. Returns the `(blocks, chain_logs)` dispatch tally. pub async fn run( supervisor: &mut Supervisor, block_streams: Vec, chain_log_streams: Vec, - extension_streams: Vec, + extension_streams: Vec, tasks: TaskSet, shutdown: impl std::future::Future + Send, ) -> (u64, u64) { // `select_all` over an empty Vec yields `None` immediately, which // would trip the "stream ended -> shut down" arm below before the - // first block / chain-log ever flows. Engine configs that subscribe to - // only one event kind (e.g. all modules use `[[subscription]] kind + // first block / chain-log ever flows. Engine configs that declare + // only one trigger kind (e.g. all modules use `[[trigger]] on // = "block"`) are valid and must not be punished. Replace each // empty side with `stream::pending()` so the corresponding select // arm is never selected; the bail-on-None semantic still fires @@ -492,7 +497,7 @@ pub async fn run( } else { select_all(chain_log_streams).boxed() }; - let mut extension_events: BoxStream<'_, _> = if extension_streams.is_empty() { + let mut extension_deliveries: BoxStream<'_, _> = if extension_streams.is_empty() { futures::stream::pending().boxed() } else { select_all(extension_streams).boxed() @@ -500,33 +505,33 @@ pub async fn run( let mut shutdown = Box::pin(shutdown); let mut dispatched_blocks: u64 = 0; let mut dispatched_chain_logs: u64 = 0; - let mut dispatched_extension_events: u64 = 0; + let mut dispatched_extension_triggers: u64 = 0; let started = Instant::now(); loop { - // Phase 1: pick the next event OR observe shutdown. The + // Phase 1: pick the next trigger OR observe shutdown. The // dispatch itself happens in phase 2 (outside the select) // so an in-flight wasmtime call never gets cancelled by a // shutdown signal arriving mid-dispatch. - enum NextEvent { + enum NextTrigger { Block(nexum::host::types::Block), // The alloy `Log` is boxed so the `Chain` tag does not push // the enum past the large-variant lint threshold. - ChainLog( + Event( ModuleId, Chain, Box, Option>, ), - Extension(ExtensionEvent), + Extension(ExtensionDelivery), // Carries the drain guard `shutdown` yielded. Shutdown(G), StreamPanic(&'static str), } let next = tokio::select! { biased; - guard = &mut shutdown => NextEvent::Shutdown(guard), + guard = &mut shutdown => NextTrigger::Shutdown(guard), next = blocks.next() => match next { - Some(Ok((chain, header))) => NextEvent::Block(nexum::host::types::Block { + Some(Ok((chain, header))) => NextTrigger::Block(nexum::host::types::Block { chain_id: chain.id(), number: header.number, hash: header.hash.as_slice().to_vec(), @@ -536,62 +541,62 @@ pub async fn run( warn!(chain_id = chain.id(), error = %err, "block stream error - continuing"); continue; } - None => NextEvent::StreamPanic("block"), + None => NextTrigger::StreamPanic("block"), }, next = chain_logs.next() => match next { Some((module, chain, log, cursor_key)) => { - NextEvent::ChainLog(module, chain, Box::new(log), cursor_key) + NextTrigger::Event(module, chain, Box::new(log), cursor_key) } - None => NextEvent::StreamPanic("chain-log"), + None => NextTrigger::StreamPanic("chain-log"), }, - next = extension_events.next() => match next { - Some(event) => NextEvent::Extension(event), + next = extension_deliveries.next() => match next { + Some(delivery) => NextTrigger::Extension(delivery), // Extension source tasks loop forever; `None` means one exited. - None => NextEvent::StreamPanic("extension-event"), + None => NextTrigger::StreamPanic("extension"), }, }; match next { - NextEvent::Block(block) => { + NextTrigger::Block(block) => { supervisor.dispatch_block(block).await; dispatched_blocks += 1; } - NextEvent::ChainLog(module, chain, log, cursor_key) => { + NextTrigger::Event(module, chain, log, cursor_key) => { supervisor - .dispatch_chain_log(&module, chain, *log, cursor_key.as_deref()) + .dispatch_event(&module, chain, *log, cursor_key.as_deref()) .await; dispatched_chain_logs += 1; } - NextEvent::Extension(event) => { - supervisor.dispatch_extension_event(event).await; - dispatched_extension_events += 1; + NextTrigger::Extension(delivery) => { + supervisor.dispatch_extension_trigger(delivery).await; + dispatched_extension_triggers += 1; } - NextEvent::Shutdown(guard) => { + NextTrigger::Shutdown(guard) => { // Drop the stream-end receivers so the reconnect // tasks observe a closed channel and exit. Then drain // the task set so the engine genuinely sees the tasks // finish before returning. drop(blocks); drop(chain_logs); - drop(extension_events); + drop(extension_deliveries); tasks.shutdown().await; info!( dispatched_blocks, dispatched_chain_logs, - dispatched_extension_events, + dispatched_extension_triggers, uptime_secs = started.elapsed().as_secs(), "graceful shutdown complete", ); drop(guard); return (dispatched_blocks, dispatched_chain_logs); } - NextEvent::StreamPanic(kind) => { + NextTrigger::StreamPanic(kind) => { // Reconnect tasks should loop forever. // Hitting `None` from `select_all` means the task // exited (panic or channel closed). Bail loudly. drop(blocks); drop(chain_logs); - drop(extension_events); + drop(extension_deliveries); tasks.shutdown().await; warn!( kind, @@ -720,7 +725,7 @@ mod tests { tasks: &mut TaskSet, initial_cursor: Option, ) -> TaggedChainLogStream { - let subs = vec![ChainLogSub { + let triggers = vec![EventTrigger { module: ModuleId::parse("mod").expect("valid module name"), chain: alloy_chains::Chain::mainnet(), filter: alloy_rpc_types_eth::Filter::default(), @@ -728,9 +733,9 @@ mod tests { initial_cursor, max_lookback: None, }]; - open_chain_log_streams(pool, subs, executor, tasks) + open_chain_log_streams(pool, triggers, executor, tasks) .pop() - .expect("one stream per subscription") + .expect("one stream per trigger") } async fn recv(stream: &mut TaggedChainLogStream) -> Log { @@ -1084,16 +1089,16 @@ mod tests { tasks.shutdown().await; } - /// `open_chain_log_streams` spawns one reconnect task per subscription. + /// `open_chain_log_streams` spawns one reconnect task per event trigger. #[tokio::test] - async fn open_chain_log_streams_opens_one_task_per_subscription() { + async fn open_chain_log_streams_opens_one_task_per_trigger() { let rpc = MockRpc::new(); let pool = pool_for(&rpc); let manager = TaskManager::new(); let executor = manager.executor(); let mut tasks = TaskSet::new(); - let subs = vec![ - ChainLogSub { + let triggers = vec![ + EventTrigger { module: ModuleId::parse("mod-a").expect("valid module name"), chain: alloy_chains::Chain::mainnet(), filter: alloy_rpc_types_eth::Filter::default(), @@ -1101,7 +1106,7 @@ mod tests { initial_cursor: None, max_lookback: None, }, - ChainLogSub { + EventTrigger { module: ModuleId::parse("mod-b").expect("valid module name"), chain: alloy_chains::Chain::mainnet(), filter: alloy_rpc_types_eth::Filter::default(), @@ -1110,8 +1115,8 @@ mod tests { max_lookback: None, }, ]; - let streams = open_chain_log_streams(&pool, subs, &executor, &mut tasks); - assert_eq!(streams.len(), 2, "one stream per subscription"); + let streams = open_chain_log_streams(&pool, triggers, &executor, &mut tasks); + assert_eq!(streams.len(), 2, "one stream per trigger"); tasks.shutdown().await; } @@ -1348,7 +1353,7 @@ mod tests { log_node.push_chain_log(alloy_rpc_types_eth::Log::default()); let block_streams = open_block_streams(&pool, &[Chain::mainnet()], &executor, &mut tasks); - let log_subs = vec![crate::supervisor::ChainLogSub { + let event_triggers = vec![crate::supervisor::EventTrigger { module: ModuleId::parse("test-module").expect("valid module name"), chain: Chain::from_id(100), filter: Filter::default(), @@ -1356,7 +1361,8 @@ mod tests { initial_cursor: None, max_lookback: None, }]; - let chain_log_streams = open_chain_log_streams(&pool, log_subs, &executor, &mut tasks); + let chain_log_streams = + open_chain_log_streams(&pool, event_triggers, &executor, &mut tasks); // 500 ms only bounds wall time; the assertion is on the tally, so a // miss means a broken select arm, not a slow scheduler. @@ -1402,7 +1408,7 @@ mod tests { let executor = manager.executor(); let mut tasks = TaskSet::new(); - // Two subscription tasks: both must drain before `run()` returns. + // Two stream tasks: both must drain before `run()` returns. let block_streams = open_block_streams( &pool, &[Chain::mainnet(), Chain::from_id(100)], diff --git a/crates/nexum-runtime/src/runtime/mod.rs b/crates/nexum-runtime/src/runtime/mod.rs index dec8614e..568533e4 100644 --- a/crates/nexum-runtime/src/runtime/mod.rs +++ b/crates/nexum-runtime/src/runtime/mod.rs @@ -1,5 +1,5 @@ //! Engine-side runtime: the event loop that drives the supervisor from live -//! chain subscriptions, and its pacing, restart, and poison policies. +//! chain streams, and its pacing, restart, and poison policies. pub mod dispatch_rate; pub mod event_loop; diff --git a/crates/nexum-runtime/src/runtime/restart_policy.rs b/crates/nexum-runtime/src/runtime/restart_policy.rs index 23ac8836..5689d10c 100644 --- a/crates/nexum-runtime/src/runtime/restart_policy.rs +++ b/crates/nexum-runtime/src/runtime/restart_policy.rs @@ -1,6 +1,6 @@ //! Supervisor module restart policy. //! -//! On a trap in `on_event` the supervisor marks the module dead and schedules +//! On a trap in `on_trigger` the supervisor marks the module dead and schedules //! a restart with exponential backoff; the next eligible dispatch retries, and //! a successful call resets the failure counter. //! diff --git a/crates/nexum-runtime/src/supervisor/admission.rs b/crates/nexum-runtime/src/supervisor/admission.rs index 1b0a9da8..6739b839 100644 --- a/crates/nexum-runtime/src/supervisor/admission.rs +++ b/crates/nexum-runtime/src/supervisor/admission.rs @@ -8,12 +8,12 @@ use crate::host::component::RuntimeTypes; use crate::host::extension::Extension; use crate::manifest::{self, CapabilityRegistry}; -pub(super) fn extension_subscription_vocabulary( +pub(super) fn extension_trigger_kinds( extensions: &[Arc>], ) -> BTreeSet<&'static str> { extensions .iter() - .flat_map(|ext| ext.subscriptions().iter().copied()) + .flat_map(|ext| ext.emits_trigger_kinds().iter().copied()) .collect() } @@ -37,7 +37,7 @@ pub(super) fn enforce_extension_sections( Ok(()) } -/// Refuses a name two wired extensions both claim: namespace, subscription +/// Refuses a name two wired extensions both claim: namespace, trigger /// kind, or manifest section. pub(super) fn enforce_extension_uniqueness( extensions: &[Arc>], @@ -50,9 +50,9 @@ pub(super) fn enforce_extension_uniqueness( if !namespaces.insert(namespace) { return Err(LoadRefusal::ExtensionNamespaceClaimed { namespace }); } - for &kind in ext.subscriptions() { + for &kind in ext.emits_trigger_kinds() { if !kinds.insert(kind) { - return Err(LoadRefusal::SubscriptionKindClaimed { kind }); + return Err(LoadRefusal::TriggerKindClaimed { kind }); } } for §ion in ext.manifest_sections() { diff --git a/crates/nexum-runtime/src/supervisor/dispatch.rs b/crates/nexum-runtime/src/supervisor/dispatch.rs index d2cd88e0..63984a6b 100644 --- a/crates/nexum-runtime/src/supervisor/dispatch.rs +++ b/crates/nexum-runtime/src/supervisor/dispatch.rs @@ -1,4 +1,4 @@ -//! Event dispatch: rate limit, refuel, invoke `on_event` under the +//! Trigger dispatch: rate limit, refuel, invoke `on_trigger` under the //! wall-clock deadline, and record the outcome. use std::time::Duration; @@ -14,9 +14,9 @@ use super::cursors::{commit_chain_log_cursor, persist_progress_marker}; use super::lifecycle::{revive_one, sweep}; use crate::bindings::nexum; use crate::host::component::RuntimeTypes; -use crate::host::extension::ExtensionEvent; +use crate::host::extension::ExtensionDelivery; use crate::host::logs::{LogRecord, LogSource}; -use crate::manifest::Subscription; +use crate::manifest::Trigger; use crate::module_id::ModuleId; impl Supervisor { @@ -25,7 +25,7 @@ impl Supervisor { let chain = Chain::from_id(block.chain_id); let chain_id = chain.id(); let block_number = block.number; - let event = nexum::host::types::Event::Block(block); + let trigger = nexum::host::types::Trigger::Block(block); let now = Instant::now(); sweep(&self.shared, &mut self.modules, now, self.stop.as_ref()).await; @@ -36,9 +36,9 @@ impl Supervisor { if !m.health.dispatchable() { return false; } - m.subscriptions + m.triggers .iter() - .any(|s| matches!(s, Subscription::Block { chain_id: cid } if chain == *cid)) + .any(|t| matches!(t, Trigger::Block { chain_id: cid } if chain == *cid)) }) .collect(); for (position, idx) in candidate_indices.iter().copied().enumerate() { @@ -52,7 +52,7 @@ impl Supervisor { metrics::counter!( "nexum_runtime_dispatch_dropped_total", "module" => self.modules[i].name.to_string(), - "event_kind" => "block", + "trigger_kind" => "block", "reason" => "shutdown", ) .increment(1); @@ -71,7 +71,7 @@ impl Supervisor { break; } if matches!( - self.dispatch_to(idx, chain_id, "block", block_number, &event, now) + self.dispatch_to(idx, chain_id, "block", block_number, &trigger, now) .await, DispatchOutcome::Ok, ) { @@ -87,9 +87,9 @@ impl Supervisor { dispatched } - /// Returns `true` only when the module accepted the event; the resume + /// Returns `true` only when the module accepted the log; the resume /// cursor persists only after a successful dispatch. - pub async fn dispatch_chain_log( + pub async fn dispatch_event( &mut self, module_name: &ModuleId, chain: Chain, @@ -98,20 +98,20 @@ impl Supervisor { ) -> bool { let now = Instant::now(); // Skipped: the cursor stays put, so `resume = true` replays the - // event at the next start. Counted anyway, so the shutdown reason is + // log at the next start. Counted anyway, so the shutdown reason is // one series rather than three behaviours. if self.stop_requested() { metrics::counter!( "nexum_runtime_dispatch_dropped_total", "module" => module_name.to_string(), - "event_kind" => "chain-log", + "trigger_kind" => "event", "reason" => "shutdown", ) .increment(1); return false; } let Some(idx) = self.modules.iter().position(|m| m.name == *module_name) else { - warn!(module = %module_name, "no such module - dropping chain-log"); + warn!(module = %module_name, "no such module - dropping event"); return false; }; @@ -120,7 +120,7 @@ impl Supervisor { return false; } - // The chain-log hot path revives only its own module, never the rest. + // The event hot path revives only its own module, never the rest. if self.modules[idx].health.due_restart(now) { revive_one(&self.shared, &mut self.modules[idx], now).await; } @@ -133,17 +133,14 @@ impl Supervisor { let block_number = log.block_number; let removed = log.removed; - let event = nexum::host::types::Event::ChainLogs(nexum::host::types::ChainLogs { - chain_id: chain.id(), - logs: vec![nexum::host::types::ChainLog::from(&log)], - }); + let trigger = nexum::host::types::Trigger::Event(super::triggers::wit_log(&log, chain)); let ok = matches!( self.dispatch_to( idx, chain.id(), - "chain-log", + "event", block_number.unwrap_or_default(), - &event, + &trigger, now, ) .await, @@ -163,7 +160,7 @@ impl Supervisor { } /// The restart sweep runs first; returns the number of modules invoked. - pub async fn dispatch_extension_event(&mut self, event: ExtensionEvent) -> usize { + pub async fn dispatch_extension_trigger(&mut self, delivery: ExtensionDelivery) -> usize { let now = Instant::now(); sweep(&self.shared, &mut self.modules, now, self.stop.as_ref()).await; @@ -173,13 +170,14 @@ impl Supervisor { if !m.health.dispatchable() { return false; } - m.subscriptions.iter().any(|s| { + m.triggers.iter().any(|t| { matches!( - s, - Subscription::Extension { kind, filters } - if kind == event.kind && filters.iter().all(|(fk, fv)| { - event.attrs.iter().any(|(ak, av)| ak == fk && av == fv) - }) + t, + Trigger::Extension { extension_kind, filters } + if extension_kind == delivery.extension_kind + && filters.iter().all(|(fk, fv)| { + delivery.attrs.iter().any(|(ak, av)| ak == fk && av == fv) + }) ) }) }) @@ -191,16 +189,16 @@ impl Supervisor { metrics::counter!( "nexum_runtime_dispatch_dropped_total", "module" => self.modules[i].name.to_string(), - "event_kind" => event.kind, + "trigger_kind" => delivery.extension_kind, "reason" => "shutdown", ) .increment(1); } break; } - // Extension events are not chain-scoped; telemetry carries the 0 sentinel. + // Extension deliveries are not chain-scoped; telemetry carries the 0 sentinel. if matches!( - self.dispatch_to(idx, 0, event.kind, 0, &event.event, now) + self.dispatch_to(idx, 0, delivery.extension_kind, 0, &delivery.trigger, now) .await, DispatchOutcome::Ok, ) { @@ -215,9 +213,9 @@ impl Supervisor { &mut self, idx: usize, chain_id: u64, - event_kind: &'static str, + trigger_kind: &'static str, block_number: u64, - event: &nexum::host::types::Event, + trigger: &nexum::host::types::Trigger, now: Instant, ) -> DispatchOutcome { let poison_policy = self.policy; @@ -231,14 +229,14 @@ impl Supervisor { debug!( module = %module.name, chain_id, - event_kind, + trigger_kind, block_number, - "dispatch rate limit exceeded - dropping event", + "dispatch rate limit exceeded - dropping trigger", ); metrics::counter!( "nexum_runtime_dispatch_dropped_total", "module" => module.name.to_string(), - "event_kind" => event_kind, + "trigger_kind" => trigger_kind, "reason" => "rate_limited", ) .increment(1); @@ -248,7 +246,7 @@ impl Supervisor { error!( module = %module.name, chain_id, - event_kind, + trigger_kind, error = %e, "set_fuel failed - skipping" ); @@ -261,7 +259,7 @@ impl Supervisor { let call = module .live .bindings - .call_on_event(&mut module.live.store, event); + .call_on_trigger(&mut module.live.store, trigger); let outcome = with_dispatch_deadline(deadline, call) .await .unwrap_or_else(|exceeded| Err(wasmtime::Error::from(exceeded))); @@ -276,7 +274,7 @@ impl Supervisor { debug!( module = %module.name, chain_id, - event_kind, + trigger_kind, block_number, latency_ms, "dispatch ok" @@ -284,7 +282,7 @@ impl Supervisor { metrics::histogram!( "nexum_runtime_event_latency_seconds", "module" => module.name.to_string(), - "event_kind" => event_kind, + "trigger_kind" => trigger_kind, ) .record(elapsed.as_secs_f64()); module.health.dispatch_succeeded(); @@ -295,12 +293,12 @@ impl Supervisor { warn!( module = %module.name, chain_id, - event_kind, + trigger_kind, block_number, latency_ms, kind, message = %crate::host::fault::fault_message(&fault), - "on-event returned fault", + "on-trigger returned fault", ); metrics::counter!( "nexum_runtime_module_errors_total", @@ -318,13 +316,13 @@ impl Supervisor { error!( module = %module.name, chain_id, - event_kind, + trigger_kind, block_number, latency_ms, failure_count = verdict.failure_count, backoff_ms = verdict.backoff.as_millis() as u64, error = %trap, - "on-event trapped - module marked dead; will retry after backoff", + "on-trigger trapped - module marked dead; will retry after backoff", ); metrics::counter!( "nexum_runtime_module_errors_total", @@ -393,7 +391,7 @@ pub(super) enum DispatchOutcome { Fault, /// Marked dead, maybe quarantined per the poison policy. Trapped, - /// `set_fuel` failed before the call; the module stays alive, the event skips. + /// `set_fuel` failed before the call; the module stays alive, the trigger skips. FuelSetFailed, /// Dropped before the guest runs; liveness untouched. RateLimited, diff --git a/crates/nexum-runtime/src/supervisor/load.rs b/crates/nexum-runtime/src/supervisor/load.rs index b0ce301e..41bb4c11 100644 --- a/crates/nexum-runtime/src/supervisor/load.rs +++ b/crates/nexum-runtime/src/supervisor/load.rs @@ -11,9 +11,7 @@ use thiserror::Error as ThisError; use tracing::{info, warn}; use wasmtime::component::{Component, Linker}; -use super::admission::{ - capability_registry, enforce_extension_sections, extension_subscription_vocabulary, -}; +use super::admission::{capability_registry, enforce_extension_sections, extension_trigger_kinds}; use super::artifact::{DigestPolicy, read_verified_component}; use super::dispatch::with_dispatch_deadline; use super::lifecycle::Health; @@ -21,13 +19,13 @@ use super::prepass::manifest_namespace; use super::store::{HostStore, ResolvedLimits, StoreSpec, fresh_run_store}; use super::{BootEnv, Shared}; use crate::bindings::nexum::host::types::Fault; -use crate::bindings::{Config, EventModule}; +use crate::bindings::{Config, TriggerModule}; use crate::digest::ContentDigest; use crate::engine_config::ModuleEntry; use crate::host::component::RuntimeTypes; use crate::host::logs::RunId; use crate::host::state::HostState; -use crate::manifest::{self, CapabilityRegistry, LoadedManifest, Subscription}; +use crate::manifest::{self, CapabilityRegistry, LoadedManifest, Trigger}; use crate::module_id::ModuleId; use crate::refusal::{Refusal, RefusalContext as _}; use crate::runtime::dispatch_rate::TokenBucket; @@ -55,10 +53,10 @@ pub enum LoadRefusal { /// The doubly claimed namespace. namespace: &'static str, }, - /// An embedder wiring bug: a subscription kind's events must have one - /// owning extension. - #[error("subscription kind {kind} is claimed twice")] - SubscriptionKindClaimed { + /// An embedder wiring bug: a trigger kind must have one owning + /// extension. + #[error("trigger kind {kind} is claimed twice")] + TriggerKindClaimed { /// The doubly claimed kind. kind: &'static str, }, @@ -69,13 +67,11 @@ pub enum LoadRefusal { /// The doubly claimed section key. section: &'static str, }, - /// Either a typo in the subscription kind or its extension is not - /// wired into this composition. - #[error( - "module {module} subscribes to unknown event kind {kind}; no wired extension declares it" - )] - UnknownEventKind { - /// The subscribing module. + /// Either a typo in the trigger kind or its extension is not wired + /// into this composition. + #[error("module {module} declares unknown trigger kind {kind}; no wired extension declares it")] + UnknownTriggerKind { + /// The declaring module. module: ModuleId, /// The unknown kind. kind: String, @@ -105,14 +101,14 @@ pub enum LoadRefusal { /// The permitted set, for the fix. permitted: String, }, - /// Chain events reach the guest through `on_event`, not an import, so - /// the subscription is gated on the same operator grant as the `chain` + /// Chain data reaches the guest through `on_trigger`, not an import, + /// so the trigger is gated on the same operator grant as the `chain` /// dependency. #[error( - "component {id} subscribes to chain events; \ + "component {id} declares a chain trigger; \ [policy].capabilities permits only: {permitted}" )] - ChainSubscriptionNotPermitted { + ChainTriggerNotPermitted { /// The entry's operator-written id. id: String, /// The permitted set, for the fix. @@ -139,7 +135,7 @@ pub(super) struct Seed { /// Restarts replace bindings, store, and run; the rate bucket carries across. pub(super) struct LiveInstance { - pub(super) bindings: EventModule, + pub(super) bindings: TriggerModule, pub(super) store: HostStore, pub(super) run: RunId, pub(super) dispatch_bucket: TokenBucket, @@ -149,7 +145,7 @@ pub(super) struct LoadedModule { pub(super) name: ModuleId, pub(super) live: LiveInstance, pub(super) seed: Seed, - pub(super) subscriptions: Vec, + pub(super) triggers: Vec, pub(super) health: Health, } @@ -190,7 +186,7 @@ fn default_init_config(config: &Config, namespace: &str) -> Config { /// Runs under the dispatch deadline so a hung host call cannot park boot or a /// restart; a deadline hit or trap is `Err`, a guest fault `Ok(Err(fault))`. async fn run_init( - bindings: &EventModule, + bindings: &TriggerModule, store: &mut HostStore, config: &Config, deadline: Duration, @@ -206,8 +202,8 @@ pub(super) async fn instantiate_module( seed: &Seed, name: &ModuleId, store: &mut HostStore, -) -> Result<(EventModule, Result<(), Fault>)> { - let bindings = EventModule::instantiate_async(&mut *store, &seed.artifact.component, linker) +) -> Result<(TriggerModule, Result<(), Fault>)> { + let bindings = TriggerModule::instantiate_async(&mut *store, &seed.artifact.component, linker) .await // wasmtime::Error is not StdError, so anyhow's with_context needs the bridge. .map_err(Error::from) @@ -249,16 +245,14 @@ fn enforce_policy_capabilities( }); } } - // A block or chain-log subscription delivers chain data without an + // A block or event trigger delivers chain data without an // import, so the `chain` grant gates it too. - let subscribes_to_chain = loaded.subscriptions.iter().any(|sub| { - matches!( - sub, - Subscription::Block { .. } | Subscription::ChainLog { .. } - ) - }); - if subscribes_to_chain && !permitted.iter().any(|p| p == "chain") { - return Err(LoadRefusal::ChainSubscriptionNotPermitted { + let declares_chain_trigger = loaded + .triggers + .iter() + .any(|t| matches!(t, Trigger::Block { .. } | Trigger::Event { .. })); + if declares_chain_trigger && !permitted.iter().any(|p| p == "chain") { + return Err(LoadRefusal::ChainTriggerNotPermitted { id: id.to_owned(), permitted: permitted_set(), }); @@ -363,18 +357,20 @@ pub(super) async fn module( false } }; - // Unserviceable subscriptions warn; an undeclared extension kind refuses. - let extension_kinds = extension_subscription_vocabulary(&shared.extensions); - for sub in &loaded_manifest.subscriptions { - match sub { - Subscription::Cron { .. } => warn!( + // Unserviceable triggers warn; an undeclared extension kind refuses. + let extension_kinds = extension_trigger_kinds(&shared.extensions); + for trigger in &loaded_manifest.triggers { + match trigger { + Trigger::Schedule { .. } => warn!( module = %module_namespace, - "cron subscriptions are declared but inert in 0.2 (lands in 0.3)", + "schedule triggers are declared but never fire until 0.3", ), - Subscription::Extension { kind, .. } if !extension_kinds.contains(kind.as_str()) => { - return Err(LoadRefusal::UnknownEventKind { + Trigger::Extension { extension_kind, .. } + if !extension_kinds.contains(extension_kind.as_str()) => + { + return Err(LoadRefusal::UnknownTriggerKind { module: module_namespace.clone(), - kind: kind.clone(), + kind: extension_kind.clone(), } .into()); } @@ -391,7 +387,7 @@ pub(super) async fn module( dispatch_bucket: TokenBucket::new(limits_cfg.dispatch, Instant::now()), }, seed, - subscriptions: loaded_manifest.subscriptions.clone(), + triggers: loaded_manifest.triggers.clone(), health: Health::from_init(init_succeeded), }) } diff --git a/crates/nexum-runtime/src/supervisor/mod.rs b/crates/nexum-runtime/src/supervisor/mod.rs index 2f6e61cd..8c117031 100644 --- a/crates/nexum-runtime/src/supervisor/mod.rs +++ b/crates/nexum-runtime/src/supervisor/mod.rs @@ -1,5 +1,5 @@ //! Multi-module supervisor: loads `engine.toml` entries, one wasmtime `Store` -//! each, and routes subscribed events. +//! each, and routes triggers. mod admission; mod artifact; @@ -9,12 +9,12 @@ mod lifecycle; pub(crate) mod load; pub(crate) mod prepass; mod store; -mod subscriptions; +mod triggers; pub use load::LoadRefusal; pub use prepass::{BootRefusal, ConfiguredChains}; pub use store::{WasiClockOverride, build_linker}; -pub use subscriptions::{ChainLogSub, SubscriptionPlan, Viability}; +pub use triggers::{EventTrigger, TriggerPlan, Viability}; use std::sync::Arc; @@ -32,7 +32,7 @@ use crate::runtime::poison_policy::PoisonPolicy; use admission::{capability_registry, enforce_extension_uniqueness}; use cursors::ChainLogCursors; use load::LoadedModule; -use prepass::{enforce_subscriptions, load_required_manifest, manifest_namespace}; +use prepass::{enforce_triggers, load_required_manifest, manifest_namespace}; /// Owns every loaded module. pub struct Supervisor { @@ -53,7 +53,7 @@ pub struct BootEnv<'a> { pub limits: &'a ResolvedModuleLimits, /// The `[policy]` surface a manifest may narrow but never widen. pub policy: &'a PolicySection, - /// Chains with an `engine.toml` entry; a subscription elsewhere refuses. + /// Chains with an `engine.toml` entry; a trigger elsewhere refuses. pub configured_chains: ConfiguredChains, /// Refuse a component whose manifest declares no digest. pub require_component_digest: bool, @@ -128,7 +128,7 @@ impl Supervisor { let registry = capability_registry(&shared.extensions); let loaded_manifest = load_required_manifest(&entry.path, entry.manifest.as_deref(), ®istry)?; - enforce_subscriptions( + enforce_triggers( manifest_namespace(&loaded_manifest).as_str(), &loaded_manifest, &env.configured_chains, @@ -154,7 +154,7 @@ impl Supervisor { } /// Halt the dispatch fan-out between guest calls once `stop` fires; a - /// skipped chain-log event replays through its resume cursor, a skipped + /// skipped event replays through its resume cursor, a skipped /// block does not. pub fn stop_on(&mut self, stop: Shutdown) { self.stop = Some(stop); diff --git a/crates/nexum-runtime/src/supervisor/prepass.rs b/crates/nexum-runtime/src/supervisor/prepass.rs index 9671d69d..dedb8abd 100644 --- a/crates/nexum-runtime/src/supervisor/prepass.rs +++ b/crates/nexum-runtime/src/supervisor/prepass.rs @@ -11,7 +11,7 @@ use tracing::{info, warn}; use super::store::{ResolvedLimits, resolve_module_limits}; use crate::engine_config::{EngineConfig, PolicySection}; -use crate::manifest::{self, CapabilityRegistry, LoadedManifest, ParseError, Subscription}; +use crate::manifest::{self, CapabilityRegistry, LoadedManifest, ParseError, Trigger}; use crate::module_id::ModuleId; use crate::refusal::{Refusal, RefusalContext as _}; @@ -69,27 +69,27 @@ pub enum BootRefusal { /// [`Self::UnconfiguredChain`] for a run on defaults: the fix is /// creating engine.toml, not editing it. #[error( - "module {name} subscribes to chain {chain_id} but no engine.toml was found \ + "module {name} declares a trigger on chain {chain_id} but no engine.toml was found \ (running on defaults, no chains configured); create engine.toml with a \ [chains.{chain_id}] entry" )] UnconfiguredChainDefaulted { - /// The subscriber's `[component].name`. + /// The declaring module's `[component].name`. name: String, - /// The chain the subscription names. + /// The chain the trigger names. chain_id: u64, }, - /// Chain access is an operator grant, so a manifest subscription - /// cannot widen the `[chains]` set from its side of the boundary. + /// Chain access is an operator grant, so a manifest trigger cannot + /// widen the `[chains]` set from its side of the boundary. #[error( - "module {name} subscribes to chain {chain_id} but engine.toml declares no \ + "module {name} declares a trigger on chain {chain_id} but engine.toml declares no \ [chains.{chain_id}] entry; configured chains: {}", fmt_chain_ids(configured) )] UnconfiguredChain { - /// The subscriber's `[component].name`. + /// The declaring module's `[component].name`. name: String, - /// The chain the subscription names. + /// The chain the trigger names. chain_id: u64, /// The chains engine.toml declares. configured: BTreeSet, @@ -217,16 +217,15 @@ impl ConfiguredChains { } } -/// Refuse any subscription naming a chain absent from `[chains]`, before any +/// Refuse any trigger naming a chain absent from `[chains]`, before any /// guest code runs. -pub(super) fn enforce_subscriptions( +pub(super) fn enforce_triggers( name: &str, loaded: &LoadedManifest, chains: &ConfiguredChains, ) -> Result<(), BootRefusal> { - for sub in &loaded.subscriptions { - let (Subscription::Block { chain_id } | Subscription::ChainLog { chain_id, .. }) = sub - else { + for trigger in &loaded.triggers { + let (Trigger::Block { chain_id } | Trigger::Event { chain_id, .. }) = trigger else { continue; }; if !chains.contains(*chain_id) { @@ -279,7 +278,7 @@ pub(super) fn enforce_total_reservation<'a>( Ok(()) } -/// Every manifest loaded, every name claimed, every subscribed chain gated, +/// Every manifest loaded, every name claimed, every triggered chain gated, /// and the reservation sum bounded, in `engine.toml` order. Limits resolve /// once here; `load::module` reuses them, so a clamp warns once per field. pub(super) fn run( @@ -294,7 +293,7 @@ pub(super) fn run( .with_refusal_context(|| format!("load module {}", entry.path.display()))?; let namespace = manifest_namespace(&loaded); claim_namespace(&mut ledger, namespace.as_str(), &entry.path)?; - enforce_subscriptions(namespace.as_str(), &loaded, &configured_chains) + enforce_triggers(namespace.as_str(), &loaded, &configured_chains) .with_refusal_context(|| format!("load module {}", entry.path.display()))?; let limits = resolve_module_limits( &entry.id, diff --git a/crates/nexum-runtime/src/supervisor/store.rs b/crates/nexum-runtime/src/supervisor/store.rs index af55bbc7..e95f1ac7 100644 --- a/crates/nexum-runtime/src/supervisor/store.rs +++ b/crates/nexum-runtime/src/supervisor/store.rs @@ -9,7 +9,7 @@ use wasmtime::{Engine, Store}; use wasmtime_wasi::{HostMonotonicClock, HostWallClock, WasiCtxBuilder}; use super::Shared; -use crate::bindings::EventModule; +use crate::bindings::TriggerModule; use crate::engine_config::{OutboundHttpLimits, PolicyCeilings}; use crate::host::component::{RuntimeTypes, StateHandle, StateStore}; use crate::host::extension::Extension; @@ -236,7 +236,9 @@ pub fn build_linker( extensions: &[Arc>], ) -> anyhow::Result>> { let mut linker = Linker::>::new(engine); - EventModule::add_to_linker::, HasSelf>>(&mut linker, |state| state)?; + TriggerModule::add_to_linker::, HasSelf>>(&mut linker, |state| { + state + })?; wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; // wasi:http only; the p2 call above already covers the shared // wasi:io/wasi:clocks interfaces. diff --git a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs index a2ee7bc8..3a980533 100644 --- a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs +++ b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs @@ -45,9 +45,9 @@ fn a_boot_refusal_increments_the_counter_under_its_parse_class() { ); } -/// `[dependencies]` is declared so the failing gate is the subscription kind. +/// `[dependencies]` is declared so the failing gate is the trigger kind. #[tokio::test] -async fn boot_refuses_an_undeclared_extension_subscription_kind() { +async fn boot_refuses_an_undeclared_extension_trigger_kind() { let Some(wasm) = example_wasm_or_skip() else { return; }; @@ -56,12 +56,12 @@ async fn boot_refuses_an_undeclared_extension_subscription_kind() { .module( TestManifest::new("example") .cap("logging") - .extension_sub("acme-status", &[]), + .extension_trigger("acme-status", &[]), ) .expect_refusal() .await .variant::( - |e| matches!(e, LoadRefusal::UnknownEventKind { kind, .. } if kind == "acme-status"), + |e| matches!(e, LoadRefusal::UnknownTriggerKind { kind, .. } if kind == "acme-status"), ); } @@ -98,14 +98,14 @@ async fn boot_refuses_a_nonexistent_explicit_manifest_path() { }); } -/// A manifest without `[dependencies]` refuses before the subscription-kind +/// A manifest without `[dependencies]` refuses before the trigger-kind /// gate and before any compile. #[tokio::test] async fn boot_refuses_a_capsless_manifest_before_any_other_gate() { // Raw TOML: the textual absence of [dependencies] is the fixture. let module = "[component]\nname = \"example\"\n\n\ [venue]\nbody_version = 2\n\n\ - [[subscription]]\nkind = \"acme-status\"\n"; + [[trigger]]\non = \"acme-status\"\n"; BootScenario::new() .module(module.to_owned()) .expect_refusal() @@ -115,7 +115,7 @@ async fn boot_refuses_a_capsless_manifest_before_any_other_gate() { }) // Operator wording pin. .names("empty one grants nothing") - .lacks("unknown event kind") + .lacks("unknown trigger kind") .lacks("no wired extension claims") .lacks("compile"); } @@ -151,7 +151,7 @@ async fn boot_denies_an_undeclared_chain_import_for_balance_tracker() { TestManifest::new("balance-tracker") .cap("logging") .cap("local-store") - .block_sub(1), + .block_trigger(1), ) .expect_refusal() .await @@ -181,20 +181,20 @@ async fn boot_refuses_a_capability_the_policy_excludes() { .lacks("compile"); } -/// Chain events arrive through `on_event` rather than an import, so a -/// permitted set that excludes `chain` refuses a chain subscription too. +/// Chain data arrives through `on_trigger` rather than an import, so a +/// permitted set that excludes `chain` refuses a chain trigger too. #[tokio::test] -async fn boot_refuses_a_chain_subscription_the_policy_excludes() { +async fn boot_refuses_a_chain_trigger_the_policy_excludes() { BootScenario::new() .policy(PolicySection { capabilities: Some(vec!["logging".to_owned()]), ..PolicySection::default() }) - .module(TestManifest::new("example").cap("logging").block_sub(1)) + .module(TestManifest::new("example").cap("logging").block_trigger(1)) .expect_refusal() .await .variant::(|e| { - matches!(e, LoadRefusal::ChainSubscriptionNotPermitted { id, permitted } + matches!(e, LoadRefusal::ChainTriggerNotPermitted { id, permitted } if id == "m0" && permitted == "logging") }) .lacks("compile"); diff --git a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs index ea145295..2f005d42 100644 --- a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs +++ b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs @@ -1,31 +1,31 @@ -//! The configured-chains gate and the chain-facing subscription surface. +//! The configured-chains gate and the chain-facing trigger surface. use super::*; #[tokio::test] -async fn empty_supervisor_returns_no_subscriptions() { +async fn empty_supervisor_returns_no_triggers() { let booted = BootScenario::over(mock_components()) .boot() .await .expect("an empty scenario boots"); - let plan = booted.supervisor.subscription_plan(); + let plan = booted.supervisor.trigger_plan(); assert!(plan.block_chains.is_empty()); - assert!(plan.chain_log_subs.is_empty()); + assert!(plan.event_triggers.is_empty()); assert_eq!(plan.viability(0), Viability::Nothing); assert_eq!(booted.supervisor.module_count(), 0); } -/// The refusal precedes compile; block and chain-log subscriptions hit the +/// The refusal precedes compile; block and event triggers hit the /// same gate with the same wording. #[tokio::test] -async fn boot_refuses_a_subscription_on_an_unconfigured_chain() { +async fn boot_refuses_a_trigger_on_an_unconfigured_chain() { for manifest in [ TestManifest::new("example") .cap("logging") - .block_sub(424_242), + .block_trigger(424_242), TestManifest::new("example") .cap("logging") - .chain_log_sub(424_242), + .event_trigger(424_242), ] { BootScenario::new() .module(manifest) @@ -36,7 +36,7 @@ async fn boot_refuses_a_subscription_on_an_unconfigured_chain() { if name == "example") }) // Operator wording pin. - .names("module example subscribes to chain 424242") + .names("module example declares a trigger on chain 424242") .names("[chains.424242]") .names("configured chains: 1, 100, 11155111") .lacks("compile"); @@ -45,11 +45,11 @@ async fn boot_refuses_a_subscription_on_an_unconfigured_chain() { /// The single-boot path reads the same configured-chains gate. #[tokio::test] -async fn boot_single_refuses_a_subscription_on_an_unconfigured_chain() { +async fn boot_single_refuses_a_trigger_on_an_unconfigured_chain() { let dir = tempfile::tempdir().expect("tempdir"); let manifest = TestManifest::new("gated") .cap("logging") - .block_sub(424_242) + .block_trigger(424_242) .write_to(dir.path()); let wasm = dir.path().join("missing.wasm"); @@ -64,7 +64,7 @@ async fn boot_single_refuses_a_subscription_on_an_unconfigured_chain() { if name == "gated") }) // Operator wording pin. - .names("module gated subscribes to chain 424242") + .names("module gated declares a trigger on chain 424242") .names("configured chains: 1, 100, 11155111") .lacks("compile"); } @@ -72,32 +72,32 @@ async fn boot_single_refuses_a_subscription_on_an_unconfigured_chain() { /// Filter values fail closed at manifest parse: an unparseable address or /// topic refuses the boot as a manifest error, before any compile. #[tokio::test] -async fn boot_refuses_an_invalid_chain_log_filter() { +async fn boot_refuses_an_invalid_event_filter() { fn is_address(e: &BootRefusal) -> bool { matches!( e, - BootRefusal::Manifest(ParseError::InvalidChainLogAddress { .. }) + BootRefusal::Manifest(ParseError::InvalidEventAddress { .. }) ) } fn is_topic(e: &BootRefusal) -> bool { matches!( e, - BootRefusal::Manifest(ParseError::InvalidChainLogTopic { .. }) + BootRefusal::Manifest(ParseError::InvalidEventTopic { .. }) ) } for (manifest, detail, variant) in [ ( TestManifest::new("example") .cap("logging") - .chain_log_sub_filtered(1, Some("0xabc"), None), + .event_trigger_filtered(1, Some("0xabc"), None), // Pinned operator wording. - "invalid chain-log address \"0xabc\"", + "invalid event address \"0xabc\"", is_address as fn(&BootRefusal) -> bool, ), ( TestManifest::new("example") .cap("logging") - .chain_log_sub_filtered(1, None, Some("not-a-topic")), + .event_trigger_filtered(1, None, Some("not-a-topic")), // Pinned operator wording. "invalid topic \"not-a-topic\"", is_topic as fn(&BootRefusal) -> bool, @@ -119,7 +119,7 @@ async fn boot_refuses_an_invalid_chain_log_filter() { /// The manifest carries typed filter values, so the collection-time filter /// build cannot fail. #[tokio::test] -async fn a_validated_chain_log_filter_survives_to_the_collected_subscription() { +async fn a_validated_event_filter_survives_to_the_collected_stream() { let Some(wasm) = example_wasm_or_skip() else { return; }; @@ -130,23 +130,21 @@ async fn a_validated_chain_log_filter_survives_to_the_collected_subscription() { .module( TestManifest::new("example") .cap("logging") - .chain_log_sub_filtered(1, Some(address), Some(topic)), + .event_trigger_filtered(1, Some(address), Some(topic)), ) .boot() .await .expect("the example boots alive"); - let subs = booted.supervisor.subscription_plan().chain_log_subs; - assert_eq!( - subs.len(), - 1, - "the alive module contributes its subscription" - ); - assert_eq!(subs[0].module.as_str(), "example"); - assert_eq!(subs[0].chain.id(), 1); - assert!(subs[0].cursor_key.is_none(), "resume defaults to off"); + let triggers = booted.supervisor.trigger_plan().event_triggers; + assert_eq!(triggers.len(), 1, "the alive module contributes its stream"); + assert_eq!(triggers[0].module.as_str(), "example"); + assert_eq!(triggers[0].chain.id(), 1); + assert!(triggers[0].cursor_key.is_none(), "resume defaults to off"); // alloy `Filter` exposes no getter; assert through its serialization. - let serialized = serde_json::to_value(&subs[0].filter).unwrap().to_string(); + let serialized = serde_json::to_value(&triggers[0].filter) + .unwrap() + .to_string(); assert!( serialized .to_lowercase() @@ -157,15 +155,15 @@ async fn a_validated_chain_log_filter_survives_to_the_collected_subscription() { } #[tokio::test] -async fn boot_admits_a_block_subscription_on_a_configured_chain_past_the_chain_gate() { +async fn boot_admits_a_block_trigger_on_a_configured_chain_past_the_chain_gate() { BootScenario::new() - .module(TestManifest::new("example").cap("logging").block_sub(1)) + .module(TestManifest::new("example").cap("logging").block_trigger(1)) .expect_refusal() .await .variant::(|e| e.kind() == std::io::ErrorKind::NotFound) // Operator wording pin. .names("read component") - .lacks("subscribes to chain"); + .lacks("declares a trigger on chain"); } #[tokio::test] @@ -178,7 +176,7 @@ async fn an_unconfigured_chain_refuses_boot_before_an_earlier_module_loads() { Entry::new( TestManifest::new("example") .cap("logging") - .block_sub(424_242), + .block_trigger(424_242), ) .wasm(second), ) @@ -191,7 +189,7 @@ async fn an_unconfigured_chain_refuses_boot_before_an_earlier_module_loads() { // Operator wording pin. .names("load module") .names("second.wasm") - .names("module example subscribes to chain 424242") + .names("module example declares a trigger on chain 424242") .names("[chains.424242]") .lacks("compile"); } @@ -203,7 +201,7 @@ async fn boot_refusal_names_the_missing_engine_toml_on_the_defaulted_path() { .module( TestManifest::new("example") .cap("logging") - .block_sub(424_242), + .block_trigger(424_242), ) .expect_refusal() .await diff --git a/crates/nexum-runtime/src/supervisor/tests/cursors.rs b/crates/nexum-runtime/src/supervisor/tests/cursors.rs index 47717f16..0d60e45f 100644 --- a/crates/nexum-runtime/src/supervisor/tests/cursors.rs +++ b/crates/nexum-runtime/src/supervisor/tests/cursors.rs @@ -1,4 +1,4 @@ -//! Chain-log filters, log projection, cursor keys and persistence. +//! Event trigger filters, log projection, cursor keys and persistence. use super::*; @@ -39,7 +39,7 @@ fn alloy_filter_no_address_no_topic() { /// A mined log carries every block-scoped field; the host projection must /// preserve each one so the guest rebuilds the native alloy log losslessly. #[test] -fn project_chain_log_preserves_mined_log() { +fn project_log_preserves_mined_log() { use alloy_primitives::{Address, B256, Bytes}; let address = Address::repeat_byte(0x11); @@ -58,8 +58,9 @@ fn project_chain_log_preserves_mined_log() { removed: true, }; - let projected = nexum::host::types::ChainLog::from(&log); + let projected = wit_log(&log, Chain::from_id(11_155_111)); + assert_eq!(projected.chain_id, 11_155_111); assert_eq!(projected.address, address.as_slice().to_vec()); assert_eq!( projected.topics, @@ -87,7 +88,7 @@ fn project_chain_log_preserves_mined_log() { /// A pending log has no block-scoped fields; the projection must leave each /// one `None` rather than collapsing an absent value onto a zero default. #[test] -fn project_chain_log_leaves_pending_fields_none() { +fn project_log_leaves_pending_fields_none() { use alloy_primitives::{Address, Bytes}; let inner = @@ -103,7 +104,7 @@ fn project_chain_log_leaves_pending_fields_none() { removed: false, }; - let projected = nexum::host::types::ChainLog::from(&log); + let projected = wit_log(&log, Chain::from_id(1)); assert!(projected.block_hash.is_none()); assert!(projected.block_number.is_none()); @@ -231,7 +232,7 @@ fn cursor_record_unseeded_writes_the_first_block() { } #[test] -fn cursor_record_is_per_subscription() { +fn cursor_record_is_per_module() { let mut cursors = ChainLogCursors::default(); assert_eq!( cursors.record("mod-a", "key", 100, false, || None), diff --git a/crates/nexum-runtime/src/supervisor/tests/dispatch.rs b/crates/nexum-runtime/src/supervisor/tests/dispatch.rs index 0bcf15a2..e0049582 100644 --- a/crates/nexum-runtime/src/supervisor/tests/dispatch.rs +++ b/crates/nexum-runtime/src/supervisor/tests/dispatch.rs @@ -274,8 +274,16 @@ async fn multi_chain_dispatch_isolates_modules_by_chain() { }; let mut booted = BootScenario::new() .wasm(wasm) - .module(TestManifest::new("module-a").cap("logging").block_sub(1)) - .module(TestManifest::new("module-b").cap("logging").block_sub(100)) + .module( + TestManifest::new("module-a") + .cap("logging") + .block_trigger(1), + ) + .module( + TestManifest::new("module-b") + .cap("logging") + .block_trigger(100), + ) .boot() .await .expect("boot"); @@ -285,13 +293,13 @@ async fn multi_chain_dispatch_isolates_modules_by_chain() { assert_eq!( booted.dispatch_block_on(1).await, 1, - "only module-a subscribed to chain 1", + "only module-a declares chain 1", ); assert_eq!(booted.supervisor.alive_count(), 2); assert_eq!( booted.dispatch_block_on(100).await, 1, - "only module-b subscribed to chain 100", + "only module-b declares chain 100", ); assert_eq!(booted.supervisor.alive_count(), 2); } @@ -306,8 +314,16 @@ async fn a_fired_stop_halts_the_block_fan_out() { }; let mut booted = BootScenario::new() .wasm(wasm) - .module(TestManifest::new("module-a").cap("logging").block_sub(1)) - .module(TestManifest::new("module-b").cap("logging").block_sub(1)) + .module( + TestManifest::new("module-a") + .cap("logging") + .block_trigger(1), + ) + .module( + TestManifest::new("module-b") + .cap("logging") + .block_trigger(1), + ) .boot() .await .expect("boot"); @@ -345,8 +361,8 @@ async fn dispatch_rate_limit_throttles_a_flood_without_starving_others() { }, ..Default::default() }) - .module(TestManifest::new("flood").cap("logging").block_sub(1)) - .module(TestManifest::new("calm").cap("logging").block_sub(100)) + .module(TestManifest::new("flood").cap("logging").block_trigger(1)) + .module(TestManifest::new("calm").cap("logging").block_trigger(100)) .boot() .await .expect("boot"); @@ -410,8 +426,12 @@ async fn multi_chain_poisoned_module_does_not_affect_other_chains() { .wasm(bomb_wasm), ) .module( - Entry::new(TestManifest::new("example").cap("logging").block_sub(100)) - .wasm(example_wasm), + Entry::new( + TestManifest::new("example") + .cap("logging") + .block_trigger(100), + ) + .wasm(example_wasm), ) .boot() .await diff --git a/crates/nexum-runtime/src/supervisor/tests/e2e.rs b/crates/nexum-runtime/src/supervisor/tests/e2e.rs index 2d507f59..b28165ef 100644 --- a/crates/nexum-runtime/src/supervisor/tests/e2e.rs +++ b/crates/nexum-runtime/src/supervisor/tests/e2e.rs @@ -53,13 +53,13 @@ fn e2e_example_component_imports_equal_declared_capabilities() { } #[tokio::test] -async fn e2e_block_subscription_dispatched() { +async fn e2e_block_trigger_dispatched() { let Some(wasm) = example_wasm_or_skip() else { return; }; let mut booted = BootScenario::new() .wasm(wasm) - .module(TestManifest::new("example").cap("logging").block_sub(1)) + .module(TestManifest::new("example").cap("logging").block_trigger(1)) .boot() .await .expect("boot"); @@ -67,7 +67,7 @@ async fn e2e_block_subscription_dispatched() { assert_eq!( booted.dispatch_block_on(1).await, 1, - "one module subscribed to chain 1 blocks", + "one module declares chain 1 blocks", ); assert_eq!( booted.supervisor.alive_count(), @@ -91,7 +91,7 @@ async fn e2e_manual_clock_override_boots_and_dispatches() { let dir = tempfile::tempdir().expect("tempdir"); let manifest = TestManifest::new("example") .cap("logging") - .block_sub(1) + .block_trigger(1) .write_to(dir.path()); let clock = ManualClock::new(); @@ -165,7 +165,7 @@ async fn e2e_http_probe_allowlisted_fetch_and_denied_path() { .cap("logging") .cap("http") .http_allow("127.0.0.1") - .block_sub(1) + .block_trigger(1) .config("probe_url", format!("{}/status", server.uri())) .config("denied_url", "http://denied.invalid/"), ) @@ -216,7 +216,7 @@ async fn e2e_policy_http_allow_narrows_the_author_list_through_boot() { .cap("http") .http_allow("127.0.0.1") .http_allow("operator-excluded.invalid") - .block_sub(1) + .block_trigger(1) .config("probe_url", format!("{}/status", server.uri())) // Denied at `admit` on the operator row, before any DNS // lookup; without the row the guest would see a transport @@ -280,7 +280,7 @@ async fn host_interface_records_are_retrievable_after_a_run() { .manifest_inline( TestManifest::new("example") .cap("logging") - .block_sub(1) + .block_trigger(1) .to_toml(), ) .launch() @@ -291,11 +291,11 @@ async fn host_interface_records_are_retrievable_after_a_run() { header.inner.number = 19_000_000; rt.push_block(header); - // The polled log read doubles as the dispatch barrier: the on_event line + // The polled log read doubles as the dispatch barrier: the on_trigger line // only lands once the event loop has dispatched the injected block. rt.wait_for_log("example", "block 19000000") .await - .expect("the on_event log line lands after dispatch"); + .expect("the on_trigger log line lands after dispatch"); let runs = rt.logs().list_runs("example"); assert_eq!(runs.len(), 1, "one run recorded for the example module"); @@ -313,7 +313,7 @@ async fn host_interface_records_are_retrievable_after_a_run() { page.records .iter() .any(|r| r.message.contains("block 19000000")), - "the on_event log line is retained", + "the on_trigger log line is retained", ); rt.shutdown(); diff --git a/crates/nexum-runtime/src/supervisor/tests/ledger.rs b/crates/nexum-runtime/src/supervisor/tests/ledger.rs index de2b9ddd..4420360c 100644 --- a/crates/nexum-runtime/src/supervisor/tests/ledger.rs +++ b/crates/nexum-runtime/src/supervisor/tests/ledger.rs @@ -40,13 +40,13 @@ fn extension_sections_must_be_claimed() { ); } -/// Two extensions colliding on a subscription kind or a manifest section +/// Two extensions colliding on a trigger kind or a manifest section /// are refused at boot; a non-colliding set passes the uniqueness pass. #[test] fn extension_claims_must_be_unique() { struct Claiming { namespace: &'static str, - subscriptions: &'static [&'static str], + kinds: &'static [&'static str], sections: &'static [&'static str], } impl Extension for Claiming { @@ -62,8 +62,8 @@ fn extension_claims_must_be_unique() { fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } - fn subscriptions(&self) -> &'static [&'static str] { - self.subscriptions + fn emits_trigger_kinds(&self) -> &'static [&'static str] { + self.kinds } fn manifest_sections(&self) -> &'static [&'static str] { self.sections @@ -71,12 +71,12 @@ fn extension_claims_must_be_unique() { } fn ext( namespace: &'static str, - subscriptions: &'static [&'static str], + kinds: &'static [&'static str], sections: &'static [&'static str], ) -> Arc> { Arc::new(Claiming { namespace, - subscriptions, + kinds, sections, }) } @@ -91,9 +91,9 @@ fn extension_claims_must_be_unique() { ext("a", &["orders"], &["venue"]), ext("b", &["orders"], &["pool"]), ]) - .expect_err("duplicate subscription kind"); + .expect_err("duplicate trigger kind"); assert!( - matches!(&err, LoadRefusal::SubscriptionKindClaimed { kind } if *kind == "orders"), + matches!(&err, LoadRefusal::TriggerKindClaimed { kind } if *kind == "orders"), "{err}" ); diff --git a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs index e7f6ca90..1ee8ea2a 100644 --- a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs +++ b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs @@ -24,25 +24,25 @@ fn price_alert(threshold: &str) -> TestManifest { let mut manifest = TestManifest::new("price-alert") .cap("logging") .cap("chain") - .block_sub(SEPOLIA); + .block_trigger(SEPOLIA); for (key, value) in price_alert_config(threshold) { manifest = manifest.config(key, value); } manifest } -/// Loaded but dead: no dispatch, no chain-facing subscription, and the -/// dropped subscriptions stay attributable. +/// Loaded but dead: no dispatch, no chain-facing stream, and the +/// dropped triggers stay attributable. #[tokio::test] -async fn init_failure_marks_module_dead_excluding_dispatch_and_subscriptions() { +async fn init_failure_marks_module_dead_excluding_dispatch_and_triggers() { let Some(wasm) = module_wasm_or_skip("price-alert") else { return; }; - // Both a block and a filtered chain-log subscription, so both filter + // Both a block and a filtered event trigger, so both filter // paths are exercised. let mut booted = BootScenario::new() .wasm(wasm) - .module(price_alert("not-a-number").chain_log_sub_filtered( + .module(price_alert("not-a-number").event_trigger_filtered( SEPOLIA, Some("0xbA3cB449bD2B4ADddBc894D8697F5170800EAdeC"), Some("0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9"), @@ -60,27 +60,27 @@ async fn init_failure_marks_module_dead_excluding_dispatch_and_subscriptions() { assert_eq!( booted.dispatch_block_on(SEPOLIA).await, 0, - "no live module is subscribed to chain 11155111 blocks", + "no live module declares chain 11155111 blocks", ); - let plan = booted.supervisor.subscription_plan(); + let plan = booted.supervisor.trigger_plan(); assert!( plan.block_chains.is_empty(), "dead module must not contribute block chains", ); assert!( - plan.chain_log_subs.is_empty(), - "dead module must not contribute chain-log subscriptions", + plan.event_triggers.is_empty(), + "dead module must not contribute chain-log streams", ); assert_eq!( plan.viability(0), - Viability::DeadHoldSubs, - "the filtered-out subscriptions must be attributed to the dead module", + Viability::DeadHoldTriggers, + "the filtered-out triggers must be attributed to the dead module", ); } -/// Positive control: the alive module's subscriptions survive the filter. +/// Positive control: the alive module's triggers survive the filter. #[tokio::test] -async fn alive_module_subscriptions_survive_alongside_dead_module() { +async fn alive_module_triggers_survive_alongside_dead_module() { let Some(price_alert_wasm) = module_wasm_or_skip("price-alert") else { return; }; @@ -90,7 +90,8 @@ async fn alive_module_subscriptions_survive_alongside_dead_module() { let booted = BootScenario::new() .module(Entry::new(price_alert("not-a-number")).wasm(price_alert_wasm)) .module( - Entry::new(TestManifest::new("example").cap("logging").block_sub(1)).wasm(example_wasm), + Entry::new(TestManifest::new("example").cap("logging").block_trigger(1)) + .wasm(example_wasm), ) .boot() .await @@ -102,7 +103,7 @@ async fn alive_module_subscriptions_survive_alongside_dead_module() { 1, "only the example is alive" ); - let plan = booted.supervisor.subscription_plan(); + let plan = booted.supervisor.trigger_plan(); assert_eq!( plan.block_chains.iter().map(|c| c.id()).collect::>(), vec![1], @@ -111,11 +112,11 @@ async fn alive_module_subscriptions_survive_alongside_dead_module() { assert_eq!( plan.viability(0), Viability::Live, - "one live subscription keeps the plan viable despite the dead module", + "one live trigger keeps the plan viable despite the dead module", ); } -/// Declares two subscription kinds and opens no event source for either. +/// Declares two trigger kinds and opens no source for either. struct Ticker; impl Extension for Ticker { @@ -131,13 +132,13 @@ impl Extension for Ticker { fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } - fn subscriptions(&self) -> &'static [&'static str] { + fn emits_trigger_kinds(&self) -> &'static [&'static str] { &["alarms", "ticks"] } } /// One health filter covers extension kinds too: a dead module's kind opens -/// no extension event source, while a live module's survives. +/// no extension source, while a live module's survives. #[tokio::test] async fn dead_module_extension_kind_is_excluded_from_the_plan() { let Some(price_alert_wasm) = module_wasm_or_skip("price-alert") else { @@ -149,14 +150,14 @@ async fn dead_module_extension_kind_is_excluded_from_the_plan() { let booted = BootScenario::new() .extensions([Arc::new(Ticker) as Arc>]) .module( - Entry::new(price_alert("not-a-number").extension_sub("alarms", &[])) + Entry::new(price_alert("not-a-number").extension_trigger("alarms", &[])) .wasm(price_alert_wasm), ) .module( Entry::new( TestManifest::new("example") .cap("logging") - .extension_sub("ticks", &[]), + .extension_trigger("ticks", &[]), ) .wasm(example_wasm), ) @@ -164,7 +165,7 @@ async fn dead_module_extension_kind_is_excluded_from_the_plan() { .await .expect("both modules load; only price-alert's init fails"); - let plan = booted.supervisor.subscription_plan(); + let plan = booted.supervisor.trigger_plan(); assert_eq!( plan.extension_kinds .iter() @@ -180,8 +181,8 @@ async fn dead_module_extension_kind_is_excluded_from_the_plan() { ); assert_eq!( plan.viability(0), - Viability::DeadHoldSubs, - "with no source opened, the dead module's subscriptions are the only ones left", + Viability::DeadHoldTriggers, + "with no source opened, the dead module's triggers are the only ones left", ); } @@ -198,7 +199,7 @@ async fn a_declared_extension_kind_alone_is_not_viable() { Entry::new( TestManifest::new("example") .cap("logging") - .extension_sub("ticks", &[]), + .extension_trigger("ticks", &[]), ) .wasm(example_wasm), ) @@ -206,7 +207,7 @@ async fn a_declared_extension_kind_alone_is_not_viable() { .await .expect("the example boots alive"); - let plan = booted.supervisor.subscription_plan(); + let plan = booted.supervisor.trigger_plan(); assert_eq!(plan.extension_kinds.len(), 1, "the live kind is declared"); assert_eq!( plan.viability(0), @@ -340,7 +341,8 @@ async fn resource_limit_dead_bomb_does_not_starve_healthy_module() { .wasm(bomb_wasm), ) .module( - Entry::new(TestManifest::new("example").cap("logging").block_sub(1)).wasm(example_wasm), + Entry::new(TestManifest::new("example").cap("logging").block_trigger(1)) + .wasm(example_wasm), ) .boot() .await @@ -378,7 +380,7 @@ async fn restart_flaky_module_recovers_after_backoff() { TestManifest::new("flaky-bomb") .cap("logging") .cap("local-store") - .block_sub(1) + .block_trigger(1) .config("fail_first_n", "1"), ) .boot() diff --git a/crates/nexum-runtime/src/supervisor/tests/mod.rs b/crates/nexum-runtime/src/supervisor/tests/mod.rs index f5ad8e8f..72598e75 100644 --- a/crates/nexum-runtime/src/supervisor/tests/mod.rs +++ b/crates/nexum-runtime/src/supervisor/tests/mod.rs @@ -25,7 +25,7 @@ use super::prepass::{ NamespaceLedger, claim_namespace, enforce_total_reservation, unconfigured_chain, }; use super::store::resolve_module_limits; -use super::subscriptions::build_alloy_filter; +use super::triggers::{build_alloy_filter, wit_log}; use super::*; use crate::bindings::nexum; use crate::digest::{ContentDigest, DigestMismatch}; diff --git a/crates/nexum-runtime/src/supervisor/subscriptions.rs b/crates/nexum-runtime/src/supervisor/triggers.rs similarity index 65% rename from crates/nexum-runtime/src/supervisor/subscriptions.rs rename to crates/nexum-runtime/src/supervisor/triggers.rs index b907fd4a..86267d65 100644 --- a/crates/nexum-runtime/src/supervisor/subscriptions.rs +++ b/crates/nexum-runtime/src/supervisor/triggers.rs @@ -1,4 +1,4 @@ -//! Project loaded modules' subscriptions into what the event loop opens; +//! Project loaded modules' triggers into what the event loop opens; //! dead modules are excluded so no stream opens for an unreachable module. use std::collections::BTreeSet; @@ -9,28 +9,28 @@ use super::Supervisor; use super::cursors::{chainlog_cursor_key, read_chain_log_cursor}; use crate::bindings::nexum; use crate::host::component::RuntimeTypes; -use crate::manifest::Subscription; +use crate::manifest::Trigger; use crate::module_id::ModuleId; impl Supervisor { /// One pass, one health filter: a dead module contributes to no field, /// so no stream of any kind opens for it. - pub fn subscription_plan(&self) -> SubscriptionPlan { + pub fn trigger_plan(&self) -> TriggerPlan { let mut block_chains: Vec = Vec::new(); - let mut chain_log_subs = Vec::new(); + let mut event_triggers = Vec::new(); let mut extension_kinds = BTreeSet::new(); - let mut dead_subscribers = false; + let mut dead_hold_triggers = false; for module in &self.modules { if !module.health.dispatchable() { - dead_subscribers |= !module.subscriptions.is_empty(); + dead_hold_triggers |= !module.triggers.is_empty(); continue; } - for sub in &module.subscriptions { - match sub { - Subscription::Block { chain_id } => { + for trigger in &module.triggers { + match trigger { + Trigger::Block { chain_id } => { block_chains.push(Chain::from_id(*chain_id)); } - Subscription::ChainLog { + Trigger::Event { chain_id, address, event_signature, @@ -39,7 +39,7 @@ impl Supervisor { } => { let filter = build_alloy_filter(*address, *event_signature); let chain = Chain::from_id(*chain_id); - // A `resume` subscription reads its durable cursor + // A `resume` trigger reads its durable cursor // once here at boot; others start at head. let (cursor_key, initial_cursor) = if *resume { let key = chainlog_cursor_key(chain, *address, *event_signature); @@ -52,7 +52,7 @@ impl Supervisor { } else { (None, None) }; - chain_log_subs.push(ChainLogSub { + event_triggers.push(EventTrigger { module: module.name.clone(), chain, filter, @@ -61,47 +61,47 @@ impl Supervisor { max_lookback: *max_lookback, }); } - Subscription::Extension { kind, .. } => { - extension_kinds.insert(kind.clone()); + Trigger::Extension { extension_kind, .. } => { + extension_kinds.insert(extension_kind.clone()); } - Subscription::Cron { .. } => {} + Trigger::Schedule { .. } => {} } } } block_chains.sort_by_key(|c| c.id()); block_chains.dedup(); - SubscriptionPlan { + TriggerPlan { block_chains, - chain_log_subs, + event_triggers, extension_kinds, - dead_subscribers, + dead_hold_triggers, } } } /// Everything the launch path opens, projected once from the live modules. -pub struct SubscriptionPlan { +pub struct TriggerPlan { /// Sorted by numeric id and deduped. pub block_chains: Vec, /// The stream tags every log with the owning module for routing. - pub chain_log_subs: Vec, - /// An extension opens an event source only for kinds appearing here. + pub event_triggers: Vec, + /// An extension opens a source only for kinds appearing here. pub extension_kinds: BTreeSet, - /// A dead module declares at least one subscription. - pub dead_subscribers: bool, + /// A dead module declares at least one trigger. + pub dead_hold_triggers: bool, } -impl SubscriptionPlan { +impl TriggerPlan { /// A declared extension kind is not yet a source: the extension gates on /// its own service state, so the caller passes how many really opened. pub fn viability(&self, open_extension_sources: usize) -> Viability { if !self.block_chains.is_empty() - || !self.chain_log_subs.is_empty() + || !self.event_triggers.is_empty() || open_extension_sources > 0 { Viability::Live - } else if self.dead_subscribers { - Viability::DeadHoldSubs + } else if self.dead_hold_triggers { + Viability::DeadHoldTriggers } else { Viability::Nothing } @@ -111,16 +111,16 @@ impl SubscriptionPlan { /// The launch verdict; boot-dead is permanent, so it is final at launch. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Viability { - /// No module declares a subscription; the engine has nothing to run. + /// No module declares a trigger; the engine has nothing to run. Nothing, - /// Every declared subscription belongs to a dead module. - DeadHoldSubs, - /// At least one event source drives the engine. + /// Every declared trigger belongs to a dead module. + DeadHoldTriggers, + /// At least one open source drives the engine. Live, } /// One module's declared interest in a chain's logs, resolved at boot. -pub struct ChainLogSub { +pub struct EventTrigger { /// Also the module's store namespace. pub module: ModuleId, /// Chain the filter runs against; it must have an `engine.toml` entry. @@ -136,21 +136,20 @@ pub struct ChainLogSub { pub max_lookback: Option, } -impl From<&alloy_rpc_types_eth::Log> for nexum::host::types::ChainLog { - /// The chain id is not on the alloy log; the batch level supplies it. - fn from(log: &alloy_rpc_types_eth::Log) -> Self { - Self { - address: log.address().as_slice().to_vec(), - topics: log.topics().iter().map(|t| t.as_slice().to_vec()).collect(), - data: log.inner.data.data.to_vec(), - block_hash: log.block_hash.map(|h| h.as_slice().to_vec()), - block_number: log.block_number, - block_timestamp: log.block_timestamp, - transaction_hash: log.transaction_hash.map(|h| h.as_slice().to_vec()), - transaction_index: log.transaction_index, - log_index: log.log_index, - removed: log.removed, - } +/// The chain id is not on the alloy log; the batch level supplies it. +pub(super) fn wit_log(log: &alloy_rpc_types_eth::Log, chain: Chain) -> nexum::host::types::Log { + nexum::host::types::Log { + chain_id: chain.id(), + address: log.address().as_slice().to_vec(), + topics: log.topics().iter().map(|t| t.as_slice().to_vec()).collect(), + data: log.inner.data.data.to_vec(), + block_hash: log.block_hash.map(|h| h.as_slice().to_vec()), + block_number: log.block_number, + block_timestamp: log.block_timestamp, + transaction_hash: log.transaction_hash.map(|h| h.as_slice().to_vec()), + transaction_index: log.transaction_index, + log_index: log.log_index, + removed: log.removed, } } diff --git a/crates/nexum-runtime/src/test_utils/harness.rs b/crates/nexum-runtime/src/test_utils/harness.rs index a2fae9af..9d78ac57 100644 --- a/crates/nexum-runtime/src/test_utils/harness.rs +++ b/crates/nexum-runtime/src/test_utils/harness.rs @@ -204,12 +204,12 @@ impl TestRuntime { self.handle.logs() } - /// Deliver a block header to the module's open block subscription. + /// Deliver a block header to the module's open block stream. pub fn push_block(&self, header: Header) { self.chain.push_block(header); } - /// Deliver a log to the module's open chain-log subscription. + /// Deliver a log to the module's open chain-log stream. pub fn push_chain_log(&self, log: Log) { self.chain.push_chain_log(log); } @@ -265,7 +265,10 @@ mod tests { use crate::test_utils::{TestManifest, example_wasm_or_skip, manifest, module_wasm_or_skip}; fn example_block_manifest() -> String { - manifest("example").cap("logging").block_sub(1).to_toml() + manifest("example") + .cap("logging") + .block_trigger(1) + .to_toml() } /// A block manifest plus a `[component].digest` pin of the wasm's bytes. @@ -276,14 +279,14 @@ mod tests { manifest(name) .cap("logging") .component_digest(digest.to_string()) - .block_sub(chain_id) + .block_trigger(chain_id) .to_toml() } fn price_alert_manifest() -> String { manifest("price-alert") .require(["logging", "chain"]) - .block_sub(1) + .block_trigger(1) .config( "oracle_address", "0x694AA1769357215DE4FAC081bf1f309aDC325306", @@ -319,7 +322,7 @@ mod tests { let record = rt .wait_for_log("example", "block 19000000") .await - .expect("the on_event log line lands after dispatch"); + .expect("the on_trigger log line lands after dispatch"); assert_eq!( record.source, crate::host::logs::LogSource::HostInterface, @@ -352,10 +355,10 @@ mod tests { rt.wait().await.expect("clean shutdown"); } - /// End-to-end on the chain-log leg: launch with a `chain-log` - /// subscription, inject a log, and read the module's log line back. + /// End-to-end on the event leg: launch with an `event` + /// trigger, inject a log, and read the module's log line back. #[tokio::test] - async fn harness_dispatches_chain_logs() { + async fn harness_dispatches_events() { let Some(wasm) = example_wasm_or_skip() else { return; }; @@ -364,17 +367,17 @@ mod tests { .manifest_inline( TestManifest::new("example") .cap("logging") - .chain_log_sub(1) + .event_trigger(1) .to_toml(), ) .launch() .await - .expect("launch example on the chain-log leg"); + .expect("launch example on the event leg"); rt.push_chain_log(Log::default()); - rt.wait_for_log("example", "received 1 chain-log entries") + rt.wait_for_log("example", "event with 0 topics on chain 1") .await - .expect("the chain-log line lands after dispatch"); + .expect("the event line lands after dispatch"); rt.shutdown(); rt.wait().await.expect("clean shutdown"); @@ -459,7 +462,7 @@ mod tests { rt.push_block(header_numbered(19_000_000)); rt.wait_for_log("example", "block 19000000") .await - .expect("the on_event log line lands after dispatch"); + .expect("the on_trigger log line lands after dispatch"); let runs = rt.logs().list_runs("example"); assert_eq!(runs.len(), 1, "one run recorded"); @@ -477,7 +480,7 @@ mod tests { /// End to end on the chain-request leg: program the mock `eth_call`, /// launch price-alert, inject a block, and read its alert line back; the - /// programmed answer is above threshold, so the module logs TRIGGERED. + /// programmed answer is above threshold, so the module logs the alert. #[tokio::test] async fn harness_serves_chain_requests_to_the_module() { use crate::host::component::ChainMethod; @@ -511,7 +514,7 @@ mod tests { .expect("launch price-alert over the harness"); rt.push_block(header_numbered(19_000_000)); - rt.wait_for_log("price-alert", "TRIGGERED") + rt.wait_for_log("price-alert", "THRESHOLD CROSSED") .await .expect("the alert line lands after the oracle read"); @@ -530,10 +533,10 @@ mod tests { rt.wait().await.expect("clean shutdown"); } - /// Both block and chain-log events dispatch in one session: the `biased` + /// Both block and event triggers dispatch in one session: the `biased` /// select in `run()` delivers both kinds without starvation. #[tokio::test] - async fn harness_delivers_block_and_chain_log_events_without_starvation() { + async fn harness_delivers_block_and_event_triggers_without_starvation() { let Some(wasm) = example_wasm_or_skip() else { return; }; @@ -542,13 +545,13 @@ mod tests { .manifest_inline( TestManifest::new("example") .cap("logging") - .block_sub(1) - .chain_log_sub(1) + .block_trigger(1) + .event_trigger(1) .to_toml(), ) .launch() .await - .expect("launch example subscribed to both blocks and chain-logs"); + .expect("launch example declaring both blocks and events"); // Both events are queued before either is awaited, so the biased // select genuinely arbitrates between two ready streams: a @@ -563,9 +566,9 @@ mod tests { rt.wait_for_log("example", "block 42 on chain") .await .expect("block event dispatched"); - rt.wait_for_log("example", "received 1 chain-log entries") + rt.wait_for_log("example", "event with 0 topics on chain 1") .await - .expect("chain-log event dispatched, neither event kind starved the other"); + .expect("event dispatched, neither trigger kind starved the other"); rt.shutdown(); rt.wait().await.expect("clean shutdown"); @@ -710,16 +713,16 @@ mod tests { record.message, ); - // The module never saw the oracle answer, so it must not trigger. + // The module never saw the oracle answer, so it must not alert. let runs = rt.logs().list_runs("price-alert"); - let triggered = runs.into_iter().any(|meta| { + let alerted = runs.into_iter().any(|meta| { rt.logs() .read(&meta.run, 0) .records .iter() - .any(|r| r.message.contains("TRIGGERED")) + .any(|r| r.message.contains("THRESHOLD CROSSED")) }); - assert!(!triggered, "an over-cap response must never reach classify"); + assert!(!alerted, "an over-cap response must never reach classify"); rt.shutdown(); rt.wait().await.expect("clean shutdown"); @@ -771,7 +774,7 @@ mod tests { let builder = TestRuntime::builder(wasm).manifest_inline( TestManifest::new("clock-reader") .cap("logging") - .block_sub(1) + .block_trigger(1) .to_toml(), ); builder @@ -838,7 +841,7 @@ mod tests { .manifest_inline( TestManifest::new("env-reader") .cap("logging") - .block_sub(1) + .block_trigger(1) .to_toml(), ) .launch() @@ -908,7 +911,7 @@ mod tests { format!("{v:064x}") } // latestRoundData() with answer = 3000 * 10^8, above the manifest's - // 2500.00 threshold, so the module logs TRIGGERED. + // 2500.00 threshold, so the module logs the alert. let result = format!( "\"0x{}{}{}{}{}\"", word(1), @@ -933,7 +936,7 @@ mod tests { booted .records("price-alert") .iter() - .any(|record| record.message.contains("TRIGGERED")), + .any(|record| record.message.contains("THRESHOLD CROSSED")), "the programmed oracle answer reached the module", ); assert!( diff --git a/crates/nexum-runtime/src/test_utils/manifest.rs b/crates/nexum-runtime/src/test_utils/manifest.rs index 37aa8dda..34a9bc0f 100644 --- a/crates/nexum-runtime/src/test_utils/manifest.rs +++ b/crates/nexum-runtime/src/test_utils/manifest.rs @@ -49,7 +49,7 @@ pub struct TestManifest { caps: Vec, http_allow: Vec, config: Vec<(String, String)>, - subscriptions: Vec, + triggers: Vec, } impl TestManifest { @@ -61,7 +61,7 @@ impl TestManifest { caps: Vec::new(), http_allow: Vec::new(), config: Vec::new(), - subscriptions: Vec::new(), + triggers: Vec::new(), } } @@ -89,45 +89,45 @@ impl TestManifest { self } - /// Add a `[[subscription]]` on new blocks for one chain. - pub fn block_sub(mut self, chain_id: u64) -> Self { - self.subscriptions.push(subscription("block", chain_id)); + /// Add a `[[trigger]]` on new blocks for one chain. + pub fn block_trigger(mut self, chain_id: u64) -> Self { + self.triggers.push(trigger("block", chain_id)); self } - /// Append an unfiltered `chain-log` subscription on `chain_id`. - pub fn chain_log_sub(mut self, chain_id: u64) -> Self { - self.subscriptions.push(subscription("chain-log", chain_id)); + /// Append an unfiltered `event` trigger on `chain_id`. + pub fn event_trigger(mut self, chain_id: u64) -> Self { + self.triggers.push(trigger("event", chain_id)); self } - /// Append a filtered `chain-log` subscription; an omitted filter key is absent + /// Append a filtered `event` trigger; an omitted filter key is absent /// from the emitted table, never empty. - pub fn chain_log_sub_filtered( + pub fn event_trigger_filtered( mut self, chain_id: u64, address: Option<&str>, event_signature: Option<&str>, ) -> Self { - let mut sub = subscription("chain-log", chain_id); + let mut table = trigger("event", chain_id); if let Some(address) = address { - sub.insert("address".into(), address.into()); + table.insert("address".into(), address.into()); } if let Some(signature) = event_signature { - sub.insert("event_signature".into(), signature.into()); + table.insert("event_signature".into(), signature.into()); } - self.subscriptions.push(sub); + self.triggers.push(table); self } - /// Append an extension subscription; no filters admits every event of the kind. - pub fn extension_sub(mut self, kind: &str, filters: &[(&str, &str)]) -> Self { - let mut sub = toml::Table::new(); - sub.insert("kind".into(), kind.into()); + /// Append an extension trigger; no filters admits every delivery of the kind. + pub fn extension_trigger(mut self, kind: &str, filters: &[(&str, &str)]) -> Self { + let mut table = toml::Table::new(); + table.insert("on".into(), kind.into()); for (key, value) in filters { - sub.insert((*key).into(), (*value).into()); + table.insert((*key).into(), (*value).into()); } - self.subscriptions.push(sub); + self.triggers.push(table); self } @@ -172,13 +172,10 @@ impl TestManifest { .collect(); root.insert("config".into(), config.into()); } - if !self.subscriptions.is_empty() { - let subs: Vec = self - .subscriptions - .iter() - .map(|s| s.clone().into()) - .collect(); - root.insert("subscription".into(), subs.into()); + if !self.triggers.is_empty() { + let triggers: Vec = + self.triggers.iter().map(|s| s.clone().into()).collect(); + root.insert("trigger".into(), triggers.into()); } toml::to_string(&root).expect("serialize the test manifest") } @@ -196,18 +193,18 @@ impl TestManifest { } } -fn subscription(kind: &str, chain_id: u64) -> toml::Table { - let mut sub = toml::Table::new(); - sub.insert("kind".into(), kind.into()); +fn trigger(on: &str, chain_id: u64) -> toml::Table { + let mut table = toml::Table::new(); + table.insert("on".into(), on.into()); let chain_id = i64::try_from(chain_id).expect("chain id fits a TOML integer"); - sub.insert("chain_id".into(), chain_id.into()); - sub + table.insert("chain_id".into(), chain_id.into()); + table } #[cfg(test)] mod tests { use super::*; - use crate::manifest::{CapabilityRegistry, Subscription, load}; + use crate::manifest::{CapabilityRegistry, Trigger, load}; /// Load through the real write-then-parse path with the core registry. fn load_core(manifest: &TestManifest) -> crate::manifest::LoadedManifest { @@ -221,13 +218,13 @@ mod tests { } #[test] - fn emitted_manifest_loads_with_name_caps_and_subscriptions() { + fn emitted_manifest_loads_with_name_caps_and_triggers() { let loaded = load_core( &TestManifest::new("example") .cap("logging") .cap("chain") - .block_sub(1) - .chain_log_sub(11_155_111), + .block_trigger(1) + .event_trigger(11_155_111), ); assert_eq!(loaded.name.as_str(), "example"); @@ -240,12 +237,12 @@ mod tests { ["chain", "logging"], ); - let subs = &loaded.subscriptions; - assert_eq!(subs.len(), 2, "both subscriptions parsed: {subs:?}"); - assert!(matches!(subs[0], Subscription::Block { chain_id: 1 })); + let triggers = &loaded.triggers; + assert_eq!(triggers.len(), 2, "both triggers parsed: {triggers:?}"); + assert!(matches!(triggers[0], Trigger::Block { chain_id: 1 })); assert!(matches!( - subs[1], - Subscription::ChainLog { + triggers[1], + Trigger::Event { chain_id: 11_155_111, address: None, event_signature: None, @@ -317,7 +314,7 @@ mod tests { } #[test] - fn chain_log_filters_and_extension_kinds_reach_the_loaded_subscriptions() { + fn event_filters_and_extension_kinds_reach_the_loaded_triggers() { const ADDRESS: &str = "0xbA3cB449bD2B4ADddBc894D8697F5170800EAdeC"; const TOPIC: &str = "0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9"; let address: alloy_primitives::Address = ADDRESS.parse().unwrap(); @@ -326,38 +323,39 @@ mod tests { let loaded = load_core( &TestManifest::new("example") .cap("logging") - .chain_log_sub_filtered(1, Some(ADDRESS), Some(TOPIC)) - .chain_log_sub_filtered(2, Some(ADDRESS), None) - .extension_sub("acme-status", &[]) - .extension_sub("acme-status", &[("scope", "primary")]), + .event_trigger_filtered(1, Some(ADDRESS), Some(TOPIC)) + .event_trigger_filtered(2, Some(ADDRESS), None) + .extension_trigger("acme-status", &[]) + .extension_trigger("acme-status", &[("scope", "primary")]), ); - let subs = &loaded.subscriptions; + let triggers = &loaded.triggers; assert!( matches!( - &subs[0], - Subscription::ChainLog { chain_id: 1, address: Some(a), event_signature: Some(t), .. } + &triggers[0], + Trigger::Event { chain_id: 1, address: Some(a), event_signature: Some(t), .. } if *a == address && *t == topic ), - "both filters land: {subs:?}", + "both filters land: {triggers:?}", ); assert!( matches!( - &subs[1], - Subscription::ChainLog { chain_id: 2, address: Some(a), event_signature: None, .. } + &triggers[1], + Trigger::Event { chain_id: 2, address: Some(a), event_signature: None, .. } if *a == address ), - "an omitted topic stays unfiltered: {subs:?}", + "an omitted topic stays unfiltered: {triggers:?}", ); assert!( - matches!(&subs[2], Subscription::Extension { kind, filters } - if kind == "acme-status" && filters.is_empty()), - "an unknown kind parses as an extension subscription: {subs:?}", + matches!(&triggers[2], Trigger::Extension { extension_kind, filters } + if extension_kind == "acme-status" && filters.is_empty()), + "an unknown kind parses as an extension trigger: {triggers:?}", ); assert!( - matches!(&subs[3], Subscription::Extension { kind, filters } - if kind == "acme-status" && filters.get("scope").is_some_and(|v| v == "primary")), - "attribute filters ride the same table: {subs:?}", + matches!(&triggers[3], Trigger::Extension { extension_kind, filters } + if extension_kind == "acme-status" + && filters.get("scope").is_some_and(|v| v == "primary")), + "attribute filters ride the same table: {triggers:?}", ); } @@ -367,14 +365,14 @@ mod tests { fn manifest_and_require_are_sugar_over_new_and_cap() { let sugar = manifest("example") .require(["logging", "chain"]) - .block_sub(1) - .chain_log_sub(100) + .block_trigger(1) + .event_trigger(100) .to_toml(); let explicit = TestManifest::new("example") .cap("logging") .cap("chain") - .block_sub(1) - .chain_log_sub(100) + .block_trigger(1) + .event_trigger(100) .to_toml(); assert_eq!(sugar, explicit); } @@ -388,8 +386,8 @@ mod tests { .cap("http") .http_allow("127.0.0.1") .config("threshold", "2500.00") - .block_sub(1) - .chain_log_sub_filtered(11_155_111, Some("0xabc"), None) + .block_trigger(1) + .event_trigger_filtered(11_155_111, Some("0xabc"), None) .to_toml(); let golden = r#"[component] name = "golden" @@ -402,14 +400,14 @@ hosts = ["127.0.0.1"] [dependencies.logging] -[[subscription]] +[[trigger]] chain_id = 1 -kind = "block" +on = "block" -[[subscription]] +[[trigger]] address = "0xabc" chain_id = 11155111 -kind = "chain-log" +on = "event" "#; assert_eq!(toml, golden); } @@ -440,11 +438,11 @@ kind = "chain-log" let dir = tempfile::tempdir().expect("tempdir"); let a = TestManifest::new("module-a") .cap("logging") - .block_sub(1) + .block_trigger(1) .write_as(&dir.path().join("a.toml")); let b = TestManifest::new("module-b") .cap("logging") - .block_sub(100) + .block_trigger(100) .write_as(&dir.path().join("b.toml")); assert_eq!(load_path(&a).name.as_str(), "module-a"); diff --git a/crates/nexum-runtime/src/test_utils/scenario.rs b/crates/nexum-runtime/src/test_utils/scenario.rs index 76170e8b..de43fed2 100644 --- a/crates/nexum-runtime/src/test_utils/scenario.rs +++ b/crates/nexum-runtime/src/test_utils/scenario.rs @@ -479,7 +479,7 @@ mod tests { }; let mut booted = BootScenario::new() .wasm(&wasm) - .module(TestManifest::new("example").cap("logging").block_sub(1)) + .module(TestManifest::new("example").cap("logging").block_trigger(1)) .boot() .await .expect("scenario boot"); @@ -500,12 +500,12 @@ mod tests { }; let mut booted = BootScenario::new() .wasm(&example) - .module(TestManifest::new("example").cap("logging").block_sub(1)) + .module(TestManifest::new("example").cap("logging").block_trigger(1)) .module( Entry::new( TestManifest::new("clock-reader") .cap("logging") - .block_sub(1), + .block_trigger(1), ) .wasm(&reader), ) @@ -550,7 +550,7 @@ mod tests { .module( TestManifest::new("clock-reader") .cap("logging") - .block_sub(1), + .block_trigger(1), ) .extensions([capture]) .clock(clock.as_override()) diff --git a/crates/nexum-sdk/Cargo.toml b/crates/nexum-sdk/Cargo.toml index 8008e612..33fe2ed7 100644 --- a/crates/nexum-sdk/Cargo.toml +++ b/crates/nexum-sdk/Cargo.toml @@ -31,8 +31,8 @@ nexum-world = { path = "../nexum-world" } alloy-primitives.workspace = true # Typed EIP-155 chain id; already in the guest graph via alloy-provider. alloy-chains.workspace = true -# The `Log` type modules receive for chain-log events is alloy's own RPC log, -# assembled from the WIT record at the binding edge (see `events`). +# The `Log` type modules receive for event triggers is alloy's own RPC log, +# assembled from the WIT record at the binding edge (see `sol_events`). alloy-rpc-types-eth.workspace = true alloy-sol-types.workspace = true # The `store` helpers' value codec (`TypedCell`, `TypedMap`, `Counter`). diff --git a/crates/nexum-sdk/src/config.rs b/crates/nexum-sdk/src/config.rs index 5ae4c083..dad8571a 100644 --- a/crates/nexum-sdk/src/config.rs +++ b/crates/nexum-sdk/src/config.rs @@ -1,5 +1,5 @@ //! Helpers over the `Vec<(String, String)>` `[config]` entries a -//! module's `on_event` receives: required and optional key lookup, and +//! module's `init` receives: required and optional key lookup, and //! fixed-point decimal parsing. use alloy_primitives::{I256, U256}; @@ -161,14 +161,14 @@ mod tests { #[test] fn scale_decimal_pads_short_fractional() { // "2500.00" with 8 decimals -> 2500 * 1e8 = 250_000_000_000 - let v = scale_decimal("2500.00", 8, "trigger").unwrap(); + let v = scale_decimal("2500.00", 8, "threshold").unwrap(); assert_eq!(v, I256::try_from(250_000_000_000_i128).unwrap()); } #[test] fn scale_decimal_truncates_long_fractional() { // "1.123456789" with 4 decimals -> "11234" - let v = scale_decimal("1.123456789", 4, "trigger").unwrap(); + let v = scale_decimal("1.123456789", 4, "threshold").unwrap(); assert_eq!(v, I256::try_from(11234_i128).unwrap()); } diff --git a/crates/nexum-sdk/src/lib.rs b/crates/nexum-sdk/src/lib.rs index 6170690e..087479dd 100644 --- a/crates/nexum-sdk/src/lib.rs +++ b/crates/nexum-sdk/src/lib.rs @@ -12,7 +12,7 @@ //! - [`host`] - the [`Host`](host::Host) seam over the core host interfaces, plus the [`Fault`](host::Fault) vocabulary. //! - [`keeper`] - keeper stores ([`CommitmentSet`](keeper::CommitmentSet), [`Gates`](keeper::Gates), [`Journal`](keeper::Journal)), the [`Poller`](keeper::Poller) seam, and the [`Retrier`](keeper::Retrier). //! - [`chain`] - typed chain access and the alloy provider seam. -//! - [`events`] - chain-log delivery. +//! - [`sol_events`] - event delivery. //! - [`store`] - typed local-store helpers ([`WriteBatch`](store::WriteBatch), [`TypedCell`](store::TypedCell), [`TypedMap`](store::TypedMap), [`Counter`](store::Counter)). //! - [`config`] - config-table lookups and decimal scaling. //! - [`address`] - EVM address parsing. @@ -34,11 +34,11 @@ pub use alloy_sol_types as sol_types; pub mod address; pub mod chain; pub mod config; -pub mod events; pub mod host; pub mod http; pub mod keeper; pub mod prelude; +pub mod sol_events; pub mod store; pub mod tracing; pub mod wit_bindgen_macro; diff --git a/crates/nexum-sdk/src/events.rs b/crates/nexum-sdk/src/sol_events.rs similarity index 89% rename from crates/nexum-sdk/src/events.rs rename to crates/nexum-sdk/src/sol_events.rs index c9ecbf8f..c2ff329a 100644 --- a/crates/nexum-sdk/src/events.rs +++ b/crates/nexum-sdk/src/sol_events.rs @@ -1,13 +1,13 @@ -//! Chain-log delivery at the guest WIT edge. +//! Event delivery at the guest WIT edge. //! //! Modules receive on-chain logs as the native [`Log`] (alloy's //! `eth_getLogs` shape). The host packs each log into the WIT -//! `chain-log` record; [`ChainLogParts`] borrows its raw fields and +//! `log` record; [`LogParts`] borrows its raw fields and //! `From` rebuilds the alloy value. use alloy_primitives::{Address, B256, Bytes, Log as PrimitiveLog, LogData}; -/// The alloy RPC log delivered to modules for chain-log events. +/// The alloy RPC log delivered to modules for event triggers. pub use alloy_rpc_types_eth::Log; /// Const so the module macro's topic parity check fails the build, not the run. @@ -22,12 +22,14 @@ pub const fn contains_topic(needle: &B256, set: &[B256]) -> bool { false } -/// Borrowed raw fields of a WIT `chain-log` record, assembled into an +/// Borrowed raw fields of a WIT `log` record, assembled into an /// alloy [`Log`] via `From`. Fixed-width byte fields are left-padded /// into their EVM word (20 bytes for the address, 32 for topics and /// hashes). #[derive(Default)] -pub struct ChainLogParts<'a> { +pub struct LogParts<'a> { + /// Chain the log came from; not part of the alloy [`Log`]. + pub chain_id: u64, /// 20-byte contract address. pub address: &'a [u8], /// Indexed topics, each a 32-byte word. @@ -50,8 +52,8 @@ pub struct ChainLogParts<'a> { pub removed: bool, } -impl From> for Log { - fn from(p: ChainLogParts<'_>) -> Self { +impl From> for Log { + fn from(p: LogParts<'_>) -> Self { Log { inner: PrimitiveLog { address: Address::left_padding_from(p.address), @@ -100,7 +102,8 @@ mod tests { let addr = [0x11u8; 20]; let topic = [0x22u8; 32]; let hash = [0x33u8; 32]; - let log: Log = ChainLogParts { + let log: Log = LogParts { + chain_id: 1, address: &addr, topics: &[topic.to_vec()], data: &[1, 2, 3], @@ -126,7 +129,7 @@ mod tests { #[test] fn pending_log_leaves_block_fields_absent() { - let log: Log = ChainLogParts { + let log: Log = LogParts { address: &[0u8; 20], ..Default::default() } @@ -140,7 +143,7 @@ mod tests { #[test] fn undersized_word_is_left_padded() { - let log: Log = ChainLogParts { + let log: Log = LogParts { address: &[0u8; 20], topics: &[vec![0xab]], ..Default::default() diff --git a/crates/nexum-sdk/src/wit_bindgen_macro.rs b/crates/nexum-sdk/src/wit_bindgen_macro.rs index f099e13c..3cadd43f 100644 --- a/crates/nexum-sdk/src/wit_bindgen_macro.rs +++ b/crates/nexum-sdk/src/wit_bindgen_macro.rs @@ -83,11 +83,12 @@ macro_rules! bind_host_via_wit_bindgen { } } - /// Rebuild the native alloy log from the wit-bindgen `chain-log` - /// record; assembly lives in `nexum_sdk::events`. - impl ::core::convert::From for $crate::events::Log { - fn from(log: nexum::host::types::ChainLog) -> Self { - $crate::events::ChainLogParts { + /// Rebuild the native alloy log from the wit-bindgen `log` + /// record; assembly lives in `nexum_sdk::sol_events`. + impl ::core::convert::From for $crate::sol_events::Log { + fn from(log: nexum::host::types::Log) -> Self { + $crate::sol_events::LogParts { + chain_id: log.chain_id, address: &log.address, topics: &log.topics, data: &log.data, diff --git a/crates/nexum-sdk/tests/wit_bindgen_fault.rs b/crates/nexum-sdk/tests/wit_bindgen_fault.rs index 1a5292f6..79d9ce1c 100644 --- a/crates/nexum-sdk/tests/wit_bindgen_fault.rs +++ b/crates/nexum-sdk/tests/wit_bindgen_fault.rs @@ -24,7 +24,8 @@ mod nexum { pub retry_after_ms: Option, } - pub struct ChainLog { + pub struct Log { + pub chain_id: u64, pub address: Vec, pub topics: Vec>, pub data: Vec, @@ -109,8 +110,9 @@ fn base_block_emits_the_adapter_type() { } #[test] -fn chain_log_lift_assembles_the_alloy_log() { - let lifted: nexum_sdk::events::Log = wire::ChainLog { +fn log_lift_assembles_the_alloy_log() { + let lifted: nexum_sdk::sol_events::Log = wire::Log { + chain_id: 1, address: vec![0x11; 20], topics: vec![vec![0x22; 32]], data: vec![1, 2, 3], diff --git a/crates/nexum-world/Cargo.toml b/crates/nexum-world/Cargo.toml index 6c40f996..306157ed 100644 --- a/crates/nexum-world/Cargo.toml +++ b/crates/nexum-world/Cargo.toml @@ -16,7 +16,7 @@ workspace = true macros = ["dep:syn"] [dependencies] -# Typed `B256` topics for the chain-log extraction, so the macro-side +# Typed `B256` topics for the event topic extraction, so the macro-side # parity check parses the same values the runtime loads. alloy-primitives.workspace = true # Derives the closed capability / fault-label vocabularies: `VariantNames` diff --git a/crates/nexum-world/src/lib.rs b/crates/nexum-world/src/lib.rs index 88fd95f9..7b0bb007 100644 --- a/crates/nexum-world/src/lib.rs +++ b/crates/nexum-world/src/lib.rs @@ -100,20 +100,20 @@ pub const WASI_GATES: [&str; 1 + WasiCap::VARIANTS.len()] = { out }; -/// A core `[[subscription]] kind`. A kind with no variant here is +/// A core `[[trigger]] on` value. A kind with no variant here is /// extension-owned, so the set is the runtime's core/extension split. #[derive( Clone, Copy, Debug, Eq, PartialEq, Hash, Display, EnumString, IntoStaticStr, VariantNames, )] #[strum(serialize_all = "kebab-case")] #[non_exhaustive] -pub enum SubscriptionKind { - /// New-block events. +pub enum TriggerKind { + /// A new block on a chain. Block, - /// Chain-log events filtered by address and topic-0. - ChainLog, - /// Cron-scheduled ticks. - Cron, + /// A contract event's log matching the address and topic-0 filters. + Event, + /// A cron expression's time arriving. + Schedule, } /// A `nexum:host/types.fault` case as a stable snake_case label, in WIT @@ -362,13 +362,13 @@ pub enum WorldError { /// The dependency key. name: String, }, - /// `[[subscription]]` is not an array of tables. - #[error("[[subscription]] must be an array of tables")] - SubscriptionsNotAnArray, - /// A chain-log subscription's `event_signature` is not a string. - #[error("[[subscription]].event_signature must be a string")] + /// `[[trigger]]` is not an array of tables. + #[error("[[trigger]] must be an array of tables")] + TriggersNotAnArray, + /// An event trigger's `event_signature` is not a string. + #[error("[[trigger]].event_signature must be a string")] EventSignatureNotAString, - /// A chain-log `event_signature` that is not 32-byte hex. + /// An event trigger `event_signature` that is not 32-byte hex. // Pinned operator wording; mirrors the runtime's load-time refusal. #[error("invalid topic {topic:?}: {source}")] InvalidTopic { @@ -475,29 +475,27 @@ pub fn manifest_capabilities(text: &str) -> Result, WorldError> { Ok(table.keys().cloned().collect()) } -/// The distinct chain-log `event_signature` topics from the manifest +/// The distinct event trigger `event_signature` topics from the manifest /// text, in declaration order. Same hex grammar as the runtime's load. -pub fn manifest_chain_log_topics(text: &str) -> Result, WorldError> { +pub fn manifest_event_topics(text: &str) -> Result, WorldError> { let value: toml::Table = text.parse().map_err(|source| WorldError::NotToml { file: "component.toml", source, })?; - let Some(subscriptions) = value.get("subscription") else { + let Some(triggers) = value.get("trigger") else { return Ok(Vec::new()); }; - let subscriptions = subscriptions - .as_array() - .ok_or(WorldError::SubscriptionsNotAnArray)?; + let triggers = triggers.as_array().ok_or(WorldError::TriggersNotAnArray)?; let mut topics = Vec::new(); - for sub in subscriptions { - let kind = sub - .get("kind") + for trigger in triggers { + let kind = trigger + .get("on") .and_then(toml::Value::as_str) - .map(str::parse::); - if !matches!(kind, Some(Ok(SubscriptionKind::ChainLog))) { + .map(str::parse::); + if !matches!(kind, Some(Ok(TriggerKind::Event))) { continue; } - let Some(raw) = sub.get("event_signature") else { + let Some(raw) = trigger.get("event_signature") else { continue; }; let raw = raw.as_str().ok_or(WorldError::EventSignatureNotAString)?; @@ -613,7 +611,7 @@ pub fn synthesize( } let mut imports = String::new(); - // `nexum:host` is a leaf package (the `event` variant carries status + // `nexum:host` is a leaf package (the `trigger` variant carries status // transitions as opaque bytes), so the base resolve set // is the host package alone; capability declarations append their // own packages. Dependency order: each directory is parsed against @@ -650,12 +648,12 @@ pub fn synthesize( let mut wit = String::from( "package nexum:module-world;\n\nworld module {\n \ - use nexum:host/types@0.1.0.{config, event, fault};\n\n", + use nexum:host/types@0.1.0.{config, trigger, fault};\n\n", ); wit.push_str(&imports); wit.push_str( "\n export init: func(config: config) -> result<_, fault>;\n \ - export on-event: func(event: event) -> result<_, fault>;\n}\n", + export on-trigger: func(trigger: trigger) -> result<_, fault>;\n}\n", ); Ok(ModuleWorld { @@ -1108,12 +1106,9 @@ logging = "yes" let err = manifest_capabilities("dependencies = 7\n").unwrap_err(); assert!(matches!(err, WorldError::DependenciesNotATable)); assert_eq!(err.to_string(), "[dependencies] must be a table"); - let err = manifest_chain_log_topics("subscription = 7\n").unwrap_err(); - assert!(matches!(err, WorldError::SubscriptionsNotAnArray)); - assert_eq!( - err.to_string(), - "[[subscription]] must be an array of tables" - ); + let err = manifest_event_topics("trigger = 7\n").unwrap_err(); + assert!(matches!(err, WorldError::TriggersNotAnArray)); + assert_eq!(err.to_string(), "[[trigger]] must be an array of tables"); let err = manifest_extensions("extensions = 7\n").unwrap_err(); assert!(matches!(err, WorldError::ExtensionsNotATable)); assert_eq!( @@ -1145,31 +1140,33 @@ logging = "yes" /// Pinned manifest grammar; the runtime's serde renames derive from it. #[test] - fn subscription_kinds_spell_the_manifest_grammar() { - assert_eq!(SubscriptionKind::VARIANTS, ["block", "chain-log", "cron"]); - assert!("log".parse::().is_err()); + fn trigger_kinds_spell_the_manifest_grammar() { + assert_eq!(TriggerKind::VARIANTS, ["block", "event", "schedule"]); + assert!("log".parse::().is_err()); + assert!("chain-log".parse::().is_err()); + assert!("cron".parse::().is_err()); } #[test] - fn chain_log_topics_are_distinct_and_in_declaration_order() { - let topics = manifest_chain_log_topics( + fn event_topics_are_distinct_and_in_declaration_order() { + let topics = manifest_event_topics( r#" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 event_signature = "0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9" -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 100 event_signature = "CF5F9DE2984132265203B5C335B25727702CA77262FF622E136BAA7362BF1DA9" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 event_signature = "0x0000000000000000000000000000000000000000000000000000000000000001" "#, @@ -1187,24 +1184,24 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000000 } #[test] - fn chain_log_topics_skip_wildcard_and_foreign_subscriptions() { + fn event_topics_skip_wildcard_and_foreign_triggers() { let text = r#" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 -[[subscription]] -kind = "acme-status" +[[trigger]] +on = "acme-status" event_signature = "not-hex-but-not-ours" "#; - assert_eq!(manifest_chain_log_topics(text).unwrap(), Vec::::new()); - assert_eq!(manifest_chain_log_topics("").unwrap(), Vec::::new()); + assert_eq!(manifest_event_topics(text).unwrap(), Vec::::new()); + assert_eq!(manifest_event_topics("").unwrap(), Vec::::new()); } #[test] - fn chain_log_topic_refusal_pins_the_operator_wording() { - let err = manifest_chain_log_topics( - "[[subscription]]\nkind = \"chain-log\"\nchain_id = 1\n\ + fn event_topic_refusal_pins_the_operator_wording() { + let err = manifest_event_topics( + "[[trigger]]\non = \"event\"\nchain_id = 1\n\ event_signature = \"not-a-topic\"\n", ) .unwrap_err(); @@ -1217,15 +1214,15 @@ event_signature = "not-hex-but-not-ours" .starts_with("invalid topic \"not-a-topic\":"), "{err}" ); - let err = manifest_chain_log_topics( - "[[subscription]]\nkind = \"chain-log\"\nchain_id = 1\nevent_signature = 7\n", + let err = manifest_event_topics( + "[[trigger]]\non = \"event\"\nchain_id = 1\nevent_signature = 7\n", ) .unwrap_err(); assert!(matches!(err, WorldError::EventSignatureNotAString)); // Operator-facing wording, pinned verbatim. assert_eq!( err.to_string(), - "[[subscription]].event_signature must be a string" + "[[trigger]].event_signature must be a string" ); } @@ -1256,7 +1253,7 @@ event_signature = "not-hex-but-not-ours" assert!( world .wit - .contains("export on-event: func(event: event) -> result<_, fault>;") + .contains("export on-trigger: func(trigger: trigger) -> result<_, fault>;") ); } diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-triggers-packaging.md similarity index 76% rename from docs/02-modules-events-packaging.md rename to docs/02-modules-triggers-packaging.md index df20ec6b..bf4f7527 100644 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-triggers-packaging.md @@ -1,9 +1,9 @@ -# Component lifecycle, event system, and packaging +# Component lifecycle, trigger system, and packaging ## The component bundle -A component is distributed as a bundle: a WASM component plus a manifest that declares its identity, its event subscriptions, and the capabilities it depends on. -The manifest is the bridge between packaging, the event system, and the runtime lifecycle. +A component is distributed as a bundle: a WASM component plus a manifest that declares its identity, its triggers, and the capabilities it depends on. +The manifest is the bridge between packaging, the trigger system, and the runtime lifecycle. See [ADR-0016](adr/0016-component-vocabulary.md) and [ADR-0020](adr/0020-retire-component-kind.md). ### Manifest (`component.toml`) @@ -34,21 +34,21 @@ local-store = {} logging = {} http = { hosts = ["api.cow.fi"] } -# Event subscriptions: what the runtime feeds this component. -[[subscription]] -kind = "block" +# Triggers: what the runtime feeds this component. +[[trigger]] +on = "block" chain_id = 42161 -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 42161 address = "0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74" event_signature = "0x0000000000000000000000000000000000000000000000000000000000000000" resume = true -[[subscription]] -kind = "cron" -schedule = "*/5 * * * *" +[[trigger]] +on = "schedule" +cron = "*/5 * * * *" # Opaque config, handed to the guest as string pairs. [config] @@ -64,10 +64,10 @@ Key design points: The pin is optional: an absent pin loads with a warning, unless `require_component_digest = true` under `[engine]` in `engine.toml` makes it a boot error. The operator can pin the same artifact independently with `digest` on its `[[modules]]` entry in `engine.toml`, and the warning is silent when that pin covers the artifact, because the bytes are verified either way. A refusal names which of the two pins the bytes disagree with, so the operator knows which file to edit. -- **`[[subscription]]` blocks are declarative.** - A component does not set up its own subscriptions imperatively. - The runtime loads each component and runs its `init` first, then derives the subscription plan from the booted supervisor and opens the event sources. - `call_init` runs during load in `crates/nexum-runtime/src/supervisor/load.rs`, and `subscription_plan` reads the already-booted supervisor in `crates/nexum-runtime/src/supervisor/subscriptions.rs`. +- **`[[trigger]]` tables are declarative.** + A component does not open its own sources imperatively. + The runtime loads each component and runs its `init` first, then derives the plan from the booted supervisor and opens the sources. + `call_init` runs during load in `crates/nexum-runtime/src/supervisor/load.rs`, and `trigger_plan` reads the already-booted supervisor in `crates/nexum-runtime/src/supervisor/triggers.rs`. - **`[dependencies]` drives what the runtime links.** Each key names a host capability, and its table carries the attributes that qualify it. A component that declares `http` imports `wasi:http/outgoing-handler`, the SDK's `http::fetch` helper wraps it, and the host checks every outgoing request against the `hosts` list on the `http` dependency. @@ -75,9 +75,9 @@ Key design points: - **The `[dependencies]` table is mandatory.** A manifest with no table at all is refused; an empty table is valid and grants nothing. `hosts` qualifies the `http` dependency and nothing else, and it is refused anywhere else rather than silently dropped. -- **Chain ids are declared per subscription**, not in a top-level `[chains]` table. - Each `[[subscription]]` names its own `chain_id`. - If `engine.toml` carries no `[chains.]` entry for a chain a subscription names, the engine refuses the boot in the prepass, before any component is compiled. +- **Chain ids are declared per trigger**, not in a top-level `[chains]` table. + Each `[[trigger]]` names its own `chain_id`. + If `engine.toml` carries no `[chains.]` entry for a chain a trigger names, the engine refuses the boot in the prepass, before any component is compiled. - **`[config]` is opaque to the runtime.** The guest receives `list>`. The host flattens each TOML scalar to its text form on the way through, and renders an array or a table as its TOML representation. @@ -137,12 +137,12 @@ stateDiagram-v2 | Value | Meaning | |---|---| -| **Alive** | Dispatchable. The only value that receives events. | +| **Alive** | Dispatchable. The only value that receives triggers. | | **Backoff** | A trap or a deadline hit ended the run. The supervisor revives it when the backoff expires. | | **Dead** | The boot-time `init` returned a fault. Permanent: the supervisor never restarts it. | | **Poisoned** | Too many failures inside the poison window. Terminal for the process; only an operator restart clears it. | -Load itself is ordered: the prepass resolves every manifest, claims namespaces, and gates subscribed chains against `[chains]`; then modules load. +Load itself is ordered: the prepass resolves every manifest, claims namespaces, and gates triggered chains against `[chains]`; then modules load. The backoff schedule doubles from 1 s and caps at 300 s, jittered into the upper half of each step so modules that failed together do not retry together. The poison policy is 5 failures inside 600 s by default, configurable under `[limits.poison]`. @@ -167,22 +167,22 @@ A successful dispatch resets the module's failure count. - **Poison recovery is operator work.** The failure ring is in memory and clears at process start. -## Event system +## Trigger system ### Architecture ```mermaid flowchart TD - subgraph SRC["Event sources"] + subgraph SRC["Sources"] BS["Block streams (per chain)"] - LW["Chain-log streams (per subscription)"] + LW["Log streams (per event trigger)"] EX["Extension streams"] end subgraph NR["nexum-runtime"] SRC EL["Event loop\n(one select over every source)"] - SUP["Supervisor\n(matches the subscription plan)"] + SUP["Supervisor\n(matches the trigger plan)"] MA["Component A"] MB["Component B"] end @@ -195,26 +195,26 @@ flowchart TD SUP --> MB ``` -### Event sources +### Triggers and their sources -| Source | Trigger | Backed by | +| Trigger | Fires on | Source | |---|---|---| -| `block` | New block on a chain | `eth_subscribe("newHeads")` over an alloy provider, or polling on an HTTP URL | -| `chain-log` | Matching log emitted | An `eth_getLogs` block-range poller, alloy's `watch_canonical_logs_from`, reorg-aware and backfilling from the start block on open | -| `cron` | Not dispatched | Parsed and inert. The supervisor warns at load. | -| extension kinds | Whatever the extension opens | `Extension::events` | +| `block` | A new block on a chain | `eth_subscribe("newHeads")` over an alloy provider, or polling on an HTTP URL | +| `event` | A log matching the filters | An `eth_getLogs` block-range poller, alloy's `watch_canonical_logs_from`, reorg-aware and backfilling from the start block on open | +| `schedule` | Not dispatched | Parsed and inert. The supervisor warns at load. | +| extension kinds | Whatever the extension delivers | `Extension::open_sources` | Block streams are shared per chain. -If two components subscribe to blocks on chain 42161, the runtime opens one block subscription and fans it out to both. -A chain-log stream is opened per subscription and tagged with the owning component. +If two components declare block triggers on chain 42161, the runtime opens one block stream and fans it out to both. +A log stream is opened per event trigger and tagged with the owning component. Only a dispatchable component contributes to the plan, so no stream opens for a dead or poisoned one. ### Dispatch There is no router struct and no per-component inbox. -One `run` loop owns the supervisor, selects one event from the merged sources, and dispatches it before it selects again (`crates/nexum-runtime/src/runtime/event_loop.rs`). -For one event, the supervisor walks the matching components serially and awaits each in turn (`crates/nexum-runtime/src/supervisor/dispatch.rs`). +One `run` loop owns the supervisor, selects one trigger from the merged sources, and dispatches it before it selects again (`crates/nexum-runtime/src/runtime/event_loop.rs`). +For one trigger, the supervisor walks the matching components serially and awaits each in turn (`crates/nexum-runtime/src/supervisor/dispatch.rs`). - **Serial, not concurrent.** A slow component delays the whole engine. @@ -224,16 +224,16 @@ For one event, the supervisor walks the matching components serially and awaits Interleaving between two different sources is not deterministic. - **Rate limited per component.** Each component holds a token bucket, checked before the guest is entered: `burst` 256 and `refill_per_sec` 128 by default, configurable under `[limits.dispatch]`. - An event over the rate is dropped, counted in `nexum_runtime_dispatch_dropped_total`, and never retried. + A trigger over the rate is dropped, counted in `nexum_runtime_dispatch_dropped_total`, and never retried. The bucket carries across a restart. - **No acknowledgement.** - A successful return from `on-event` is not an ack. + A successful return from `on-trigger` is not an ack. A component that needs progress tracking writes it to the local store itself. - **Cursors commit only after a successful dispatch.** - A `resume` chain-log subscription persists its cursor after each successful dispatch, so a dropped or failed dispatch is re-delivered on the next boot rather than skipped. -- **Catch-up is the component's job, except for chain-log backfill.** - The engine backfills the gap for a `resume` subscription on reconnect, capped by `max_lookback`. - Anything else, for example a gap across a restart on a non-resume subscription, is for the component to detect in `init` and to backfill through `chain::request`. + A `resume` event trigger persists its cursor after each successful dispatch, so a dropped or failed dispatch is re-delivered on the next boot rather than skipped. +- **Catch-up is the component's job, except for log backfill.** + The engine backfills the gap for a `resume` trigger on reconnect, capped by `max_lookback`. + Anything else, for example a gap across a restart on a non-resume trigger, is for the component to detect in `init` and to backfill through `chain::request`. ### What bounds one dispatch @@ -248,16 +248,16 @@ Two mechanisms, and no others: The host does not use epoch interruption. `wasmtime_config` sets `wasm_component_model` and `consume_fuel` and nothing else (`crates/nexum-runtime/src/builder.rs:148-149`). -### Event encoding +### Trigger encoding -Events cross the WASM boundary as the `event` variant in `wit/nexum-host/types.wit`: +Triggers cross the WASM boundary as the `trigger` variant in `wit/nexum-host/types.wit`: ```wit -variant event { +variant trigger { block(block), - chain-logs(chain-logs), - tick(tick), - custom(custom-event), + event(log), + schedule(schedule-tick), + extension(extension-trigger), } record block { @@ -267,28 +267,28 @@ record block { timestamp: u64, } -record tick { +record schedule-tick { fired-at: u64, } -record custom-event { - kind: string, +record extension-trigger { + extension-kind: string, payload: list, } ``` The canonical ABI carries the data, handled by `bindgen!`. Every `u64` timestamp in the package is milliseconds since the Unix epoch, UTC. -`custom` is the generic extension event: the core routes it by `kind` and never reads `payload`, and the subscribing component decodes it against the extension that emitted it. +`extension` is the generic extension trigger: the core routes it by `extension-kind` and never reads `payload`, and the declaring component decodes it against the extension that emitted it. ## The `nexum:host` world The universal package is `nexum:host@0.1.0`, and it is a leaf: it imports no other package. -`wit/nexum-host/event-module.wit` declares the world a module is built against. +`wit/nexum-host/trigger-module.wit` declares the world a module is built against. ```wit -world event-module { - use types.{config, event, fault}; +world trigger-module { + use types.{config, trigger, fault}; import chain; import identity; @@ -297,7 +297,7 @@ world event-module { import logging; export init: func(config: config) -> result<_, fault>; - export on-event: func(event: event) -> result<_, fault>; + export on-trigger: func(trigger: trigger) -> result<_, fault>; } ``` @@ -319,7 +319,7 @@ An operator deploys a module: path = "/var/nexum/twap-monitor/twap_monitor.wasm" 2. The prepass resolves the sibling component.toml, claims the name as a - local-store namespace, and checks that every subscribed chain has a + local-store namespace, and checks that every triggered chain has a [chains.] entry. 3. The supervisor reads the artifact, verifies sha256 against @@ -331,16 +331,16 @@ An operator deploys a module: 5. It builds a fresh Store under the resolved limits, instantiates, and calls init(config). -6. Once every component has loaded, the runtime derives the subscription - plan and opens the event sources: - - one block stream per subscribed chain - - one chain-log stream per chain-log subscription, seeded from its +6. Once every component has loaded, the runtime derives the plan and + opens the sources: + - one block stream per triggered chain + - one log stream per event trigger, seeded from its durable cursor when resume = true -7. Events flow: +7. Triggers flow: block 19_000_001 on Arbitrum -> event loop -> supervisor matches the plan - -> await on-event(event::block(...)) on each matching component + -> await on-trigger(trigger::block(...)) on each matching component -> the component calls chain::request and local-store::set -> Ok(()) commits the block marker and the loop selects again diff --git a/docs/07-rpc-namespace-design.md b/docs/07-rpc-namespace-design.md index f10d7f33..cb56cdc3 100644 --- a/docs/07-rpc-namespace-design.md +++ b/docs/07-rpc-namespace-design.md @@ -86,8 +86,8 @@ A component that only needs raw JSON calls `host.request(chain_id, method, param ## Handlers are synchronous -`#[nexum_sdk::module]` dispatches events to synchronous named handlers: `init`, `on_block`, `on_chain_logs`, `on_tick`, and `on_custom`. -An absent handler is a no-op for that event. +`#[nexum_sdk::module]` dispatches triggers to synchronous named handlers: `init`, `on_block`, `on_event`, `on_schedule`, and `on_extension`. +An absent handler is a no-op for that trigger. An impl with no recognized handler, or with an `on_`-prefixed name outside the set, is refused at macro expansion. There is no `block_on` wrapper around a handler and no provider injection. A handler that wants the alloy provider builds it with `host.provider(chain)` and drives the call with `block_on` itself. diff --git a/docs/adr/0001-operator-config-separate-and-trusted.md b/docs/adr/0001-operator-config-separate-and-trusted.md index 3e5038dc..907780b9 100644 --- a/docs/adr/0001-operator-config-separate-and-trusted.md +++ b/docs/adr/0001-operator-config-separate-and-trusted.md @@ -6,7 +6,7 @@ status: accepted > Amendment: the schema names in this record are historical. > The manifest is `component.toml`; a `module.toml` is not read. -> It defines `[component]`, `[component.resources]`, `[dependencies]`, `[config]`, and `[[subscription]]`, and it admits extension-owned top-level sections that the wired extensions parse. +> It defines `[component]`, `[component.resources]`, `[dependencies]`, `[config]`, and `[[trigger]]`, and it admits extension-owned top-level sections that the wired extensions parse. > `engine.toml` defines `[engine]`, `[limits]`, `[policy]`, `[chains.]`, `[extensions]`, and `[[modules]]`; there is no `[[adapters]]`. > The decision itself, two files with one trust direction, is unchanged and stays accepted. @@ -71,7 +71,7 @@ It costs a host interface for the guest to read its grant, a fault case for call - A deployment needs both files. A missing `engine.toml` gives no chains and the default `state_dir`. - The engine then refuses at boot every module that subscribes to a chain. + The engine then refuses at boot every module that declares a chain trigger. A `chain.request` to an unconfigured chain returns an `unsupported` fault. - The component digest in `[module]` proves that the artifact matches what the author published. It does not prove that the operator authorized the artifact, because one party writes both the hash and the bytes it covers. diff --git a/docs/adr/0014-local-store-durability-model.md b/docs/adr/0014-local-store-durability-model.md index 06116f10..ec424351 100644 --- a/docs/adr/0014-local-store-durability-model.md +++ b/docs/adr/0014-local-store-durability-model.md @@ -6,7 +6,7 @@ status: accepted > Amendment: this record was edited after acceptance. > [ADR-0019](0019-modules-react-to-triggers.md) retired the export name `on-event`, and the text below carries the decided name, `on-trigger`. -> The WIT rename lands in a later code issue, so the export in the tree may still read `on-event`. +> The WIT rename landed with #239. ## Context diff --git a/docs/adr/0018-one-operator-policy-surface.md b/docs/adr/0018-one-operator-policy-surface.md index 0d1fb149..8b6ec71b 100644 --- a/docs/adr/0018-one-operator-policy-surface.md +++ b/docs/adr/0018-one-operator-policy-surface.md @@ -8,7 +8,7 @@ status: accepted > The per-dispatch fuel key was renamed, and the Decision text below carries the current name, `max_fuel_per_dispatch`. > `[limits.watch]` and `[limits.quota]` were retired and the per-dispatch deadline moved to `[limits.dispatch].deadline_secs`; the Decision text below carries the current names. > [ADR-0019](0019-modules-react-to-triggers.md) retired the export name `on-event`, and the Capabilities text below carries the decided name, `on-trigger`. -> The WIT rename lands in a later code issue, so the export in the tree may still read `on-event`. +> The WIT rename landed with #239. > [ADR-0022](0022-cut-guest-to-guest-calling.md) landed the digest-pin dial from the Context list as `digest` on the `[[modules]]` entry it pins, not as a `[policy.component]` row; the pin binds one artifact to one entry, so it lives on the entry. > ADR-0022 also cut the service load path, so the `[[services]]` Consequence below carries a mark in place. @@ -53,7 +53,7 @@ It fails closed by capacity, not by enumeration: a component the operator never The effective permitted set for a component is the `capabilities` list of its `[policy.component]` row, else the `[policy]` list, else every capability the runtime supports. Every `[dependencies]` key the manifest declares must be in the permitted set, or the component refuses at boot. The component's imports are already checked against the declared set, so the imports cannot exceed the operator grant either. -A block or chain-log subscription delivers chain data through `on-trigger` without an import, so it also refuses when the permitted set excludes `chain`. +A block or event trigger delivers chain data through `on-trigger` without an import, so it also refuses when the permitted set excludes `chain`. The grant is whole or the component does not boot, per ADR-0001. ### Egress diff --git a/docs/adr/0019-modules-react-to-triggers.md b/docs/adr/0019-modules-react-to-triggers.md index 8ac39450..128f6ed4 100644 --- a/docs/adr/0019-modules-react-to-triggers.md +++ b/docs/adr/0019-modules-react-to-triggers.md @@ -9,6 +9,7 @@ amends: 0014-local-store-durability-model.md, 0018-one-operator-policy-surface.m > Amendment: this record was edited after acceptance. > [ADR-0020](0020-retire-component-kind.md) retired the manifest `kind` field and so discharged the `module` versus `service` half of the ADR-0016 spelling deferral that the Supersession section below leaves open. > The sentence carries the mark in place. +> The trigger rename respelled two core kinds, `chain-log` to `event` and `cron` to `schedule`, and the Decision text below carries the current names. ## Context @@ -30,7 +31,7 @@ The naive rule "two concepts that are not one-to-one cannot share a word" is wro A trigger is why a module ran. The word covers the kind, the manifest declaration of one, and one delivered occurrence. -Every layer names a kind with one string: the core set `block`, `chain-log` and `cron`, plus the kinds the composition root's extensions declare. +Every layer names a kind with one string: the core set `block`, `event` and `schedule`, plus the kinds the composition root's extensions declare. The vocabulary is open across deployments and closed for one composition root, because the load path refuses an undeclared extension kind. Every layer spells each member the same way, so a reader never has to ask which layer holds the name. diff --git a/docs/design/capability-and-service-model.md b/docs/design/capability-and-service-model.md index 7b450436..75a96e99 100644 --- a/docs/design/capability-and-service-model.md +++ b/docs/design/capability-and-service-model.md @@ -104,7 +104,7 @@ That would attach concept 3's word permanently to concept 4. It does not exist. A `[[services]]` entry is selected by `shared.kinds.get(name)` at `supervisor/load.rs`, against a `BTreeMap<&'static str, ServiceRow>` built from extension Rust. A service's type is therefore its manifest `name`, which is author-controlled and checkable against nothing. -`synthesize` emits only `world module` with fixed `init` and `on-event` exports, so no component can have a synthesized world that exports an interface. +`synthesize` emits only `world module` with fixed `init` and `on-trigger` exports, so no component can have a synthesized world that exports an interface. Every `[[services]]` entry on main is an extension backend. **Becomes.** @@ -197,7 +197,7 @@ One thing shared by accident that should not be: `build_provider_linker` calls o Nothing checks that the two agree. One thing the first draft claimed as justified and is not: the per-component fuel budget does not survive a service call. -`SupervisedStore::call` refuels before every routed call, so N service calls inside one `on-event` cost N full provider budgets, none charged to the caller. +`SupervisedStore::call` refuels before every routed call, so N service calls inside one `on-trigger` cost N full provider budgets, none charged to the caller. The same holds for the state quota and the memory ceiling. This is a defect the trampoline work must repair, not a property the model already has. diff --git a/docs/design/linker-extension-seam.md b/docs/design/linker-extension-seam.md index ff35890d..11cb4d1e 100644 --- a/docs/design/linker-extension-seam.md +++ b/docs/design/linker-extension-seam.md @@ -2,7 +2,7 @@ ## What -The core host binds the `nexum:host/event-module` world: the `nexum:host` interfaces (chain, identity, local-store, remote-store, logging) plus the allowlisted `wasi:http` outgoing surface. +The core host binds the `nexum:host/trigger-module` world: the `nexum:host` interfaces (chain, identity, local-store, remote-store, logging) plus the allowlisted `wasi:http` outgoing surface. A domain capability is not a core seam. It plugs into the host through an extension assembled at the composition root, so the core runtime compiles and runs with no domain backend at all and no extension registered. @@ -19,7 +19,7 @@ Its members: The clock is the WASI override's wall clock when a test sets one, else the real host clock, so extension time and guest time share one source. - `manifest_sections`, `admit_worker`: the non-core manifest sections it claims and its install-time predicate over them. An `Err` refuses the install fail-fast. -- `subscriptions`, `events`: the manifest subscription kinds it emits and the event sources it opens once the engine is booted. +- `emits_trigger_kinds`, `open_sources`: the manifest trigger kinds it emits and the sources it opens once the engine is booted. An extension defines its own `bindgen!` for its world, which generates a `Host` trait local to the extension, and implements it for the foreign `HostState`. That is orphan-legal, because the trait is local. diff --git a/docs/production.md b/docs/production.md index 80863f39..5928ceb8 100644 --- a/docs/production.md +++ b/docs/production.md @@ -8,7 +8,7 @@ A downstream composition root that registers extensions runs the same way, under - The engine built in release: `cargo build -p nexum-cli --release` gives `target/release/nexum`. - Every component `.wasm` artifact present on a path the service user can read. -- An `engine.toml` with `state_dir` on a persistent path (never `/tmp`), `log_level = "info"`, `[engine.metrics] enabled = true` with `bind_addr = "127.0.0.1:9100"`, one `[chains.]` per subscribed chain with a paid RPC URL, and one `[[modules]]` per module, each with an operator-written `id`. +- An `engine.toml` with `state_dir` on a persistent path (never `/tmp`), `log_level = "info"`, `[engine.metrics] enabled = true` with `bind_addr = "127.0.0.1:9100"`, one `[chains.]` per triggered chain with a paid RPC URL, and one `[[modules]]` per module, each with an operator-written `id`. - `require_component_digest = true` under `[engine]`, with every manifest carrying a `[component].digest` pin. - A `digest` on each `[[modules]]` entry, set to the artifact's sha256. This is the operator's own pin, in trusted config: the default sibling manifest lives in the same trust domain as the artifact, so its `[component].digest` does not hold against a compromised artifact store. @@ -76,7 +76,7 @@ WantedBy=multi-user.target ``` A stop halts dispatch at the next guest-call boundary, drains the one call in flight, commits its cursor, and exits 0. -Modules the halt cut out of a block fan-out do not receive that block; an undispatched chain-log event replays at the next start through its `resume` cursor (section 4). +Modules the halt cut out of a block fan-out do not receive that block; an undispatched event replays at the next start through its `resume` cursor (section 4). The drain is bounded by `[limits.shutdown] drain_secs`, which defaults to `deadline_secs` plus 30 s, so an untuned drain outlasts the one deadline-bounded call it can be left waiting on. A drain past the bound therefore means a wedged task, not a long dispatch, and it forces exit 1 so `Restart=on-failure` restarts the engine. Keep `TimeoutStopSec` above the resolved bound, or systemd's SIGKILL pre-empts the forced exit. @@ -143,14 +143,14 @@ If a restored file does not open, roll forward from the previous snapshot, or st The runtime writes two kinds of key inside each component's own namespace, both after a successful dispatch and both best-effort: - `last_dispatched_block:`, a u64 little-endian progress marker for block dispatch. -- `chainlog_cursor:`, the resume cursor for a `resume = true` chain-log subscription. +- `chainlog_cursor:`, the resume cursor for a `resume = true` event trigger. The engine reads it once at boot, re-opens at that block, and backfills the gap on reconnect, capped by `max_lookback`. A reorg retraction pulls the cursor back. Every other key in a component's namespace is the component's own. A forced exit (a drain past `[limits.shutdown] drain_secs`) terminates the process before the in-flight dispatch commits its cursor, and both keys stay at the last committed dispatch. -A `resume = true` subscription then replays the in-flight event at the next start; a block event is not replayed. +A `resume = true` trigger then replays the in-flight log at the next start; a block is not replayed. ## 5. Logs @@ -176,8 +176,8 @@ With `enabled = false` the recorder is still installed, so call sites stay live, | Metric | Type | Labels | Meaning | |---|---|---|---| | `nexum_runtime_boot_refusals_total` | counter | `error_kind` | Boot refusals by error kind. | -| `nexum_runtime_event_latency_seconds` | histogram | `module`, `event_kind` | Wall-clock seconds to dispatch one event. | -| `nexum_runtime_dispatch_dropped_total` | counter | `module`, `event_kind`, `reason` | Events dropped before dispatch. `reason = "rate_limited"` is the per-component dispatch rate limit (`[limits.dispatch]`, default `burst = 256` and `refill_per_sec = 128`). `reason = "shutdown"` is a stop landing mid fan-out: the fan-out follows `[[modules]]` order, so the same trailing modules are skipped at every stop. A block is not replayed; a chain-log event is, from its cursor. | +| `nexum_runtime_event_latency_seconds` | histogram | `module`, `trigger_kind` | Wall-clock seconds to dispatch one trigger. | +| `nexum_runtime_dispatch_dropped_total` | counter | `module`, `trigger_kind`, `reason` | Triggers dropped before dispatch. `reason = "rate_limited"` is the per-component dispatch rate limit (`[limits.dispatch]`, default `burst = 256` and `refill_per_sec = 128`). `reason = "shutdown"` is a stop landing mid fan-out: the fan-out follows `[[modules]]` order, so the same trailing modules are skipped at every stop. A block is not replayed; an event is, from its cursor. | | `nexum_runtime_module_errors_total` | counter | `module`, `error_kind` | Module faults and traps. `error_kind = "trap"` is a wasmtime trap; other values are fault labels. | | `nexum_runtime_module_restarts_total` | counter | `module` | Module restart attempts. | | `nexum_runtime_module_poisoned` | gauge | `module` | `1` once a module crosses `[limits.poison]` (default 5 failures in 600 s). Stays `1` until the process restarts. | @@ -304,4 +304,4 @@ A logging-level change also needs a restart. - [ADR-0003](adr/0003-local-store-namespacing.md): local-store namespacing. - [ADR-0014](adr/0014-local-store-durability-model.md): the local-store durability model. - [ADR-0016](adr/0016-component-vocabulary.md): the `[component]` and `[dependencies]` vocabulary. -- [Component lifecycle, event system, and packaging](02-modules-events-packaging.md). +- [Component lifecycle, trigger system, and packaging](02-modules-triggers-packaging.md). diff --git a/modules/example/src/lib.rs b/modules/example/src/lib.rs index d560dbcb..116dbff8 100644 --- a/modules/example/src/lib.rs +++ b/modules/example/src/lib.rs @@ -1,6 +1,6 @@ //! # example (reference module) //! -//! Minimal reference module: one handler per event, each logging a +//! Minimal reference module: one handler per trigger, each logging a //! one-line summary. The smallest demonstration of //! `#[nexum_sdk::module]`, which supplies the wit-bindgen call, host //! adapter, dispatch, and `export!`. @@ -42,29 +42,33 @@ impl ExampleModule { Ok(()) } - fn on_chain_logs(batch: types::ChainLogs) -> Result<(), Fault> { + fn on_event(log: types::Log) -> Result<(), Fault> { logging::log( logging::Level::Info, - &format!("received {} chain-log entries", batch.logs.len()), + &format!( + "event with {} topics on chain {}", + log.topics.len(), + log.chain_id, + ), ); Ok(()) } - fn on_tick(tick: types::Tick) -> Result<(), Fault> { + fn on_schedule(tick: types::ScheduleTick) -> Result<(), Fault> { logging::log( logging::Level::Info, - &format!("tick fired at {}ms", tick.fired_at), + &format!("schedule fired at {}ms", tick.fired_at), ); Ok(()) } - fn on_custom(event: types::CustomEvent) -> Result<(), Fault> { + fn on_extension(trigger: types::ExtensionTrigger) -> Result<(), Fault> { logging::log( logging::Level::Info, &format!( - "custom event kind {} ({} payload bytes)", - event.kind, - event.payload.len(), + "extension trigger kind {} ({} payload bytes)", + trigger.extension_kind, + trigger.payload.len(), ), ); Ok(()) diff --git a/modules/examples/balance-tracker/component.toml b/modules/examples/balance-tracker/component.toml index b623134c..8cddbfe2 100644 --- a/modules/examples/balance-tracker/component.toml +++ b/modules/examples/balance-tracker/component.toml @@ -13,8 +13,8 @@ logging = {} chain = {} local-store = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 11155111 # --- config ----------------------------------------------------------- diff --git a/modules/examples/http-probe/component.toml b/modules/examples/http-probe/component.toml index 50105e9e..29dea732 100644 --- a/modules/examples/http-probe/component.toml +++ b/modules/examples/http-probe/component.toml @@ -13,8 +13,8 @@ logging = {} # before a connection is made. http = { hosts = ["api.cow.fi"] } -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 11155111 # --- config ----------------------------------------------------------- diff --git a/modules/examples/price-alert/component.toml b/modules/examples/price-alert/component.toml index f53d56f9..52eb8f33 100644 --- a/modules/examples/price-alert/component.toml +++ b/modules/examples/price-alert/component.toml @@ -11,8 +11,8 @@ version = "0.1.0" logging = {} chain = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 11155111 # --- config ----------------------------------------------------------- diff --git a/modules/examples/price-alert/src/logic.rs b/modules/examples/price-alert/src/logic.rs index a51cdd23..ea65a2c4 100644 --- a/modules/examples/price-alert/src/logic.rs +++ b/modules/examples/price-alert/src/logic.rs @@ -56,7 +56,7 @@ pub fn on_block( answer = %answer, threshold = %settings.threshold_scaled, direction = ?settings.direction, - "price-alert: TRIGGERED", + "price-alert: THRESHOLD CROSSED", ); } else { tracing::info!( @@ -131,12 +131,12 @@ mod tests { use nexum_sdk::host::{ChainError, Fault}; use nexum_sdk_test::{MockHost, capture_tracing}; - fn sample_settings(trigger_scaled_dec: i128, direction: Direction) -> Settings { + fn sample_settings(threshold_scaled_dec: i128, direction: Direction) -> Settings { Settings { oracle_address: "0x694AA1769357215DE4FAC081bf1f309aDC325306" .parse() .unwrap(), - threshold_scaled: I256::try_from(trigger_scaled_dec).unwrap(), + threshold_scaled: I256::try_from(threshold_scaled_dec).unwrap(), direction, every_n_blocks: 1, } @@ -284,9 +284,9 @@ mod tests { } #[test] - fn on_block_idle_when_price_above_below_trigger() { + fn on_block_idle_when_price_above_below_threshold() { let host = MockHost::new(); - let settings = sample_settings(/*trigger*/ 250_050_000_000, Direction::Below); + let settings = sample_settings(/*threshold*/ 250_050_000_000, Direction::Below); programmed_eth_call( &host, settings.oracle_address, @@ -304,7 +304,7 @@ mod tests { } #[test] - fn on_block_triggers_below_threshold() { + fn on_block_crosses_below_threshold() { let host = MockHost::new(); let settings = sample_settings(250_050_000_000, Direction::Below); programmed_eth_call( @@ -318,13 +318,13 @@ mod tests { // `expect_one` on the WARN level pins the single-alert count. let ev = logs.expect_one(|e| e.level == Level::WARN); - assert_eq!(ev.message, "price-alert: TRIGGERED"); + assert_eq!(ev.message, "price-alert: THRESHOLD CROSSED"); assert_eq!(ev.field_str("direction").as_deref(), Some("Below")); assert_eq!(ev.field_str("answer").as_deref(), Some("200000000000")); } #[test] - fn on_block_triggers_above_threshold() { + fn on_block_crosses_above_threshold() { let host = MockHost::new(); let settings = sample_settings(100, Direction::Above); programmed_eth_call( @@ -337,7 +337,7 @@ mod tests { result.unwrap(); let ev = logs.expect_one(|e| e.level == Level::WARN); - assert_eq!(ev.message, "price-alert: TRIGGERED"); + assert_eq!(ev.message, "price-alert: THRESHOLD CROSSED"); assert_eq!(ev.field_str("direction").as_deref(), Some("Above")); } @@ -358,7 +358,7 @@ mod tests { // through the host logging call, so it lands on `host.logging`. assert!(host.logging.contains("eth_call failed")); // No facade event at all: the module returns before emitting - // either the ok or TRIGGERED line. + // either the ok or THRESHOLD CROSSED line. assert!(logs.is_empty()); } diff --git a/modules/fixtures/clock-reader/Cargo.toml b/modules/fixtures/clock-reader/Cargo.toml index 5a2be467..1a8b4dce 100644 --- a/modules/fixtures/clock-reader/Cargo.toml +++ b/modules/fixtures/clock-reader/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Test fixture: on every event reads the WASI wall clock through std and logs it. Lets a test assert the guest observes a WasiClockOverride end to end." +description = "Test fixture: on every trigger reads the WASI wall clock through std and logs it. Lets a test assert the guest observes a WasiClockOverride end to end." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/clock-reader/component.toml b/modules/fixtures/clock-reader/component.toml index 11790269..d83952a0 100644 --- a/modules/fixtures/clock-reader/component.toml +++ b/modules/fixtures/clock-reader/component.toml @@ -1,5 +1,5 @@ -# clock-reader test fixture. Subscribes to a single chain's blocks so the -# supervisor invokes `on_event` once per block; the handler reads the WASI +# clock-reader test fixture. Declares a block trigger on a single chain so +# the supervisor invokes `on_trigger` once per block; the handler reads the WASI # wall clock through std and logs it. The integration test boots this # fixture under a pinned clock override and asserts the logged time matches # the override, proving the guest observes virtualized time end to end. @@ -11,6 +11,6 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/clock-reader/src/lib.rs b/modules/fixtures/clock-reader/src/lib.rs index 21ee7b01..ebf835d6 100644 --- a/modules/fixtures/clock-reader/src/lib.rs +++ b/modules/fixtures/clock-reader/src/lib.rs @@ -15,7 +15,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -31,7 +31,7 @@ impl Guest for ClockReader { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // Whole seconds since the epoch is parseable and stable: the // override pins wall time to an exact instant, so the guest reads // that instant back rather than the ambient host clock. diff --git a/modules/fixtures/env-reader/Cargo.toml b/modules/fixtures/env-reader/Cargo.toml index 83b398c5..4246decf 100644 --- a/modules/fixtures/env-reader/Cargo.toml +++ b/modules/fixtures/env-reader/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Test fixture: on every event counts the guest-visible environment variables, process arguments, and stdin bytes through std and logs all three. Lets a test assert the guest observes an empty wasi:cli/environment and an empty wasi:cli/stdin end to end." +description = "Test fixture: on every trigger counts the guest-visible environment variables, process arguments, and stdin bytes through std and logs all three. Lets a test assert the guest observes an empty wasi:cli/environment and an empty wasi:cli/stdin end to end." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/env-reader/component.toml b/modules/fixtures/env-reader/component.toml index a3d77b46..19a013af 100644 --- a/modules/fixtures/env-reader/component.toml +++ b/modules/fixtures/env-reader/component.toml @@ -1,5 +1,5 @@ -# env-reader test fixture. Subscribes to a single chain's blocks so the -# supervisor invokes `on_event` once per block; the handler counts the +# env-reader test fixture. Declares a block trigger on a single chain so the +# supervisor invokes `on_trigger` once per block; the handler counts the # environment variables and process arguments std can see and logs both. # The integration test boots this fixture from a host process whose own # environment and argv are non-empty and asserts the guest observed zero @@ -12,6 +12,6 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/env-reader/src/lib.rs b/modules/fixtures/env-reader/src/lib.rs index 050393b5..8a6f494c 100644 --- a/modules/fixtures/env-reader/src/lib.rs +++ b/modules/fixtures/env-reader/src/lib.rs @@ -15,7 +15,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -31,7 +31,7 @@ impl Guest for EnvReader { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // Counts alone decide the assertion; the keys are logged too so a // failure names what leaked rather than only how much. let vars: Vec = std::env::vars().map(|(k, _)| k).collect(); diff --git a/modules/fixtures/flaky-bomb/Cargo.toml b/modules/fixtures/flaky-bomb/Cargo.toml index 66122923..62eb8b59 100644 --- a/modules/fixtures/flaky-bomb/Cargo.toml +++ b/modules/fixtures/flaky-bomb/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Evil-by-design fixture: traps on the first N events (via unreachable!) and succeeds afterwards. The supervisor must exercise its exponential-backoff restart policy + reset the failure counter when the module recovers." +description = "Evil-by-design fixture: traps on the first N triggers (via unreachable!) and succeeds afterwards. The supervisor must exercise its exponential-backoff restart policy + reset the failure counter when the module recovers." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/flaky-bomb/component.toml b/modules/fixtures/flaky-bomb/component.toml index a0d2f085..cc9d2614 100644 --- a/modules/fixtures/flaky-bomb/component.toml +++ b/modules/fixtures/flaky-bomb/component.toml @@ -1,4 +1,4 @@ -# flaky-bomb test fixture. Subscribes to blocks; `on_event` +# flaky-bomb test fixture. Declares a block trigger; `on_trigger` # traps via `unreachable!()` on the first N attempts, then recovers. # Drives the supervisor's exponential-backoff restart policy through # its full lifecycle. @@ -11,12 +11,12 @@ version = "0.1.0" logging = {} local-store = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 [config] -# Number of consecutive events to trap on before recovering. Tests +# Number of consecutive triggers to trap on before recovering. Tests # typically synthesize a manifest with `fail_first_n = "1"` to keep # the test wall-clock short (only one 1 s backoff window to wait). fail_first_n = "1" diff --git a/modules/fixtures/flaky-bomb/src/lib.rs b/modules/fixtures/flaky-bomb/src/lib.rs index 49df3817..a7a68a53 100644 --- a/modules/fixtures/flaky-bomb/src/lib.rs +++ b/modules/fixtures/flaky-bomb/src/lib.rs @@ -12,7 +12,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -44,7 +44,7 @@ impl Guest for FlakyBomb { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // Read + increment the attempt counter from local-store. // Survives wasm-side state resets (the supervisor's restart // path tears down the Store; local-store is host-side and diff --git a/modules/fixtures/fuel-bomb/Cargo.toml b/modules/fixtures/fuel-bomb/Cargo.toml index e01498bc..11fec0f6 100644 --- a/modules/fixtures/fuel-bomb/Cargo.toml +++ b/modules/fixtures/fuel-bomb/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Evil-by-design fixture: on every event runs an unbounded loop to exhaust the wasmtime fuel budget. Engine must trap with OutOfFuel + mark the module dead." +description = "Evil-by-design fixture: on every trigger runs an unbounded loop to exhaust the wasmtime fuel budget. Engine must trap with OutOfFuel + mark the module dead." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/fuel-bomb/component.toml b/modules/fixtures/fuel-bomb/component.toml index 7abfd987..0ddbae11 100644 --- a/modules/fixtures/fuel-bomb/component.toml +++ b/modules/fixtures/fuel-bomb/component.toml @@ -1,6 +1,6 @@ -# fuel-bomb test fixture. Subscribes to a single chain's -# blocks so the supervisor invokes `on_event` once; the unbounded -# loop in `on_event` then exhausts the wasmtime fuel budget and the +# fuel-bomb test fixture. Declares a block trigger on a single chain +# so the supervisor invokes `on_trigger` once; the unbounded +# loop in `on_trigger` then exhausts the wasmtime fuel budget and the # host traps `OutOfFuel`. The integration test asserts the trap is # caught + the module is marked dead. @@ -11,6 +11,6 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/fuel-bomb/src/lib.rs b/modules/fixtures/fuel-bomb/src/lib.rs index 6ab9e741..4aa44d1d 100644 --- a/modules/fixtures/fuel-bomb/src/lib.rs +++ b/modules/fixtures/fuel-bomb/src/lib.rs @@ -1,6 +1,6 @@ //! # fuel-bomb (test fixture) //! -//! Exhausts the fuel budget on every `on_event` via an unbounded loop. +//! Exhausts the fuel budget on every `on_trigger` via an unbounded loop. //! The engine traps with `OutOfFuel`; the supervisor must catch it, //! mark the module dead, and keep dispatching others. Test-only. @@ -11,7 +11,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -27,7 +27,7 @@ impl Guest for FuelBomb { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // Unbounded loop. `std::hint::black_box` prevents the // optimiser from constant-folding this away, so the loop // genuinely burns wasmtime fuel one branch + add at a time. diff --git a/modules/fixtures/memory-bomb/Cargo.toml b/modules/fixtures/memory-bomb/Cargo.toml index 83876207..b9d93d58 100644 --- a/modules/fixtures/memory-bomb/Cargo.toml +++ b/modules/fixtures/memory-bomb/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Evil-by-design fixture: on every event allocates past the 64 MiB memory cap to force a memory-growth trap. Engine must trap + mark the module dead without taking down the supervisor." +description = "Evil-by-design fixture: on every trigger allocates past the 64 MiB memory cap to force a memory-growth trap. Engine must trap + mark the module dead without taking down the supervisor." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/memory-bomb/component.toml b/modules/fixtures/memory-bomb/component.toml index 91035789..714769d9 100644 --- a/modules/fixtures/memory-bomb/component.toml +++ b/modules/fixtures/memory-bomb/component.toml @@ -1,5 +1,5 @@ -# memory-bomb test fixture. Subscribes to blocks; the -# `on_event` handler allocates 128 MiB which exceeds the default 64 +# memory-bomb test fixture. Declares a block trigger; the +# `on_trigger` handler allocates 128 MiB which exceeds the default 64 # MiB per-module cap. The host traps + the integration test asserts # the supervisor marks the module dead. @@ -10,6 +10,6 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/memory-bomb/src/lib.rs b/modules/fixtures/memory-bomb/src/lib.rs index 92b35665..61f21f08 100644 --- a/modules/fixtures/memory-bomb/src/lib.rs +++ b/modules/fixtures/memory-bomb/src/lib.rs @@ -1,7 +1,7 @@ //! # memory-bomb (test fixture) //! //! Allocates past the default 64 MiB per-module memory cap on every -//! `on_event`. The `StoreLimits` refuse the grow, the guest allocator sees +//! `on_trigger`. The `StoreLimits` refuse the grow, the guest allocator sees //! the failure and aborts, the supervisor marks the module dead, and other //! modules keep dispatching. Test-only. @@ -12,7 +12,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -31,7 +31,7 @@ impl Guest for MemoryBomb { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // The default per-module cap is 64 MiB (`DEFAULT_MEMORY_LIMIT` in // `crates/nexum-runtime/src/engine_config/policy.rs`). Asking for // 128 MiB diff --git a/modules/fixtures/panic-bomb/Cargo.toml b/modules/fixtures/panic-bomb/Cargo.toml index 1752971d..75fc5d20 100644 --- a/modules/fixtures/panic-bomb/Cargo.toml +++ b/modules/fixtures/panic-bomb/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Evil-by-design fixture: installs the nexum-sdk tracing facade in init and panics on every event. One death must leave Stderr, HostInterface, and Panic records on the run." +description = "Evil-by-design fixture: installs the nexum-sdk tracing facade in init and panics on every trigger. One death must leave Stderr, HostInterface, and Panic records on the run." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/panic-bomb/component.toml b/modules/fixtures/panic-bomb/component.toml index a106aa9e..cc5d8f43 100644 --- a/modules/fixtures/panic-bomb/component.toml +++ b/modules/fixtures/panic-bomb/component.toml @@ -1,5 +1,5 @@ -# panic-bomb test fixture. Subscribes to blocks; `init` installs the -# nexum-sdk tracing facade (subscriber + panic hook) and `on_event` +# panic-bomb test fixture. Declares a block trigger; `init` installs the +# nexum-sdk tracing facade (subscriber + panic hook) and `on_trigger` # panics. The hook writes the panic to stderr and forwards it over the # host logging call before the trap reaches the supervisor, so the # integration test asserts one dead run carries Stderr, HostInterface, @@ -12,6 +12,6 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/panic-bomb/src/lib.rs b/modules/fixtures/panic-bomb/src/lib.rs index 8d9e823c..49ffe1ca 100644 --- a/modules/fixtures/panic-bomb/src/lib.rs +++ b/modules/fixtures/panic-bomb/src/lib.rs @@ -1,7 +1,7 @@ //! # panic-bomb (test fixture) //! //! Installs the nexum-sdk tracing facade (subscriber + panic hook) in -//! `init` and panics on every `on_event`. The hook forwards the panic +//! `init` and panics on every `on_trigger`. The hook forwards the panic //! to stderr and the host logging call before the trap reaches the //! supervisor, so one death leaves Stderr, HostInterface, and Panic //! records. Test-only. @@ -13,7 +13,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -51,7 +51,7 @@ impl Guest for PanicBomb { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { panic!("panic-bomb detonated"); } } diff --git a/modules/fixtures/slow-host/Cargo.toml b/modules/fixtures/slow-host/Cargo.toml index 706b1961..b00aa696 100644 --- a/modules/fixtures/slow-host/Cargo.toml +++ b/modules/fixtures/slow-host/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Evil-by-design fixture: on_event issues a single chain::request host call that the test wires to a mock provider which parks the request far past the dispatch deadline. Proves the supervisor cuts off a blocked host call and recovers on a fresh store, which fuel and epoch metering cannot do." +description = "Evil-by-design fixture: on_trigger issues a single chain::request host call that the test wires to a mock provider which parks the request far past the dispatch deadline. Proves the supervisor cuts off a blocked host call and recovers on a fresh store, which fuel and epoch metering cannot do." [lib] crate-type = ["cdylib"] diff --git a/modules/fixtures/slow-host/component.toml b/modules/fixtures/slow-host/component.toml index 2a5c8936..c3b2b3b8 100644 --- a/modules/fixtures/slow-host/component.toml +++ b/modules/fixtures/slow-host/component.toml @@ -1,5 +1,5 @@ -# slow-host test fixture. Subscribes to a single chain's blocks so the -# supervisor invokes `on_event` once per block; the handler issues one +# slow-host test fixture. Declares a block trigger on a single chain so the +# supervisor invokes `on_trigger` once per block; the handler issues one # `chain::request` host call and returns Ok. The integration test wires # the chain capability to a mock provider that parks the first request # far past a short `[limits.dispatch].deadline_secs` override, so the @@ -17,6 +17,6 @@ version = "0.1.0" logging = {} chain = {} -[[subscription]] -kind = "block" +[[trigger]] +on = "block" chain_id = 1 diff --git a/modules/fixtures/slow-host/src/lib.rs b/modules/fixtures/slow-host/src/lib.rs index de5dad3d..307cd6f6 100644 --- a/modules/fixtures/slow-host/src/lib.rs +++ b/modules/fixtures/slow-host/src/lib.rs @@ -15,7 +15,7 @@ wit_bindgen::generate!({ path: [ "../../../wit/nexum-host", ], - world: "nexum:host/event-module", + world: "nexum:host/trigger-module", generate_all, }); @@ -31,13 +31,13 @@ impl Guest for SlowHost { Ok(()) } - fn on_event(_event: types::Event) -> Result<(), Fault> { + fn on_trigger(_trigger: types::Trigger) -> Result<(), Fault> { // A single read-only RPC. The test's mock provider decides how long // it takes to answer; the guest just awaits it. `eth_blockNumber` // with empty params is the cheapest well-formed request in the // permitted read surface. let _ = chain::request(1, "eth_blockNumber", "[]"); - logging::log(logging::Level::Info, "slow-host on_event returned"); + logging::log(logging::Level::Info, "slow-host on_trigger returned"); Ok(()) } } diff --git a/modules/fixtures/topic-parity/Cargo.toml b/modules/fixtures/topic-parity/Cargo.toml index 2c786e03..847d8455 100644 --- a/modules/fixtures/topic-parity/Cargo.toml +++ b/modules/fixtures/topic-parity/Cargo.toml @@ -14,6 +14,6 @@ crate-type = ["cdylib"] [dependencies] nexum-sdk = { path = "../../../crates/nexum-sdk" } -# Declares the subscribed event, whose SIGNATURE_HASH the parity check reads. +# Declares the Solidity events, whose SIGNATURE_HASH the parity check reads. alloy-sol-types = { version = "1.6", default-features = false, features = ["std"] } wit-bindgen = { version = "0.59", default-features = false, features = ["macros", "realloc"] } diff --git a/modules/fixtures/topic-parity/component.toml b/modules/fixtures/topic-parity/component.toml index c5f506c5..fd7d9439 100644 --- a/modules/fixtures/topic-parity/component.toml +++ b/modules/fixtures/topic-parity/component.toml @@ -1,5 +1,5 @@ # topic-parity build fixture. Never launched: it exists so CI compiles the -# `subscribes(...)` parity check the module macro emits. The two +# `sol_events(...)` parity check the module macro emits. The two # event_signature values below are the topic-0 of the two `sol!` events in # src/lib.rs; edit either side alone and the build refuses. @@ -10,12 +10,12 @@ version = "0.1.0" [dependencies] logging = {} -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 1 event_signature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" -[[subscription]] -kind = "chain-log" +[[trigger]] +on = "event" chain_id = 100 event_signature = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925" diff --git a/modules/fixtures/topic-parity/src/lib.rs b/modules/fixtures/topic-parity/src/lib.rs index 2525a611..aafae95d 100644 --- a/modules/fixtures/topic-parity/src/lib.rs +++ b/modules/fixtures/topic-parity/src/lib.rs @@ -1,6 +1,6 @@ //! # topic-parity (build fixture) //! -//! Compile-only: `subscribes(...)` names the events below, so the macro +//! Compile-only: `sol_events(...)` names the events below, so the macro //! emits the const parity check against `component.toml`. A drift on either //! side fails this crate's build. @@ -19,12 +19,12 @@ sol! { struct TopicParity; -#[nexum_sdk::module(subscribes(Transfer, Approval))] +#[nexum_sdk::module(sol_events(Transfer, Approval))] impl TopicParity { - fn on_chain_logs(batch: types::ChainLogs) -> Result<(), Fault> { + fn on_event(log: types::Log) -> Result<(), Fault> { logging::log( logging::Level::Info, - &format!("received {} chain-log entries", batch.logs.len()), + &format!("event with {} topics", log.topics.len()), ); Ok(()) } diff --git a/wit/nexum-host/event-module.wit b/wit/nexum-host/trigger-module.wit similarity index 76% rename from wit/nexum-host/event-module.wit rename to wit/nexum-host/trigger-module.wit index 7d43f2f3..1dee2bf4 100644 --- a/wit/nexum-host/event-module.wit +++ b/wit/nexum-host/trigger-module.wit @@ -1,9 +1,9 @@ package nexum:host@0.1.0; -/// Event-driven module: automation, background processing. +/// Trigger-driven module: automation, background processing. /// No UI capabilities. Runs on any conforming host. -world event-module { - use types.{config, event, fault}; +world trigger-module { + use types.{config, trigger, fault}; // Six core primitives (always provided by a conforming host). import chain; @@ -19,5 +19,5 @@ world event-module { // hosts list on the http dependency in component.toml. export init: func(config: config) -> result<_, fault>; - export on-event: func(event: event) -> result<_, fault>; + export on-trigger: func(trigger: trigger) -> result<_, fault>; } diff --git a/wit/nexum-host/types.wit b/wit/nexum-host/types.wit index c40a3f5e..2a1ea494 100644 --- a/wit/nexum-host/types.wit +++ b/wit/nexum-host/types.wit @@ -14,12 +14,14 @@ interface types { timestamp: u64, } - /// One decoded log, mirroring the RPC `eth_getLogs` shape field for - /// field so the guest can rebuild the native alloy log losslessly. - /// Fixed-width byte fields are carried raw: `address` is 20 bytes, - /// each topic and hash is 32. The block-scoped fields are absent on a - /// pending log (mined logs carry them all). - record chain-log { + /// One decoded log, mirroring the RPC `eth_getLogs` shape so the + /// guest can rebuild the native alloy log losslessly. `chain-id` is + /// the one non-mirror field: the alloy log carries no chain id, so + /// it rides here. Fixed-width byte fields are carried raw: `address` + /// is 20 bytes, each topic and hash is 32. The block-scoped fields + /// are absent on a pending log (mined logs carry them all). + record log { + chain-id: chain-id, address: list, topics: list>, data: list, @@ -32,39 +34,31 @@ interface types { removed: bool, } - /// A batch of logs delivered from one subscription. The alloy log type - /// carries no chain id, so it sits here once: every log in a delivery - /// shares the chain of the subscription that produced it. - record chain-logs { - chain-id: chain-id, - logs: list, - } - /// Fired by the host on a configured cadence. `fired-at` is the host's /// wall-clock time (ms since Unix epoch, UTC) at which the tick was /// generated. - record tick { + record schedule-tick { fired-at: u64, } - /// The generic extension event: a domain extension's own event kind - /// and its opaque payload. The core routes by `kind` and never reads - /// `payload`; the subscribing module decodes it against the extension - /// that emitted it. - record custom-event { - /// Extension-scoped event kind, matched against a module's - /// `[[subscription]]` kind. - kind: string, + /// The generic extension trigger: a domain extension's own kind and + /// its opaque payload. The core routes by `extension-kind` and never + /// reads `payload`; the declaring module decodes it against the + /// extension that emitted it. + record extension-trigger { + /// Extension-scoped kind, matched against a module's + /// `[[trigger]] on` value. + extension-kind: string, /// Opaque bytes the emitting extension defines and the module /// decodes. payload: list, } - variant event { + variant trigger { block(block), - chain-logs(chain-logs), - tick(tick), - custom(custom-event), + event(log), + schedule(schedule-tick), + extension(extension-trigger), } /// Opaque config from the component.toml [config] section.