script/dom/indexeddb/
idbfactory.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/. */
4use std::collections::HashSet;
5use std::rc::Rc;
6
7use base::generic_channel::GenericSend;
8use dom_struct::dom_struct;
9use js::context::JSContext;
10use js::jsval::UndefinedValue;
11use js::rust::HandleValue;
12use profile_traits::generic_callback::GenericCallback;
13use script_bindings::inheritance::Castable;
14use servo_url::origin::ImmutableOrigin;
15use storage_traits::indexeddb::{
16    BackendError, BackendResult, DatabaseInfo, IndexedDBThreadMsg, OpenDatabaseResult,
17    SyncOperation,
18};
19use stylo_atoms::Atom;
20use uuid::Uuid;
21
22use crate::dom::bindings::cell::DomRefCell;
23use crate::dom::bindings::codegen::Bindings::IDBFactoryBinding::{
24    IDBDatabaseInfo, IDBFactoryMethods,
25};
26use crate::dom::bindings::error::{Error, ErrorToJsval, Fallible};
27use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
28use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
29use crate::dom::bindings::root::{Dom, DomRoot};
30use crate::dom::bindings::str::DOMString;
31use crate::dom::bindings::trace::HashMapTracedValues;
32use crate::dom::event::{Event, EventBubbles, EventCancelable};
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::indexeddb::idbdatabase::IDBDatabase;
35use crate::dom::indexeddb::idbopendbrequest::IDBOpenDBRequest;
36use crate::dom::promise::Promise;
37use crate::indexeddb::{convert_value_to_key, map_backend_error_to_dom_error};
38use crate::script_runtime::CanGc;
39
40/// A non-jstraceable string wrapper for use in `HashMapTracedValues`.
41#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
42pub(crate) struct DBName(pub(crate) String);
43
44#[dom_struct]
45pub struct IDBFactory {
46    reflector_: Reflector,
47    /// <https://www.w3.org/TR/IndexedDB-2/#connection>
48    /// The connections pending #open-a-database-connection.
49    pending_connections:
50        DomRefCell<HashMapTracedValues<DBName, HashMapTracedValues<Uuid, Dom<IDBOpenDBRequest>>>>,
51}
52
53impl IDBFactory {
54    pub fn new_inherited() -> IDBFactory {
55        IDBFactory {
56            reflector_: Reflector::new(),
57            pending_connections: Default::default(),
58        }
59    }
60
61    pub fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<IDBFactory> {
62        reflect_dom_object(Box::new(IDBFactory::new_inherited()), global, can_gc)
63    }
64
65    /// <https://w3c.github.io/IndexedDB/#open-a-database-connection>
66    /// The steps that continue on the script-thread.
67    fn handle_open_db(
68        &self,
69        name: String,
70        response: OpenDatabaseResult,
71        request_id: Uuid,
72        can_gc: CanGc,
73    ) {
74        let name = DBName(name);
75        let request = {
76            let mut pending = self.pending_connections.borrow_mut();
77            let Some(entry) = pending.get_mut(&name) else {
78                return debug_assert!(false, "There should be a pending connection for {:?}", name);
79            };
80            let Some(request) = entry.get_mut(&request_id) else {
81                return debug_assert!(
82                    false,
83                    "There should be a pending connection for {:?}",
84                    request_id
85                );
86            };
87            request.as_rooted()
88        };
89        let global = request.global();
90        let finished = match response {
91            OpenDatabaseResult::Connection { version, upgraded } => {
92                // Step 2.2: Otherwise,
93                // set request’s result to result,
94                // set request’s done flag,
95                // and fire an event named success at request.
96                request.dispatch_success(name.0.clone(), version, upgraded, can_gc);
97                true
98            },
99            OpenDatabaseResult::Upgrade {
100                version,
101                old_version,
102                transaction,
103            } => {
104                // TODO: link with backend connection concept.
105                let connection = IDBDatabase::new(
106                    &global,
107                    DOMString::from_string(name.0.clone()),
108                    version,
109                    can_gc,
110                );
111                request.set_connection(&connection);
112                request.upgrade_db_version(&connection, old_version, version, transaction, can_gc);
113                false
114            },
115            OpenDatabaseResult::VersionError => {
116                // Step 2.1 If result is an error, see dispatch_error().
117                self.dispatch_error(name.clone(), request_id, Error::Version(None), can_gc);
118                true
119            },
120            OpenDatabaseResult::AbortError => {
121                // Step 2.1 If result is an error, see dispatch_error().
122                self.dispatch_error(name.clone(), request_id, Error::Abort(None), can_gc);
123                true
124            },
125        };
126        if finished {
127            self.note_end_of_open(&name, &request.get_id());
128        }
129    }
130
131    fn handle_backend_error(
132        &self,
133        name: String,
134        request_id: Uuid,
135        backend_error: BackendError,
136        can_gc: CanGc,
137    ) {
138        self.dispatch_error(
139            DBName(name),
140            request_id,
141            map_backend_error_to_dom_error(backend_error),
142            can_gc,
143        );
144    }
145
146    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
147    /// The error dispatching part from within a task part.
148    fn dispatch_error(&self, name: DBName, request_id: Uuid, dom_exception: Error, can_gc: CanGc) {
149        // Step 5.3.1: If result is an error, then:
150        let request = {
151            let mut pending = self.pending_connections.borrow_mut();
152            let Some(entry) = pending.get_mut(&name) else {
153                return debug_assert!(false, "There should be a pending connection for {:?}", name);
154            };
155            let Some(request) = entry.get_mut(&request_id) else {
156                return debug_assert!(
157                    false,
158                    "There should be a pending connection for {:?}",
159                    request_id
160                );
161            };
162            request.as_rooted()
163        };
164        let global = request.global();
165
166        // Step 5.3.1.1: Set request’s result to undefined.
167        request.set_result(HandleValue::undefined());
168
169        // Step 5.3.1.2: Set request’s error to result.
170        request.set_error(Some(dom_exception), can_gc);
171
172        // Step 5.3.1.3: Set request’s done flag to true.
173        // TODO.
174
175        // Step 5.3.1.4: Fire an event named error at request
176        // with its bubbles
177        // and cancelable attributes initialized to true.
178        let event = Event::new(
179            &global,
180            Atom::from("error"),
181            EventBubbles::Bubbles,
182            EventCancelable::Cancelable,
183            can_gc,
184        );
185        event.fire(request.upcast(), can_gc);
186    }
187
188    /// <https://w3c.github.io/IndexedDB/#open-a-database-connection>
189    pub fn open_database(
190        &self,
191        name: DOMString,
192        version: Option<u64>,
193        request: &IDBOpenDBRequest,
194    ) -> Result<(), ()> {
195        let global = self.global();
196        let request_id = request.get_id();
197
198        {
199            let mut pending = self.pending_connections.borrow_mut();
200            let outer = pending.entry(DBName(name.to_string())).or_default();
201            outer.insert(request_id, Dom::from_ref(request));
202        }
203
204        let response_listener = Trusted::new(self);
205
206        let task_source = global
207            .task_manager()
208            .database_access_task_source()
209            .to_sendable();
210        let name = name.to_string();
211        let name_copy = name.clone();
212        let callback = GenericCallback::new(global.time_profiler_chan().clone(), move |message| {
213            let response_listener = response_listener.clone();
214            let name = name_copy.clone();
215            let request_id = request_id;
216            let backend_result = match message {
217                Ok(inner) => inner,
218                Err(err) => Err(BackendError::DbErr(format!("{err:?}"))),
219            };
220            task_source.queue(task!(set_request_result_to_database: move || {
221                let factory = response_listener.root();
222                match backend_result {
223                    Ok(response) => {
224                        factory.handle_open_db(name, response, request_id, CanGc::note())
225                    }
226                    Err(error) => factory.handle_backend_error(name, request_id, error, CanGc::note()),
227                }
228            }));
229        })
230        .expect("Could not create open database callback");
231
232        let open_operation = SyncOperation::OpenDatabase(
233            callback,
234            global.origin().immutable().clone(),
235            name.to_string(),
236            version,
237            request.get_id(),
238        );
239
240        // Note: algo continues in parallel.
241        if global
242            .storage_threads()
243            .send(IndexedDBThreadMsg::Sync(open_operation))
244            .is_err()
245        {
246            return Err(());
247        }
248        Ok(())
249    }
250
251    pub(crate) fn note_end_of_open(&self, name: &DBName, id: &Uuid) {
252        let mut pending = self.pending_connections.borrow_mut();
253        let empty = {
254            let Some(entry) = pending.get_mut(name) else {
255                return debug_assert!(false, "There should be a pending connection for {:?}", name);
256            };
257            entry.remove(id);
258            entry.is_empty()
259        };
260        if empty {
261            pending.remove(name);
262        }
263    }
264
265    pub(crate) fn abort_pending_upgrades(&self) {
266        let global = self.global();
267
268        // Note: pending connections removed in `handle_open_db`.
269        let pending = self.pending_connections.borrow();
270        let pending_upgrades = pending
271            .iter()
272            .map(|(key, val)| {
273                let ids: HashSet<Uuid> = val.iter().map(|(k, _v)| *k).collect();
274                (key.0.clone(), ids)
275            })
276            .collect();
277        let origin = global.origin().immutable().clone();
278        if global
279            .storage_threads()
280            .send(IndexedDBThreadMsg::Sync(
281                SyncOperation::AbortPendingUpgrades {
282                    pending_upgrades,
283                    origin,
284                },
285            ))
286            .is_err()
287        {
288            error!("Failed to send SyncOperation::AbortPendingUpgrade");
289        }
290    }
291}
292
293impl IDBFactoryMethods<crate::DomTypeHolder> for IDBFactory {
294    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
295    fn Open(&self, name: DOMString, version: Option<u64>) -> Fallible<DomRoot<IDBOpenDBRequest>> {
296        // Step 1: If version is 0 (zero), throw a TypeError.
297        if version == Some(0) {
298            return Err(Error::Type(
299                "The version must be an integer >= 1".to_owned(),
300            ));
301        };
302
303        // Step 2: Let origin be the origin of the global scope used to
304        // access this IDBFactory.
305        // TODO: update to 3.0 spec.
306        // Let environment be this’s relevant settings object.
307        let global = self.global();
308        let origin = global.origin();
309
310        // Step 3: if origin is an opaque origin,
311        // throw a "SecurityError" DOMException and abort these steps.
312        // TODO: update to 3.0 spec.
313        // Let storageKey be the result of running obtain a storage key given environment.
314        if let ImmutableOrigin::Opaque(_) = origin.immutable() {
315            return Err(Error::Security(None));
316        }
317
318        // Step 4: Let request be a new open request.
319        let request = IDBOpenDBRequest::new(&self.global(), CanGc::note());
320
321        // Step 5: Runs in parallel
322        if self.open_database(name, version, &request).is_err() {
323            return Err(Error::Operation(None));
324        }
325
326        // Step 6: Return a new IDBOpenDBRequest object for request.
327        Ok(request)
328    }
329
330    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-deletedatabase>
331    fn DeleteDatabase(&self, name: DOMString) -> Fallible<DomRoot<IDBOpenDBRequest>> {
332        // Step 1: Let environment be this’s relevant settings object.
333        let global = self.global();
334
335        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
336        // If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
337        // TODO: use a storage key.
338        let origin = global.origin();
339
340        // Legacy step 2: if origin is an opaque origin,
341        // throw a "SecurityError" DOMException and abort these steps.
342        // TODO: remove when a storage key is used.
343        if let ImmutableOrigin::Opaque(_) = origin.immutable() {
344            return Err(Error::Security(None));
345        }
346
347        // Step 3: Let request be a new open request
348        let request = IDBOpenDBRequest::new(&self.global(), CanGc::note());
349
350        // Step 4: Runs in parallel
351        if request.delete_database(name.to_string()).is_err() {
352            return Err(Error::Operation(None));
353        }
354
355        // Step 5: Return request
356        Ok(request)
357    }
358
359    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-databases>
360    fn Databases(&self, cx: &mut JSContext) -> Rc<Promise> {
361        // Step 1: Let environment be this’s relevant settings object
362        let global = self.global();
363
364        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
365        // If failure is returned, then return a promise rejected with a "SecurityError" DOMException
366        // TODO: implement storage keys.
367
368        // Step 3: Let p be a new promise.
369        let p = Promise::new(&global, CanGc::from_cx(cx));
370
371        // Note: the option is required to pass the promise to a task from within the generic callback,
372        // see #41356
373        let mut trusted_promise: Option<TrustedPromise> = Some(TrustedPromise::new(p.clone()));
374
375        // Step 4: Run these steps in parallel:
376        // Note implementing by communicating with the backend.
377        let task_source = global
378            .task_manager()
379            .database_access_task_source()
380            .to_sendable();
381        let callback = GenericCallback::new(global.time_profiler_chan().clone(), move |message| {
382            let result: BackendResult<Vec<DatabaseInfo>> = message.unwrap();
383            let Some(trusted_promise) = trusted_promise.take() else {
384                return error!("Callback for `DataBases` called twice.");
385            };
386
387            // Step 3.5: Queue a database task to resolve p with result.
388            task_source.queue(task!(set_request_result_to_database: move |cx| {
389                let promise = trusted_promise.root();
390                match result {
391                    Err(err) => {
392                        let error = map_backend_error_to_dom_error(err);
393                        rooted!(&in(cx) let mut rval = UndefinedValue());
394                        error
395                            .clone()
396                            .to_jsval(cx.into(), &promise.global(), rval.handle_mut(), CanGc::from_cx(cx));
397                        promise.reject_native(&rval.handle(), CanGc::from_cx(cx));
398                    },
399                    Ok(info_list) => {
400                        let info_list: Vec<IDBDatabaseInfo> = info_list
401                            .into_iter()
402                            .map(|info| IDBDatabaseInfo {
403                                name: Some(DOMString::from(info.name)),
404                                version: Some(info.version),
405                        })
406                        .collect();
407                        promise.resolve_native(&info_list, CanGc::from_cx(cx));
408                },
409            }
410            }));
411        })
412        .expect("Could not create delete database callback");
413
414        let get_operation =
415            SyncOperation::GetDatabases(callback, global.origin().immutable().clone());
416        if global
417            .storage_threads()
418            .send(IndexedDBThreadMsg::Sync(get_operation))
419            .is_err()
420        {
421            error!("Failed to send SyncOperation::GetDatabases");
422        }
423
424        // Step 5: Return p.
425        p
426    }
427
428    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbfactory-cmp>
429    fn Cmp(&self, cx: &mut JSContext, first: HandleValue, second: HandleValue) -> Fallible<i16> {
430        let first_key = convert_value_to_key(cx, first, None)?.into_result()?;
431        let second_key = convert_value_to_key(cx, second, None)?.into_result()?;
432        let cmp = first_key.partial_cmp(&second_key);
433        if let Some(cmp) = cmp {
434            match cmp {
435                std::cmp::Ordering::Less => Ok(-1),
436                std::cmp::Ordering::Equal => Ok(0),
437                std::cmp::Ordering::Greater => Ok(1),
438            }
439        } else {
440            Ok(i16::MAX)
441        }
442    }
443}