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