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