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