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