summaryrefslogtreecommitdiff
path: root/cli/cache/emit.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-07-19 11:58:18 -0400
committerGitHub <noreply@github.com>2022-07-19 11:58:18 -0400
commit0ab262b901348e9251262a02bef17d14ed13b997 (patch)
treefc5a6e3926ea7480714cbc844098eca6c43c1ab5 /cli/cache/emit.rs
parente99d64acedb6e111d33f53599da494865978f1aa (diff)
feat: emit files on demand and fix racy emit (#15220)
Diffstat (limited to 'cli/cache/emit.rs')
-rw-r--r--cli/cache/emit.rs232
1 files changed, 185 insertions, 47 deletions
diff --git a/cli/cache/emit.rs b/cli/cache/emit.rs
index e1469b862..61039a966 100644
--- a/cli/cache/emit.rs
+++ b/cli/cache/emit.rs
@@ -1,71 +1,209 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+use std::path::PathBuf;
+
use deno_ast::ModuleSpecifier;
+use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
+use deno_core::serde_json;
+use serde::Deserialize;
+use serde::Serialize;
-use super::CacheType;
-use super::Cacher;
+use super::DiskCache;
+use super::FastInsecureHasher;
-/// Emit cache for a single file.
-#[derive(Debug, Clone, PartialEq)]
-pub struct SpecifierEmitCacheData {
+#[derive(Debug, Deserialize, Serialize)]
+struct EmitMetadata {
pub source_hash: String,
- pub text: String,
- pub map: Option<String>,
+ pub emit_hash: String,
+ // purge the cache between cli versions
+ pub cli_version: String,
}
-pub trait EmitCache {
- /// Gets the emit data from the cache.
- fn get_emit_data(
- &self,
- specifier: &ModuleSpecifier,
- ) -> Option<SpecifierEmitCacheData>;
- /// Sets the emit data in the cache.
- fn set_emit_data(
- &self,
- specifier: ModuleSpecifier,
- data: SpecifierEmitCacheData,
- ) -> Result<(), AnyError>;
- /// Gets the stored hash of the source of the provider specifier
- /// to tell if the emit is out of sync with the source.
- /// TODO(13302): this is actually not reliable and should be removed
- /// once switching to an sqlite db
- fn get_source_hash(&self, specifier: &ModuleSpecifier) -> Option<String>;
- /// Gets the emitted JavaScript of the TypeScript source.
- /// TODO(13302): remove this once switching to an sqlite db
- fn get_emit_text(&self, specifier: &ModuleSpecifier) -> Option<String>;
+/// The cache that stores previously emitted files.
+#[derive(Clone)]
+pub struct EmitCache {
+ disk_cache: DiskCache,
+ cli_version: String,
}
-impl<T: Cacher> EmitCache for T {
- fn get_emit_data(
+impl EmitCache {
+ pub fn new(disk_cache: DiskCache) -> Self {
+ Self {
+ disk_cache,
+ cli_version: crate::version::deno(),
+ }
+ }
+
+ /// Gets the emitted code with embedded sourcemap from the cache.
+ ///
+ /// The expected source hash is used in order to verify
+ /// that you're getting a value from the cache that is
+ /// for the provided source.
+ ///
+ /// Cached emits from previous CLI releases will not be returned
+ /// or emits that do not match the source.
+ pub fn get_emit_code(
&self,
specifier: &ModuleSpecifier,
- ) -> Option<SpecifierEmitCacheData> {
- Some(SpecifierEmitCacheData {
- source_hash: self.get_source_hash(specifier)?,
- text: self.get_emit_text(specifier)?,
- map: self.get(CacheType::SourceMap, specifier),
- })
+ expected_source_hash: u64,
+ ) -> Option<String> {
+ let meta_filename = self.get_meta_filename(specifier)?;
+ let emit_filename = self.get_emit_filename(specifier)?;
+
+ // load and verify the meta data file is for this source and CLI version
+ let bytes = self.disk_cache.get(&meta_filename).ok()?;
+ let meta: EmitMetadata = serde_json::from_slice(&bytes).ok()?;
+ if meta.source_hash != expected_source_hash.to_string()
+ || meta.cli_version != self.cli_version
+ {
+ return None;
+ }
+
+ // load and verify the emit is for the meta data
+ let emit_bytes = self.disk_cache.get(&emit_filename).ok()?;
+ if meta.emit_hash != compute_emit_hash(&emit_bytes) {
+ return None;
+ }
+
+ // everything looks good, return it
+ let emit_text = String::from_utf8(emit_bytes).ok()?;
+ Some(emit_text)
}
- fn get_source_hash(&self, specifier: &ModuleSpecifier) -> Option<String> {
- self.get(CacheType::Version, specifier)
+ /// Gets the filepath which stores the emit.
+ pub fn get_emit_filepath(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Option<PathBuf> {
+ Some(
+ self
+ .disk_cache
+ .location
+ .join(self.get_emit_filename(specifier)?),
+ )
}
- fn get_emit_text(&self, specifier: &ModuleSpecifier) -> Option<String> {
- self.get(CacheType::Emit, specifier)
+ /// Sets the emit code in the cache.
+ pub fn set_emit_code(
+ &self,
+ specifier: &ModuleSpecifier,
+ source_hash: u64,
+ code: &str,
+ ) {
+ if let Err(err) = self.set_emit_code_result(specifier, source_hash, code) {
+ // should never error here, but if it ever does don't fail
+ if cfg!(debug_assertions) {
+ panic!("Error saving emit data ({}): {}", specifier, err);
+ } else {
+ log::debug!("Error saving emit data({}): {}", specifier, err);
+ }
+ }
}
- fn set_emit_data(
+ fn set_emit_code_result(
&self,
- specifier: ModuleSpecifier,
- data: SpecifierEmitCacheData,
+ specifier: &ModuleSpecifier,
+ source_hash: u64,
+ code: &str,
) -> Result<(), AnyError> {
- self.set(CacheType::Version, &specifier, data.source_hash)?;
- self.set(CacheType::Emit, &specifier, data.text)?;
- if let Some(map) = data.map {
- self.set(CacheType::SourceMap, &specifier, map)?;
- }
+ let meta_filename = self
+ .get_meta_filename(specifier)
+ .ok_or_else(|| anyhow!("Could not get meta filename."))?;
+ let emit_filename = self
+ .get_emit_filename(specifier)
+ .ok_or_else(|| anyhow!("Could not get emit filename."))?;
+
+ // save the metadata
+ let metadata = EmitMetadata {
+ cli_version: self.cli_version.to_string(),
+ source_hash: source_hash.to_string(),
+ emit_hash: compute_emit_hash(code.as_bytes()),
+ };
+ self
+ .disk_cache
+ .set(&meta_filename, &serde_json::to_vec(&metadata)?)?;
+
+ // save the emit source
+ self.disk_cache.set(&emit_filename, code.as_bytes())?;
+
Ok(())
}
+
+ fn get_meta_filename(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
+ self
+ .disk_cache
+ .get_cache_filename_with_extension(specifier, "meta")
+ }
+
+ fn get_emit_filename(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
+ self
+ .disk_cache
+ .get_cache_filename_with_extension(specifier, "js")
+ }
+}
+
+fn compute_emit_hash(bytes: &[u8]) -> String {
+ // it's ok to use an insecure hash here because
+ // if someone can change the emit source then they
+ // can also change the version hash
+ FastInsecureHasher::new().write(bytes).finish().to_string()
+}
+
+#[cfg(test)]
+mod test {
+ use test_util::TempDir;
+
+ use super::*;
+
+ #[test]
+ pub fn emit_cache_general_use() {
+ let temp_dir = TempDir::new();
+ let disk_cache = DiskCache::new(temp_dir.path());
+ let cache = EmitCache {
+ disk_cache: disk_cache.clone(),
+ cli_version: "1.0.0".to_string(),
+ };
+
+ let specifier1 =
+ ModuleSpecifier::from_file_path(temp_dir.path().join("file1.ts"))
+ .unwrap();
+ let specifier2 =
+ ModuleSpecifier::from_file_path(temp_dir.path().join("file2.ts"))
+ .unwrap();
+ assert_eq!(cache.get_emit_code(&specifier1, 1), None);
+ let emit_code1 = "text1".to_string();
+ let emit_code2 = "text2".to_string();
+ cache.set_emit_code(&specifier1, 10, &emit_code1);
+ cache.set_emit_code(&specifier2, 2, &emit_code2);
+ // providing the incorrect source hash
+ assert_eq!(cache.get_emit_code(&specifier1, 5), None);
+ // providing the correct source hash
+ assert_eq!(
+ cache.get_emit_code(&specifier1, 10),
+ Some(emit_code1.clone()),
+ );
+ assert_eq!(cache.get_emit_code(&specifier2, 2), Some(emit_code2),);
+
+ // try changing the cli version (should not load previous ones)
+ let cache = EmitCache {
+ disk_cache: disk_cache.clone(),
+ cli_version: "2.0.0".to_string(),
+ };
+ assert_eq!(cache.get_emit_code(&specifier1, 10), None);
+ cache.set_emit_code(&specifier1, 5, &emit_code1);
+
+ // recreating the cache should still load the data because the CLI version is the same
+ let cache = EmitCache {
+ disk_cache,
+ cli_version: "2.0.0".to_string(),
+ };
+ assert_eq!(cache.get_emit_code(&specifier1, 5), Some(emit_code1));
+
+ // adding when already exists should not cause issue
+ let emit_code3 = "asdf".to_string();
+ cache.set_emit_code(&specifier1, 20, &emit_code3);
+ assert_eq!(cache.get_emit_code(&specifier1, 5), None);
+ assert_eq!(cache.get_emit_code(&specifier1, 20), Some(emit_code3));
+ }
}