script/dom/indexeddb/
idbtransaction.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;
6use std::collections::HashMap;
7
8use base::generic_channel::{GenericSend, GenericSender};
9use dom_struct::dom_struct;
10use profile_traits::generic_channel::channel;
11use script_bindings::codegen::GenericUnionTypes::StringOrStringSequence;
12use storage_traits::indexeddb::{IndexedDBThreadMsg, KeyPath, SyncOperation};
13use stylo_atoms::Atom;
14use uuid::Uuid;
15
16use crate::dom::bindings::cell::DomRefCell;
17use crate::dom::bindings::codegen::Bindings::DOMStringListBinding::DOMStringListMethods;
18use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters;
19use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{
20    IDBTransactionMethods, IDBTransactionMode,
21};
22use crate::dom::bindings::error::{Error, Fallible};
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::Trusted;
25use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
26use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
27use crate::dom::bindings::str::DOMString;
28use crate::dom::domexception::DOMException;
29use crate::dom::domstringlist::DOMStringList;
30use crate::dom::event::{Event, EventBubbles, EventCancelable};
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::indexeddb::idbdatabase::IDBDatabase;
34use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
35use crate::dom::indexeddb::idbrequest::IDBRequest;
36use crate::script_runtime::CanGc;
37
38#[dom_struct]
39pub struct IDBTransaction {
40    eventtarget: EventTarget,
41    object_store_names: Dom<DOMStringList>,
42    mode: IDBTransactionMode,
43    db: Dom<IDBDatabase>,
44    error: MutNullableDom<DOMException>,
45
46    store_handles: DomRefCell<HashMap<String, Dom<IDBObjectStore>>>,
47    // https://www.w3.org/TR/IndexedDB-2/#transaction-request-list
48    requests: DomRefCell<Vec<Dom<IDBRequest>>>,
49    // https://www.w3.org/TR/IndexedDB-2/#transaction-active-flag
50    active: Cell<bool>,
51    // https://www.w3.org/TR/IndexedDB-2/#transaction-finish
52    finished: Cell<bool>,
53    // Tracks how many IDBRequest instances are still pending for this
54    // transaction. The value is incremented when a request is added to the
55    // transaction’s request list and decremented once the request has
56    // finished.
57    pending_request_count: Cell<usize>,
58
59    // An unique identifier, used to commit and revert this transaction
60    // FIXME:(rasviitanen) Replace this with a channel
61    serial_number: u64,
62
63    /// The id of the associated open request, if any.
64    #[no_trace]
65    open_request_id: Option<Uuid>,
66}
67
68impl IDBTransaction {
69    fn new_inherited(
70        connection: &IDBDatabase,
71        mode: IDBTransactionMode,
72        scope: &DOMStringList,
73        serial_number: u64,
74        open_request_id: Option<Uuid>,
75    ) -> IDBTransaction {
76        IDBTransaction {
77            eventtarget: EventTarget::new_inherited(),
78            object_store_names: Dom::from_ref(scope),
79            mode,
80            db: Dom::from_ref(connection),
81            error: Default::default(),
82
83            store_handles: Default::default(),
84            requests: Default::default(),
85            active: Cell::new(true),
86            finished: Cell::new(false),
87            pending_request_count: Cell::new(0),
88            serial_number,
89            open_request_id,
90        }
91    }
92
93    /// Does a blocking call to get an id from the backend.
94    /// TODO: remove in favor of something like `new_with_id` below.
95    pub fn new(
96        global: &GlobalScope,
97        connection: &IDBDatabase,
98        mode: IDBTransactionMode,
99        scope: &DOMStringList,
100        can_gc: CanGc,
101    ) -> DomRoot<IDBTransaction> {
102        let serial_number = IDBTransaction::register_new(global, connection.get_name());
103        reflect_dom_object(
104            Box::new(IDBTransaction::new_inherited(
105                connection,
106                mode,
107                scope,
108                serial_number,
109                None,
110            )),
111            global,
112            can_gc,
113        )
114    }
115
116    /// Create a new WebIDL object,
117    /// based on an existign transaction on the backend.
118    /// The two are linked via the `transaction_id`.
119    pub(crate) fn new_with_id(
120        global: &GlobalScope,
121        connection: &IDBDatabase,
122        mode: IDBTransactionMode,
123        scope: &DOMStringList,
124        transaction_id: u64,
125        open_request_id: Option<Uuid>,
126        can_gc: CanGc,
127    ) -> DomRoot<IDBTransaction> {
128        reflect_dom_object(
129            Box::new(IDBTransaction::new_inherited(
130                connection,
131                mode,
132                scope,
133                transaction_id,
134                open_request_id,
135            )),
136            global,
137            can_gc,
138        )
139    }
140
141    // Registers a new transaction in the idb thread, and gets an unique serial number in return.
142    // The serial number is used when placing requests against a transaction
143    // and allows us to commit/abort transactions running in our idb thread.
144    // FIXME:(rasviitanen) We could probably replace this with a channel instead,
145    // and queue requests directly to that channel.
146    fn register_new(global: &GlobalScope, db_name: DOMString) -> u64 {
147        let (sender, receiver) = channel(global.time_profiler_chan().clone()).unwrap();
148
149        global
150            .storage_threads()
151            .send(IndexedDBThreadMsg::Sync(SyncOperation::RegisterNewTxn(
152                sender,
153                global.origin().immutable().clone(),
154                db_name.to_string(),
155            )))
156            .unwrap();
157
158        receiver.recv().unwrap()
159    }
160
161    pub fn set_active_flag(&self, status: bool) {
162        self.active.set(status);
163        // When the transaction becomes inactive and no requests are pending,
164        // it can transition to the finished state.
165        if !status && self.pending_request_count.get() == 0 && !self.finished.get() {
166            self.finished.set(true);
167            self.dispatch_complete();
168        }
169    }
170
171    pub fn is_active(&self) -> bool {
172        self.active.get()
173    }
174
175    pub fn get_mode(&self) -> IDBTransactionMode {
176        self.mode
177    }
178
179    pub fn get_db_name(&self) -> DOMString {
180        self.db.get_name()
181    }
182
183    pub fn get_serial_number(&self) -> u64 {
184        self.serial_number
185    }
186
187    pub fn add_request(&self, request: &IDBRequest) {
188        self.requests.borrow_mut().push(Dom::from_ref(request));
189        // Increase the number of outstanding requests so that we can detect when
190        // the transaction is allowed to finish.
191        self.pending_request_count
192            .set(self.pending_request_count.get() + 1);
193    }
194
195    /// Must be called by an `IDBRequest` when it finishes (either success or
196    /// error). When the last pending request has completed and the transaction
197    /// is no longer active, the `"complete"` event is dispatched and any
198    /// associated `IDBOpenDBRequest` `"success"` event is fired afterwards.
199    pub fn request_finished(&self) {
200        if self.pending_request_count.get() == 0 {
201            return;
202        }
203        let remaining = self.pending_request_count.get() - 1;
204        self.pending_request_count.set(remaining);
205
206        if remaining == 0 && !self.active.get() && !self.finished.get() {
207            self.finished.set(true);
208            self.dispatch_complete();
209        }
210    }
211
212    fn dispatch_complete(&self) {
213        let global = self.global();
214        let this = Trusted::new(self);
215        global.task_manager().database_access_task_source().queue(
216            task!(send_complete_notification: move || {
217                let this = this.root();
218                let global = this.global();
219                let event = Event::new(
220                    &global,
221                    Atom::from("complete"),
222                    EventBubbles::DoesNotBubble,
223                    EventCancelable::NotCancelable,
224                    CanGc::note()
225                );
226                event.fire(this.upcast(), CanGc::note());
227            }),
228        );
229    }
230
231    fn get_idb_thread(&self) -> GenericSender<IndexedDBThreadMsg> {
232        self.global().storage_threads().sender()
233    }
234
235    fn object_store_parameters(
236        &self,
237        object_store_name: &DOMString,
238    ) -> Option<IDBObjectStoreParameters> {
239        let global = self.global();
240        let idb_sender = global.storage_threads().sender();
241        let (sender, receiver) =
242            channel(global.time_profiler_chan().clone()).expect("failed to create channel");
243
244        let origin = global.origin().immutable().clone();
245        let db_name = self.db.get_name().to_string();
246        let object_store_name = object_store_name.to_string();
247
248        let operation = SyncOperation::HasKeyGenerator(
249            sender,
250            origin.clone(),
251            db_name.clone(),
252            object_store_name.clone(),
253        );
254
255        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
256
257        // First unwrap for ipc
258        // Second unwrap will never happen unless this db gets manually deleted somehow
259        let auto_increment = receiver.recv().ok()?.ok()?;
260
261        let (sender, receiver) = channel(self.global().time_profiler_chan().clone())?;
262        let operation = SyncOperation::KeyPath(sender, origin, db_name, object_store_name);
263
264        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
265
266        // First unwrap for ipc
267        // Second unwrap will never happen unless this db gets manually deleted somehow
268        let key_path = receiver.recv().unwrap().ok()?;
269        let key_path = key_path.map(|key_path| match key_path {
270            KeyPath::String(s) => StringOrStringSequence::String(DOMString::from_string(s)),
271            KeyPath::Sequence(seq) => StringOrStringSequence::StringSequence(
272                seq.into_iter().map(DOMString::from_string).collect(),
273            ),
274        });
275        Some(IDBObjectStoreParameters {
276            autoIncrement: auto_increment,
277            keyPath: key_path,
278        })
279    }
280}
281
282impl IDBTransactionMethods<crate::DomTypeHolder> for IDBTransaction {
283    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-db>
284    fn Db(&self) -> DomRoot<IDBDatabase> {
285        DomRoot::from_ref(&*self.db)
286    }
287
288    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-objectstore>
289    fn ObjectStore(&self, name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<IDBObjectStore>> {
290        // Step 1: If transaction has finished, throw an "InvalidStateError" DOMException.
291        if self.finished.get() {
292            return Err(Error::InvalidState(None));
293        }
294
295        // Step 2: Check that the object store exists
296        if !self.object_store_names.Contains(name.clone()) {
297            return Err(Error::NotFound(None));
298        }
299
300        // Step 3: Each call to this method on the same
301        // IDBTransaction instance with the same name
302        // returns the same IDBObjectStore instance.
303        if let Some(store) = self.store_handles.borrow().get(&*name.str()) {
304            return Ok(DomRoot::from_ref(store));
305        }
306
307        let parameters = self.object_store_parameters(&name);
308        let store = IDBObjectStore::new(
309            &self.global(),
310            self.db.get_name(),
311            name.clone(),
312            parameters.as_ref(),
313            can_gc,
314            self,
315        );
316        self.store_handles
317            .borrow_mut()
318            .insert(name.to_string(), Dom::from_ref(&*store));
319        Ok(store)
320    }
321
322    /// <https://www.w3.org/TR/IndexedDB-2/#commit-transaction>
323    fn Commit(&self) -> Fallible<()> {
324        // Step 1
325        let (sender, receiver) = channel(self.global().time_profiler_chan().clone()).unwrap();
326        let start_operation = SyncOperation::Commit(
327            sender,
328            self.global().origin().immutable().clone(),
329            self.db.get_name().to_string(),
330            self.serial_number,
331        );
332
333        self.get_idb_thread()
334            .send(IndexedDBThreadMsg::Sync(start_operation))
335            .unwrap();
336
337        let result = receiver.recv().unwrap();
338
339        // Step 2
340        if let Err(_result) = result {
341            // FIXME:(rasviitanen) also support Unknown error
342            return Err(Error::QuotaExceeded {
343                quota: None,
344                requested: None,
345            });
346        }
347
348        // Step 3
349        // FIXME:(rasviitanen) https://www.w3.org/TR/IndexedDB-2/#commit-a-transaction
350
351        // Steps 3.1 and 3.3
352        self.dispatch_complete();
353
354        Ok(())
355    }
356
357    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-abort>
358    fn Abort(&self) -> Fallible<()> {
359        // FIXME:(rasviitanen)
360        // This only sets the flags, and does not abort the transaction
361        // see https://www.w3.org/TR/IndexedDB-2/#abort-a-transaction
362        if self.finished.get() {
363            return Err(Error::InvalidState(None));
364        }
365
366        self.active.set(false);
367
368        if self.mode == IDBTransactionMode::Versionchange {
369            let name = self.db.get_name().to_string();
370            let global = self.global();
371            let origin = global.origin().immutable().clone();
372            let Some(id) = self.open_request_id else {
373                debug_assert!(
374                    false,
375                    "A Versionchange transaction should have an open request id."
376                );
377                return Err(Error::InvalidState(None));
378            };
379            if global
380                .storage_threads()
381                .send(IndexedDBThreadMsg::Sync(
382                    SyncOperation::AbortPendingUpgrade { name, id, origin },
383                ))
384                .is_err()
385            {
386                error!("Failed to send SyncOperation::AbortPendingUpgrade");
387            }
388        }
389
390        Ok(())
391    }
392
393    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-objectstorenames>
394    fn ObjectStoreNames(&self) -> DomRoot<DOMStringList> {
395        self.object_store_names.as_rooted()
396    }
397
398    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-mode>
399    fn Mode(&self) -> IDBTransactionMode {
400        self.mode
401    }
402
403    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-mode
404    // fn Durability(&self) -> IDBTransactionDurability {
405    //     // FIXME:(arihant2math) Durability is not implemented at all
406    //     unimplemented!();
407    // }
408
409    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-error>
410    fn GetError(&self) -> Option<DomRoot<DOMException>> {
411        self.error.get()
412    }
413
414    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-onabort
415    event_handler!(abort, GetOnabort, SetOnabort);
416
417    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-oncomplete
418    event_handler!(complete, GetOncomplete, SetOncomplete);
419
420    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-onerror
421    event_handler!(error, GetOnerror, SetOnerror);
422}