Skip to main content

script/dom/indexeddb/
idbdatabase.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/. */
4
5use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use js::context::JSContext;
9use profile_traits::generic_channel::channel;
10use script_bindings::cell::DomRefCell;
11use script_bindings::reflector::reflect_dom_object;
12use servo_base::generic_channel::{GenericSend, GenericSender};
13use storage_traits::indexeddb::{IndexedDBThreadMsg, KeyPath, SyncOperation};
14use stylo_atoms::Atom;
15use uuid::Uuid;
16
17use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::{
18    IDBDatabaseMethods, IDBObjectStoreParameters, IDBTransactionOptions,
19};
20use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::IDBTransactionMode;
21use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence;
22use crate::dom::bindings::error::{Error, Fallible};
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::root::{DomRoot, MutNullableDom};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::domstringlist::DOMStringList;
28use crate::dom::eventtarget::EventTarget;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::indexeddb::idbobjectstore::{IDBObjectStore, IDBObjectStoreAbortState};
31use crate::dom::indexeddb::idbtransaction::IDBTransaction;
32use crate::dom::indexeddb::idbversionchangeevent::IDBVersionChangeEvent;
33use crate::indexeddb::is_valid_key_path;
34use crate::script_runtime::CanGc;
35
36#[dom_struct]
37pub struct IDBDatabase {
38    eventtarget: EventTarget,
39    /// <https://w3c.github.io/IndexedDB/#database-name>
40    name: DOMString,
41    /// <https://w3c.github.io/IndexedDB/#database-version>
42    version: Cell<u64>,
43    /// <https://w3c.github.io/IndexedDB/#object-store>
44    object_store_names: DomRefCell<Vec<DOMString>>,
45    /// <https://w3c.github.io/IndexedDB/#database-upgrade-transaction>
46    upgrade_transaction: MutNullableDom<IDBTransaction>,
47
48    #[no_trace]
49    #[ignore_malloc_size_of = "Uuid"]
50    id: Uuid,
51
52    // Flags
53    /// <https://w3c.github.io/IndexedDB/#connection-close-pending-flag>
54    close_pending: Cell<bool>,
55}
56
57impl IDBDatabase {
58    pub fn new_inherited(name: DOMString, id: Uuid, version: u64) -> IDBDatabase {
59        IDBDatabase {
60            eventtarget: EventTarget::new_inherited(),
61            name,
62            id,
63            version: Cell::new(version),
64            object_store_names: Default::default(),
65            upgrade_transaction: Default::default(),
66            close_pending: Cell::new(false),
67        }
68    }
69
70    pub fn new(
71        global: &GlobalScope,
72        name: DOMString,
73        id: Uuid,
74        version: u64,
75        can_gc: CanGc,
76    ) -> DomRoot<IDBDatabase> {
77        reflect_dom_object(
78            Box::new(IDBDatabase::new_inherited(name, id, version)),
79            global,
80            can_gc,
81        )
82    }
83
84    fn get_idb_thread(&self) -> GenericSender<IndexedDBThreadMsg> {
85        self.global().storage_threads().sender()
86    }
87
88    pub fn get_name(&self) -> DOMString {
89        self.name.clone()
90    }
91
92    pub fn object_stores(&self) -> DomRoot<DOMStringList> {
93        DOMStringList::new(
94            &self.global(),
95            self.object_store_names.borrow().clone(),
96            CanGc::deprecated_note(),
97        )
98    }
99
100    pub(crate) fn object_store_names_snapshot(&self) -> Vec<DOMString> {
101        // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
102        // Step 4. Set connection’s object store set to the set of object stores in database if database previously existed,
103        // or the empty set if database was newly created.
104        self.object_store_names.borrow().clone()
105    }
106
107    pub(crate) fn set_object_store_names_from_backend(&self, names: Vec<String>) {
108        // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
109        // Step 4. NOTE: This reverts the value of objectStoreNames returned by the IDBDatabase object.
110        *self.object_store_names.borrow_mut() = names.into_iter().map(Into::into).collect();
111    }
112
113    pub(crate) fn restore_object_store_names(&self, names: Vec<DOMString>) {
114        // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
115        // Step 4. NOTE: This reverts the value of objectStoreNames returned by the IDBDatabase object.
116        *self.object_store_names.borrow_mut() = names;
117    }
118
119    pub(crate) fn rename_object_store_name(&self, old_name: &DOMString, new_name: DOMString) {
120        let mut object_store_names = self.object_store_names.borrow_mut();
121        if let Some(position) = object_store_names.iter().position(|name| name == old_name) {
122            object_store_names[position] = new_name;
123        }
124    }
125
126    pub(crate) fn object_store_exists(&self, name: &DOMString) -> bool {
127        self.object_store_names
128            .borrow()
129            .iter()
130            .any(|store_name| store_name == name)
131    }
132
133    /// <https://w3c.github.io/IndexedDB/#dom-idbdatabase-version>
134    pub(crate) fn version(&self) -> u64 {
135        // The version getter steps are to return this’s version.
136        self.version.get()
137    }
138
139    pub(crate) fn set_version(&self, version: u64) {
140        self.version.set(version);
141    }
142
143    pub fn set_transaction(&self, transaction: &IDBTransaction) {
144        self.upgrade_transaction.set(Some(transaction));
145    }
146
147    pub(crate) fn clear_upgrade_transaction(&self, transaction: &IDBTransaction) {
148        let current = self
149            .upgrade_transaction
150            .get()
151            .expect("clear_upgrade_transaction called but no upgrade transaction is set");
152
153        debug_assert!(
154            &*current == transaction,
155            "clear_upgrade_transaction called with non-current transaction"
156        );
157
158        self.upgrade_transaction.set(None);
159    }
160
161    /// <https://w3c.github.io/IndexedDB/#eventdef-idbdatabase-versionchange>
162    pub fn dispatch_versionchange(
163        &self,
164        cx: &mut JSContext,
165        old_version: u64,
166        new_version: Option<u64>,
167    ) {
168        let global = self.global();
169        let _ = IDBVersionChangeEvent::fire_version_change_event(
170            cx,
171            &global,
172            self.upcast(),
173            Atom::from("versionchange"),
174            old_version,
175            new_version,
176        );
177    }
178}
179
180impl IDBDatabaseMethods<crate::DomTypeHolder> for IDBDatabase {
181    /// <https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction>
182    fn Transaction(
183        &self,
184        store_names: StringOrStringSequence,
185        mode: IDBTransactionMode,
186        options: &IDBTransactionOptions,
187    ) -> Fallible<DomRoot<IDBTransaction>> {
188        // Step 1. If a live upgrade transaction is associated with the connection,
189        // throw an "InvalidStateError" DOMException.
190        if self.upgrade_transaction.get().is_some() {
191            return Err(Error::InvalidState(None));
192        }
193
194        // Step 2. If this’s close pending flag is true, then throw an
195        // "InvalidStateError" DOMException.
196        if self.close_pending.get() {
197            return Err(Error::InvalidState(None));
198        }
199
200        // Step 3. Let scope be the set of unique strings in storeNames if it is
201        // a sequence, or a set containing one string equal to storeNames otherwise.
202        let mut scope = match store_names {
203            StringOrStringSequence::String(name) => vec![name],
204            StringOrStringSequence::StringSequence(sequence) => sequence,
205        };
206        scope.sort_unstable_by(|left, right| {
207            left.str().encode_utf16().cmp(right.str().encode_utf16())
208        });
209        scope.dedup();
210
211        // Step 4. If any string in scope is not the name of an object store in
212        // the connected database, throw a "NotFoundError" DOMException.
213        if scope.iter().any(|name| !self.object_store_exists(name)) {
214            return Err(Error::NotFound(None));
215        }
216
217        // Step 5. If scope is empty, throw an "InvalidAccessError" DOMException.
218        if scope.is_empty() {
219            return Err(Error::InvalidAccess(None));
220        }
221
222        // Step 6. If mode is not "readonly" or "readwrite", throw a TypeError.
223        if mode != IDBTransactionMode::Readonly && mode != IDBTransactionMode::Readwrite {
224            return Err(Error::Type(c"Invalid transaction mode".to_owned()));
225        }
226
227        // Step 7. Let transaction be a newly created transaction with this
228        // connection, mode, options’ durability member, and the set of object
229        // stores named in scope.
230        let durability = options.durability;
231        let scope = DOMStringList::new(&self.global(), scope, CanGc::deprecated_note());
232        let transaction = IDBTransaction::new(
233            &self.global(),
234            self,
235            mode,
236            durability,
237            &scope,
238            CanGc::deprecated_note(),
239        );
240
241        // Step 8. Set transaction’s cleanup event loop to the current event loop.
242        transaction.set_cleanup_event_loop();
243        // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
244        // NOTE: These steps are invoked by [HTML]. They ensure that transactions created
245        // by a script call to transaction() are deactivated once the task that invoked
246        // the script has completed. The steps are run at most once for each transaction.
247        // https://w3c.github.io/IndexedDB/#transaction-concept
248        // A transaction optionally has a cleanup event loop which is an event loop.
249        self.global()
250            .get_indexeddb()
251            .register_indexeddb_transaction(&transaction);
252
253        // Step 9. Return an IDBTransaction object representing transaction.
254        Ok(transaction)
255    }
256
257    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-createobjectstore>
258    fn CreateObjectStore(
259        &self,
260        cx: &mut JSContext,
261        name: DOMString,
262        options: &IDBObjectStoreParameters,
263    ) -> Fallible<DomRoot<IDBObjectStore>> {
264        // Step 1. Let database be this’s associated database.
265
266        // Step 2. Let transaction be database’s upgrade transaction if it is not null,
267        // or throw an "InvalidStateError" DOMException otherwise.
268        let transaction = match self.upgrade_transaction.get() {
269            Some(txn) => txn,
270            None => return Err(Error::InvalidState(None)),
271        };
272
273        // Step 3. If transaction’s state is not active, then throw a
274        // "TransactionInactiveError" DOMException.
275        if !transaction.is_active() {
276            return Err(Error::TransactionInactive(None));
277        }
278
279        // Step 4. Let keyPath be options’s keyPath member if it is not undefined
280        // or null, or null otherwise.
281        let key_path = options.keyPath.as_ref();
282
283        // Step 5. If keyPath is not null and is not a valid key path, throw a
284        // "SyntaxError" DOMException.
285        if let Some(path) = key_path &&
286            !is_valid_key_path(cx, path)?
287        {
288            return Err(Error::Syntax(None));
289        }
290
291        // Step 6. If an object store named name already exists in database throw
292        // a "ConstraintError" DOMException.
293        if self.object_store_names.borrow().contains(&name) {
294            return Err(Error::Constraint(None));
295        }
296
297        // Step 7. Let autoIncrement be options’s autoIncrement member.
298        let auto_increment = options.autoIncrement;
299
300        // Step 8. If autoIncrement is true and keyPath is an empty string or any
301        // sequence (empty or otherwise), throw an "InvalidAccessError" DOMException.
302        if auto_increment {
303            match key_path {
304                Some(StringOrStringSequence::String(path)) if path.is_empty() => {
305                    return Err(Error::InvalidAccess(None));
306                },
307                Some(StringOrStringSequence::StringSequence(_)) => {
308                    return Err(Error::InvalidAccess(None));
309                },
310                _ => {},
311            }
312        }
313
314        // Step 9. Let store be a new object store in database. Set the created
315        // object store’s name to name. If autoIncrement is true, then the
316        // created object store uses a key generator. If keyPath is not null,
317        // set the created object store’s key path to keyPath.
318        let object_store = IDBObjectStore::new(
319            &self.global(),
320            self.name.clone(),
321            name.clone(),
322            Some(options),
323            IDBObjectStoreAbortState {
324                newly_created_during_transaction: true,
325                rollback_indexes_on_abort: vec![],
326                key_generator_current_number: if auto_increment { Some(1_i64) } else { None },
327            },
328            CanGc::from_cx(cx),
329            &transaction,
330        );
331
332        let (sender, receiver) = channel(self.global().time_profiler_chan().clone()).unwrap();
333
334        let key_paths = key_path.map(|p| match p {
335            StringOrStringSequence::String(s) => KeyPath::String(s.to_string()),
336            StringOrStringSequence::StringSequence(s) => {
337                KeyPath::Sequence(s.iter().map(|s| s.to_string()).collect())
338            },
339        });
340        let operation = SyncOperation::CreateObjectStore(
341            sender,
342            self.global().origin().immutable().clone(),
343            self.name.to_string(),
344            name.to_string(),
345            key_paths,
346            auto_increment,
347        );
348
349        self.get_idb_thread()
350            .send(IndexedDBThreadMsg::Sync(operation))
351            .unwrap();
352
353        if receiver
354            .recv()
355            .expect("Could not receive object store creation status")
356            .is_err()
357        {
358            warn!("Object store creation failed in idb thread");
359            return Err(Error::InvalidState(None));
360        };
361
362        self.object_store_names.borrow_mut().push(name);
363        transaction.register_object_store_handle(&object_store.get_name(), &object_store);
364
365        // Step 10. Return a new object store handle associated with store and transaction.
366        Ok(object_store)
367    }
368
369    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-deleteobjectstore>
370    fn DeleteObjectStore(&self, name: DOMString) -> Fallible<()> {
371        // Steps 1 & 2
372        let transaction = self.upgrade_transaction.get();
373        let transaction = match transaction {
374            Some(transaction) => transaction,
375            None => return Err(Error::InvalidState(None)),
376        };
377
378        // Step 3
379        if !transaction.is_active() {
380            return Err(Error::TransactionInactive(None));
381        }
382
383        // Step 4
384        if !self.object_store_names.borrow().contains(&name) {
385            return Err(Error::NotFound(None));
386        }
387
388        // Step 5
389        self.object_store_names
390            .borrow_mut()
391            .retain(|store_name| *store_name != name);
392
393        // Step 6
394        // FIXME:(arihant2math) Remove from index set ...
395
396        // Step 7
397        let (sender, receiver) = channel(self.global().time_profiler_chan().clone()).unwrap();
398
399        let operation = SyncOperation::DeleteObjectStore(
400            sender,
401            self.global().origin().immutable().clone(),
402            self.name.to_string(),
403            String::from(name),
404        );
405
406        self.get_idb_thread()
407            .send(IndexedDBThreadMsg::Sync(operation))
408            .unwrap();
409
410        if receiver
411            .recv()
412            .expect("Could not receive object store deletion status")
413            .is_err()
414        {
415            warn!("Object store deletion failed in idb thread");
416            return Err(Error::InvalidState(None));
417        };
418        Ok(())
419    }
420
421    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-name>
422    fn Name(&self) -> DOMString {
423        self.name.clone()
424    }
425
426    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-version>
427    fn Version(&self) -> u64 {
428        self.version()
429    }
430
431    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-objectstorenames>
432    fn ObjectStoreNames(&self, can_gc: CanGc) -> DomRoot<DOMStringList> {
433        DOMStringList::new_sorted(&self.global(), &*self.object_store_names.borrow(), can_gc)
434    }
435
436    /// <https://w3c.github.io/IndexedDB/#dom-idbdatabase-close>
437    fn Close(&self) {
438        // Step 1: Run close a database connection with this connection.
439
440        // <https://w3c.github.io/IndexedDB/#close-a-database-connection>
441        // Step 1: Set connection’s close pending flag to true.
442        self.close_pending.set(true);
443
444        // Note: rest of algo runs in-parallel.
445        let operation = SyncOperation::CloseDatabase(
446            self.global().origin().immutable().clone(),
447            self.id,
448            self.name.to_string(),
449        );
450        let _ = self
451            .get_idb_thread()
452            .send(IndexedDBThreadMsg::Sync(operation));
453    }
454
455    // https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-onabort
456    event_handler!(abort, GetOnabort, SetOnabort);
457
458    // https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-onclose
459    event_handler!(close, GetOnclose, SetOnclose);
460
461    // https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-onerror
462    event_handler!(error, GetOnerror, SetOnerror);
463
464    // https://www.w3.org/TR/IndexedDB-3/#dom-idbdatabase-onversionchange
465    event_handler!(versionchange, GetOnversionchange, SetOnversionchange);
466}