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