Skip to main content

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