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 dom_struct::dom_struct;
8use js::context::JSContext;
9use js::jsval::UndefinedValue;
10use js::rust::HandleValue;
11use profile_traits::generic_callback::GenericCallback;
12use script_bindings::inheritance::Castable;
13use servo_base::generic_channel::GenericSend;
14use servo_config::pref;
15use servo_url::origin::ImmutableOrigin;
16use storage_traits::indexeddb::{
17    BackendResult, ConnectionMsg, DatabaseInfo, IndexedDBThreadMsg, 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::idbopendbrequest::IDBOpenDBRequest;
35use crate::dom::promise::Promise;
36use crate::dom::types::IDBTransaction;
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-3/#connection>
48    /// The connections opened through this factory.
49    /// We store the open request, which contains the connection.
50    /// TODO: remove when we are sure they are not needed anymore.
51    connections:
52        DomRefCell<HashMapTracedValues<DBName, HashMapTracedValues<Uuid, Dom<IDBOpenDBRequest>>>>,
53
54    /// <https://www.w3.org/TR/IndexedDB-3/#transaction>
55    /// Active transactions associated with this factory's global.
56    indexeddb_transactions: DomRefCell<HashMapTracedValues<DBName, Vec<Dom<IDBTransaction>>>>,
57
58    #[no_trace]
59    callback: DomRefCell<Option<GenericCallback<ConnectionMsg>>>,
60}
61
62impl IDBFactory {
63    /// <https://storage.spec.whatwg.org/#obtain-a-storage-key-for-non-storage-purposes>
64    fn obtain_storage_key_for_non_storage_purposes(environment: &GlobalScope) -> ImmutableOrigin {
65        // Step 1: Let origin be environment’s origin if environment is an environment settings object; otherwise environment’s creation URL’s origin.
66        // Step 2: Return a tuple consisting of origin.
67        environment.origin().immutable().clone()
68    }
69
70    /// <https://storage.spec.whatwg.org/#obtain-a-storage-key>
71    fn obtain_storage_key(environment: &GlobalScope) -> Option<ImmutableOrigin> {
72        // Step 1: Let key be the result of running obtain a storage key for non-storage purposes
73        // with environment.
74        let key = Self::obtain_storage_key_for_non_storage_purposes(environment);
75
76        // Step 2: If key's origin is an opaque origin, then return failure.
77        if let ImmutableOrigin::Opaque(_) = key {
78            return None;
79        }
80
81        // Step 3: If the user has disabled storage, then return failure.
82        if !pref!(dom_indexeddb_enabled) {
83            return None;
84        }
85
86        // Step 4: Return key.
87        Some(key)
88    }
89
90    pub fn new_inherited() -> IDBFactory {
91        IDBFactory {
92            reflector_: Reflector::new(),
93            connections: Default::default(),
94            indexeddb_transactions: Default::default(),
95            callback: Default::default(),
96        }
97    }
98
99    pub(crate) fn register_indexeddb_transaction(&self, txn: &IDBTransaction) {
100        let db_name = DBName(txn.get_db_name().to_string());
101        let mut map = self.indexeddb_transactions.borrow_mut();
102        let bucket = map.entry(db_name).or_default();
103        if !bucket.iter().any(|entry| &**entry == txn) {
104            bucket.push(Dom::from_ref(txn));
105        }
106        txn.set_registered_in_global();
107    }
108
109    pub(crate) fn unregister_indexeddb_transaction(&self, txn: &IDBTransaction) {
110        let db_name = DBName(txn.get_db_name().to_string());
111        let mut map = self.indexeddb_transactions.borrow_mut();
112        if let Some(bucket) = map.get_mut(&db_name) {
113            bucket.retain(|entry| &**entry != txn);
114            if bucket.is_empty() {
115                map.remove(&db_name);
116            }
117        }
118        txn.clear_registered_in_global();
119    }
120
121    pub(crate) fn cleanup_indexeddb_transactions(&self) -> bool {
122        // We implement the HTML-triggered deactivation effect by tracking script-created
123        // transactions on the global and deactivating them at the microtask checkpoint.
124        let snapshot: Vec<DomRoot<IDBTransaction>> = {
125            let mut map = self.indexeddb_transactions.borrow_mut();
126
127            // Transactions are normally unregistered when they finish (commit/abort),
128            // but unregister can occur in a queued task (e.g. finalize_abort), so we can
129            // briefly observe finished transactions here. Prune them defensively.
130            let keys: Vec<DBName> = map.iter().map(|(k, _)| k.clone()).collect();
131            for key in keys {
132                if let Some(bucket) = map.get_mut(&key) {
133                    bucket.retain(|txn| !txn.is_finished());
134                    if bucket.is_empty() {
135                        map.remove(&key);
136                    }
137                }
138            }
139
140            map.iter()
141                .flat_map(|(_db, bucket)| bucket.iter())
142                .map(|txn| DomRoot::from_ref(&**txn))
143                .collect()
144        };
145        // https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint
146        // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
147        // To cleanup Indexed Database transactions, run the following steps.
148        // They will return true if any transactions were cleaned up, or false otherwise.
149        // Step 1: If there are no transactions with cleanup event loop matching the current event loop, return false.
150        // Step 2: For each transaction transaction with cleanup event loop matching the current event loop:
151        // Step 2.1: Set transaction’s state to inactive.
152        // Step 2.2: Clear transaction’s cleanup event loop.
153        // Step 3: Return true.
154        let any_matching = snapshot
155            .iter()
156            .any(|txn| txn.cleanup_event_loop_matches_current());
157
158        if !any_matching {
159            return false;
160        }
161
162        for txn in snapshot {
163            if txn.cleanup_event_loop_matches_current() {
164                txn.set_active_flag(false);
165                txn.clear_cleanup_event_loop();
166                if txn.is_usable() {
167                    txn.maybe_commit();
168                }
169            }
170        }
171
172        // Prune finished transactions again after maybe_commit() progress.
173        let mut map = self.indexeddb_transactions.borrow_mut();
174        let keys: Vec<DBName> = map.iter().map(|(k, _)| k.clone()).collect();
175        for key in keys {
176            if let Some(bucket) = map.get_mut(&key) {
177                bucket.retain(|txn| !txn.is_finished());
178                if bucket.is_empty() {
179                    map.remove(&key);
180                }
181            }
182        }
183
184        true
185    }
186
187    pub(crate) fn maybe_commit_txn(&self, db_name: &str, txn_serial: u64) {
188        let key = DBName(db_name.to_string());
189        let snapshot: Vec<DomRoot<IDBTransaction>> = {
190            let map = self.indexeddb_transactions.borrow();
191            let Some(bucket) = map.get(&key) else {
192                return;
193            };
194            bucket.iter().map(|t| DomRoot::from_ref(&**t)).collect()
195        };
196
197        for txn in snapshot {
198            if txn.get_serial_number() == txn_serial {
199                txn.maybe_commit();
200                break;
201            }
202        }
203    }
204
205    /// <https://w3c.github.io/IndexedDB/#dom-idbrequest-transaction>
206    /// Clear IDBOpenDBRequest.transaction once the upgrade transaction is finished.
207    pub(crate) fn clear_open_request_transaction_for_txn(&self, transaction: &IDBTransaction) {
208        let requests: Vec<DomRoot<IDBOpenDBRequest>> = {
209            let pending = self.connections.borrow();
210            pending
211                .iter()
212                .flat_map(|(_db_name, entry)| entry.iter())
213                .map(|(_id, request)| request.as_rooted())
214                .collect()
215        };
216        let mut cleared = 0usize;
217        for request in requests {
218            cleared += request.clear_transaction_if_matches(transaction) as usize;
219        }
220
221        debug_assert_eq!(
222            cleared, 1,
223            "A versionchange transaction should belong to exactly one IDBOpenDBRequest."
224        );
225    }
226
227    pub fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<IDBFactory> {
228        reflect_dom_object(Box::new(IDBFactory::new_inherited()), global, can_gc)
229    }
230
231    /// Setup the callback to the backend service, if this hasn't been done already.
232    fn get_or_setup_callback(&self) -> GenericCallback<ConnectionMsg> {
233        if let Some(cb) = self.callback.borrow().as_ref() {
234            return cb.clone();
235        }
236
237        let global = self.global();
238        let response_listener = Trusted::new(self);
239
240        let task_source = global
241            .task_manager()
242            .database_access_task_source()
243            .to_sendable();
244        let callback = GenericCallback::new(global.time_profiler_chan().clone(), move |message| {
245            let response_listener = response_listener.clone();
246            let response = match message {
247                Ok(inner) => inner,
248                Err(err) => return error!("Error in IndexedDB factory callback {:?}.", err),
249            };
250            // Step 5.3: Queue a database task to run these steps:
251            task_source.queue(task!(set_request_result_to_database: move |cx| {
252                let factory = response_listener.root();
253                factory.handle_connection_message(cx, response)
254            }));
255        })
256        .expect("Could not create open database callback");
257
258        *self.callback.borrow_mut() = Some(callback.clone());
259
260        callback
261    }
262
263    fn get_request(&self, name: String, request_id: &Uuid) -> Option<DomRoot<IDBOpenDBRequest>> {
264        let name = DBName(name);
265        let mut pending = self.connections.borrow_mut();
266        let Some(entry) = pending.get_mut(&name) else {
267            debug_assert!(false, "There should be a pending connection for {:?}", name);
268            return None;
269        };
270        let Some(request) = entry.get_mut(request_id) else {
271            debug_assert!(
272                false,
273                "There should be a pending connection for {:?}",
274                request_id
275            );
276            return None;
277        };
278        Some(request.as_rooted())
279    }
280
281    /// <https://w3c.github.io/IndexedDB/#open-a-database-connection>
282    /// The steps that continue on the script-thread.
283    /// This covers interacting with the current open request,
284    /// as well as with other open connections preventing the request from making progress.
285    fn handle_connection_message(&self, cx: &mut JSContext, response: ConnectionMsg) {
286        match response {
287            ConnectionMsg::Connection {
288                name,
289                id,
290                version,
291                upgraded,
292                object_store_names,
293            } => {
294                let Some(request) = self.get_request(name.clone(), &id) else {
295                    return debug_assert!(
296                        false,
297                        "There should be a request to handle ConnectionMsg::Connection."
298                    );
299                };
300
301                // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
302                // Step 3. Set transaction’s scope to connection’s object store set.
303                let connection = request.get_or_init_connection(
304                    &self.global(),
305                    name.clone(),
306                    version,
307                    upgraded,
308                    CanGc::from_cx(cx),
309                );
310                connection.set_object_store_names_from_backend(object_store_names);
311
312                // Step 2.2: Otherwise,
313                // set request’s result to result,
314                // set request’s done flag,
315                // and fire an event named success at request.
316                request.dispatch_success(name, version, upgraded, CanGc::from_cx(cx));
317            },
318            ConnectionMsg::Upgrade {
319                name,
320                id,
321                version,
322                old_version,
323                transaction,
324                object_store_names,
325            } => {
326                let global = self.global();
327
328                let Some(request) = self.get_request(name.clone(), &id) else {
329                    return debug_assert!(
330                        false,
331                        "There should be a request to handle ConnectionMsg::Upgrade."
332                    );
333                };
334
335                let connection = request.get_or_init_connection(
336                    &global,
337                    name,
338                    version,
339                    false,
340                    CanGc::from_cx(cx),
341                );
342                // https://w3c.github.io/IndexedDB/#upgrade-transaction-steps
343                // Step 3. Set transaction’s scope to connection’s object store set.
344                connection.set_object_store_names_from_backend(object_store_names);
345                request.upgrade_db_version(
346                    &connection,
347                    old_version,
348                    version,
349                    transaction,
350                    CanGc::from_cx(cx),
351                );
352            },
353            ConnectionMsg::VersionError { name, id } => {
354                // Step 2.1 If result is an error, see dispatch_error().
355                self.dispatch_error(cx, name, id, Error::Version(None));
356            },
357            ConnectionMsg::AbortError { name, id } => {
358                // Step 2.1 If result is an error, see dispatch_error().
359                self.dispatch_error(cx, name, id, Error::Abort(None));
360            },
361            ConnectionMsg::DatabaseError { name, id, error } => {
362                // Step 2.1 If result is an error, see dispatch_error().
363                self.dispatch_error(cx, name, id, map_backend_error_to_dom_error(error));
364            },
365            ConnectionMsg::VersionChange {
366                name,
367                id,
368                version,
369                old_version,
370            } => {
371                let global = self.global();
372                let Some(request) = self.get_request(name.clone(), &id) else {
373                    return debug_assert!(
374                        false,
375                        "There should be a request to handle ConnectionMsg::VersionChange."
376                    );
377                };
378                let connection = request.get_or_init_connection(
379                    &global,
380                    name.clone(),
381                    version,
382                    false,
383                    CanGc::from_cx(cx),
384                );
385
386                // Step 10.2: fire a version change event named versionchange at entry with db’s version and version.
387                connection.dispatch_versionchange(old_version, Some(version), CanGc::from_cx(cx));
388
389                // Step 10.3: Wait for all of the events to be fired.
390                // Note: backend is at this step; sending a message to continue algo there.
391                let operation = SyncOperation::NotifyEndOfVersionChange {
392                    id,
393                    name,
394                    old_version,
395                    origin: global.origin().immutable().clone(),
396                };
397                if global
398                    .storage_threads()
399                    .send(IndexedDBThreadMsg::Sync(operation))
400                    .is_err()
401                {
402                    error!("Failed to send SyncOperation::NotifyEndOfVersionChange.");
403                }
404            },
405            ConnectionMsg::Blocked {
406                name,
407                id,
408                version,
409                old_version,
410            } => {
411                let Some(request) = self.get_request(name, &id) else {
412                    return debug_assert!(
413                        false,
414                        "There should be a request to handle ConnectionMsg::VersionChange."
415                    );
416                };
417
418                // Step 10.4: fire a version change event named blocked at request with db’s version and version.
419                request.dispatch_blocked(old_version, Some(version), CanGc::from_cx(cx));
420            },
421            ConnectionMsg::TxnMaybeCommit { db_name, txn } => {
422                let factory = Trusted::new(self);
423                self.global()
424                    .task_manager()
425                    .dom_manipulation_task_source()
426                    .queue(task!(indexeddb_maybe_commit_txn: move || {
427                        let factory = factory.root();
428                        factory.maybe_commit_txn(&db_name, txn);
429                    }));
430            },
431        }
432    }
433
434    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
435    /// The error dispatching part from within a task part.
436    fn dispatch_error(
437        &self,
438        cx: &mut JSContext,
439        name: String,
440        request_id: Uuid,
441        dom_exception: Error,
442    ) {
443        let name = DBName(name);
444
445        // Step 5.3.1: If result is an error, then:
446        let request = {
447            let mut pending = self.connections.borrow_mut();
448            let Some(entry) = pending.get_mut(&name) else {
449                return debug_assert!(false, "There should be a pending connection for {:?}", name);
450            };
451            let Some(request) = entry.get_mut(&request_id) else {
452                return debug_assert!(
453                    false,
454                    "There should be a pending connection for {:?}",
455                    request_id
456                );
457            };
458            request.as_rooted()
459        };
460        let global = request.global();
461
462        // Step 5.3.1.1: Set request’s result to undefined.
463        request.set_result(HandleValue::undefined());
464
465        // Step 5.3.1.2: Set request’s error to result.
466        request.set_error(Some(dom_exception), CanGc::from_cx(cx));
467        // Open requests expose a transaction only while `upgradeneeded` is being dispatched;
468        // otherwise `IDBOpenDBRequest.transaction` must be null.
469        // https://w3c.github.io/IndexedDB/#dom-idbrequest-transaction
470        // https://w3c.github.io/IndexedDB/#open-a-database-connection
471        // Open requests that have completed with an error must not retain an upgrade transaction.
472        request.clear_transaction();
473
474        // Step 5.3.1.3: Set request’s done flag to true.
475        request.set_ready_state_done();
476
477        // Step 5.3.1.4: Fire an event named error at request
478        // with its bubbles
479        // and cancelable attributes initialized to true.
480        let event = Event::new(
481            &global,
482            Atom::from("error"),
483            EventBubbles::Bubbles,
484            EventCancelable::Cancelable,
485            CanGc::from_cx(cx),
486        );
487        event.fire(request.upcast(), CanGc::from_cx(cx));
488    }
489
490    /// <https://w3c.github.io/IndexedDB/#open-a-database-connection>
491    fn open_database(
492        &self,
493        storage_key: ImmutableOrigin,
494        name: DOMString,
495        version: Option<u64>,
496        request: &IDBOpenDBRequest,
497    ) -> Result<(), ()> {
498        let global = self.global();
499        let request_id = request.get_id();
500
501        {
502            let mut pending = self.connections.borrow_mut();
503            let outer = pending.entry(DBName(name.to_string())).or_default();
504            outer.insert(request_id, Dom::from_ref(request));
505        }
506
507        let callback = self.get_or_setup_callback();
508
509        // Step 5: Run these steps in parallel:
510        // Step 5.1: Let result be the result of opening a database connection,
511        // with storageKey, name, version if given and undefined otherwise, and request.
512        let open_operation = SyncOperation::OpenDatabase(
513            callback,
514            storage_key,
515            name.to_string(),
516            version,
517            request.get_id(),
518        );
519
520        // Note: algo continues in parallel.
521        if global
522            .storage_threads()
523            .send(IndexedDBThreadMsg::Sync(open_operation))
524            .is_err()
525        {
526            return Err(());
527        }
528        Ok(())
529    }
530
531    pub(crate) fn abort_pending_upgrades(&self) {
532        let global = self.global();
533        let pending = self.connections.borrow();
534        let pending_upgrades = pending
535            .iter()
536            .map(|(key, val)| {
537                let ids: HashSet<Uuid> = val.iter().map(|(k, _v)| *k).collect();
538                (key.0.clone(), ids)
539            })
540            .collect();
541        let origin = global.origin().immutable().clone();
542        if global
543            .storage_threads()
544            .send(IndexedDBThreadMsg::Sync(
545                SyncOperation::AbortPendingUpgrades {
546                    pending_upgrades,
547                    origin,
548                },
549            ))
550            .is_err()
551        {
552            error!("Failed to send SyncOperation::AbortPendingUpgrade");
553        }
554    }
555}
556
557impl IDBFactoryMethods<crate::DomTypeHolder> for IDBFactory {
558    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
559    fn Open(&self, name: DOMString, version: Option<u64>) -> Fallible<DomRoot<IDBOpenDBRequest>> {
560        // Step 1: If version is 0 (zero), throw a TypeError.
561        if version == Some(0) {
562            return Err(Error::Type(
563                c"The version must be an integer >= 1".to_owned(),
564            ));
565        };
566
567        // Step 2: Let environment be this’s relevant settings object.
568        let global = self.global();
569
570        // Step 3: Let storageKey be the result of running obtain a storage key given environment.
571        // If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
572        let Some(storage_key) = Self::obtain_storage_key(&global) else {
573            return Err(Error::Security(None));
574        };
575
576        // Step 4: Let request be a new open request.
577        let request = IDBOpenDBRequest::new(&self.global(), CanGc::deprecated_note());
578
579        // Step 5: Runs in parallel
580        if self
581            .open_database(storage_key, name, version, &request)
582            .is_err()
583        {
584            return Err(Error::Operation(None));
585        }
586
587        // Step 6: Return a new IDBOpenDBRequest object for request.
588        Ok(request)
589    }
590
591    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-deletedatabase>
592    fn DeleteDatabase(&self, name: DOMString) -> Fallible<DomRoot<IDBOpenDBRequest>> {
593        // Step 1: Let environment be this’s relevant settings object.
594        let global = self.global();
595
596        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
597        // If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
598        let Some(storage_key) = Self::obtain_storage_key(&global) else {
599            return Err(Error::Security(None));
600        };
601
602        // Step 3: Let request be a new open request
603        let request = IDBOpenDBRequest::new(&self.global(), CanGc::deprecated_note());
604
605        // Step 4: Runs in parallel
606        if request
607            .delete_database(storage_key, name.to_string())
608            .is_err()
609        {
610            return Err(Error::Operation(None));
611        }
612
613        // Step 5: Return request
614        Ok(request)
615    }
616
617    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-databases>
618    fn Databases(&self, cx: &mut JSContext) -> Rc<Promise> {
619        // Step 1: Let environment be this’s relevant settings object.
620        let global = self.global();
621
622        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
623        // If failure is returned, then return a promise rejected with a "SecurityError" DOMException.
624        let storage_key = match Self::obtain_storage_key(&global) {
625            Some(storage_key) => storage_key,
626            None => {
627                let p = Promise::new(&global, CanGc::from_cx(cx));
628                p.reject_error(Error::Security(None), CanGc::from_cx(cx));
629                return p;
630            },
631        };
632
633        // Step 3: Let p be a new promise.
634        let p = Promise::new(&global, CanGc::from_cx(cx));
635
636        // Note: the option is required to pass the promise to a task from within the generic callback,
637        // see #41356
638        let mut trusted_promise: Option<TrustedPromise> = Some(TrustedPromise::new(p.clone()));
639
640        // Step 4: Run these steps in parallel:
641        // Note implementing by communicating with the backend.
642        let task_source = global
643            .task_manager()
644            .database_access_task_source()
645            .to_sendable();
646        let callback = GenericCallback::new(global.time_profiler_chan().clone(), move |message| {
647            let result: BackendResult<Vec<DatabaseInfo>> = message.unwrap();
648            let Some(trusted_promise) = trusted_promise.take() else {
649                return error!("Callback for `DataBases` called twice.");
650            };
651
652            // Step 4.4: Queue a database task to resolve p with result.
653            task_source.queue(task!(set_request_result_to_database: move |cx| {
654                let promise = trusted_promise.root();
655                match result {
656                    Err(err) => {
657                        let error = map_backend_error_to_dom_error(err);
658                        rooted!(&in(cx) let mut rval = UndefinedValue());
659                        error
660                            .to_jsval(cx.into(), &promise.global(), rval.handle_mut(), CanGc::from_cx(cx));
661                        promise.reject_native(&rval.handle(), CanGc::from_cx(cx));
662                    },
663                    Ok(info_list) => {
664                        let info_list: Vec<IDBDatabaseInfo> = info_list
665                            .into_iter()
666                            .map(|info| IDBDatabaseInfo {
667                                name: Some(DOMString::from(info.name)),
668                                version: Some(info.version),
669                        })
670                        .collect();
671                        promise.resolve_native(&info_list, CanGc::from_cx(cx));
672                },
673            }
674            }));
675        })
676        .expect("Could not create databases callback");
677
678        let get_operation = SyncOperation::GetDatabases(callback, storage_key);
679        if global
680            .storage_threads()
681            .send(IndexedDBThreadMsg::Sync(get_operation))
682            .is_err()
683        {
684            error!("Failed to send SyncOperation::GetDatabases");
685        }
686
687        // Step 5: Return p.
688        p
689    }
690
691    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbfactory-cmp>
692    fn Cmp(&self, cx: &mut JSContext, first: HandleValue, second: HandleValue) -> Fallible<i16> {
693        let first_key = convert_value_to_key(cx, first, None)?.into_result()?;
694        let second_key = convert_value_to_key(cx, second, None)?.into_result()?;
695        let cmp = first_key.partial_cmp(&second_key);
696        if let Some(cmp) = cmp {
697            match cmp {
698                std::cmp::Ordering::Less => Ok(-1),
699                std::cmp::Ordering::Equal => Ok(0),
700                std::cmp::Ordering::Greater => Ok(1),
701            }
702        } else {
703            Ok(i16::MAX)
704        }
705    }
706}