#![allow(clippy::pedantic)]
use std::any::{Any, TypeId};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use futures_util::future::BoxFuture;
use thiserror::Error;
use crate::health::HealthStatus;
use crate::runtime::component::{AnyComponent, AnyComponentError, Component, ComponentContext};
use crate::runtime::lifecycle::ShutdownToken;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RegistryError {
#[error("component `{name}` is already registered")]
AlreadyRegistered {
name: String,
},
#[error("component `{name}` is not registered")]
NotFound {
name: String,
},
#[error("component `{name}` depends on missing component `{dep}`")]
MissingDependency {
name: String,
dep: String,
},
#[error("cycle detected in component dependencies: {cycle:?}")]
Cycle {
cycle: Vec<String>,
},
#[error("init failed for component `{name}`: {message}")]
Init {
name: String,
message: String,
},
#[error("start failed for component `{name}`: {message}")]
Start {
name: String,
message: String,
},
#[error("stop failed for component `{name}`: {message}")]
Stop {
name: String,
message: String,
},
#[error("reload failed for component `{name}`: {message}")]
Reload {
name: String,
message: String,
},
#[error("component registry lock poisoned")]
LockPoisoned,
#[error("component `{name}` exists but is not of type `{type_id:?}`")]
TypeMismatch {
name: String,
type_id: TypeId,
},
#[error("component `{name}` has already been initialized")]
AlreadyInitialized {
name: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ComponentState {
Pending,
Initializing,
Initialized,
Starting,
Running,
Stopping,
Stopped,
Failed,
}
#[derive(Debug, Clone)]
pub struct ComponentDescriptor {
pub name: String,
pub depends_on: Vec<String>,
pub config: serde_json::Value,
}
#[async_trait]
pub trait ComponentFactory: Send + Sync {
fn name(&self) -> &str;
fn kind(&self) -> &'static str;
fn depends_on(&self) -> Vec<String>;
async fn build(
self: Box<Self>,
config: serde_json::Value,
ctx: &ComponentContext,
) -> Result<Box<dyn AnyComponent>, RegistryError>;
}
pub struct TypedFactory<C: Component> {
name: String,
extra_deps: Vec<String>,
_marker: std::marker::PhantomData<fn() -> C>,
}
impl<C: Component> TypedFactory<C> {
#[must_use]
pub fn new(name: impl Into<String>, extra_deps: Vec<String>) -> Self {
Self {
name: name.into(),
extra_deps,
_marker: std::marker::PhantomData,
}
}
}
#[async_trait]
impl<C: Component> ComponentFactory for TypedFactory<C> {
fn name(&self) -> &str {
&self.name
}
fn kind(&self) -> &'static str {
C::NAME
}
fn depends_on(&self) -> Vec<String> {
let mut deps: Vec<String> = C::depends_on().iter().map(|s| (*s).to_string()).collect();
for d in &self.extra_deps {
if !deps.contains(d) {
deps.push(d.clone());
}
}
deps
}
async fn build(
self: Box<Self>,
config: serde_json::Value,
ctx: &ComponentContext,
) -> Result<Box<dyn AnyComponent>, RegistryError> {
let name = self.name.clone();
let cfg: C::Config = serde_json::from_value(config).map_err(|e| RegistryError::Init {
name: name.clone(),
message: format!("config deserialize: {e}"),
})?;
let instance = C::init(&cfg, ctx).await.map_err(|e| RegistryError::Init {
name: name.clone(),
message: e.to_string(),
})?;
Ok(Box::new(TypedAnyComponent {
name,
kind: C::NAME,
instance: Arc::new(instance),
}))
}
}
pub struct TypedAnyComponent<C: Component> {
name: String,
kind: &'static str,
instance: Arc<C>,
}
impl<C: Component> TypedAnyComponent<C> {
#[must_use]
pub fn new(instance: C) -> Self {
Self {
name: C::NAME.to_owned(),
kind: C::NAME,
instance: Arc::new(instance),
}
}
}
#[async_trait]
impl<C: Component> AnyComponent for TypedAnyComponent<C> {
fn name(&self) -> &'static str {
self.kind
}
fn as_any_arc(&self) -> Arc<dyn Any + Send + Sync> {
self.instance.clone()
}
fn start(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
let name = self.name.clone();
let instance = self.instance.clone();
Box::pin(async move {
instance
.start()
.await
.map_err(|e| AnyComponentError::Component {
name,
message: e.to_string(),
})
})
}
fn stop(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
let name = self.name.clone();
let instance = self.instance.clone();
Box::pin(async move {
instance
.stop()
.await
.map_err(|e| AnyComponentError::Component {
name,
message: e.to_string(),
})
})
}
fn health(&self) -> BoxFuture<'_, HealthStatus> {
let instance = self.instance.clone();
Box::pin(async move { instance.health().await })
}
fn pre_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
let name = self.name.clone();
let instance = self.instance.clone();
Box::pin(async move {
instance
.pre_replace_hook()
.await
.map_err(|e| AnyComponentError::Component {
name,
message: e.to_string(),
})
})
}
fn post_replace(&self) -> BoxFuture<'_, Result<(), AnyComponentError>> {
let name = self.name.clone();
let instance = self.instance.clone();
Box::pin(async move {
instance
.post_replace_hook()
.await
.map_err(|e| AnyComponentError::Component {
name,
message: e.to_string(),
})
})
}
}
pub struct ComponentRegistry {
inner: Arc<RegistryInner>,
}
struct RegistryInner {
factories: RwLock<HashMap<String, Box<dyn ComponentFactory>>>,
descriptors: RwLock<HashMap<String, ComponentDescriptor>>,
instances: RwLock<HashMap<String, Arc<dyn AnyComponent>>>,
states: RwLock<HashMap<String, ComponentState>>,
topo: RwLock<Option<Vec<String>>>,
shutdown: ShutdownToken,
init_started: AtomicBool,
}
impl Default for ComponentRegistry {
fn default() -> Self {
Self::new()
}
}
impl ComponentRegistry {
#[must_use]
pub fn new() -> Self {
Self::with_shutdown(ShutdownToken::new())
}
#[must_use]
pub fn with_shutdown(shutdown: ShutdownToken) -> Self {
Self {
inner: Arc::new(RegistryInner {
factories: RwLock::new(HashMap::new()),
descriptors: RwLock::new(HashMap::new()),
instances: RwLock::new(HashMap::new()),
states: RwLock::new(HashMap::new()),
topo: RwLock::new(None),
shutdown,
init_started: AtomicBool::new(false),
}),
}
}
#[must_use]
pub fn shutdown(&self) -> ShutdownToken {
self.inner.shutdown.clone()
}
pub fn register_factory(
&self,
descriptor: ComponentDescriptor,
factory: Box<dyn ComponentFactory>,
) -> Result<(), RegistryError> {
let name = descriptor.name.clone();
{
let mut factories = self
.inner
.factories
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
if factories.contains_key(&name) {
return Err(RegistryError::AlreadyRegistered { name });
}
factories.insert(name.clone(), factory);
}
{
let mut descriptors = self
.inner
.descriptors
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
descriptors.insert(name.clone(), descriptor);
}
{
let mut states = self
.inner
.states
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
states.insert(name, ComponentState::Pending);
}
self.invalidate_topo();
Ok(())
}
pub fn register_typed<C: Component>(
&self,
instance_name: impl Into<String>,
config: serde_json::Value,
) -> Result<(), RegistryError> {
let name = instance_name.into();
let factory = TypedFactory::<C>::new(name.clone(), Vec::new());
let descriptor = ComponentDescriptor {
name: name.clone(),
depends_on: factory.depends_on(),
config,
};
self.register_factory(descriptor, Box::new(factory))
}
pub fn unregister(&self, name: &str) -> Result<ComponentDescriptor, RegistryError> {
{
let mut factories = self
.inner
.factories
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
factories.remove(name);
}
let descriptor = {
let mut descriptors = self
.inner
.descriptors
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
descriptors
.remove(name)
.ok_or_else(|| RegistryError::NotFound {
name: name.to_string(),
})?
};
{
let mut states = self
.inner
.states
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
states.remove(name);
}
self.invalidate_topo();
Ok(descriptor)
}
#[must_use]
pub fn len(&self) -> usize {
self.inner
.descriptors
.read()
.map(|m| m.len())
.unwrap_or_default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.recompute_topo().unwrap_or_default()
}
pub fn get<C: Component>(&self, name: &str) -> Result<Arc<C>, RegistryError> {
let instances = self
.inner
.instances
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
let instance = instances.get(name).ok_or_else(|| RegistryError::NotFound {
name: name.to_string(),
})?;
let any = instance.as_any_arc();
let type_id = any.type_id();
any.downcast::<C>()
.map_err(|_| RegistryError::TypeMismatch {
name: name.to_string(),
type_id,
})
}
#[must_use]
pub fn is_initialized(&self, name: &str) -> bool {
self.inner
.states
.read()
.ok()
.and_then(|m| m.get(name).copied())
.is_some_and(|s| {
matches!(
s,
ComponentState::Initialized
| ComponentState::Running
| ComponentState::Starting
| ComponentState::Stopping
| ComponentState::Stopped
)
})
}
#[must_use]
pub fn state_of(&self, name: &str) -> Option<ComponentState> {
self.inner.states.read().ok()?.get(name).copied()
}
pub async fn init_all(&self) -> Result<(), RegistryError> {
self.inner.init_started.store(true, Ordering::SeqCst);
let order = self.recompute_topo()?;
let ctx = ComponentContext::new(self.inner.shutdown.child());
let mut first_error: Option<RegistryError> = None;
for name in order {
let state = self.state_of(&name);
if matches!(
state,
Some(
ComponentState::Initialized
| ComponentState::Running
| ComponentState::Starting
| ComponentState::Stopping
| ComponentState::Stopped,
)
) {
continue;
}
let factory_box = {
let mut factories = self
.inner
.factories
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
factories.remove(&name)
};
let Some(factory_box) = factory_box else {
continue;
};
let config = {
let descriptors = self
.inner
.descriptors
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
descriptors
.get(&name)
.map(|d| d.config.clone())
.unwrap_or_default()
};
self.set_state(&name, ComponentState::Initializing);
match factory_box.build(config, &ctx).await {
Ok(any) => {
let arc: Arc<dyn AnyComponent> = any.into();
self.inner
.instances
.write()
.map_err(|_| RegistryError::LockPoisoned)?
.insert(name.clone(), arc);
self.set_state(&name, ComponentState::Initialized);
}
Err(e) => {
self.set_state(&name, ComponentState::Failed);
if first_error.is_none() {
first_error = Some(e);
}
}
}
}
first_error.map_or(Ok(()), Err)
}
pub async fn start_all(&self) -> Result<(), RegistryError> {
let order = self.recompute_topo()?;
let mut first_error: Option<RegistryError> = None;
for name in order {
let state = self.state_of(&name);
if !matches!(state, Some(ComponentState::Initialized)) {
continue;
}
self.set_state(&name, ComponentState::Starting);
let instance = {
let instances = self
.inner
.instances
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
instances.get(&name).cloned()
};
let Some(instance) = instance else {
continue;
};
match instance.start().await {
Ok(()) => {
self.set_state(&name, ComponentState::Running);
}
Err(e) => {
self.set_state(&name, ComponentState::Failed);
if first_error.is_none() {
first_error = Some(RegistryError::Start {
name: name.clone(),
message: e.to_string(),
});
}
}
}
}
first_error.map_or(Ok(()), Err)
}
pub async fn stop_all(&self) -> Result<(), RegistryError> {
let order = self.recompute_topo()?;
let mut first_error: Option<RegistryError> = None;
for name in order.into_iter().rev() {
let state = self.state_of(&name);
if !matches!(
state,
Some(ComponentState::Running | ComponentState::Initialized)
) {
continue;
}
self.set_state(&name, ComponentState::Stopping);
let instance = {
let instances = self
.inner
.instances
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
instances.get(&name).cloned()
};
let Some(instance) = instance else {
continue;
};
if let Err(e) = instance.stop().await {
if first_error.is_none() {
first_error = Some(RegistryError::Stop {
name: name.clone(),
message: e.to_string(),
});
}
}
self.set_state(&name, ComponentState::Stopped);
}
first_error.map_or(Ok(()), Err)
}
pub async fn replace_instance(
&self,
name: &str,
new_instance: Box<dyn AnyComponent>,
) -> Result<Arc<dyn AnyComponent>, RegistryError> {
let old_instance = {
let instances = self
.inner
.instances
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
instances
.get(name)
.cloned()
.ok_or_else(|| RegistryError::NotFound {
name: name.to_string(),
})?
};
if !matches!(self.state_of(name), Some(ComponentState::Running)) {
return Err(RegistryError::Reload {
name: name.to_string(),
message: "component is not in Running state".to_string(),
});
}
if let Err(e) = old_instance.pre_replace().await {
return Err(RegistryError::Reload {
name: name.to_string(),
message: format!("pre_replace hook failed: {e}"),
});
}
if let Err(e) = new_instance.start().await {
return Err(RegistryError::Reload {
name: name.to_string(),
message: format!("new instance start failed: {e}"),
});
}
let new_arc: Arc<dyn AnyComponent> = new_instance.into();
{
let mut instances = self
.inner
.instances
.write()
.map_err(|_| RegistryError::LockPoisoned)?;
instances.insert(name.to_string(), new_arc);
}
self.set_state(name, ComponentState::Running);
if let Err(e) = old_instance.post_replace().await {
tracing::warn!(
component = name,
error = %e,
"post_replace hook failed (best-effort; continuing)"
);
}
Ok(old_instance)
}
#[must_use]
pub async fn health(&self) -> HashMap<String, HealthStatus> {
let order = self.recompute_topo().unwrap_or_default();
let mut out = HashMap::new();
for name in order {
let instance = self
.inner
.instances
.read()
.ok()
.and_then(|m| m.get(&name).cloned());
if let Some(instance) = instance {
let h = instance.health().await;
out.insert(name, h);
} else {
out.insert(name, HealthStatus::unhealthy("not initialized"));
}
}
out
}
fn set_state(&self, name: &str, state: ComponentState) {
if let Ok(mut states) = self.inner.states.write() {
states.insert(name.to_string(), state);
}
}
fn invalidate_topo(&self) {
if let Ok(mut topo) = self.inner.topo.write() {
*topo = None;
}
}
pub fn recompute_topo(&self) -> Result<Vec<String>, RegistryError> {
if let Ok(topo) = self.inner.topo.read() {
if let Some(order) = topo.as_ref() {
return Ok(order.clone());
}
}
let descriptors = self
.inner
.descriptors
.read()
.map_err(|_| RegistryError::LockPoisoned)?;
if descriptors.is_empty() {
if let Ok(mut cached) = self.inner.topo.write() {
*cached = Some(Vec::new());
}
return Ok(Vec::new());
}
let mut graph: HashMap<String, Vec<String>> = HashMap::new();
let mut in_degree: HashMap<String, usize> = HashMap::new();
for (name, desc) in descriptors.iter() {
graph.entry(name.clone()).or_default();
in_degree.entry(name.clone()).or_insert(0);
for dep in &desc.depends_on {
if !descriptors.contains_key(dep) {
return Err(RegistryError::MissingDependency {
name: name.clone(),
dep: dep.clone(),
});
}
graph.entry(dep.clone()).or_default().push(name.clone());
*in_degree.entry(name.clone()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<String> = {
#[allow(clippy::filter_map_bool_then)]
in_degree
.iter()
.filter_map(|(n, d)| (*d == 0).then(|| n.clone()))
.collect()
};
let mut initial: Vec<String> = queue.drain(..).collect();
initial.sort_unstable();
for n in initial {
queue.push_back(n);
}
let mut order: Vec<String> = Vec::with_capacity(in_degree.len());
while let Some(n) = queue.pop_front() {
order.push(n.clone());
if let Some(deps) = graph.get(&n) {
for m in deps {
if let Some(d) = in_degree.get_mut(m) {
*d -= 1;
if *d == 0 {
queue.push_back(m.clone());
}
}
}
}
}
if order.len() != in_degree.len() {
let leftover: Vec<String> = {
#[allow(clippy::filter_map_bool_then)]
in_degree
.iter()
.filter_map(|(n, d)| (*d > 0).then(|| n.clone()))
.collect()
};
return Err(RegistryError::Cycle { cycle: leftover });
}
if let Ok(mut cached) = self.inner.topo.write() {
*cached = Some(order.clone());
}
Ok(order)
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct EchoConfig {
label: String,
}
struct EchoComponent {
label: String,
}
#[async_trait]
impl Component for EchoComponent {
const NAME: &'static str = "test.echo";
type Config = EchoConfig;
type Error = std::io::Error;
async fn init(cfg: &Self::Config, _ctx: &ComponentContext) -> Result<Self, Self::Error> {
Ok(Self {
label: cfg.label.clone(),
})
}
}
fn cfg(label: &str) -> serde_json::Value {
serde_json::json!({ "label": label })
}
#[tokio::test]
async fn register_init_start_stop_lifecycle() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("a-label"))
.unwrap_or_else(|e| panic!("{e}"));
reg.register_typed::<EchoComponent>("b", cfg("b-label"))
.unwrap_or_else(|e| panic!("{e}"));
reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
assert!(reg.is_initialized("a"));
assert!(reg.is_initialized("b"));
reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(reg.state_of("a"), Some(ComponentState::Running));
reg.stop_all().await.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(reg.state_of("a"), Some(ComponentState::Stopped));
}
#[tokio::test]
async fn topo_respects_dependencies() {
struct Dep;
#[async_trait]
impl Component for Dep {
const NAME: &'static str = "test.dep";
type Config = serde_json::Value;
type Error = std::io::Error;
async fn init(
_cfg: &Self::Config,
_ctx: &ComponentContext,
) -> Result<Self, Self::Error> {
Ok(Dep)
}
}
struct User;
#[async_trait]
impl Component for User {
const NAME: &'static str = "test.user";
type Config = serde_json::Value;
type Error = std::io::Error;
async fn init(
_cfg: &Self::Config,
_ctx: &ComponentContext,
) -> Result<Self, Self::Error> {
Ok(User)
}
}
let reg = ComponentRegistry::new();
reg.register_typed::<User>("user", serde_json::json!({}))
.unwrap_or_else(|e| panic!("{e}"));
reg.register_typed::<Dep>("dep", serde_json::json!({}))
.unwrap_or_else(|e| panic!("{e}"));
reg.unregister("user").unwrap_or_else(|e| panic!("{e}"));
reg.register_factory(
ComponentDescriptor {
name: "user".into(),
depends_on: vec!["dep".into()],
config: serde_json::json!({}),
},
Box::new(TypedFactory::<User>::new("user", vec!["dep".into()])),
)
.unwrap_or_else(|e| panic!("{e}"));
let order = reg.recompute_topo().unwrap_or_else(|e| panic!("{e}"));
assert_eq!(order, vec!["dep".to_string(), "user".to_string()]);
}
#[tokio::test]
async fn duplicate_registration_rejected() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("a"))
.unwrap_or_else(|e| panic!("{e}"));
let err = match reg.register_typed::<EchoComponent>("a", cfg("b")) {
Ok(_) => panic!("expected Err, got Ok"),
Err(e) => e,
};
assert!(matches!(err, RegistryError::AlreadyRegistered { .. }));
}
#[tokio::test]
async fn missing_dependency_detected() {
let reg = ComponentRegistry::new();
let factory = TypedFactory::<EchoComponent>::new("a", vec!["missing".to_string()]);
reg.register_factory(
ComponentDescriptor {
name: "a".into(),
depends_on: vec!["missing".into()],
config: cfg("a"),
},
Box::new(factory),
)
.unwrap_or_else(|e| panic!("{e}"));
let err = match reg.init_all().await {
Ok(_) => panic!("expected Err, got Ok"),
Err(e) => e,
};
assert!(matches!(err, RegistryError::MissingDependency { .. }));
}
#[tokio::test]
async fn cycle_detected() {
let reg = ComponentRegistry::new();
let factory_a = TypedFactory::<EchoComponent>::new("a", vec!["b".to_string()]);
let factory_b = TypedFactory::<EchoComponent>::new("b", vec!["a".to_string()]);
reg.register_factory(
ComponentDescriptor {
name: "a".into(),
depends_on: vec!["b".into()],
config: cfg("a"),
},
Box::new(factory_a),
)
.unwrap_or_else(|e| panic!("{e}"));
reg.register_factory(
ComponentDescriptor {
name: "b".into(),
depends_on: vec!["a".into()],
config: cfg("b"),
},
Box::new(factory_b),
)
.unwrap_or_else(|e| panic!("{e}"));
let err = match reg.init_all().await {
Ok(_) => panic!("expected Err, got Ok"),
Err(e) => e,
};
assert!(matches!(err, RegistryError::Cycle { .. }));
}
#[tokio::test]
async fn health_aggregates_initialized_components() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("a"))
.unwrap_or_else(|e| panic!("{e}"));
reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
reg.start_all().await.unwrap_or_else(|e| panic!("{e}"));
let h = reg.health().await;
assert!(h.get("a").map(|s| s.is_healthy()).unwrap_or(false));
}
#[tokio::test]
async fn get_downcasts_to_concrete_type() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("hello"))
.unwrap_or_else(|e| panic!("{e}"));
reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
let c: Arc<EchoComponent> = reg.get("a").unwrap_or_else(|e| panic!("{e}"));
assert_eq!(c.label, "hello");
}
#[tokio::test]
async fn get_returns_type_mismatch_on_wrong_type() {
#[derive(Debug)]
struct Other;
#[async_trait]
impl Component for Other {
const NAME: &'static str = "test.other";
type Config = serde_json::Value;
type Error = std::io::Error;
async fn init(
_cfg: &Self::Config,
_ctx: &ComponentContext,
) -> Result<Self, Self::Error> {
Ok(Other)
}
}
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("a"))
.unwrap_or_else(|e| panic!("{e}"));
reg.init_all().await.unwrap_or_else(|e| panic!("{e}"));
let err = match reg.get::<Other>("a") {
Ok(_) => panic!("expected Err, got Ok"),
Err(e) => e,
};
assert!(matches!(err, RegistryError::TypeMismatch { .. }));
}
#[tokio::test]
async fn unregister_removes_state() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("a", cfg("a"))
.unwrap_or_else(|e| panic!("{e}"));
reg.unregister("a").unwrap_or_else(|e| panic!("{e}"));
assert!(!reg.is_initialized("a"));
assert_eq!(reg.state_of("a"), None);
}
#[tokio::test]
async fn names_returns_topo_order() {
let reg = ComponentRegistry::new();
reg.register_typed::<EchoComponent>("zzz", cfg("z"))
.unwrap_or_else(|e| panic!("{e}"));
reg.register_typed::<EchoComponent>("aaa", cfg("a"))
.unwrap_or_else(|e| panic!("{e}"));
let names = reg.names();
assert_eq!(names.first().map(String::as_str), Some("aaa"));
assert_eq!(names.last().map(String::as_str), Some("zzz"));
}
}