[go: up one dir, main page]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
use std::convert::TryInto;

use serde::Serialize;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use worker_sys::ext::CacheStorageExt;

use crate::request::Request;
use crate::response::Response;
use crate::Result;

/// Provides access to the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache).
/// Because `match` is a reserved keyword in Rust, the `match` method has been renamed to `get`.
///
/// Our implementation of the Cache API respects the following HTTP headers on the response passed to `put()`:
///
/// - `Cache-Control`
///   - Controls caching directives.
///     This is consistent with [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives).
///     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.
/// - `Cache-Tag`
///   - Allows resource purging by tag(s) later (Enterprise only).
/// - `ETag`
///   - Allows `cache.get()` to evaluate conditional requests with If-None-Match.
/// - `Expires`
///   - A string that specifies when the resource becomes invalid.
/// - `Last-Modified`
///   - Allows `cache.get()` to evaluate conditional requests with If-Modified-Since.
///
/// This differs from the web browser Cache API as they do not honor any headers on the request or response.
///
/// 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()`.
///
/// Use the `Cache-Control` method to store the response without the `Set-Cookie` header.
#[derive(Debug)]
pub struct Cache {
    inner: web_sys::Cache,
}

impl Default for Cache {
    fn default() -> Self {
        let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();

        Self {
            inner: global.caches().unwrap().default(),
        }
    }
}

impl Cache {
    /// Opens a [`Cache`] by name. To access the default global cache, use [`Cache::default()`](`Default::default`).
    pub async fn open(name: String) -> Self {
        let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
        let cache = global.caches().unwrap().open(&name);

        // unwrap is safe because this promise never rejects
        // https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open
        let inner = JsFuture::from(cache).await.unwrap().into();

        Self { inner }
    }

    /// Adds to the cache a [`Response`] keyed to the given request.
    ///
    /// The [`Response`] should include a `cache-control` header with `max-age` or `s-maxage` directives,
    /// otherwise the Cache API will not cache the response.
    /// The `stale-while-revalidate` and `stale-if-error` directives are not supported
    /// when using the `cache.put` or `cache.get` methods.
    /// For more information about how the Cache works, visit the documentation at [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
    /// and [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives)
    ///
    /// The Cache API will throw an error if:
    /// - the request passed is a method other than GET.
    /// - the response passed has a status of 206 Partial Content.
    /// - the response passed contains the header `Vary: *` (required by the Cache API specification).
    pub async fn put<'a, K: Into<CacheKey<'a>>>(&self, key: K, response: Response) -> Result<()> {
        let promise = match key.into() {
            CacheKey::Url(url) => self.inner.put_with_str(url.as_str(), &response.into()),
            CacheKey::Request(request) => self
                .inner
                .put_with_request(&request.try_into()?, &response.into()),
        };
        let _ = JsFuture::from(promise).await?;
        Ok(())
    }

    /// 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`.
    ///
    /// 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.
    /// In addition, the `stale-while-revalidate` and `stale-if-error` directives are not supported
    /// when using the `cache.put` or `cache.get` methods.
    ///
    /// Our implementation of the Cache API respects the following HTTP headers on the request passed to `get()`:
    ///
    /// - Range
    ///   - 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.
    /// - If-Modified-Since
    ///   - 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`.
    /// - If-None-Match
    ///   - 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.`
    pub async fn get<'a, K: Into<CacheKey<'a>>>(
        &self,
        key: K,
        ignore_method: bool,
    ) -> Result<Option<Response>> {
        let mut options = web_sys::CacheQueryOptions::new();
        options.ignore_method(ignore_method);

        let promise = match key.into() {
            CacheKey::Url(url) => self
                .inner
                .match_with_str_and_options(url.as_str(), &options),
            CacheKey::Request(request) => self
                .inner
                .match_with_request_and_options(&request.try_into()?, &options),
        };

        // `match` returns either a response or undefined
        let result = JsFuture::from(promise).await?;
        if result.is_undefined() {
            Ok(None)
        } else {
            let edge_response: web_sys::Response = result.into();
            let response = Response::from(edge_response);
            Ok(Some(response))
        }
    }

    /// Deletes the [`Response`] object associated with the key.
    ///
    /// Returns:
    ///   - Success, if the response was cached but is now deleted
    ///   - ResponseNotFound, if the response was not in the cache at the time of deletion
    pub async fn delete<'a, K: Into<CacheKey<'a>>>(
        &self,
        key: K,
        ignore_method: bool,
    ) -> Result<CacheDeletionOutcome> {
        let mut options = web_sys::CacheQueryOptions::new();
        options.ignore_method(ignore_method);

        let promise = match key.into() {
            CacheKey::Url(url) => self
                .inner
                .delete_with_str_and_options(url.as_str(), &options),
            CacheKey::Request(request) => self
                .inner
                .delete_with_request_and_options(&request.try_into()?, &options),
        };
        let result = JsFuture::from(promise).await?;

        // Unwrap is safe because we know this is a boolean
        // https://developers.cloudflare.com/workers/runtime-apis/cache#delete
        if result.as_bool().unwrap() {
            Ok(CacheDeletionOutcome::Success)
        } else {
            Ok(CacheDeletionOutcome::ResponseNotFound)
        }
    }
}

/// The `String` or `Request` object used as the lookup key. `String`s are interpreted as the URL for a new `Request` object.
pub enum CacheKey<'a> {
    Url(String),
    Request(&'a Request),
}

impl From<&str> for CacheKey<'_> {
    fn from(url: &str) -> Self {
        Self::Url(url.to_string())
    }
}

impl From<String> for CacheKey<'_> {
    fn from(url: String) -> Self {
        Self::Url(url)
    }
}

impl From<&String> for CacheKey<'_> {
    fn from(url: &String) -> Self {
        Self::Url(url.clone())
    }
}

impl<'a> From<&'a Request> for CacheKey<'a> {
    fn from(request: &'a Request) -> Self {
        Self::Request(request)
    }
}

/// Successful outcomes when attempting to delete a `Response` from the cache
#[derive(Serialize)]
pub enum CacheDeletionOutcome {
    /// The response was cached but is now deleted
    Success,
    /// The response was not in the cache at the time of deletion.
    ResponseNotFound,
}