Skip to main content

script/dom/serviceworker/
cachestorage.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::RefCell;
6use std::collections::VecDeque;
7
8use dom_struct::dom_struct;
9use js::context::JSContext;
10use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
11use script_bindings::root::DomRoot;
12use servo_base::generic_channel::{GenericCallback, GenericSend};
13use servo_url::ImmutableOrigin;
14use storage_traits::cache_storage::{CacheStorageThreadMessage, CacheStorageThreadResponse};
15use storage_traits::client_storage::{StorageIdentifier, StorageProxyMap, StorageType};
16
17use crate::dom::bindings::codegen::Bindings::CacheStorageBinding::CacheStorageMethods;
18use crate::dom::bindings::error::Error;
19use crate::dom::bindings::refcounted::Trusted;
20use crate::dom::bindings::reflector::DomGlobal;
21use crate::dom::bindings::str::DOMString;
22use crate::dom::globalscope::GlobalScope;
23use crate::dom::serviceworker::cache::Cache;
24use crate::dom::{Promise, RootedPromise, TracedPromise};
25
26/// <https://w3c.github.io/ServiceWorker/#cachestorage>
27#[dom_struct]
28pub(crate) struct CacheStorage {
29    reflector_: Reflector,
30
31    #[no_trace]
32    #[ignore_malloc_size_of = "GenericCallback"]
33    callback: RefCell<Option<GenericCallback<CacheStorageThreadResponse>>>,
34
35    // Dequeue of pending promises for backend operations.
36    pending_promises: RefCell<VecDeque<TracedPromise>>,
37}
38
39impl CacheStorage {
40    fn new_inherited() -> CacheStorage {
41        CacheStorage {
42            reflector_: Reflector::new(),
43            callback: Default::default(),
44            pending_promises: Default::default(),
45        }
46    }
47
48    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<CacheStorage> {
49        reflect_dom_object_with_cx(Box::new(CacheStorage::new_inherited()), global, cx)
50    }
51
52    /// Setup the callback to the backend service, if this hasn't been done already.
53    fn get_or_setup_callback(&self) -> GenericCallback<CacheStorageThreadResponse> {
54        if let Some(cb) = self.callback.borrow().as_ref() {
55            return cb.clone();
56        }
57
58        let global = self.global();
59        let response_listener = Trusted::new(self);
60
61        let task_source = global
62            .task_manager()
63            .dom_manipulation_task_source()
64            .to_sendable();
65        let callback = GenericCallback::new(move |message| {
66            let response_listener = response_listener.clone();
67            let response = match message {
68                Ok(inner) => Some(inner),
69                Err(err) => {
70                    error!("Error in CacheStorage callback {:?}.", err);
71                    None
72                },
73            };
74            task_source.queue(task!(set_request_result_to_database: move |cx| {
75                let cache_storage = response_listener.root();
76                cache_storage.handle_response(cx, response)
77            }));
78        })
79        .expect("Could not create CacheStorage callback");
80
81        *self.callback.borrow_mut() = Some(callback.clone());
82
83        callback
84    }
85
86    fn handle_response(&self, cx: &mut JSContext, response: Option<CacheStorageThreadResponse>) {
87        let response = match response {
88            Some(response) => response,
89            None => {
90                let Some(promise) = self
91                    .pending_promises
92                    .borrow_mut()
93                    .pop_front()
94                    .map(|promise| promise.root(cx))
95                else {
96                    error!("No pending promise for CacheStorage response.");
97                    return;
98                };
99                promise.reject_error(
100                    cx,
101                    Error::Operation(Some("No response from CacheStorage backend.".to_string())),
102                );
103                return;
104            },
105        };
106        match response {
107            // <https://w3c.github.io/ServiceWorker/#cache-storage-has>
108            // the steps resolving the promise with the result.
109            // Note: spec forgets to queue a task see <https://github.com/w3c/ServiceWorker/issues/1831>
110            CacheStorageThreadResponse::HasCacheResult(result) => {
111                let Some(promise) = self
112                    .pending_promises
113                    .borrow_mut()
114                    .pop_front()
115                    .map(|promise| promise.root(cx))
116                else {
117                    debug_assert!(false, "No pending promise for HasCacheResult response.");
118                    return;
119                };
120                let Ok(has_cache) = result else {
121                    promise.reject_error(
122                        cx,
123                        Error::Operation(Some(
124                            result
125                                .err()
126                                .unwrap_or_else(|| "HasCacheResult error".to_string()),
127                        )),
128                    );
129                    return;
130                };
131                // Step 2.1:For each key → value of the relevant name to cache map:
132                // Step 2.1.1: If cacheName matches key, resolve promise with true and abort these steps.
133                // Step 2.2: Resolve promise with false.
134                // Note: promise resolved with the result obtained in parallel.
135                promise.resolve_native(cx, &has_cache);
136            },
137            // <https://w3c.github.io/ServiceWorker/#cache-storage-open>
138            // the steps resolving the promise with the result.
139            CacheStorageThreadResponse::OpenCacheResult { result, cache_name } => {
140                let Some(promise) = self
141                    .pending_promises
142                    .borrow_mut()
143                    .pop_front()
144                    .map(|promise| promise.root(cx))
145                else {
146                    debug_assert!(false, "No pending promise for OpenCacheResult response.");
147                    return;
148                };
149                if result.is_err() {
150                    promise.reject_error(
151                        cx,
152                        Error::Operation(Some(
153                            result
154                                .err()
155                                .unwrap_or_else(|| "OpenCacheResult error".to_string()),
156                        )),
157                    );
158                    return;
159                };
160                // Resolve promise with a new Cache object that represents value.
161                let cache = Cache::new(cx, &self.global(), DOMString::from(cache_name));
162                promise.resolve_native(cx, &cache);
163            },
164            // <https://w3c.github.io/ServiceWorker/#dom-cachestorage-delete>
165            CacheStorageThreadResponse::DeleteCacheResult(result) => {
166                let Some(promise) = self
167                    .pending_promises
168                    .borrow_mut()
169                    .pop_front()
170                    .map(|promise| promise.root(cx))
171                else {
172                    debug_assert!(false, "No pending promise for DeleteCacheResult response.");
173                    return;
174                };
175                let Ok(deleted) = result else {
176                    promise.reject_error(
177                        cx,
178                        Error::Operation(Some(
179                            result
180                                .err()
181                                .unwrap_or_else(|| "DeleteCacheResult error".to_string()),
182                        )),
183                    );
184                    return;
185                };
186                promise.resolve_native(cx, &deleted);
187            },
188            CacheStorageThreadResponse::KeysResult(_) => debug_assert!(
189                false,
190                "Unexpected KeysResult response in CacheStorage handle_response."
191            ),
192        }
193    }
194}
195
196/// <https://w3c.github.io/ServiceWorker/#relevant-name-to-cache-map>
197fn relevant_name_to_cache_map(
198    global: &GlobalScope,
199    origin: ImmutableOrigin,
200) -> Result<StorageProxyMap, Error> {
201    // The relevant name to cache map for a CacheStorage object
202    // is the name to cache map associated with the result of
203    // running obtain a local storage bottle map with
204    // the object’s relevant settings object and "caches".
205    let handle = global.storage_threads().client_storage_handle();
206    let message = handle
207        .obtain_a_storage_bottle_map(
208            StorageType::Local,
209            global.webview_id(),
210            StorageIdentifier::Caches,
211            origin,
212        )
213        .recv();
214    let Ok(response) = message else {
215        return Err(Error::Operation(Some(
216            "Could not obtain a local storage bottle map.".to_string(),
217        )));
218    };
219    let Ok(proxy_map) = response else {
220        return Err(Error::Operation(Some(
221            "Could not obtain a local storage bottle map.".to_string(),
222        )));
223    };
224    Ok(proxy_map)
225}
226
227impl CacheStorageMethods<crate::DomTypeHolder> for CacheStorage {
228    /// <https://w3c.github.io/ServiceWorker/#cache-storage-has>
229    fn Has(&self, cx: &mut JSContext, cache_name: DOMString) -> RootedPromise {
230        let global = self.global();
231
232        // Step 1: Let promise be a new promise.
233        let promise = Promise::new_rooted(cx, &global);
234
235        // Step 2: Run the following substeps in parallel:
236        let callback = self.get_or_setup_callback();
237        let origin = global.origin().immutable().clone();
238        let proxy_map = match relevant_name_to_cache_map(&global, origin.clone()) {
239            Ok(proxy_map) => proxy_map,
240            Err(err) => {
241                promise.reject_error(cx, err);
242                return promise;
243            },
244        };
245        if global
246            .storage_threads()
247            .send(CacheStorageThreadMessage::HasCache {
248                cache_name: cache_name.to_string(),
249                callback,
250                proxy: proxy_map,
251                origin,
252            })
253            .is_err()
254        {
255            promise.reject_error(
256                cx,
257                Error::Operation(Some("Could not run the parallel steps.".to_string())),
258            );
259            return promise;
260        }
261
262        self.pending_promises
263            .borrow_mut()
264            .push_back(promise.to_traced());
265
266        promise
267    }
268
269    /// <https://w3c.github.io/ServiceWorker/#dom-cachestorage-open>
270    fn Open(&self, cx: &mut JSContext, cache_name: DOMString) -> RootedPromise {
271        // Step 1: Let promise be a new promise.
272        let global = self.global();
273        let promise = Promise::new_rooted(cx, &global);
274
275        // Step 2: Run the following substeps in parallel:
276        let callback = self.get_or_setup_callback();
277        let origin = global.origin().immutable().clone();
278        let proxy_map = match relevant_name_to_cache_map(&global, origin.clone()) {
279            Ok(proxy_map) => proxy_map,
280            Err(err) => {
281                promise.reject_error(cx, err);
282                return promise;
283            },
284        };
285        if global
286            .storage_threads()
287            .send(CacheStorageThreadMessage::OpenCache {
288                cache_name: cache_name.to_string(),
289                callback,
290                proxy: proxy_map,
291                origin,
292            })
293            .is_err()
294        {
295            promise.reject_error(
296                cx,
297                Error::Operation(Some("Could not run the parallel steps.".to_string())),
298            );
299            return promise;
300        }
301
302        self.pending_promises
303            .borrow_mut()
304            .push_back(promise.to_traced());
305
306        // Step 3: Return promise.
307        promise
308    }
309
310    /// <https://w3c.github.io/ServiceWorker/#dom-cachestorage-delete>
311    fn Delete(&self, cx: &mut JSContext, cache_name: DOMString) -> RootedPromise {
312        // Step 1: Let promise be the result of running the algorithm specified in has(cacheName) method with cacheName.
313        // Step 2: Return the result of reacting to promise with a fulfillment handler that,
314        // when called with argument cacheExists, performs the following substeps:
315        // Step 2.1: If cacheExists is false, then
316        // Step 2.1.1: Return false.
317        // Note: we skip the promise, and will run the equivalent steps directly in the backend.
318
319        // Step 2.2: Let cacheJobPromise be a new promise.
320        let global = self.global();
321        let promise = Promise::new_rooted(cx, &global);
322
323        // Step 3: Run the following substeps in parallel:
324        let callback = self.get_or_setup_callback();
325        let origin = global.origin().immutable().clone();
326        let proxy_map = match relevant_name_to_cache_map(&global, origin.clone()) {
327            Ok(proxy_map) => proxy_map,
328            Err(err) => {
329                promise.reject_error(cx, err);
330                return promise;
331            },
332        };
333        if global
334            .storage_threads()
335            .send(CacheStorageThreadMessage::DeleteCache {
336                cache_name: cache_name.to_string(),
337                callback,
338                proxy: proxy_map,
339                origin,
340            })
341            .is_err()
342        {
343            promise.reject_error(
344                cx,
345                Error::Operation(Some("Could not run the parallel steps.".to_string())),
346            );
347            return promise;
348        }
349
350        self.pending_promises
351            .borrow_mut()
352            .push_back(promise.to_traced());
353
354        // Step 4: Return cacheJobPromise.
355        promise
356    }
357}