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