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