khora_agents/asset_agent/
loader.rs1use anyhow::{anyhow, Result};
18use khora_core::asset::Asset;
19use khora_lanes::asset_lane::AssetLoaderLane;
20use khora_telemetry::{
21 metrics::registry::{CounterHandle, HistogramHandle},
22 MetricsRegistry, ScopedMetricTimer,
23};
24use std::{any::Any, collections::HashMap, sync::Arc};
25
26trait AnyLoaderLane: Send + Sync {
28 fn load_any(&self, bytes: &[u8], metrics: &LoaderMetrics) -> Result<Box<dyn Any + Send>>;
29}
30
31struct AssetLoaderLaneWrapper<A: Asset, L: AssetLoaderLane<A>>(L, std::marker::PhantomData<A>);
33
34impl<A: Asset, L: AssetLoaderLane<A> + Send + Sync> AnyLoaderLane for AssetLoaderLaneWrapper<A, L> {
35 fn load_any(&self, bytes: &[u8], metrics: &LoaderMetrics) -> Result<Box<dyn Any + Send>> {
36 let _timer = ScopedMetricTimer::new(&metrics.load_time_ms);
38
39 let asset: A = self.0.load(bytes).map_err(|e| anyhow!(e.to_string()))?;
41
42 metrics.assets_loaded_total.increment()?;
44
45 Ok(Box::new(asset))
47 }
48}
49
50struct LoaderMetrics {
52 load_time_ms: HistogramHandle,
54 assets_loaded_total: CounterHandle,
56}
57
58impl LoaderMetrics {
59 fn new(registry: &MetricsRegistry) -> Self {
60 Self {
61 load_time_ms: registry
62 .register_histogram(
63 "assets",
64 "load_time",
65 "Asset decoding time",
66 "ms",
67 vec![1.0, 5.0, 16.0, 33.0, 100.0, 500.0],
68 )
69 .expect("Failed to register asset load time metric"),
70 assets_loaded_total: registry
71 .register_counter(
72 "assets",
73 "loaded_total",
74 "Total number of assets loaded from disk",
75 )
76 .expect("Failed to register asset count metric"),
77 }
78 }
79}
80
81pub(crate) struct AssetLoaderLaneRegistry {
83 metrics: LoaderMetrics,
85 loaders: HashMap<String, Box<dyn AnyLoaderLane>>,
87}
88
89impl AssetLoaderLaneRegistry {
90 pub(crate) fn new(metrics_registry: Arc<MetricsRegistry>) -> Self {
92 Self {
93 loaders: HashMap::new(),
94 metrics: LoaderMetrics::new(&metrics_registry),
95 }
96 }
97
98 pub(crate) fn register<A: Asset>(
100 &mut self,
101 type_name: &str,
102 loader: impl AssetLoaderLane<A> + Send + Sync + 'static,
103 ) {
104 let wrapped = AssetLoaderLaneWrapper(loader, std::marker::PhantomData);
105 self.loaders
106 .insert(type_name.to_string(), Box::new(wrapped));
107 }
108
109 pub(crate) fn load<A: Asset>(&self, type_name: &str, bytes: &[u8]) -> Result<A> {
111 let loader = self
112 .loaders
113 .get(type_name)
114 .ok_or_else(|| anyhow!("No loader registered for asset type '{}'", type_name))?;
115
116 let asset_any = loader.load_any(bytes, &self.metrics)?;
117
118 let asset_boxed = asset_any.downcast::<A>().map_err(|_| {
119 anyhow!(
120 "Loader for type '{}' returned a different asset type than requested.",
121 type_name
122 )
123 })?;
124
125 Ok(*asset_boxed)
126 }
127}