rustbolt_sources/
cached_source.rs

1use std::{
2  borrow::Cow,
3  hash::{BuildHasherDefault, Hash, Hasher},
4  sync::{Arc, OnceLock},
5};
6
7use dashmap::{mapref::entry::Entry, DashMap};
8use rustc_hash::FxHasher;
9
10use crate::{
11  helpers::{
12    stream_and_get_source_and_map, stream_chunks_of_raw_source,
13    stream_chunks_of_source_map, StreamChunks,
14  },
15  rope::Rope,
16  MapOptions, Source, SourceMap,
17};
18
19/// It tries to reused cached results from other methods to avoid calculations,
20/// usually used after modify is finished.
21///
22/// - [webpack-sources docs](https://github.com/webpack/webpack-sources/#cachedsource).
23///
24/// ```
25/// use rustbolt_sources::{
26///   BoxSource, CachedSource, ConcatSource, MapOptions, OriginalSource,
27///   RawSource, Source, SourceExt, SourceMap,
28/// };
29///
30/// let mut concat = ConcatSource::new([
31///   RawSource::from("Hello World\n".to_string()).boxed(),
32///   OriginalSource::new(
33///     "console.log('test');\nconsole.log('test2');\n",
34///     "console.js",
35///   )
36///   .boxed(),
37/// ]);
38/// concat.add(OriginalSource::new("Hello2\n", "hello.md"));
39///
40/// let cached = CachedSource::new(concat);
41///
42/// assert_eq!(
43///   cached.source(),
44///   "Hello World\nconsole.log('test');\nconsole.log('test2');\nHello2\n"
45/// );
46/// // second time will be fast.
47/// assert_eq!(
48///   cached.source(),
49///   "Hello World\nconsole.log('test');\nconsole.log('test2');\nHello2\n"
50/// );
51/// ```
52pub struct CachedSource<T> {
53  inner: Arc<T>,
54  cached_hash: Arc<OnceLock<u64>>,
55  cached_maps:
56    Arc<DashMap<MapOptions, Option<SourceMap>, BuildHasherDefault<FxHasher>>>,
57}
58
59impl<T> CachedSource<T> {
60  /// Create a [CachedSource] with the original [Source].
61  pub fn new(inner: T) -> Self {
62    Self {
63      inner: Arc::new(inner),
64      cached_hash: Default::default(),
65      cached_maps: Default::default(),
66    }
67  }
68
69  /// Get the original [Source].
70  pub fn original(&self) -> &T {
71    &self.inner
72  }
73}
74
75impl<T: Source + Hash + PartialEq + Eq + 'static> Source for CachedSource<T> {
76  fn source(&self) -> Cow<str> {
77    self.inner.source()
78  }
79
80  fn rope(&self) -> Rope<'_> {
81    self.inner.rope()
82  }
83
84  fn buffer(&self) -> Cow<[u8]> {
85    self.inner.buffer()
86  }
87
88  fn size(&self) -> usize {
89    self.source().len()
90  }
91
92  fn map(&self, options: &MapOptions) -> Option<SourceMap> {
93    if let Some(map) = self.cached_maps.get(options) {
94      map.clone()
95    } else {
96      let map = self.inner.map(options);
97      self.cached_maps.insert(options.clone(), map.clone());
98      map
99    }
100  }
101
102  fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
103    self.inner.to_writer(writer)
104  }
105}
106
107impl<T: Source + Hash + PartialEq + Eq + 'static> StreamChunks
108  for CachedSource<T>
109{
110  fn stream_chunks<'a>(
111    &'a self,
112    options: &MapOptions,
113    on_chunk: crate::helpers::OnChunk<'_, 'a>,
114    on_source: crate::helpers::OnSource<'_, 'a>,
115    on_name: crate::helpers::OnName<'_, 'a>,
116  ) -> crate::helpers::GeneratedInfo {
117    let cached_map = self.cached_maps.entry(options.clone());
118    match cached_map {
119      Entry::Occupied(entry) => {
120        let source = self.rope();
121        if let Some(map) = entry.get() {
122          #[allow(unsafe_code)]
123          // SAFETY: We guarantee that once a `SourceMap` is stored in the cache, it will never be removed.
124          // Therefore, even if we force its lifetime to be longer, the reference remains valid.
125          // This is based on the following assumptions:
126          // 1. `SourceMap` will be valid for the entire duration of the application.
127          // 2. The cached `SourceMap` will not be manually removed or replaced, ensuring the reference's safety.
128          let map =
129            unsafe { std::mem::transmute::<&SourceMap, &'a SourceMap>(map) };
130          stream_chunks_of_source_map(
131            source, map, on_chunk, on_source, on_name, options,
132          )
133        } else {
134          stream_chunks_of_raw_source(
135            source, options, on_chunk, on_source, on_name,
136          )
137        }
138      }
139      Entry::Vacant(entry) => {
140        let (generated_info, map) = stream_and_get_source_and_map(
141          &self.inner as &T,
142          options,
143          on_chunk,
144          on_source,
145          on_name,
146        );
147        entry.insert(map);
148        generated_info
149      }
150    }
151  }
152}
153
154impl<T> Clone for CachedSource<T> {
155  fn clone(&self) -> Self {
156    Self {
157      inner: self.inner.clone(),
158      cached_hash: self.cached_hash.clone(),
159      cached_maps: self.cached_maps.clone(),
160    }
161  }
162}
163
164impl<T: Source + Hash + PartialEq + Eq + 'static> Hash for CachedSource<T> {
165  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
166    (self.cached_hash.get_or_init(|| {
167      let mut hasher = FxHasher::default();
168      self.inner.hash(&mut hasher);
169      hasher.finish()
170    }))
171    .hash(state);
172  }
173}
174
175impl<T: PartialEq> PartialEq for CachedSource<T> {
176  fn eq(&self, other: &Self) -> bool {
177    self.inner == other.inner
178  }
179}
180
181impl<T: Eq> Eq for CachedSource<T> {}
182
183impl<T: std::fmt::Debug> std::fmt::Debug for CachedSource<T> {
184  fn fmt(
185    &self,
186    f: &mut std::fmt::Formatter<'_>,
187  ) -> Result<(), std::fmt::Error> {
188    f.debug_struct("CachedSource")
189      .field("inner", self.inner.as_ref())
190      .field("cached_hash", self.cached_hash.as_ref())
191      .field("cached_maps", &(!self.cached_maps.is_empty()))
192      .finish()
193  }
194}
195
196#[cfg(test)]
197mod tests {
198  use crate::{
199    ConcatSource, OriginalSource, RawSource, ReplaceSource, SourceExt,
200    SourceMapSource, WithoutOriginalOptions,
201  };
202
203  use super::*;
204
205  #[test]
206  fn line_number_should_not_add_one() {
207    let source = ConcatSource::new([
208      CachedSource::new(RawSource::from("\n")).boxed(),
209      SourceMapSource::new(WithoutOriginalOptions {
210        value: "\nconsole.log(1);\n".to_string(),
211        name: "index.js".to_string(),
212        source_map: SourceMap::new(
213          ";AACA",
214          vec!["index.js".into()],
215          vec!["// DELETE IT\nconsole.log(1)".into()],
216          vec![],
217        ),
218      })
219      .boxed(),
220    ]);
221    let map = source.map(&Default::default()).unwrap();
222    assert_eq!(map.mappings(), ";;AACA");
223  }
224
225  #[test]
226  fn should_allow_to_store_and_share_cached_data() {
227    let original = OriginalSource::new("Hello World", "test.txt");
228    let source = CachedSource::new(original);
229    let clone = source.clone();
230
231    // fill up cache
232    let map_options = MapOptions::default();
233    source.source();
234    source.buffer();
235    source.size();
236    source.map(&map_options);
237
238    assert_eq!(
239      *clone.cached_maps.get(&map_options).unwrap().value(),
240      source.map(&map_options)
241    );
242  }
243
244  #[test]
245  fn should_return_the_correct_size_for_binary_files() {
246    let source = OriginalSource::new(
247      String::from_utf8(vec![0; 256]).unwrap(),
248      "file.wasm",
249    );
250    let cached_source = CachedSource::new(source);
251
252    assert_eq!(cached_source.size(), 256);
253    assert_eq!(cached_source.size(), 256);
254  }
255
256  #[test]
257  fn should_return_the_correct_size_for_cached_binary_files() {
258    let source = OriginalSource::new(
259      String::from_utf8(vec![0; 256]).unwrap(),
260      "file.wasm",
261    );
262    let cached_source = CachedSource::new(source);
263
264    cached_source.source();
265    assert_eq!(cached_source.size(), 256);
266    assert_eq!(cached_source.size(), 256);
267  }
268
269  #[test]
270  fn should_return_the_correct_size_for_text_files() {
271    let source = OriginalSource::new("TestTestTest", "file.js");
272    let cached_source = CachedSource::new(source);
273
274    assert_eq!(cached_source.size(), 12);
275    assert_eq!(cached_source.size(), 12);
276  }
277
278  #[test]
279  fn should_return_the_correct_size_for_cached_text_files() {
280    let source = OriginalSource::new("TestTestTest", "file.js");
281    let cached_source = CachedSource::new(source);
282
283    cached_source.source();
284    assert_eq!(cached_source.size(), 12);
285    assert_eq!(cached_source.size(), 12);
286  }
287
288  #[test]
289  fn should_produce_correct_output_for_cached_raw_source() {
290    let map_options = MapOptions {
291      columns: true,
292      final_source: true,
293    };
294
295    let source = RawSource::from("Test\nTest\nTest\n");
296    let mut on_chunk_count = 0;
297    let mut on_source_count = 0;
298    let mut on_name_count = 0;
299    let generated_info = source.stream_chunks(
300      &map_options,
301      &mut |_chunk, _mapping| {
302        on_chunk_count += 1;
303      },
304      &mut |_source_index, _source, _source_content| {
305        on_source_count += 1;
306      },
307      &mut |_name_index, _name| {
308        on_name_count += 1;
309      },
310    );
311
312    let cached_source = CachedSource::new(source);
313    cached_source.stream_chunks(
314      &map_options,
315      &mut |_chunk, _mapping| {},
316      &mut |_source_index, _source, _source_content| {},
317      &mut |_name_index, _name| {},
318    );
319
320    let mut cached_on_chunk_count = 0;
321    let mut cached_on_source_count = 0;
322    let mut cached_on_name_count = 0;
323    let cached_generated_info = cached_source.stream_chunks(
324      &map_options,
325      &mut |_chunk, _mapping| {
326        cached_on_chunk_count += 1;
327      },
328      &mut |_source_index, _source, _source_content| {
329        cached_on_source_count += 1;
330      },
331      &mut |_name_index, _name| {
332        cached_on_name_count += 1;
333      },
334    );
335
336    assert_eq!(on_chunk_count, cached_on_chunk_count);
337    assert_eq!(on_source_count, cached_on_source_count);
338    assert_eq!(on_name_count, cached_on_name_count);
339    assert_eq!(generated_info, cached_generated_info);
340  }
341
342  #[test]
343  fn should_have_correct_buffer_if_cache_buffer_from_cache_source() {
344    let buf = vec![128u8];
345    let source = CachedSource::new(RawSource::from(buf.clone()));
346
347    source.source();
348    assert_eq!(source.buffer(), buf.as_slice());
349  }
350
351  #[test]
352  fn hash_should_different_when_map_are_different() {
353    let hash1 = {
354      let mut source =
355        ReplaceSource::new(OriginalSource::new("Hello", "hello.txt").boxed());
356      source.insert(5, " world", None);
357      let cache = CachedSource::new(source);
358      let mut hasher = FxHasher::default();
359      cache.hash(&mut hasher);
360      hasher.finish()
361    };
362
363    let hash2 = {
364      let source = OriginalSource::new("Hello world", "hello.txt").boxed();
365      let cache = CachedSource::new(source);
366      let mut hasher = FxHasher::default();
367      cache.hash(&mut hasher);
368      hasher.finish()
369    };
370
371    assert!(hash1 != hash2);
372  }
373}