use std::{path::Path, sync::Arc, time::Duration};
use sqry_core::graph::CodeGraph;
use tracing::warn;
pub trait SqrydHook: Send + Sync + std::fmt::Debug {
fn on_publish(&self, workspace_root: &Path, graph: Arc<CodeGraph>);
}
impl<T: SqrydHook + ?Sized> SqrydHook for Arc<T> {
fn on_publish(&self, workspace_root: &Path, graph: Arc<CodeGraph>) {
(**self).on_publish(workspace_root, graph);
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoOpHook;
impl SqrydHook for NoOpHook {
fn on_publish(&self, _workspace_root: &Path, _graph: Arc<CodeGraph>) {
}
}
pub type SharedHook = Arc<dyn SqrydHook>;
#[must_use]
pub fn noop_hook() -> SharedHook {
Arc::new(NoOpHook)
}
pub fn spawn_hook<F, Fut, E>(
timeout: Duration,
workspace_root: std::path::PathBuf,
task_label: &'static str,
fut_factory: F,
) where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<(), E>> + Send + 'static,
E: std::fmt::Display + Send + 'static,
{
tokio::spawn(async move {
let fut = fut_factory();
match tokio::time::timeout(timeout, fut).await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
warn!(
task = task_label,
workspace = %workspace_root.display(),
error = %err,
"sqryd hook {task_label} failed (absorbed; query path continues)",
);
}
Err(_elapsed) => {
warn!(
task = task_label,
workspace = %workspace_root.display(),
timeout_ms = timeout.as_millis() as u64,
"sqryd hook {task_label} timed out (absorbed; query path continues)",
);
}
}
});
}
#[doc(hidden)]
#[derive(Debug, Default)]
pub struct RecordingHook {
pub invocations: parking_lot::Mutex<Vec<std::path::PathBuf>>,
}
impl RecordingHook {
#[must_use]
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
#[must_use]
pub fn invocation_count(&self) -> usize {
self.invocations.lock().len()
}
#[must_use]
pub fn invocation_roots(&self) -> Vec<std::path::PathBuf> {
self.invocations.lock().clone()
}
}
impl SqrydHook for RecordingHook {
fn on_publish(&self, workspace_root: &Path, _graph: Arc<CodeGraph>) {
self.invocations.lock().push(workspace_root.to_path_buf());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_hook_compiles_through_shared_dispatch() {
let hook: SharedHook = noop_hook();
let graph = Arc::new(CodeGraph::new());
hook.on_publish(Path::new("/repos/example"), graph);
}
#[test]
fn recording_hook_captures_invocations_in_order() {
let hook = RecordingHook::new();
let graph = Arc::new(CodeGraph::new());
hook.on_publish(Path::new("/repos/a"), Arc::clone(&graph));
hook.on_publish(Path::new("/repos/b"), Arc::clone(&graph));
assert_eq!(hook.invocation_count(), 2);
let roots = hook.invocation_roots();
assert_eq!(roots[0], Path::new("/repos/a"));
assert_eq!(roots[1], Path::new("/repos/b"));
}
#[tokio::test]
async fn spawn_hook_absorbs_error() {
spawn_hook::<_, _, &'static str>(
Duration::from_millis(100),
std::path::PathBuf::from("/repos/example"),
"test-hook",
|| async { Err("simulated failure") },
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
#[tokio::test]
async fn spawn_hook_absorbs_timeout() {
spawn_hook::<_, _, &'static str>(
Duration::from_millis(10),
std::path::PathBuf::from("/repos/example"),
"test-hook",
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
}