use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use super::hooks::events::{HookEvent, HookResult};
use super::manager::{ExtensionLoadFailure, ExtensionManager};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtensionLoaderEvent {
Started,
Loaded {
plugin: String,
loaded: usize,
failed: usize,
},
Failed {
failure: ExtensionLoadFailure,
loaded: usize,
failed: usize,
},
Finished {
loaded: Vec<String>,
failed: Vec<ExtensionLoadFailure>,
},
}
impl ExtensionLoaderEvent {
pub fn progress_counts(&self) -> Option<(usize, usize)> {
match self {
ExtensionLoaderEvent::Started => Some((0, 0)),
ExtensionLoaderEvent::Loaded { loaded, failed, .. } => Some((*loaded, *failed)),
ExtensionLoaderEvent::Failed { loaded, failed, .. } => Some((*loaded, *failed)),
ExtensionLoaderEvent::Finished { loaded, failed } => Some((loaded.len(), failed.len())),
}
}
}
pub async fn emit_session_start(hook_bus: &Arc<super::hooks::HookBus>, session_id: &str) -> bool {
let event = HookEvent::on_session_start(session_id);
if let HookResult::Inject { content } = hook_bus.emit(&event).await {
tracing::debug!(
len = content.len(),
"on_session_start injected session-scoped context"
);
hook_bus
.set_session_injection_for(session_id, content)
.await;
return true;
}
false
}
pub async fn emit_session_end(
hook_bus: &Arc<super::hooks::HookBus>,
session_id: &str,
transcript: Option<Vec<crate::SharedMessage>>,
budget: std::time::Duration,
) -> bool {
let event = HookEvent::on_session_end(session_id, transcript);
let completed = match tokio::time::timeout(budget, hook_bus.emit_concurrent(&event)).await {
Ok(_) => {
tracing::debug!(session = %session_id, "on_session_end hooks completed");
true
}
Err(_elapsed) => {
tracing::warn!(
session = %session_id,
budget_secs = budget.as_secs(),
"on_session_end hooks timed out — extensions may not have flushed"
);
false
}
};
hook_bus.clear_session_injection(session_id).await;
completed
}
pub fn spawn_discover_and_load(
manager: Arc<RwLock<ExtensionManager>>,
tx: mpsc::UnboundedSender<ExtensionLoaderEvent>,
session_id: Option<String>,
) -> tokio::task::JoinHandle<()> {
let ready_guard = crate::host::EngineHost::current()
.filter(|host| Arc::ptr_eq(host.ext_manager(), &manager))
.map(|host| host.extensions_loading_guard());
tokio::spawn(async move {
let _ = tx.send(ExtensionLoaderEvent::Started);
let (loaded, failed) = manager
.write()
.await
.discover_and_load_with_progress(|event| {
let _ = tx.send(event);
})
.await;
if let Some(session_id) = session_id {
let hook_bus = manager.read().await.hook_bus().clone();
emit_session_start(&hook_bus, &session_id).await;
}
drop(ready_guard);
let _ = tx.send(ExtensionLoaderEvent::Finished { loaded, failed });
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn progress_counts_report_finished_totals() {
let event = ExtensionLoaderEvent::Finished {
loaded: vec!["a".into(), "b".into()],
failed: vec![ExtensionLoadFailure {
plugin: "bad".into(),
manifest_path: None,
reason: "oops".into(),
hint: "fix it".into(),
}],
};
assert_eq!(event.progress_counts(), Some((2, 1)));
}
}