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
19pub 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 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 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 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 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}