Skip to main content

script/dom/serviceworker/
cache.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::ServoUrl;
14use storage_traits::cache_storage::{CacheStorageThreadMessage, CacheStorageThreadResponse};
15
16use crate::dom::bindings::codegen::Bindings::CacheBinding::CacheMethods;
17use crate::dom::bindings::codegen::GenericBindings::CacheBinding::CacheQueryOptions;
18use crate::dom::bindings::codegen::UnionTypes::RequestOrUSVString;
19use crate::dom::bindings::error::Error;
20use crate::dom::bindings::refcounted::Trusted;
21use crate::dom::bindings::reflector::DomGlobal;
22use crate::dom::bindings::str::DOMString;
23use crate::dom::fetch::request::Request;
24use crate::dom::globalscope::GlobalScope;
25use crate::dom::{Promise, RootedPromise, TracedPromise};
26
27/// <https://w3c.github.io/ServiceWorker/#cache>
28#[dom_struct]
29pub(crate) struct Cache {
30    reflector_: Reflector,
31
32    /// The name used to identify
33    /// <https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list>
34    name: DOMString,
35
36    #[no_trace]
37    #[ignore_malloc_size_of = "GenericCallback"]
38    callback: RefCell<Option<GenericCallback<CacheStorageThreadResponse>>>,
39
40    // Dequeue of pending promises for backend operations.
41    pending_promises: RefCell<VecDeque<TracedPromise>>,
42}
43
44impl Cache {
45    fn new_inherited(name: DOMString) -> Cache {
46        Cache {
47            reflector_: Reflector::new(),
48            name,
49            callback: Default::default(),
50            pending_promises: Default::default(),
51        }
52    }
53
54    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope, name: DOMString) -> DomRoot<Cache> {
55        reflect_dom_object_with_cx(Box::new(Cache::new_inherited(name)), global, cx)
56    }
57
58    /// Setup the callback to the backend service, if this hasn't been done already.
59    pub(crate) fn get_or_setup_callback(&self) -> GenericCallback<CacheStorageThreadResponse> {
60        if let Some(cb) = self.callback.borrow().as_ref() {
61            return cb.clone();
62        }
63
64        let global = self.global();
65        let response_listener = Trusted::new(self);
66
67        let task_source = global
68            .task_manager()
69            .dom_manipulation_task_source()
70            .to_sendable();
71        let callback = GenericCallback::new(move |message| {
72            let response_listener = response_listener.clone();
73            let response = match message {
74                Ok(inner) => Some(inner),
75                Err(err) => {
76                    error!("Error in Cache callback {:?}.", err);
77                    None
78                },
79            };
80            task_source.queue(task!(set_request_result_to_database: move |cx| {
81                let cache = response_listener.root();
82                cache.handle_response(cx, response)
83            }));
84        })
85        .expect("Could not create Cache callback");
86
87        *self.callback.borrow_mut() = Some(callback.clone());
88
89        callback
90    }
91
92    fn handle_response(&self, cx: &mut JSContext, response: Option<CacheStorageThreadResponse>) {
93        let response = match response {
94            Some(response) => response,
95            None => {
96                let Some(promise) = self
97                    .pending_promises
98                    .borrow_mut()
99                    .pop_front()
100                    .map(|promise| promise.root(cx))
101                else {
102                    error!("No pending promise for Cache response.");
103                    return;
104                };
105                promise.reject_error(
106                    cx,
107                    Error::Operation(Some("No response from Cache backend.".to_string())),
108                );
109                return;
110            },
111        };
112        match response {
113            // <https://w3c.github.io/ServiceWorker/#cache-keys>
114            CacheStorageThreadResponse::KeysResult(results) => {
115                // Step 5.4: Queue a task,
116                // on promise’s relevant settings object’s responsible event loop using the DOM manipulation task source,
117                // to perform the following steps:
118                // Note: we are inside the task.
119
120                // Step 5.4.1: Let requestList be a list.
121                let mut request_list: Vec<DomRoot<Request>> = Vec::new();
122
123                let Some(promise) = self
124                    .pending_promises
125                    .borrow_mut()
126                    .pop_front()
127                    .map(|promise| promise.root(cx))
128                else {
129                    debug_assert!(false, "No pending promise for Cache KeysResult response.");
130                    return;
131                };
132
133                // Step 5.4.2: For each request of requests:
134                // Step 5.4.2.1: Add a new Request object associated with request
135                // and a new associated Headers object whose guard is "immutable" to requestList.
136                for url in results.unwrap_or_default() {
137                    let Ok(url) = ServoUrl::parse(&url) else {
138                        promise.reject_error(cx, Error::Type(c"Invalid URL".to_owned()));
139                        return;
140                    };
141                    let request = Request::new(cx, &self.global(), None, url);
142                    // TODO: associate request with a header.
143                    request_list.push(request);
144                }
145
146                // Step 5.4.3: Resolve promise with a frozen array created from requestList, in realm.
147                promise.resolve_native(cx, &request_list);
148            },
149            CacheStorageThreadResponse::DeleteCacheResult(_result) => debug_assert!(
150                false,
151                "Unexpected DeleteCacheResult response in Cache handle_response."
152            ),
153            CacheStorageThreadResponse::HasCacheResult(_result) => debug_assert!(
154                false,
155                "Unexpected HasCacheResult response in Cache handle_response."
156            ),
157            CacheStorageThreadResponse::OpenCacheResult { .. } => debug_assert!(
158                false,
159                "Unexpected OpenCacheResult response in Cache handle_response."
160            ),
161        }
162    }
163}
164
165impl CacheMethods<crate::DomTypeHolder> for Cache {
166    /// <https://w3c.github.io/ServiceWorker/#dom-cache-keys>
167    fn Keys(
168        &self,
169        cx: &mut JSContext,
170        request: Option<RequestOrUSVString>,
171        _options: &CacheQueryOptions,
172    ) -> RootedPromise {
173        // Step 1: Let r be null.
174        let mut r: Option<DomRoot<Request>> = None;
175
176        // Step 3: Let realm be this’s relevant realm.
177        // Note: the global is used as the realm; step re-ordered to make it available in Step 2.
178        let global = self.global();
179
180        // Step 4: Let promise be a new promise.
181        // Note: step re-ordered to make it available in Step 2.
182        let promise = Promise::new_rooted(cx, &global);
183
184        // Step 2: If the optional argument request is not omitted, then:
185        if let Some(request) = request {
186            // Step 2.1: If request is a Request object, then:
187            if let RequestOrUSVString::Request(request) = request {
188                // Step 2.1.1: Set r to request.
189                r = Some(request);
190            // Step 2.2: Else if request is a string, then:
191            } else if let RequestOrUSVString::USVString(request_string) = request {
192                // Step 2.2.1: Set r to the associated request of the result of
193                // invoking the initial value of Request as
194                // constructor with request as its argument.
195                let Ok(url) = ServoUrl::parse(&request_string) else {
196                    // If this throws an exception, return a promise rejected with that exception.
197                    // Note: only the url parse can error.
198                    promise.reject_error(cx, Error::Type(c"Invalid URL".to_owned()));
199                    return promise;
200                };
201                let request = Request::new(cx, &global, None, url);
202                r = Some(request);
203            }
204        }
205
206        // TODO: use r in the backend.
207        let _ = r;
208
209        // Step 5: Run these substeps in parallel:
210        let callback = self.get_or_setup_callback();
211        if global
212            .storage_threads()
213            .send(CacheStorageThreadMessage::Keys {
214                cache_name: self.name.to_string(),
215                callback,
216                origin: global.origin().immutable().clone(),
217            })
218            .is_err()
219        {
220            promise.reject_error(
221                cx,
222                Error::Operation(Some("Could not run the parallel steps.".to_string())),
223            );
224            return promise;
225        }
226
227        self.pending_promises
228            .borrow_mut()
229            .push_back(promise.to_traced());
230
231        promise
232    }
233}