scryer-engine 0.2.1

Tree-sitter and stack-graphs AST indexing engine for Scryer code intelligence
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;

use lru::LruCache;
use tokio::sync::{Mutex, RwLock};

use super::engine::{StackGraphEngine, load_rust_language};
use tree_sitter_stack_graphs::StackGraphLanguage;

/// In-memory cache of precomputed external dependency package files and partial paths.
#[derive(Debug, Clone)]
pub struct CachedDependencyPackage {
    pub package_id: u64,
    pub crate_name: String,
    pub entry_file: PathBuf,
    pub files: Vec<(PathBuf, String)>,
    pub paths_count: usize,
}

/// Thread-safe LRU-cached pool of active `StackGraphEngine` instances keyed by `project_id`.
pub struct ProjectEnginePool {
    engines: Arc<RwLock<LruCache<u64, Arc<Mutex<StackGraphEngine>>>>>,
    dependency_cache: Arc<RwLock<HashMap<u64, CachedDependencyPackage>>>,
    sgl: Arc<StackGraphLanguage>,
    max_active_engines: usize,
}

impl ProjectEnginePool {
    /// Create a new project engine pool with an LRU bound on concurrent active projects.
    pub fn new(max_active_engines: usize) -> anyhow::Result<Self> {
        let capacity = NonZeroUsize::new(max_active_engines.max(1))
            .ok_or_else(|| anyhow::anyhow!("Capacity must be non-zero"))?;
        let sgl = Arc::new(load_rust_language()?);

        Ok(Self {
            engines: Arc::new(RwLock::new(LruCache::new(capacity))),
            dependency_cache: Arc::new(RwLock::new(HashMap::new())),
            sgl,
            max_active_engines,
        })
    }

    /// Access the preloaded language rules.
    pub fn language(&self) -> &Arc<StackGraphLanguage> {
        &self.sgl
    }

    /// Maximum active engines capacity.
    pub fn capacity(&self) -> usize {
        self.max_active_engines
    }

    /// Current number of active engines in memory.
    pub async fn len(&self) -> usize {
        let guard = self.engines.read().await;
        guard.len()
    }

    /// Returns true if the pool currently contains no active engines.
    pub async fn is_empty(&self) -> bool {
        let guard = self.engines.read().await;
        guard.is_empty()
    }

    /// Get existing engine for `project_id` or instantiate a new, isolated engine on demand.
    pub async fn get_or_create(&self, project_id: u64) -> Arc<Mutex<StackGraphEngine>> {
        let mut guard = self.engines.write().await;
        if let Some(engine) = guard.get(&project_id) {
            return Arc::clone(engine);
        }

        let engine = Arc::new(Mutex::new(StackGraphEngine::new(Arc::clone(&self.sgl))));
        guard.put(project_id, Arc::clone(&engine));
        engine
    }

    /// Check if an engine is already in memory without promoting or allocating.
    pub async fn contains(&self, project_id: u64) -> bool {
        let guard = self.engines.read().await;
        guard.peek(&project_id).is_some()
    }

    /// Evict a specific project engine from memory (e.g. upon project deletion).
    pub async fn evict(&self, project_id: u64) -> Option<Arc<Mutex<StackGraphEngine>>> {
        let mut guard = self.engines.write().await;
        guard.pop(&project_id)
    }

    /// Clear all active project engines.
    pub async fn clear(&self) {
        let mut guard = self.engines.write().await;
        guard.clear();
    }

    /// Cache precomputed dependency package files and paths.
    pub async fn cache_dependency(&self, dep: CachedDependencyPackage) {
        let mut guard = self.dependency_cache.write().await;
        guard.insert(dep.package_id, dep);
    }

    /// Retrieve a cached dependency package by its ID.
    pub async fn get_cached_dependency(&self, package_id: u64) -> Option<CachedDependencyPackage> {
        let guard = self.dependency_cache.read().await;
        guard.get(&package_id).cloned()
    }

    /// Apply cached dependencies to an active `StackGraphEngine`.
    pub async fn apply_dependencies_to_engine(
        &self,
        engine: &mut StackGraphEngine,
        package_ids: &[u64],
    ) -> anyhow::Result<usize> {
        let guard = self.dependency_cache.read().await;
        let mut total_added = 0;

        for &pkg_id in package_ids {
            if let Some(dep) = guard.get(&pkg_id) {
                let _ = engine.register_crate_root(&dep.crate_name, &dep.entry_file);
                for (path, content) in &dep.files {
                    if let Ok(file_handle) = engine.add_file(path, content) {
                        engine.dependency_files.insert(file_handle);
                        engine
                            .file_to_crate
                            .insert(file_handle, dep.crate_name.clone());
                        total_added += 1;
                    }
                }
            }
        }

        Ok(total_added)
    }
}