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-2/#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            } => {
264                let Some(request) = self.get_request(name.clone(), &id) else {
265                    return debug_assert!(
266                        false,
267                        "There should be a request to handle ConnectionMsg::Connection."
268                    );
269                };
270
271                // Step 2.2: Otherwise,
272                // set request’s result to result,
273                // set request’s done flag,
274                // and fire an event named success at request.
275                request.dispatch_success(name, version, upgraded, can_gc);
276            },
277            ConnectionMsg::Upgrade {
278                name,
279                id,
280                version,
281                old_version,
282                transaction,
283            } => {
284                let global = self.global();
285
286                let Some(request) = self.get_request(name.clone(), &id) else {
287                    return debug_assert!(
288                        false,
289                        "There should be a request to handle ConnectionMsg::Upgrade."
290                    );
291                };
292
293                let connection =
294                    request.get_or_init_connection(&global, name, version, false, can_gc);
295                request.upgrade_db_version(&connection, old_version, version, transaction, can_gc);
296            },
297            ConnectionMsg::VersionError { name, id } => {
298                // Step 2.1 If result is an error, see dispatch_error().
299                self.dispatch_error(name, id, Error::Version(None), can_gc);
300            },
301            ConnectionMsg::AbortError { name, id } => {
302                // Step 2.1 If result is an error, see dispatch_error().
303                self.dispatch_error(name, id, Error::Abort(None), can_gc);
304            },
305            ConnectionMsg::DatabaseError { name, id, error } => {
306                // Step 2.1 If result is an error, see dispatch_error().
307                self.dispatch_error(name, id, map_backend_error_to_dom_error(error), can_gc);
308            },
309            ConnectionMsg::VersionChange {
310                name,
311                id,
312                version,
313                old_version,
314            } => {
315                let global = self.global();
316                let Some(request) = self.get_request(name.clone(), &id) else {
317                    return debug_assert!(
318                        false,
319                        "There should be a request to handle ConnectionMsg::VersionChange."
320                    );
321                };
322                let connection =
323                    request.get_or_init_connection(&global, name.clone(), version, false, can_gc);
324
325                // Step 10.2: fire a version change event named versionchange at entry with db’s version and version.
326                connection.dispatch_versionchange(old_version, Some(version), can_gc);
327
328                // Step 10.3: Wait for all of the events to be fired.
329                // Note: backend is at this step; sending a message to continue algo there.
330                let operation = SyncOperation::NotifyEndOfVersionChange {
331                    id,
332                    name,
333                    old_version,
334                    origin: global.origin().immutable().clone(),
335                };
336                if global
337                    .storage_threads()
338                    .send(IndexedDBThreadMsg::Sync(operation))
339                    .is_err()
340                {
341                    error!("Failed to send SyncOperation::NotifyEndOfVersionChange.");
342                }
343            },
344            ConnectionMsg::Blocked {
345                name,
346                id,
347                version,
348                old_version,
349            } => {
350                let Some(request) = self.get_request(name, &id) else {
351                    return debug_assert!(
352                        false,
353                        "There should be a request to handle ConnectionMsg::VersionChange."
354                    );
355                };
356
357                // Step 10.4: fire a version change event named blocked at request with db’s version and version.
358                request.dispatch_blocked(old_version, Some(version), can_gc);
359            },
360            ConnectionMsg::TxnMaybeCommit { db_name, txn } => {
361                let factory = Trusted::new(self);
362                self.global()
363                    .task_manager()
364                    .dom_manipulation_task_source()
365                    .queue(task!(indexeddb_maybe_commit_txn: move || {
366                        let factory = factory.root();
367                        factory.maybe_commit_txn(&db_name, txn);
368                    }));
369            },
370        }
371    }
372
373    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
374    /// The error dispatching part from within a task part.
375    fn dispatch_error(&self, name: String, request_id: Uuid, dom_exception: Error, can_gc: CanGc) {
376        let name = DBName(name);
377
378        // Step 5.3.1: If result is an error, then:
379        let request = {
380            let mut pending = self.connections.borrow_mut();
381            let Some(entry) = pending.get_mut(&name) else {
382                return debug_assert!(false, "There should be a pending connection for {:?}", name);
383            };
384            let Some(request) = entry.get_mut(&request_id) else {
385                return debug_assert!(
386                    false,
387                    "There should be a pending connection for {:?}",
388                    request_id
389                );
390            };
391            request.as_rooted()
392        };
393        let global = request.global();
394
395        // Step 5.3.1.1: Set request’s result to undefined.
396        request.set_result(HandleValue::undefined());
397
398        // Step 5.3.1.2: Set request’s error to result.
399        request.set_error(Some(dom_exception), can_gc);
400        // Open requests expose a transaction only while `upgradeneeded` is being dispatched;
401        // otherwise `IDBOpenDBRequest.transaction` must be null.
402        // https://w3c.github.io/IndexedDB/#dom-idbrequest-transaction
403        // https://w3c.github.io/IndexedDB/#open-a-database-connection
404        // Open requests that have completed with an error must not retain an upgrade transaction.
405        request.clear_transaction();
406
407        // Step 5.3.1.3: Set request’s done flag to true.
408        request.set_ready_state_done();
409
410        // Step 5.3.1.4: Fire an event named error at request
411        // with its bubbles
412        // and cancelable attributes initialized to true.
413        let event = Event::new(
414            &global,
415            Atom::from("error"),
416            EventBubbles::Bubbles,
417            EventCancelable::Cancelable,
418            can_gc,
419        );
420        event.fire(request.upcast(), can_gc);
421    }
422
423    /// <https://w3c.github.io/IndexedDB/#open-a-database-connection>
424    fn open_database(
425        &self,
426        name: DOMString,
427        version: Option<u64>,
428        request: &IDBOpenDBRequest,
429    ) -> Result<(), ()> {
430        let global = self.global();
431        let request_id = request.get_id();
432
433        {
434            let mut pending = self.connections.borrow_mut();
435            let outer = pending.entry(DBName(name.to_string())).or_default();
436            outer.insert(request_id, Dom::from_ref(request));
437        }
438
439        let callback = self.get_or_setup_callback();
440
441        let open_operation = SyncOperation::OpenDatabase(
442            callback,
443            global.origin().immutable().clone(),
444            name.to_string(),
445            version,
446            request.get_id(),
447        );
448
449        // Note: algo continues in parallel.
450        if global
451            .storage_threads()
452            .send(IndexedDBThreadMsg::Sync(open_operation))
453            .is_err()
454        {
455            return Err(());
456        }
457        Ok(())
458    }
459
460    pub(crate) fn abort_pending_upgrades(&self) {
461        let global = self.global();
462        let pending = self.connections.borrow();
463        let pending_upgrades = pending
464            .iter()
465            .map(|(key, val)| {
466                let ids: HashSet<Uuid> = val.iter().map(|(k, _v)| *k).collect();
467                (key.0.clone(), ids)
468            })
469            .collect();
470        let origin = global.origin().immutable().clone();
471        if global
472            .storage_threads()
473            .send(IndexedDBThreadMsg::Sync(
474                SyncOperation::AbortPendingUpgrades {
475                    pending_upgrades,
476                    origin,
477                },
478            ))
479            .is_err()
480        {
481            error!("Failed to send SyncOperation::AbortPendingUpgrade");
482        }
483    }
484}
485
486impl IDBFactoryMethods<crate::DomTypeHolder> for IDBFactory {
487    /// <https://w3c.github.io/IndexedDB/#dom-idbfactory-open>
488    fn Open(&self, name: DOMString, version: Option<u64>) -> Fallible<DomRoot<IDBOpenDBRequest>> {
489        // Step 1: If version is 0 (zero), throw a TypeError.
490        if version == Some(0) {
491            return Err(Error::Type(
492                c"The version must be an integer >= 1".to_owned(),
493            ));
494        };
495
496        // Step 2: Let origin be the origin of the global scope used to
497        // access this IDBFactory.
498        // TODO: update to 3.0 spec.
499        // Let environment be this’s relevant settings object.
500        let global = self.global();
501        let origin = global.origin();
502
503        // Step 3: if origin is an opaque origin,
504        // throw a "SecurityError" DOMException and abort these steps.
505        // TODO: update to 3.0 spec.
506        // Let storageKey be the result of running obtain a storage key given environment.
507        if let ImmutableOrigin::Opaque(_) = origin.immutable() {
508            return Err(Error::Security(None));
509        }
510
511        // Step 4: Let request be a new open request.
512        let request = IDBOpenDBRequest::new(&self.global(), CanGc::note());
513
514        // Step 5: Runs in parallel
515        if self.open_database(name, version, &request).is_err() {
516            return Err(Error::Operation(None));
517        }
518
519        // Step 6: Return a new IDBOpenDBRequest object for request.
520        Ok(request)
521    }
522
523    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-deletedatabase>
524    fn DeleteDatabase(&self, name: DOMString) -> Fallible<DomRoot<IDBOpenDBRequest>> {
525        // Step 1: Let environment be this’s relevant settings object.
526        let global = self.global();
527
528        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
529        // If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
530        // TODO: use a storage key.
531        let origin = global.origin();
532
533        // Legacy step 2: if origin is an opaque origin,
534        // throw a "SecurityError" DOMException and abort these steps.
535        // TODO: remove when a storage key is used.
536        if let ImmutableOrigin::Opaque(_) = origin.immutable() {
537            return Err(Error::Security(None));
538        }
539
540        // Step 3: Let request be a new open request
541        let request = IDBOpenDBRequest::new(&self.global(), CanGc::note());
542
543        // Step 4: Runs in parallel
544        if request.delete_database(name.to_string()).is_err() {
545            return Err(Error::Operation(None));
546        }
547
548        // Step 5: Return request
549        Ok(request)
550    }
551
552    /// <https://www.w3.org/TR/IndexedDB/#dom-idbfactory-databases>
553    fn Databases(&self, cx: &mut JSContext) -> Rc<Promise> {
554        // Step 1: Let environment be this’s relevant settings object
555        let global = self.global();
556
557        // Step 2: Let storageKey be the result of running obtain a storage key given environment.
558        // If failure is returned, then return a promise rejected with a "SecurityError" DOMException
559        // TODO: implement storage keys.
560
561        // Step 3: Let p be a new promise.
562        let p = Promise::new(&global, CanGc::from_cx(cx));
563
564        // Note: the option is required to pass the promise to a task from within the generic callback,
565        // see #41356
566        let mut trusted_promise: Option<TrustedPromise> = Some(TrustedPromise::new(p.clone()));
567
568        // Step 4: Run these steps in parallel:
569        // Note implementing by communicating with the backend.
570        let task_source = global
571            .task_manager()
572            .database_access_task_source()
573            .to_sendable();
574        let callback = GenericCallback::new(global.time_profiler_chan().clone(), move |message| {
575            let result: BackendResult<Vec<DatabaseInfo>> = message.unwrap();
576            let Some(trusted_promise) = trusted_promise.take() else {
577                return error!("Callback for `DataBases` called twice.");
578            };
579
580            // Step 3.5: Queue a database task to resolve p with result.
581            task_source.queue(task!(set_request_result_to_database: move |cx| {
582                let promise = trusted_promise.root();
583                match result {
584                    Err(err) => {
585                        let error = map_backend_error_to_dom_error(err);
586                        rooted!(&in(cx) let mut rval = UndefinedValue());
587                        error
588                            .clone()
589                            .to_jsval(cx.into(), &promise.global(), rval.handle_mut(), CanGc::from_cx(cx));
590                        promise.reject_native(&rval.handle(), CanGc::from_cx(cx));
591                    },
592                    Ok(info_list) => {
593                        let info_list: Vec<IDBDatabaseInfo> = info_list
594                            .into_iter()
595                            .map(|info| IDBDatabaseInfo {
596                                name: Some(DOMString::from(info.name)),
597                                version: Some(info.version),
598                        })
599                        .collect();
600                        promise.resolve_native(&info_list, CanGc::from_cx(cx));
601                },
602            }
603            }));
604        })
605        .expect("Could not create delete database callback");
606
607        let get_operation =
608            SyncOperation::GetDatabases(callback, global.origin().immutable().clone());
609        if global
610            .storage_threads()
611            .send(IndexedDBThreadMsg::Sync(get_operation))
612            .is_err()
613        {
614            error!("Failed to send SyncOperation::GetDatabases");
615        }
616
617        // Step 5: Return p.
618        p
619    }
620
621    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbfactory-cmp>
622    fn Cmp(&self, cx: &mut JSContext, first: HandleValue, second: HandleValue) -> Fallible<i16> {
623        let first_key = convert_value_to_key(cx, first, None)?.into_result()?;
624        let second_key = convert_value_to_key(cx, second, None)?.into_result()?;
625        let cmp = first_key.partial_cmp(&second_key);
626        if let Some(cmp) = cmp {
627            match cmp {
628                std::cmp::Ordering::Less => Ok(-1),
629                std::cmp::Ordering::Equal => Ok(0),
630                std::cmp::Ordering::Greater => Ok(1),
631            }
632        } else {
633            Ok(i16::MAX)
634        }
635    }
636}