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