[go: up one dir, main page]

worker/
cache.rs

1use std::convert::TryInto;
2
3use serde::Serialize;
4use wasm_bindgen::JsCast;
5use wasm_bindgen_futures::JsFuture;
6use worker_sys::ext::CacheStorageExt;
7
8use crate::request::Request;
9use crate::response::Response;
10use crate::Result;
11
12/// Provides access to the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache).
13/// Because `match` is a reserved keyword in Rust, the `match` method has been renamed to `get`.
14///
15/// Our implementation of the Cache API respects the following HTTP headers on the response passed to `put()`:
16///
17/// - `Cache-Control`
18///   - Controls caching directives.
19///     This is consistent with [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives).
20///     Refer to [Edge TTL](https://developers.cloudflare.com/cache/how-to/configure-cache-status-code#edge-ttl) for a list of HTTP response codes and their TTL when Cache-Control directives are not present.
21/// - `Cache-Tag`
22///   - Allows resource purging by tag(s) later (Enterprise only).
23/// - `ETag`
24///   - Allows `cache.get()` to evaluate conditional requests with If-None-Match.
25/// - `Expires`
26///   - A string that specifies when the resource becomes invalid.
27/// - `Last-Modified`
28///   - Allows `cache.get()` to evaluate conditional requests with If-Modified-Since.
29///
30/// This differs from the web browser Cache API as they do not honor any headers on the request or response.
31///
32/// Responses with `Set-Cookie` headers are never cached, because this sometimes indicates that the response contains unique data. To store a response with a `Set-Cookie` header, either delete that header or set `Cache-Control: private=Set-Cookie` on the response before calling `cache.put()`.
33///
34/// Use the `Cache-Control` method to store the response without the `Set-Cookie` header.
35#[derive(Debug)]
36pub struct Cache {
37    inner: web_sys::Cache,
38}
39
40impl Default for Cache {
41    fn default() -> Self {
42        let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
43
44        Self {
45            inner: global.caches().unwrap().default(),
46        }
47    }
48}
49
50impl Cache {
51    /// Opens a [`Cache`] by name. To access the default global cache, use [`Cache::default()`](`Default::default`).
52    pub async fn open(name: String) -> Self {
53        let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
54        let cache = global.caches().unwrap().open(&name);
55
56        // unwrap is safe because this promise never rejects
57        // https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open
58        let inner = JsFuture::from(cache).await.unwrap().into();
59
60        Self { inner }
61    }
62
63    /// Adds to the cache a [`Response`] keyed to the given request.
64    ///
65    /// The [`Response`] should include a `cache-control` header with `max-age` or `s-maxage` directives,
66    /// otherwise the Cache API will not cache the response.
67    /// The `stale-while-revalidate` and `stale-if-error` directives are not supported
68    /// when using the `cache.put` or `cache.get` methods.
69    /// For more information about how the Cache works, visit the documentation at [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
70    /// and [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives)
71    ///
72    /// The Cache API will throw an error if:
73    /// - the request passed is a method other than GET.
74    /// - the response passed has a status of 206 Partial Content.
75    /// - the response passed contains the header `Vary: *` (required by the Cache API specification).
76    pub async fn put<'a, K: Into<CacheKey<'a>>>(&self, key: K, response: Response) -> Result<()> {
77        let promise = match key.into() {
78            CacheKey::Url(url) => self.inner.put_with_str(url.as_str(), &response.into()),
79            CacheKey::Request(request) => self
80                .inner
81                .put_with_request(&request.try_into()?, &response.into()),
82        };
83        let _ = JsFuture::from(promise).await?;
84        Ok(())
85    }
86
87    /// Returns the [`Response`] object keyed to that request. Never sends a subrequest to the origin. If no matching response is found in cache, returns `None`.
88    ///
89    /// Unlike the browser Cache API, Cloudflare Workers do not support the `ignoreSearch` or `ignoreVary` options on `get()`. You can accomplish this behavior by removing query strings or HTTP headers at `put()` time.
90    /// In addition, the `stale-while-revalidate` and `stale-if-error` directives are not supported
91    /// when using the `cache.put` or `cache.get` methods.
92    ///
93    /// Our implementation of the Cache API respects the following HTTP headers on the request passed to `get()`:
94    ///
95    /// - Range
96    ///   - Results in a `206` response if a matching response with a Content-Length header is found. Your Cloudflare cache always respects range requests, even if an `Accept-Ranges` header is on the response.
97    /// - If-Modified-Since
98    ///   - Results in a `304` response if a matching response is found with a `Last-Modified` header with a value after the time specified in `If-Modified-Since`.
99    /// - If-None-Match
100    ///   - Results in a `304` response if a matching response is found with an `ETag` header with a value that matches a value in `If-None-Match.`
101    pub async fn get<'a, K: Into<CacheKey<'a>>>(
102        &self,
103        key: K,
104        ignore_method: bool,
105    ) -> Result<Option<Response>> {
106        let options = web_sys::CacheQueryOptions::new();
107        options.set_ignore_method(ignore_method);
108
109        let promise = match key.into() {
110            CacheKey::Url(url) => self
111                .inner
112                .match_with_str_and_options(url.as_str(), &options),
113            CacheKey::Request(request) => self
114                .inner
115                .match_with_request_and_options(&request.try_into()?, &options),
116        };
117
118        // `match` returns either a response or undefined
119        let result = JsFuture::from(promise).await?;
120        if result.is_undefined() {
121            Ok(None)
122        } else {
123            let edge_response: web_sys::Response = result.into();
124            let response = Response::from(edge_response);
125            Ok(Some(response))
126        }
127    }
128
129    /// Deletes the [`Response`] object associated with the key.
130    ///
131    /// Returns:
132    ///   - Success, if the response was cached but is now deleted
133    ///   - ResponseNotFound, if the response was not in the cache at the time of deletion
134    pub async fn delete<'a, K: Into<CacheKey<'a>>>(
135        &self,
136        key: K,
137        ignore_method: bool,
138    ) -> Result<CacheDeletionOutcome> {
139        let options = web_sys::CacheQueryOptions::new();
140        options.set_ignore_method(ignore_method);
141
142        let promise = match key.into() {
143            CacheKey::Url(url) => self
144                .inner
145                .delete_with_str_and_options(url.as_str(), &options),
146            CacheKey::Request(request) => self
147                .inner
148                .delete_with_request_and_options(&request.try_into()?, &options),
149        };
150        let result = JsFuture::from(promise).await?;
151
152        // Unwrap is safe because we know this is a boolean
153        // https://developers.cloudflare.com/workers/runtime-apis/cache#delete
154        if result.as_bool().unwrap() {
155            Ok(CacheDeletionOutcome::Success)
156        } else {
157            Ok(CacheDeletionOutcome::ResponseNotFound)
158        }
159    }
160}
161
162/// The `String` or `Request` object used as the lookup key. `String`s are interpreted as the URL for a new `Request` object.
163#[derive(Debug)]
164pub enum CacheKey<'a> {
165    Url(String),
166    Request(&'a Request),
167}
168
169impl From<&str> for CacheKey<'_> {
170    fn from(url: &str) -> Self {
171        Self::Url(url.to_string())
172    }
173}
174
175impl From<String> for CacheKey<'_> {
176    fn from(url: String) -> Self {
177        Self::Url(url)
178    }
179}
180
181impl From<&String> for CacheKey<'_> {
182    fn from(url: &String) -> Self {
183        Self::Url(url.clone())
184    }
185}
186
187impl<'a> From<&'a Request> for CacheKey<'a> {
188    fn from(request: &'a Request) -> Self {
189        Self::Request(request)
190    }
191}
192
193/// Successful outcomes when attempting to delete a `Response` from the cache
194#[derive(Serialize, Debug, Clone)]
195pub enum CacheDeletionOutcome {
196    /// The response was cached but is now deleted
197    Success,
198    /// The response was not in the cache at the time of deletion.
199    ResponseNotFound,
200}