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