use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use skif_js_syntax::{
CachedModule, MemoryModuleCache, ModuleCache, ModuleInfo, ModuleKey, ParseCoalescer, ParseOptions, SyntaxError,
parse_cached,
};
#[derive(Clone)]
pub struct SharedModules {
cache: Arc<dyn ModuleCache>,
coalescer: Option<Arc<ParseCoalescer>>,
roots: Option<Arc<[PathBuf]>>,
}
impl SharedModules {
#[must_use]
pub fn for_run(roots: Vec<PathBuf>, host: Option<Arc<dyn ModuleCache>>) -> Option<Self> {
let several = roots.len() > 1;
let coalescer = several.then(|| Arc::new(ParseCoalescer::new()));
match host {
Some(cache) => Some(Self {
cache,
coalescer,
roots: None,
}),
None if several => Some(Self {
cache: Arc::new(MemoryModuleCache::new()),
coalescer,
roots: Some(roots.into()),
}),
None => None,
}
}
pub(crate) fn host(cache: Arc<dyn ModuleCache>) -> Self {
Self {
cache,
coalescer: None,
roots: None,
}
}
pub(crate) fn parser<'a>(&'a self, root: &'a Path) -> ModuleParser<'a> {
let local = self.roots.as_deref().map(|roots| Local {
root,
nested: roots
.iter()
.map(PathBuf::as_path)
.filter(|other| *other != root && other.starts_with(root))
.collect(),
});
ModuleParser {
cache: self.cache.as_ref(),
coalescer: self.coalescer.as_deref(),
local,
}
}
}
impl std::fmt::Debug for SharedModules {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedModules")
.field("coalesced", &self.coalescer.is_some())
.field("roots", &self.roots)
.finish_non_exhaustive()
}
}
struct Local<'a> {
root: &'a Path,
nested: Vec<&'a Path>,
}
impl Local<'_> {
fn contains(&self, path: &Path) -> bool {
path.starts_with(self.root) && !self.nested.iter().any(|nested| path.starts_with(nested))
}
}
pub(crate) struct ModuleParser<'a> {
cache: &'a dyn ModuleCache,
coalescer: Option<&'a ParseCoalescer>,
local: Option<Local<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Served {
Uncached,
Hit,
Parsed,
Waited,
}
impl ModuleParser<'_> {
pub(crate) fn parse(
&self,
path: &Path,
source: &str,
options: ParseOptions,
) -> (Result<ModuleInfo, SyntaxError>, Served) {
if self.local.as_ref().is_some_and(|local| local.contains(path)) {
return (parse_cached(None, path, source, &options), Served::Uncached);
}
let probe = Probe {
cache: self.cache,
found: AtomicBool::new(false),
stored: AtomicBool::new(false),
};
let parsed = match self.coalescer {
Some(coalescer) => coalescer.parse_cached(&probe, path, source, &options),
None => parse_cached(Some(&probe), path, source, &options),
};
let served = if probe.stored.into_inner() || parsed.is_err() {
Served::Parsed
} else if probe.found.into_inner() {
Served::Hit
} else {
Served::Waited
};
(parsed, served)
}
}
struct Probe<'a> {
cache: &'a dyn ModuleCache,
found: AtomicBool,
stored: AtomicBool,
}
impl ModuleCache for Probe<'_> {
fn get(&self, key: ModuleKey) -> Option<CachedModule> {
let found = self.cache.get(key);
if found.is_some() {
self.found.store(true, Ordering::Relaxed);
}
found
}
fn put(&self, key: ModuleKey, module: CachedModule) {
self.stored.store(true, Ordering::Relaxed);
self.cache.put(key, module);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::sync::Mutex;
use super::*;
#[derive(Default)]
struct Recording {
inner: MemoryModuleCache,
stored: Mutex<Vec<ModuleKey>>,
}
impl ModuleCache for Recording {
fn get(&self, key: ModuleKey) -> Option<CachedModule> {
self.inner.get(key)
}
fn put(&self, key: ModuleKey, module: CachedModule) {
self.stored.lock().unwrap().push(key);
self.inner.put(key, module);
}
}
const SOURCE: &str = "export const a = 1;\n";
fn roots(paths: &[&str]) -> Vec<PathBuf> {
paths.iter().map(PathBuf::from).collect()
}
fn served(modules: &SharedModules, root: &str, paths: &[&str]) -> Vec<Served> {
let parser = modules.parser(Path::new(root));
paths
.iter()
.map(|path| {
let (parsed, served) = parser.parse(Path::new(path), SOURCE, ParseOptions::default());
assert_eq!(
parsed.unwrap(),
parse_cached(None, Path::new(path), SOURCE, &ParseOptions::default()).unwrap()
);
served
})
.collect()
}
#[test]
fn one_root_without_a_host_cache_shares_nothing() {
assert!(SharedModules::for_run(roots(&["/r/a"]), None).is_none());
assert!(SharedModules::for_run(Vec::new(), None).is_none());
}
#[test]
fn only_a_run_over_several_roots_coalesces() {
let host = || Arc::new(MemoryModuleCache::new()) as Arc<dyn ModuleCache>;
let one = SharedModules::for_run(roots(&["/r/a"]), Some(host())).unwrap();
assert!(one.coalescer.is_none());
assert!(one.roots.is_none());
let several = SharedModules::for_run(roots(&["/r/a", "/r/b"]), Some(host())).unwrap();
assert!(several.coalescer.is_some());
assert!(several.roots.is_none());
let own = SharedModules::for_run(roots(&["/r/a", "/r/b"]), None).unwrap();
assert!(own.coalescer.is_some());
assert_eq!(own.roots.as_deref(), Some(roots(&["/r/a", "/r/b"]).as_slice()));
}
#[test]
fn the_cache_of_a_run_holds_only_the_files_a_root_does_not_own() {
let modules = SharedModules::for_run(roots(&["/r", "/r/nested", "/r/sibling"]), None).unwrap();
assert_eq!(
served(
&modules,
"/r",
&["/r/src/own.ts", "/r/nested/src/n.ts", "/shared/lib.ts"]
),
[Served::Uncached, Served::Parsed, Served::Parsed]
);
assert_eq!(
served(
&modules,
"/r/nested",
&["/r/nested/src/n.ts", "/shared/lib.ts", "/r/src/own.ts"]
),
[Served::Uncached, Served::Hit, Served::Parsed]
);
assert_eq!(
served(&modules, "/r/sibling", &["/r/sibling/x.ts", "/r/nested/src/n.ts"]),
[Served::Uncached, Served::Hit]
);
}
#[test]
fn a_host_cache_holds_every_file() {
let host = Arc::new(Recording::default());
let modules = SharedModules::for_run(
roots(&["/r/a", "/r/b"]),
Some(Arc::clone(&host) as Arc<dyn ModuleCache>),
)
.unwrap();
assert_eq!(
served(&modules, "/r/a", &["/r/a/own.ts", "/shared/lib.ts"]),
[Served::Parsed, Served::Parsed]
);
assert_eq!(
served(&modules, "/r/b", &["/r/a/own.ts", "/shared/lib.ts"]),
[Served::Hit, Served::Hit]
);
assert_eq!(host.stored.lock().unwrap().len(), 2);
assert_eq!(host.inner.len(), 2);
}
#[test]
fn a_parse_error_is_a_miss_that_stores_nothing() {
let host = Arc::new(Recording::default());
let modules = SharedModules::host(Arc::clone(&host) as Arc<dyn ModuleCache>);
let (parsed, served) =
modules
.parser(Path::new("/r"))
.parse(Path::new("/r/notes.txt"), "", ParseOptions::default());
assert!(matches!(parsed, Err(SyntaxError::Unsupported { .. })));
assert_eq!(served, Served::Parsed);
assert!(host.stored.lock().unwrap().is_empty());
}
}