Skip to main content

deno_cache/
lib.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::rc::Rc;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use bytes::Bytes;
12use deno_core::AsyncRefCell;
13use deno_core::AsyncResult;
14use deno_core::ByteString;
15use deno_core::OpState;
16use deno_core::Resource;
17use deno_core::ResourceId;
18use deno_core::op2;
19use deno_core::serde::Deserialize;
20use deno_core::serde::Serialize;
21use deno_error::JsErrorBox;
22use futures::Stream;
23use tokio::io::AsyncRead;
24use tokio::io::AsyncReadExt;
25
26mod lsc_shard;
27mod lscache;
28mod sqlite;
29
30pub use lsc_shard::CacheShard;
31pub use lscache::LscBackend;
32pub use sqlite::SqliteBackedCache;
33use tokio_util::io::StreamReader;
34
35#[derive(Debug, thiserror::Error, deno_error::JsError)]
36pub enum CacheError {
37  #[class(type)]
38  #[error("CacheStorage is not available in this context")]
39  ContextUnsupported,
40  #[class(type)]
41  #[error("Cache name cannot be empty")]
42  EmptyName,
43  #[class(type)]
44  #[error("Cache is not available")]
45  NotAvailable,
46  #[class(type)]
47  #[error("Cache not found")]
48  NotFound,
49  #[class(type)]
50  #[error("Cache deletion is not supported")]
51  DeletionNotSupported,
52  #[class(type)]
53  #[error("Content-Encoding is not allowed in response headers")]
54  ContentEncodingNotAllowed,
55  #[class(generic)]
56  #[error(transparent)]
57  Sqlite(#[from] rusqlite::Error),
58  #[class(generic)]
59  #[error(transparent)]
60  JoinError(#[from] tokio::task::JoinError),
61  #[class(inherit)]
62  #[error(transparent)]
63  Resource(#[from] deno_core::error::ResourceError),
64  #[class(inherit)]
65  #[error(transparent)]
66  Other(JsErrorBox),
67  #[class(inherit)]
68  #[error("{0}")]
69  Io(#[from] std::io::Error),
70  #[class(type)]
71  #[error(transparent)]
72  InvalidHeaderName(#[from] hyper::header::InvalidHeaderName),
73  #[class(type)]
74  #[error(transparent)]
75  InvalidHeaderValue(#[from] hyper::header::InvalidHeaderValue),
76  #[class(type)]
77  #[error(transparent)]
78  Hyper(#[from] hyper::Error),
79  #[class(generic)]
80  #[error(transparent)]
81  ClientError(#[from] hyper_util::client::legacy::Error),
82  #[class(generic)]
83  #[error("Failed to create cache storage directory {}", .dir.display())]
84  CacheStorageDirectory {
85    dir: PathBuf,
86    #[source]
87    source: std::io::Error,
88  },
89  #[class(generic)]
90  #[error("cache {method} request failed: {status}")]
91  RequestFailed {
92    method: &'static str,
93    status: hyper::StatusCode,
94  },
95}
96
97#[derive(Clone)]
98pub struct CreateCache(pub Arc<dyn Fn() -> Result<CacheImpl, CacheError>>);
99
100deno_core::extension!(deno_cache,
101  deps = [ deno_webidl, deno_web, deno_fetch ],
102  ops = [
103    op_cache_storage_open,
104    op_cache_storage_has,
105    op_cache_storage_delete,
106    op_cache_put,
107    op_cache_match,
108    op_cache_delete,
109  ],
110  esm = [ "01_cache.js" ],
111  options = {
112    maybe_create_cache: Option<CreateCache>,
113  },
114  state = |state, options| {
115    if let Some(create_cache) = options.maybe_create_cache {
116      state.put(create_cache);
117    }
118  },
119);
120
121#[derive(Deserialize, Serialize, Debug, Clone)]
122#[serde(rename_all = "camelCase")]
123pub struct CachePutRequest {
124  pub cache_id: i64,
125  pub request_url: String,
126  pub request_headers: Vec<(ByteString, ByteString)>,
127  pub response_headers: Vec<(ByteString, ByteString)>,
128  pub response_status: u16,
129  pub response_status_text: String,
130  pub response_rid: Option<ResourceId>,
131}
132
133#[derive(Deserialize, Serialize, Debug)]
134#[serde(rename_all = "camelCase")]
135pub struct CacheMatchRequest {
136  pub cache_id: i64,
137  pub request_url: String,
138  pub request_headers: Vec<(ByteString, ByteString)>,
139}
140
141#[derive(Debug, Deserialize, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct CacheMatchResponse(CacheMatchResponseMeta, Option<ResourceId>);
144
145#[derive(Debug, Deserialize, Serialize)]
146#[serde(rename_all = "camelCase")]
147pub struct CacheMatchResponseMeta {
148  pub response_status: u16,
149  pub response_status_text: String,
150  pub request_headers: Vec<(ByteString, ByteString)>,
151  pub response_headers: Vec<(ByteString, ByteString)>,
152}
153
154#[derive(Deserialize, Serialize, Debug)]
155#[serde(rename_all = "camelCase")]
156pub struct CacheDeleteRequest {
157  pub cache_id: i64,
158  pub request_url: String,
159}
160
161#[async_trait(?Send)]
162pub trait Cache: Clone + 'static {
163  type CacheMatchResourceType: Resource;
164
165  async fn storage_open(&self, cache_name: String) -> Result<i64, CacheError>;
166  async fn storage_has(&self, cache_name: String) -> Result<bool, CacheError>;
167  async fn storage_delete(
168    &self,
169    cache_name: String,
170  ) -> Result<bool, CacheError>;
171
172  /// Put a resource into the cache.
173  async fn put(
174    &self,
175    request_response: CachePutRequest,
176    resource: Option<Rc<dyn Resource>>,
177  ) -> Result<(), CacheError>;
178
179  async fn r#match(
180    &self,
181    request: CacheMatchRequest,
182  ) -> Result<
183    Option<(CacheMatchResponseMeta, Option<Self::CacheMatchResourceType>)>,
184    CacheError,
185  >;
186  async fn delete(
187    &self,
188    request: CacheDeleteRequest,
189  ) -> Result<bool, CacheError>;
190}
191
192#[derive(Clone)]
193pub enum CacheImpl {
194  Sqlite(SqliteBackedCache),
195  Lsc(LscBackend),
196}
197
198#[async_trait(?Send)]
199impl Cache for CacheImpl {
200  type CacheMatchResourceType = CacheResponseResource;
201
202  async fn storage_open(&self, cache_name: String) -> Result<i64, CacheError> {
203    match self {
204      Self::Sqlite(cache) => cache.storage_open(cache_name).await,
205      Self::Lsc(cache) => cache.storage_open(cache_name).await,
206    }
207  }
208
209  async fn storage_has(&self, cache_name: String) -> Result<bool, CacheError> {
210    match self {
211      Self::Sqlite(cache) => cache.storage_has(cache_name).await,
212      Self::Lsc(cache) => cache.storage_has(cache_name).await,
213    }
214  }
215
216  async fn storage_delete(
217    &self,
218    cache_name: String,
219  ) -> Result<bool, CacheError> {
220    match self {
221      Self::Sqlite(cache) => cache.storage_delete(cache_name).await,
222      Self::Lsc(cache) => cache.storage_delete(cache_name).await,
223    }
224  }
225
226  async fn put(
227    &self,
228    request_response: CachePutRequest,
229    resource: Option<Rc<dyn Resource>>,
230  ) -> Result<(), CacheError> {
231    match self {
232      Self::Sqlite(cache) => cache.put(request_response, resource).await,
233      Self::Lsc(cache) => cache.put(request_response, resource).await,
234    }
235  }
236
237  async fn r#match(
238    &self,
239    request: CacheMatchRequest,
240  ) -> Result<
241    Option<(CacheMatchResponseMeta, Option<Self::CacheMatchResourceType>)>,
242    CacheError,
243  > {
244    match self {
245      Self::Sqlite(cache) => cache.r#match(request).await,
246      Self::Lsc(cache) => cache.r#match(request).await,
247    }
248  }
249
250  async fn delete(
251    &self,
252    request: CacheDeleteRequest,
253  ) -> Result<bool, CacheError> {
254    match self {
255      Self::Sqlite(cache) => cache.delete(request).await,
256      Self::Lsc(cache) => cache.delete(request).await,
257    }
258  }
259}
260
261pub enum CacheResponseResource {
262  Sqlite(AsyncRefCell<tokio::fs::File>),
263  Lsc(AsyncRefCell<Pin<Box<dyn AsyncRead>>>),
264}
265
266impl CacheResponseResource {
267  fn sqlite(file: tokio::fs::File) -> Self {
268    Self::Sqlite(AsyncRefCell::new(file))
269  }
270
271  fn lsc(
272    body: impl Stream<Item = Result<Bytes, std::io::Error>> + 'static,
273  ) -> Self {
274    Self::Lsc(AsyncRefCell::new(Box::pin(StreamReader::new(body))))
275  }
276
277  async fn read(
278    self: Rc<Self>,
279    data: &mut [u8],
280  ) -> Result<usize, std::io::Error> {
281    let nread = match &*self {
282      CacheResponseResource::Sqlite(_) => {
283        let resource = deno_core::RcRef::map(&self, |r| match r {
284          Self::Sqlite(r) => r,
285          _ => unreachable!(),
286        });
287        let mut file = resource.borrow_mut().await;
288        file.read(data).await?
289      }
290      CacheResponseResource::Lsc(_) => {
291        let resource = deno_core::RcRef::map(&self, |r| match r {
292          Self::Lsc(r) => r,
293          _ => unreachable!(),
294        });
295        let mut file = resource.borrow_mut().await;
296        file.read(data).await?
297      }
298    };
299
300    Ok(nread)
301  }
302}
303
304impl Resource for CacheResponseResource {
305  deno_core::impl_readable_byob!();
306
307  fn name(&self) -> Cow<'_, str> {
308    "CacheResponseResource".into()
309  }
310}
311
312#[op2]
313#[number]
314pub async fn op_cache_storage_open(
315  state: Rc<RefCell<OpState>>,
316  #[string] cache_name: String,
317) -> Result<i64, CacheError> {
318  let cache = get_cache(&state)?;
319  cache.storage_open(cache_name).await
320}
321
322#[op2]
323pub async fn op_cache_storage_has(
324  state: Rc<RefCell<OpState>>,
325  #[string] cache_name: String,
326) -> Result<bool, CacheError> {
327  let cache = get_cache(&state)?;
328  cache.storage_has(cache_name).await
329}
330
331#[op2]
332pub async fn op_cache_storage_delete(
333  state: Rc<RefCell<OpState>>,
334  #[string] cache_name: String,
335) -> Result<bool, CacheError> {
336  let cache = get_cache(&state)?;
337  cache.storage_delete(cache_name).await
338}
339
340#[op2]
341pub async fn op_cache_put(
342  state: Rc<RefCell<OpState>>,
343  #[serde] request_response: CachePutRequest,
344) -> Result<(), CacheError> {
345  let cache = get_cache(&state)?;
346  let resource = match request_response.response_rid {
347    Some(rid) => Some(
348      state
349        .borrow_mut()
350        .resource_table
351        .take_any(rid)
352        .map_err(CacheError::Resource)?,
353    ),
354    None => None,
355  };
356  cache.put(request_response, resource).await
357}
358
359#[op2]
360#[serde]
361pub async fn op_cache_match(
362  state: Rc<RefCell<OpState>>,
363  #[serde] request: CacheMatchRequest,
364) -> Result<Option<CacheMatchResponse>, CacheError> {
365  let cache = get_cache(&state)?;
366  match cache.r#match(request).await? {
367    Some((meta, None)) => Ok(Some(CacheMatchResponse(meta, None))),
368    Some((meta, Some(resource))) => {
369      let rid = state.borrow_mut().resource_table.add(resource);
370      Ok(Some(CacheMatchResponse(meta, Some(rid))))
371    }
372    None => Ok(None),
373  }
374}
375
376#[op2]
377pub async fn op_cache_delete(
378  state: Rc<RefCell<OpState>>,
379  #[serde] request: CacheDeleteRequest,
380) -> Result<bool, CacheError> {
381  let cache = get_cache(&state)?;
382  cache.delete(request).await
383}
384
385pub fn get_cache(
386  state: &Rc<RefCell<OpState>>,
387) -> Result<CacheImpl, CacheError> {
388  let mut state = state.borrow_mut();
389  if let Some(cache) = state.try_borrow::<CacheImpl>() {
390    Ok(cache.clone())
391  } else if let Some(create_cache) = state.try_borrow::<CreateCache>() {
392    let cache = create_cache.0()?;
393    state.put(cache);
394    Ok(state.borrow::<CacheImpl>().clone())
395  } else {
396    Err(CacheError::ContextUnsupported)
397  }
398}
399
400/// Check if headers, mentioned in the vary header, of query request
401/// and cached request are equal.
402pub fn vary_header_matches(
403  vary_header: &ByteString,
404  query_request_headers: &[(ByteString, ByteString)],
405  cached_request_headers: &[(ByteString, ByteString)],
406) -> bool {
407  let vary_header = match std::str::from_utf8(vary_header) {
408    Ok(vary_header) => vary_header,
409    Err(_) => return false,
410  };
411  let headers = get_headers_from_vary_header(vary_header);
412  for header in headers {
413    let query_header = get_header(&header, query_request_headers);
414    let cached_header = get_header(&header, cached_request_headers);
415    if query_header != cached_header {
416      return false;
417    }
418  }
419  true
420}
421
422#[test]
423fn test_vary_header_matches() {
424  let vary_header = ByteString::from("accept-encoding");
425  let query_request_headers = vec![(
426    ByteString::from("accept-encoding"),
427    ByteString::from("gzip"),
428  )];
429  let cached_request_headers = vec![(
430    ByteString::from("accept-encoding"),
431    ByteString::from("gzip"),
432  )];
433  assert!(vary_header_matches(
434    &vary_header,
435    &query_request_headers,
436    &cached_request_headers
437  ));
438  let vary_header = ByteString::from("accept-encoding");
439  let query_request_headers = vec![(
440    ByteString::from("accept-encoding"),
441    ByteString::from("gzip"),
442  )];
443  let cached_request_headers =
444    vec![(ByteString::from("accept-encoding"), ByteString::from("br"))];
445  assert!(!vary_header_matches(
446    &vary_header,
447    &query_request_headers,
448    &cached_request_headers
449  ));
450}
451
452/// Get headers from the vary header.
453pub fn get_headers_from_vary_header(vary_header: &str) -> Vec<String> {
454  vary_header
455    .split(',')
456    .map(|s| s.trim().to_lowercase())
457    .collect()
458}
459
460#[test]
461fn test_get_headers_from_vary_header() {
462  let headers = get_headers_from_vary_header("accept-encoding");
463  assert_eq!(headers, vec!["accept-encoding"]);
464  let headers = get_headers_from_vary_header("accept-encoding, user-agent");
465  assert_eq!(headers, vec!["accept-encoding", "user-agent"]);
466}
467
468/// Get value for the header with the given name.
469pub fn get_header(
470  name: &str,
471  headers: &[(ByteString, ByteString)],
472) -> Option<ByteString> {
473  headers
474    .iter()
475    .find(|(k, _)| {
476      if let Ok(k) = std::str::from_utf8(k) {
477        k.eq_ignore_ascii_case(name)
478      } else {
479        false
480      }
481    })
482    .map(|(_, v)| v.to_owned())
483}
484
485#[test]
486fn test_get_header() {
487  let headers = vec![
488    (
489      ByteString::from("accept-encoding"),
490      ByteString::from("gzip"),
491    ),
492    (
493      ByteString::from("content-type"),
494      ByteString::from("application/json"),
495    ),
496    (
497      ByteString::from("vary"),
498      ByteString::from("accept-encoding"),
499    ),
500  ];
501  let value = get_header("accept-encoding", &headers);
502  assert_eq!(value, Some(ByteString::from("gzip")));
503  let value = get_header("content-type", &headers);
504  assert_eq!(value, Some(ByteString::from("application/json")));
505  let value = get_header("vary", &headers);
506  assert_eq!(value, Some(ByteString::from("accept-encoding")));
507}
508
509/// Serialize headers into bytes.
510pub fn serialize_headers(headers: &[(ByteString, ByteString)]) -> Vec<u8> {
511  let mut serialized_headers = Vec::new();
512  for (name, value) in headers {
513    serialized_headers.extend_from_slice(name);
514    serialized_headers.extend_from_slice(b"\r\n");
515    serialized_headers.extend_from_slice(value);
516    serialized_headers.extend_from_slice(b"\r\n");
517  }
518  serialized_headers
519}
520
521/// Deserialize bytes into headers.
522pub fn deserialize_headers(
523  serialized_headers: &[u8],
524) -> Vec<(ByteString, ByteString)> {
525  let mut headers = Vec::new();
526  let mut piece = None;
527  let mut start = 0;
528  for (i, byte) in serialized_headers.iter().enumerate() {
529    if byte == &b'\r' && serialized_headers.get(i + 1) == Some(&b'\n') {
530      if piece.is_none() {
531        piece = Some(start..i);
532      } else {
533        let name = piece.unwrap();
534        let value = start..i;
535        headers.push((
536          ByteString::from(&serialized_headers[name]),
537          ByteString::from(&serialized_headers[value]),
538        ));
539        piece = None;
540      }
541      start = i + 2;
542    }
543  }
544  assert!(piece.is_none());
545  assert_eq!(start, serialized_headers.len());
546  headers
547}