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::domstring::TracedDOMString;
11use script_bindings::reflector::{Reflector, reflect_dom_object};
12use script_bindings::root::DomRoot;
13use servo_base::generic_channel::{GenericCallback, GenericSend};
14use servo_url::ServoUrl;
15use storage_traits::cache_storage::{CacheStorageThreadMessage, CacheStorageThreadResponse};
16
17use crate::dom::bindings::codegen::Bindings::CacheBinding::CacheMethods;
18use crate::dom::bindings::codegen::GenericBindings::CacheBinding::CacheQueryOptions;
19use crate::dom::bindings::codegen::UnionTypes::RequestOrUSVString;
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::fetch::request::Request;
25use crate::dom::globalscope::GlobalScope;
26use crate::dom::{Promise, RootedPromise, TracedPromise};
27
28/// <https://w3c.github.io/ServiceWorker/#cache>
29#[dom_struct]
30pub(crate) struct Cache {
31    reflector_: Reflector,
32
33    /// The name used to identify
34    /// <https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list>
35    name: TracedDOMString,
36
37    #[no_trace]
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: name.traced(),
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(cx, Box::new(Cache::new_inherited(name)), global)
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 =
142                        Request::new(cx, &self.global(), None, url, None /* state */);
143                    // TODO: associate request with a header.
144                    request_list.push(request);
145                }
146
147                // Step 5.4.3: Resolve promise with a frozen array created from requestList, in realm.
148                promise.resolve_native(cx, &request_list);
149            },
150            CacheStorageThreadResponse::DeleteCacheResult(_result) => debug_assert!(
151                false,
152                "Unexpected DeleteCacheResult response in Cache handle_response."
153            ),
154            CacheStorageThreadResponse::HasCacheResult(_result) => debug_assert!(
155                false,
156                "Unexpected HasCacheResult response in Cache handle_response."
157            ),
158            CacheStorageThreadResponse::OpenCacheResult { .. } => debug_assert!(
159                false,
160                "Unexpected OpenCacheResult response in Cache handle_response."
161            ),
162        }
163    }
164}
165
166impl CacheMethods<crate::DomTypeHolder> for Cache {
167    /// <https://w3c.github.io/ServiceWorker/#dom-cache-keys>
168    fn Keys(
169        &self,
170        cx: &mut JSContext,
171        request: Option<RequestOrUSVString>,
172        _options: &CacheQueryOptions,
173    ) -> RootedPromise {
174        // Step 1: Let r be null.
175        let mut r: Option<DomRoot<Request>> = None;
176
177        // Step 3: Let realm be this’s relevant realm.
178        // Note: the global is used as the realm; step re-ordered to make it available in Step 2.
179        let global = self.global();
180
181        // Step 4: Let promise be a new promise.
182        // Note: step re-ordered to make it available in Step 2.
183        let promise = Promise::new(cx, &global);
184
185        // Step 2: If the optional argument request is not omitted, then:
186        if let Some(request) = request {
187            // Step 2.1: If request is a Request object, then:
188            if let RequestOrUSVString::Request(request) = request {
189                // Step 2.1.1: Set r to request.
190                r = Some(request);
191            // Step 2.2: Else if request is a string, then:
192            } else if let RequestOrUSVString::USVString(request_string) = request {
193                // Step 2.2.1: Set r to the associated request of the result of
194                // invoking the initial value of Request as
195                // constructor with request as its argument.
196                let Ok(url) = ServoUrl::parse(&request_string) else {
197                    // If this throws an exception, return a promise rejected with that exception.
198                    // Note: only the url parse can error.
199                    promise.reject_error(cx, Error::Type(c"Invalid URL".to_owned()));
200                    return promise;
201                };
202                let request = Request::new(cx, &global, None, url, None /* state */);
203                r = Some(request);
204            }
205        }
206
207        // TODO: use r in the backend.
208        let _ = r;
209
210        // Step 5: Run these substeps in parallel:
211        let callback = self.get_or_setup_callback();
212        if global
213            .storage_threads()
214            .send(CacheStorageThreadMessage::Keys {
215                cache_name: self.name.to_string(),
216                callback,
217                origin: global.origin().immutable().clone(),
218            })
219            .is_err()
220        {
221            promise.reject_error(
222                cx,
223                Error::Operation(Some("Could not run the parallel steps.".to_string())),
224            );
225            return promise;
226        }
227
228        self.pending_promises
229            .borrow_mut()
230            .push_back(promise.to_traced());
231
232        promise
233    }
234}