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