1use std::collections::BTreeMap;
9
10use skif_core::HashMap;
11use std::hash::Hasher as _;
12use std::path::Path;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
15use std::thread::{self, ThreadId};
16
17use serde::{Deserialize, Serialize};
18use siphasher::sip128::{Hasher128 as _, SipHasher13};
19
20use crate::error::SyntaxError;
21use crate::model::{ModuleInfo, ParseOptions};
22use crate::style::{is_stylesheet, parse_stylesheet};
23
24pub const CACHE_SCHEMA: u32 = 5;
28
29pub const CACHE_SCHEMA_DIGEST: u128 = 0x5590_3ede_e8ea_3a95_93dd_202f_6d05_dbd5;
32
33#[must_use]
36pub fn cache_version() -> String {
37 format!("{}+{CACHE_SCHEMA}", env!("CARGO_PKG_VERSION"))
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct ModuleKey(u128);
45
46impl ModuleKey {
47 #[must_use]
49 pub fn new(path: &Path, source: &str, options: &ParseOptions) -> Self {
50 let ParseOptions {
52 in_file_references,
53 member_accesses,
54 } = *options;
55 let mut hasher = SipHasher13::new_with_keys(0, 0);
56 hasher.write(&CACHE_SCHEMA.to_le_bytes());
57 write_field(&mut hasher, env!("CARGO_PKG_VERSION").as_bytes());
58 hasher.write_u8(u8::from(is_stylesheet(path)));
59 hasher.write_u8(u8::from(in_file_references));
60 hasher.write_u8(u8::from(member_accesses));
61 write_field(
62 &mut hasher,
63 path.file_name().map_or_default(std::ffi::OsStr::as_encoded_bytes),
64 );
65 write_field(&mut hasher, source.as_bytes());
66 Self(hasher.finish128().as_u128())
67 }
68
69 #[must_use]
71 pub const fn to_bytes(self) -> [u8; 16] {
72 self.0.to_le_bytes()
73 }
74
75 #[must_use]
77 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
78 Self(u128::from_le_bytes(bytes))
79 }
80}
81
82fn write_field(hasher: &mut SipHasher13, bytes: &[u8]) {
84 hasher.write(&(bytes.len() as u64).to_le_bytes());
85 hasher.write(bytes);
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(transparent)]
92pub struct CachedModule(ModuleInfo);
93
94pub trait ModuleCache: Send + Sync {
99 fn get(&self, key: ModuleKey) -> Option<CachedModule>;
101 fn put(&self, key: ModuleKey, module: CachedModule);
103}
104
105#[derive(Debug, Default)]
110pub struct MemoryModuleCache {
111 entries: Mutex<BTreeMap<ModuleKey, (Arc<CachedModule>, bool)>>,
112}
113
114impl MemoryModuleCache {
115 #[must_use]
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 #[must_use]
123 pub fn len(&self) -> usize {
124 self.entries.lock().unwrap_or_else(PoisonError::into_inner).len()
125 }
126
127 #[must_use]
129 pub fn is_empty(&self) -> bool {
130 self.len() == 0
131 }
132
133 pub fn prune_unused(&self) {
135 let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
136 entries.retain(|_, (_, used)| std::mem::replace(used, false));
137 }
138}
139
140impl ModuleCache for MemoryModuleCache {
141 fn get(&self, key: ModuleKey) -> Option<CachedModule> {
142 let found = {
143 let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
144 let (module, used) = entries.get_mut(&key)?;
145 *used = true;
146 Arc::clone(module)
147 };
148 Some((*found).clone())
150 }
151
152 fn put(&self, key: ModuleKey, module: CachedModule) {
153 self.entries
154 .lock()
155 .unwrap_or_else(PoisonError::into_inner)
156 .insert(key, (Arc::new(module), true));
157 }
158}
159
160#[derive(Default)]
174pub struct ParseCoalescer {
175 slots: Mutex<HashMap<ModuleKey, Arc<Slot>>>,
176}
177
178struct Slot {
180 claimer: ThreadId,
182 waiters: AtomicUsize,
185 outcome: Mutex<Option<Outcome>>,
186 ready: Condvar,
187}
188
189#[derive(Clone)]
191enum Outcome {
192 Parsed(Arc<CachedModule>),
193 Failed(SyntaxError),
194 Abandoned,
196}
197
198enum Turn<'a> {
200 Wait(Arc<Slot>),
202 Parse(Claim<'a>),
204 Inline,
206}
207
208impl ParseCoalescer {
209 #[must_use]
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn parse_cached(
220 &self,
221 cache: &dyn ModuleCache,
222 path: &Path,
223 source: &str,
224 options: &ParseOptions,
225 ) -> Result<ModuleInfo, SyntaxError> {
226 let key = ModuleKey::new(path, source, options);
227 self.get_or_parse(cache, key, &mut || {
228 parse_uncached(path, source, *options).map(CachedModule)
229 })
230 .map(|CachedModule(info)| info)
231 }
232
233 fn get_or_parse(
236 &self,
237 cache: &dyn ModuleCache,
238 key: ModuleKey,
239 parse: &mut dyn FnMut() -> Result<CachedModule, SyntaxError>,
240 ) -> Result<CachedModule, SyntaxError> {
241 loop {
242 if let Some(found) = cache.get(key) {
243 return Ok(found);
244 }
245 let slot = match self.join_or_claim(key) {
246 Turn::Wait(slot) => slot,
247 Turn::Parse(mut claim) => {
248 let result = match cache.get(key) {
251 Some(found) => Ok(found),
252 None => parse().inspect(|module| cache.put(key, module.clone())),
253 };
254 claim.release(|| match &result {
255 Ok(module) => Outcome::Parsed(Arc::new(module.clone())),
256 Err(error) => Outcome::Failed(error.clone()),
257 });
258 return result;
259 }
260 Turn::Inline => return parse(),
261 };
262 match slot.wait() {
263 Outcome::Parsed(module) => return Ok(Arc::unwrap_or_clone(module)),
264 Outcome::Failed(error) => return Err(error),
265 Outcome::Abandoned => {}
266 }
267 }
268 }
269
270 fn slots(&self) -> MutexGuard<'_, HashMap<ModuleKey, Arc<Slot>>> {
271 self.slots.lock().unwrap_or_else(PoisonError::into_inner)
272 }
273
274 fn join_or_claim(&self, key: ModuleKey) -> Turn<'_> {
276 let current = thread::current().id();
277 let mut slots = self.slots();
278 if let Some(slot) = slots.get(&key) {
279 if slot.claimer == current {
280 return Turn::Inline;
281 }
282 slot.waiters.fetch_add(1, Ordering::Relaxed);
283 return Turn::Wait(Arc::clone(slot));
284 }
285 let slot = Arc::new(Slot {
286 claimer: current,
287 waiters: AtomicUsize::new(0),
288 outcome: Mutex::new(None),
289 ready: Condvar::new(),
290 });
291 slots.insert(key, Arc::clone(&slot));
292 Turn::Parse(Claim {
293 coalescer: self,
294 key,
295 slot,
296 released: false,
297 })
298 }
299}
300
301impl std::fmt::Debug for ParseCoalescer {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 f.debug_struct("ParseCoalescer")
304 .field("parsing", &self.slots().len())
305 .finish_non_exhaustive()
306 }
307}
308
309impl Slot {
310 fn wait(&self) -> Outcome {
312 let mut outcome = self.outcome.lock().unwrap_or_else(PoisonError::into_inner);
313 loop {
314 if let Some(outcome) = outcome.as_ref() {
315 return outcome.clone();
316 }
317 outcome = self.ready.wait(outcome).unwrap_or_else(PoisonError::into_inner);
318 }
319 }
320}
321
322struct Claim<'a> {
325 coalescer: &'a ParseCoalescer,
326 key: ModuleKey,
327 slot: Arc<Slot>,
328 released: bool,
329}
330
331impl Claim<'_> {
332 fn release(&mut self, outcome: impl FnOnce() -> Outcome) {
335 self.released = true;
336 self.coalescer.slots().remove(&self.key);
337 if self.slot.waiters.load(Ordering::Relaxed) == 0 {
338 return;
339 }
340 *self.slot.outcome.lock().unwrap_or_else(PoisonError::into_inner) = Some(outcome());
341 self.slot.ready.notify_all();
342 }
343}
344
345impl Drop for Claim<'_> {
346 fn drop(&mut self) {
347 if !self.released {
348 self.release(|| Outcome::Abandoned);
349 }
350 }
351}
352
353pub fn parse_cached(
360 cache: Option<&dyn ModuleCache>,
361 path: &Path,
362 source: &str,
363 options: &ParseOptions,
364) -> Result<ModuleInfo, SyntaxError> {
365 let Some(cache) = cache else {
366 return parse_uncached(path, source, *options);
367 };
368 let key = ModuleKey::new(path, source, options);
369 if let Some(CachedModule(info)) = cache.get(key) {
370 return Ok(info);
371 }
372 let info = parse_uncached(path, source, *options)?;
373 cache.put(key, CachedModule(info.clone()));
374 Ok(info)
375}
376
377fn parse_uncached(path: &Path, source: &str, options: ParseOptions) -> Result<ModuleInfo, SyntaxError> {
379 if is_stylesheet(path) {
380 Ok(parse_stylesheet(path, source))
381 } else {
382 crate::parse_module(path, source, &options)
383 }
384}
385
386const _: fn() = || {
387 fn assert_send_sync<T: Send + Sync>() {}
388 assert_send_sync::<ModuleKey>();
389 assert_send_sync::<CachedModule>();
390 assert_send_sync::<MemoryModuleCache>();
391 assert_send_sync::<ParseCoalescer>();
392};
393
394#[cfg(test)]
395#[allow(clippy::unwrap_used, clippy::expect_used)]
396mod tests {
397 use std::path::PathBuf;
398 use std::sync::atomic::{AtomicUsize, Ordering};
399
400 use super::*;
401
402 fn probe_corpus() -> Vec<(PathBuf, String)> {
405 let inline: [(&str, &str); 16] = [
406 (
407 "a.ts",
408 "import { a, type B } from './b';\nimport * as ns from 'ns';\nexport const c = a + ns.x;\n\
409 export enum E { A, B }\nexport function f(): B { return E.A as never; }\n\
410 export default class K {}\n",
411 ),
412 (
413 "b.tsx",
414 "'use client';\nimport React from 'react';\nexport const App = () => <div>{React.version}</div>;\n",
415 ),
416 (
417 "c.js",
418 "const x = require('x');\nmodule.exports = { x };\nimport('./lazy');\nimport(`./dir/${x}`);\n",
419 ),
420 (
421 "d.d.ts",
422 "declare module 'virtual:thing' {\n export const v: number;\n}\n",
423 ),
424 (
425 "e.mjs",
426 "export * from './e2';\nexport { default as g } from './g';\nimport.meta.glob('./*.js');\n",
427 ),
428 (
429 "f.vue",
430 "<script setup lang=\"ts\">\nimport Child from './Child.vue';\nimport { ref } from 'vue';\n\
431 const n = ref(0);\n</script>\n<template><Child :n=\"n\" /><app-card v-for=\"item in items\" />\
432 <component is=\"AppBadge\" /></template>\n<style lang=\"scss\">@use './theme';</style>\n",
433 ),
434 (
435 "g.svelte",
436 "<script>\n import Button from './Button.svelte';\n export let label;\n</script>\n\
437 <Button>{label}</Button>\n",
438 ),
439 (
440 "h.astro",
441 "---\nimport Layout from '../layouts/Layout.astro';\nconst title = 'x';\n---\n\
442 <Layout title={title}><p>hi</p></Layout>\n",
443 ),
444 (
445 "i.mdx",
446 "import { Chart } from './chart';\nexport const meta = { title: 'x' };\n\n# Title\n\n<Chart />\n",
447 ),
448 (
449 "j.tsrx",
450 "import { thing } from './thing';\nexport component App() { <div>{thing}</div> }\n",
451 ),
452 (
453 "k.css",
454 "@import './base.css';\n.a { composes: b from './other.css'; }\n",
455 ),
456 (
457 "l.scss",
458 "@use 'sass:math';\n@use './vars' as v;\n@forward './mixins';\n.a { b: v.$c; }\n",
459 ),
460 (
461 "n.marko",
462 "import format from './format';\nimport Card from '<card>';\n<div>${format(input.value)}</div>\n\
463 style.less {\n @import './theme.less';\n}\n",
464 ),
465 (
466 "m.test.ts",
467 "vi.mock('./x');\nvi.mock(import('./y'), () => ({}));\njest.requireActual('./z');\n\
468 jest.mock('v', () => ({}), { virtual: true });\n",
469 ),
470 (
471 "n.ts",
472 "import * as ns from './ns';\nimport * as chars from './chars';\n\
473 export const read = ns.E.A + chars.a;\nexport function scan(chars: string) { return chars.length; }\n\
474 export async function lazy() { const ns = await import('./ns'); return ns.b; }\n\
475 export interface Options { a: string }\nexport const Service = make();\n\
476 export type Service = { run(options: Options): void };\n",
477 ),
478 (
479 "o.mjs",
480 "import { execSync, spawn } from 'node:child_process';\nimport path from 'node:path';\n\
481 execSync('npm run build', { cwd: '/tmp/app' });\nspawn('tsc', ['-b'], { cwd: process.cwd() });\n\
482 execSync('pnpm exec rspack', { cwd: path.join(import.meta.dirname, '../sub') });\n",
483 ),
484 ];
485 let mut corpus: Vec<(PathBuf, String)> = inline
486 .iter()
487 .map(|(name, source)| (PathBuf::from(name), (*source).to_owned()))
488 .collect();
489 let bench = Path::new(env!("CARGO_MANIFEST_DIR")).join("benches").join("corpus");
490 let mut files: Vec<PathBuf> = std::fs::read_dir(&bench)
491 .unwrap()
492 .map(|entry| entry.unwrap().path())
493 .collect();
494 files.sort();
495 assert!(!files.is_empty(), "the bench corpus is empty");
496 for file in files {
497 let source = std::fs::read_to_string(&file).unwrap();
498 corpus.push((PathBuf::from(file.file_name().unwrap()), source));
499 }
500 corpus.push((PathBuf::from("broken.ts"), "export const = ;;; {{{".to_owned()));
502 corpus
503 }
504
505 fn key(name: &str, source: &str, options: ParseOptions) -> ModuleKey {
506 ModuleKey::new(Path::new(name), source, &options)
507 }
508
509 #[test]
510 fn the_key_changes_with_each_input() {
511 let options = ParseOptions::default();
512 let base = key("a.ts", "export const a = 1;", options);
513 let others = [
514 key("a.ts", "export const a = 2;", options),
515 key("b.ts", "export const a = 1;", options),
516 key("a.d.ts", "export const a = 1;", options),
517 key("a.tsx", "export const a = 1;", options),
518 key(
519 "a.ts",
520 "export const a = 1;",
521 ParseOptions {
522 in_file_references: false,
523 ..options
524 },
525 ),
526 key(
527 "a.ts",
528 "export const a = 1;",
529 ParseOptions {
530 member_accesses: false,
531 ..options
532 },
533 ),
534 ];
535 for other in others {
536 assert_ne!(base, other);
537 }
538 assert_ne!(key("a.css", "", options), key("a.js", "", options));
540 }
541
542 #[test]
543 fn the_key_ignores_the_directory() {
544 let options = ParseOptions::default();
545 assert_eq!(
546 ModuleKey::new(Path::new("/one/src/a.ts"), "x", &options),
547 ModuleKey::new(Path::new("/two/lib/a.ts"), "x", &options)
548 );
549 }
550
551 #[test]
552 fn the_key_round_trips_through_bytes() {
553 let key = key("a.ts", "x", ParseOptions::default());
554 assert_eq!(ModuleKey::from_bytes(key.to_bytes()), key);
555 }
556
557 #[test]
558 fn parse_results_round_trip_through_serde() {
559 for (path, source) in probe_corpus() {
560 let info = parse_cached(None, &path, &source, &ParseOptions::default()).unwrap();
561 let cached = CachedModule(info);
562 let json = serde_json::to_string(&cached).unwrap();
563 let back: CachedModule = serde_json::from_str(&json).unwrap();
564 assert_eq!(back, cached, "{}", path.display());
565 }
566 }
567
568 #[test]
569 fn the_cache_schema_matches_the_parse_output() {
570 let mut hasher = SipHasher13::new_with_keys(0, 0);
571 for (path, source) in probe_corpus() {
572 for options in [
573 ParseOptions::default(),
574 ParseOptions {
575 in_file_references: false,
576 member_accesses: false,
577 },
578 ] {
579 let info = parse_cached(None, &path, &source, &options).unwrap();
580 write_field(&mut hasher, path.as_os_str().as_encoded_bytes());
581 write_field(&mut hasher, &serde_json::to_vec(&info).unwrap());
582 }
583 }
584 let digest = hasher.finish128().as_u128();
585 assert_eq!(
586 digest, CACHE_SCHEMA_DIGEST,
587 "the parse output changed: raise CACHE_SCHEMA (now {CACHE_SCHEMA}) and set CACHE_SCHEMA_DIGEST to \
588 {digest:#034x}"
589 );
590 }
591
592 #[derive(Default)]
594 struct Counting {
595 inner: MemoryModuleCache,
596 hits: AtomicUsize,
597 puts: AtomicUsize,
598 }
599
600 impl ModuleCache for Counting {
601 fn get(&self, key: ModuleKey) -> Option<CachedModule> {
602 let found = self.inner.get(key);
603 if found.is_some() {
604 self.hits.fetch_add(1, Ordering::Relaxed);
605 }
606 found
607 }
608
609 fn put(&self, key: ModuleKey, module: CachedModule) {
610 self.puts.fetch_add(1, Ordering::Relaxed);
611 self.inner.put(key, module);
612 }
613 }
614
615 #[test]
616 fn parse_cached_stores_on_a_miss_and_returns_the_stored_result_on_a_hit() {
617 let cache = Counting::default();
618 let options = ParseOptions::default();
619 let path = Path::new("a.ts");
620 let first = parse_cached(Some(&cache), path, "import x from 'x'; export { x };", &options).unwrap();
621 let second = parse_cached(Some(&cache), path, "import x from 'x'; export { x };", &options).unwrap();
622 assert_eq!(first, second);
623 assert_eq!(cache.puts.load(Ordering::Relaxed), 1);
624 assert_eq!(cache.hits.load(Ordering::Relaxed), 1);
625 assert!(parse_cached(Some(&cache), Path::new("a.txt"), "", &options).is_err());
627 assert_eq!(cache.puts.load(Ordering::Relaxed), 1);
628 }
629
630 #[test]
631 fn prune_unused_keeps_only_the_entries_used_since_the_last_prune() {
632 let cache = MemoryModuleCache::new();
633 let options = ParseOptions::default();
634 let (a, b) = (key("a.ts", "a", options), key("b.ts", "b", options));
635 cache.put(a, CachedModule(ModuleInfo::default()));
636 cache.put(b, CachedModule(ModuleInfo::default()));
637 assert_eq!(cache.len(), 2);
638 cache.prune_unused();
640 assert_eq!(cache.len(), 2);
641 assert!(cache.get(a).is_some());
642 cache.prune_unused();
643 assert_eq!(cache.len(), 1);
644 assert!(cache.get(a).is_some());
645 assert!(cache.get(b).is_none());
646 cache.prune_unused();
647 cache.prune_unused();
648 assert!(cache.is_empty());
649 }
650
651 #[test]
652 fn parse_options_that_differ_do_not_share_an_entry() {
653 let cache = Counting::default();
654 let path = Path::new("a.ts");
655 let source = "const local = 1;\nexport const a = local;\nexport const b = a;\n";
656 let full = ParseOptions::default();
657 let bare = ParseOptions {
658 in_file_references: false,
659 member_accesses: false,
660 };
661 let with_references = parse_cached(Some(&cache), path, source, &full).unwrap();
662 let without = parse_cached(Some(&cache), path, source, &bare).unwrap();
663 assert_eq!(cache.puts.load(Ordering::Relaxed), 2);
664 assert_eq!(cache.hits.load(Ordering::Relaxed), 0);
665 assert_eq!(cache.inner.len(), 2);
666 assert_eq!(
668 parse_cached(Some(&cache), path, source, &full).unwrap(),
669 with_references
670 );
671 assert_eq!(parse_cached(Some(&cache), path, source, &bare).unwrap(), without);
672 assert_eq!(with_references, parse_cached(None, path, source, &full).unwrap());
673 assert_eq!(without, parse_cached(None, path, source, &bare).unwrap());
674 assert_eq!(cache.hits.load(Ordering::Relaxed), 2);
675 }
676
677 type Parse = Result<CachedModule, SyntaxError>;
678
679 fn module(source: &str) -> CachedModule {
681 CachedModule(crate::parse_module(Path::new("a.ts"), source, &ParseOptions::default()).unwrap())
682 }
683
684 fn unsupported() -> SyntaxError {
685 SyntaxError::Unsupported {
686 extension: "txt".to_owned(),
687 }
688 }
689
690 fn waiting(coalescer: &ParseCoalescer, key: ModuleKey) -> usize {
692 coalescer
693 .slots()
694 .get(&key)
695 .map_or(0, |slot| slot.waiters.load(Ordering::SeqCst))
696 }
697
698 fn until_waiting(coalescer: &ParseCoalescer, key: ModuleKey, count: usize, gave_up: impl Fn() -> bool) {
701 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
702 while waiting(coalescer, key) < count && !gave_up() {
703 assert!(
704 std::time::Instant::now() < deadline,
705 "{count} lookups never waited for the parse"
706 );
707 std::thread::yield_now();
708 }
709 }
710
711 fn must_not_parse() -> Parse {
713 panic!("a waiting lookup parsed the key itself")
714 }
715
716 fn claimer_and_waiter(
720 coalescer: &ParseCoalescer,
721 cache: &dyn ModuleCache,
722 key: ModuleKey,
723 finish: impl FnOnce() -> Parse + Send,
724 mut waiter_parse: impl FnMut() -> Parse + Send,
725 ) -> (std::thread::Result<Parse>, Parse) {
726 let (started, has_started) = std::sync::mpsc::channel();
727 let (release, released) = std::sync::mpsc::channel::<()>();
728 std::thread::scope(|scope| {
729 let first = scope.spawn(move || {
730 let mut finish = Some(finish);
731 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
732 coalescer.get_or_parse(cache, key, &mut || {
733 started.send(()).unwrap();
734 released.recv().unwrap();
735 finish.take().unwrap()()
736 })
737 }))
738 });
739 has_started.recv().unwrap();
740 let second = scope.spawn(move || coalescer.get_or_parse(cache, key, &mut waiter_parse));
741 until_waiting(coalescer, key, 1, || second.is_finished());
742 release.send(()).unwrap();
743 (first.join().unwrap(), second.join().unwrap())
744 })
745 }
746
747 #[test]
748 fn a_lookup_of_a_key_being_parsed_is_handed_that_parse() {
749 let store = Counting::default();
750 let coalescer = ParseCoalescer::new();
751 let key = key("a.ts", "a", ParseOptions::default());
752 let parsed = module("export const parsed = 1;");
753 let (first, second) = claimer_and_waiter(&coalescer, &store, key, || Ok(parsed.clone()), must_not_parse);
754 assert_eq!(first.unwrap(), Ok(parsed.clone()));
755 assert_eq!(second, Ok(parsed.clone()));
756 assert_eq!(store.puts.load(Ordering::Relaxed), 1);
758 assert_eq!(store.hits.load(Ordering::Relaxed), 0);
759 assert_eq!(store.inner.get(key), Some(parsed));
760 assert_eq!(waiting(&coalescer, key), 0);
761 assert!(coalescer.slots().is_empty());
762 }
763
764 #[test]
765 fn a_failed_parse_is_handed_to_its_waiters_and_stores_nothing() {
766 let store = Counting::default();
767 let coalescer = ParseCoalescer::new();
768 let key = key("a.ts", "a", ParseOptions::default());
769 let (first, second) = claimer_and_waiter(&coalescer, &store, key, || Err(unsupported()), must_not_parse);
770 assert_eq!(first.unwrap(), Err(unsupported()));
771 assert_eq!(second, Err(unsupported()));
772 assert_eq!(store.puts.load(Ordering::Relaxed), 0);
773 assert!(store.inner.is_empty());
774 let mut parses = 0;
776 let found = coalescer.get_or_parse(&store, key, &mut || {
777 parses += 1;
778 Ok(module("export const later = 1;"))
779 });
780 assert_eq!(parses, 1);
781 assert_eq!(found, Ok(module("export const later = 1;")));
782 assert_eq!(store.inner.len(), 1);
783 }
784
785 #[test]
786 fn a_panicking_parse_leaves_one_waiter_to_parse_the_key_again() {
787 let store = Counting::default();
788 let coalescer = ParseCoalescer::new();
789 let key = key("a.ts", "a", ParseOptions::default());
790 let waiter_parses = AtomicUsize::new(0);
791 let (first, second) = claimer_and_waiter(
792 &coalescer,
793 &store,
794 key,
795 || -> Parse { panic!("the parser failed") },
796 || {
797 waiter_parses.fetch_add(1, Ordering::SeqCst);
798 Ok(module("export const retried = 1;"))
799 },
800 );
801 assert!(first.is_err());
802 assert_eq!(second, Ok(module("export const retried = 1;")));
804 assert_eq!(waiter_parses.load(Ordering::SeqCst), 1);
805 assert_eq!(store.puts.load(Ordering::Relaxed), 1);
806 assert_eq!(store.inner.len(), 1);
807 assert_eq!(store.inner.get(key), Some(module("export const retried = 1;")));
808 assert!(coalescer.slots().is_empty());
809 }
810
811 #[test]
812 fn many_threads_asking_for_one_key_parse_it_once() {
813 let store = MemoryModuleCache::new();
814 let coalescer = ParseCoalescer::new();
815 let key = key("a.ts", "a", ParseOptions::default());
816 let parses = AtomicUsize::new(0);
817 let barrier = std::sync::Barrier::new(8);
818 let found: Vec<Parse> = std::thread::scope(|scope| {
819 let mut lookups = Vec::new();
820 for _ in 0..8 {
821 lookups.push(scope.spawn(|| {
822 barrier.wait();
823 coalescer.get_or_parse(&store, key, &mut || {
824 parses.fetch_add(1, Ordering::SeqCst);
825 until_waiting(&coalescer, key, 7, || false);
827 Ok(module("export const once = 1;"))
828 })
829 }));
830 }
831 lookups.into_iter().map(|lookup| lookup.join().unwrap()).collect()
832 });
833 assert_eq!(parses.load(Ordering::SeqCst), 1);
834 assert!(found.iter().all(|found| *found == Ok(module("export const once = 1;"))));
835 assert_eq!(store.len(), 1);
836 }
837
838 #[test]
839 fn a_lookup_of_a_key_its_own_thread_is_parsing_parses_inline() {
840 let store = MemoryModuleCache::new();
841 let coalescer = ParseCoalescer::new();
842 let key = key("a.ts", "a", ParseOptions::default());
843 let found = coalescer.get_or_parse(&store, key, &mut || {
844 coalescer.get_or_parse(&store, key, &mut || Ok(module("export const inner = 1;")))
845 });
846 assert_eq!(found, Ok(module("export const inner = 1;")));
847 assert_eq!(store.get(key), Some(module("export const inner = 1;")));
848 assert!(coalescer.slots().is_empty());
849 }
850
851 #[test]
852 fn parse_cached_through_a_coalescer_stores_once_and_reports_parse_errors() {
853 let store = Counting::default();
854 let coalescer = ParseCoalescer::new();
855 let options = ParseOptions::default();
856 let path = Path::new("a.ts");
857 let first = coalescer
858 .parse_cached(&store, path, "export const a = 1;", &options)
859 .unwrap();
860 let second = coalescer
861 .parse_cached(&store, path, "export const a = 1;", &options)
862 .unwrap();
863 assert_eq!(first, second);
864 assert_eq!(
865 first,
866 parse_cached(None, path, "export const a = 1;", &options).unwrap()
867 );
868 assert_eq!(store.puts.load(Ordering::Relaxed), 1);
869 assert_eq!(store.hits.load(Ordering::Relaxed), 1);
870 assert_eq!(
871 coalescer.parse_cached(&store, Path::new("a.txt"), "", &options),
872 Err(unsupported())
873 );
874 assert_eq!(store.puts.load(Ordering::Relaxed), 1);
875 }
876}