microde-application
The Rust runtime is published as the microde-application crate. Add it with
cargo add microde-application
The API uses the same composition model as the TypeScript runtime while using
Rust-native traits, owned futures, and Result-based errors.
API index
Runtime
MicrodeApplicationinstalls modules, binds relationships, and coordinates execution.MicrodeModuledefines the module lifecycle.MicrodeContextlets modules request a stop or terminate immediately.MicrodeStopRequestdescribes an orderly shutdown request.MicrodeExecutionResultreports the final outcome.MicrodeApplicationStateandModuleKinddescribe runtime behavior.MicrodeErroris the error type returned by the runtime.
Modules and composition
ModuleFutureis the owned future returned by lifecycle methods.ModuleHandleandModuleInstanceIdidentify an exact installed module.Portdeclares a provider contract;Providerexports a value for it.DependencyandReferencedeclare relationship slots.SetupContextandRunContextresolve bound values in lifecycle methods.
Supporting traits and descriptors
RelationshipKind,RelationshipDescriptor, andRelationshipSlotdescribe relationship metadata used by composition.RunRelationshipis implemented by slots accepted byRunContext.MicrodeContextHandleis an independently owned module context.
Module implementation
Modules declare their kind and override lifecycle methods as needed:
use microde_application::{MicrodeModule, ModuleFuture, ModuleKind};
struct Worker;
impl MicrodeModule for Worker {
const KIND: ModuleKind = ModuleKind::Passive;
fn run(&mut self) -> ModuleFuture {
Box::pin(async {
println!("worker complete");
Ok(())
})
}
}
initialize, setup_with_context, run_with_context, stop, teardown,
shutdown, and cleanup have default implementations. Override only the
phases the module needs.
Installation and composition
install_named returns an opaque ModuleHandle for one stable module instance.
Handles are required when binding relationships and cannot be used across
different MicrodeApplication values.
let database = service.install_named("database", |_| DatabaseModule::new())?;
let orders = service.install_named("orders", |_| OrdersModule::new())?;
service.bind(&orders, &orders_database, &database)?;
Calling serve or run seals the composition. Bindings, providers, and dependency cycles
are validated before any lifecycle callback starts.
Ports, providers, and relationships
Port<T> identifies a typed provider contract. A module returns its owned
Provider values from providers(). Consumers declare Dependency<T> and
Reference<T> relationship slots from a port:
let database_port = Port::<Database>::new("database");
let database_slot = Dependency::new("database", database_port.clone());
let peer_slot = Reference::new("peer", database_port);
Dependencies participate in the lifecycle DAG and are available through
SetupContext and RunContext. References do not affect lifecycle order and
are available only through RunContext, so reference cycles are allowed.
Lifecycle contexts
Use SetupContext::use_dependency during setup and
RunContext::use_relationship during run:
fn setup_with_context(&mut self, context: SetupContext) -> ModuleFuture {
let database: Database = context.use_dependency(&self.database_slot)?;
Box::pin(async move { Ok(()) })
}
fn run_with_context(&mut self, context: RunContext) -> ModuleFuture {
let peer: Database = context.use_relationship(&self.peer_slot)?;
Box::pin(async move { Ok(()) })
}
Lifecycle order is dependency-first with stable instance IDs as tie-breakers. Teardown, shutdown, and cleanup use the exact reverse order.