use std::path::{Path, PathBuf};
use std::time::Duration;
use frame_core::component::{
ChildArgument, ChildSpec, ComponentId, ComponentMeta, SupervisionPolicy,
};
use frame_core::registry::ComponentRegistry;
use frame_core::runtime::{ComponentRuntime, RuntimePolicy, mailbox_integer};
use frame_host::{AppError, AppSpec, ComponentInstall, FrameConfig, PageServer};
use frame_state::{
ComponentSchema, ComponentStateStore, ComponentStoreHandle, EntityId, StateError,
};
const COMPONENT_BEAM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/{{GLEAM_NAME}}.beam"));
const FFI_BEAM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/{{GLEAM_NAME}}_ffi.beam"));
const CHILD: &str = "entity_actor";
const LIVENESS_MESSAGE: i64 = 0;
const STOP_MESSAGE: i64 = 2;
const ENTITY_BYTES: &[u8] = br#"{"kind":"welcome","message":"hello from {{NAME}}"}"#;
const CONFIG_PATH: &str = "frame.toml";
const STATE_PATH: &str = ".frame/state";
const SCHEDULER_THREADS: usize = 2;
const OPERATION_TIMEOUT: Duration = Duration::from_secs(3);
const MAX_RESTARTS: usize = 3;
const RESTART_WINDOW: Duration = Duration::from_secs(30);
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let config = FrameConfig::load(Path::new(CONFIG_PATH))?;
let page = PageServer::resolve(&config.frame)?;
println!("{{NAME}} serving at http://{}", page.local_addr());
let state = ComponentStateStore::open(STATE_PATH)?;
let _reconcile_report = state.reconcile()?; let store = resolve_store_handle(&state)?;
frame_host::run_application(config, app_spec(store), page)?;
Ok(())
}
pub fn bounded_round_trip(state_path: &Path) -> Result<EntityId, Box<dyn std::error::Error>> {
let state_path = absolute(state_path)?;
let state = ComponentStateStore::open(&state_path)?;
let _reconcile_report = state.reconcile()?;
let store = resolve_store_handle(&state)?;
let runtime = ComponentRuntime::compose(RuntimePolicy {
scheduler_threads: SCHEDULER_THREADS,
operation_timeout: OPERATION_TIMEOUT,
})?;
let support = runtime.load_support_module(FFI_BEAM)?;
let id = component_id();
runtime
.registry()
.register(component_meta(), COMPONENT_BEAM.to_vec())?;
runtime.registry().start(id)?;
if runtime.registry().probe_child(id, CHILD)? != LIVENESS_MESSAGE {
return Err("component liveness mailbox round-trip returned the wrong value".into());
}
let entity = round_trip_entity(runtime.registry(), &store)?;
runtime.registry().stop(id)?;
runtime.unload_support_module(support)?;
if runtime.live_process_count() != 0 {
return Err("ordered stop leaked a live process tree".into());
}
runtime.shutdown()?;
Ok(entity)
}
fn resolve_store_handle(state: &ComponentStateStore) -> Result<ComponentStoreHandle, StateError> {
match state.component_handle(component_id()) {
Ok(handle) => Ok(handle),
Err(StateError::NotActive { component }) if component == component_id() => {
state.declare_schema(component_id(), ComponentSchema::lww())?;
state.install_storage(component_id())
}
Err(error) => Err(error),
}
}
fn app_spec(store: ComponentStoreHandle) -> AppSpec {
AppSpec {
components: vec![ComponentInstall {
meta: component_meta(),
bytecode: COMPONENT_BEAM.to_vec(),
support_modules: vec![FFI_BEAM.to_vec()],
}],
policy: RuntimePolicy {
scheduler_threads: SCHEDULER_THREADS,
operation_timeout: OPERATION_TIMEOUT,
},
readiness: Box::new(|registry| {
let answer = registry
.probe_child(component_id(), CHILD)
.map_err(|error| AppError::new(format!("liveness probe failed: {error}")))?;
if answer == LIVENESS_MESSAGE {
Ok(())
} else {
Err(AppError::new(format!(
"component answered the liveness probe with {answer}, expected {LIVENESS_MESSAGE}"
)))
}
}),
announce: Box::new(move |registry, announcer| {
let entity = round_trip_entity(registry, &store)
.map_err(|error| AppError::new(format!("entity round trip failed: {error}")))?;
announcer
.announce_fact(serde_json::json!({
"componentId": component_id().to_string(),
"entityId": entity.to_string(),
}))
.map_err(|error| AppError::new(format!("fact announcement refused: {error}")))
}),
}
}
fn round_trip_entity(
registry: &ComponentRegistry,
store: &ComponentStoreHandle,
) -> Result<EntityId, Box<dyn std::error::Error>> {
let id = component_id();
let entity = store.put(ENTITY_BYTES)?;
let entity_ack = mailbox_integer(entity.as_bytes(), STOP_MESSAGE + 1)?;
registry.send_child(id, CHILD, entity_ack)?;
if registry.probe_child(id, CHILD)? != entity_ack {
return Err("component entity-id mailbox round-trip returned the wrong value".into());
}
store.commit()?;
if store.get(entity)?.as_deref() != Some(ENTITY_BYTES) {
return Err("component store did not return the round-tripped entity".into());
}
Ok(entity)
}
fn component_id() -> ComponentId {
ComponentId::derive("frame.generated", "{{NAME}}")
}
fn component_meta() -> ComponentMeta {
ComponentMeta {
id: component_id(),
name: "{{NAME}} component".to_owned(),
version: "0.1.0".to_owned(),
requires: Vec::new(),
provides: Vec::new(),
needs: Vec::new(),
children: vec![ChildSpec {
name: CHILD.to_owned(),
module: "{{GLEAM_NAME}}".to_owned(),
function: "run".to_owned(),
arguments: vec![ChildArgument::SupervisorPid],
liveness_message: LIVENESS_MESSAGE,
stop_message: STOP_MESSAGE,
}],
supervision: Some(SupervisionPolicy {
max_restarts: MAX_RESTARTS,
window: RESTART_WINDOW,
}),
}
}
fn absolute(path: &Path) -> Result<PathBuf, std::io::Error> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()?.join(path))
}
}