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, HashSet};
7
8use dom_struct::dom_struct;
9use profile_traits::generic_callback::GenericCallback;
10use profile_traits::generic_channel::channel;
11use script_bindings::codegen::GenericUnionTypes::StringOrStringSequence;
12use servo_base::generic_channel::{GenericSend, GenericSender};
13use servo_base::id::ScriptEventLoopId;
14use storage_traits::indexeddb::{
15    IndexedDBIndex, IndexedDBThreadMsg, IndexedDBTxnMode, KeyPath, SyncOperation, TxnCompleteMsg,
16};
17use stylo_atoms::Atom;
18
19use crate::dom::bindings::cell::DomRefCell;
20use crate::dom::bindings::codegen::Bindings::DOMStringListBinding::DOMStringListMethods;
21use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters;
22use crate::dom::bindings::codegen::Bindings::IDBObjectStoreBinding::IDBIndexParameters;
23use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{
24    IDBTransactionMethods, IDBTransactionMode,
25};
26use crate::dom::bindings::error::{Error, Fallible, create_dom_exception};
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
30use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
31use crate::dom::bindings::str::DOMString;
32use crate::dom::domexception::DOMException;
33use crate::dom::domstringlist::DOMStringList;
34use crate::dom::event::{Event, EventBubbles, EventCancelable};
35use crate::dom::eventtarget::EventTarget;
36use crate::dom::globalscope::GlobalScope;
37use crate::dom::indexeddb::idbdatabase::IDBDatabase;
38use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
39use crate::dom::indexeddb::idbrequest::IDBRequest;
40use crate::script_runtime::CanGc;
41
42#[dom_struct]
43pub struct IDBTransaction {
44    eventtarget: EventTarget,
45    object_store_names: Dom<DOMStringList>,
46    mode: IDBTransactionMode,
47    db: Dom<IDBDatabase>,
48    error: MutNullableDom<DOMException>,
49
50    store_handles: DomRefCell<HashMap<String, Dom<IDBObjectStore>>>,
51    // https://www.w3.org/TR/IndexedDB-3/#transaction-request-list
52    requests: DomRefCell<Vec<Dom<IDBRequest>>>,
53    // https://www.w3.org/TR/IndexedDB-3/#transaction-active-flag
54    active: Cell<bool>,
55    // https://www.w3.org/TR/IndexedDB-3/#transaction-finish
56    finished: Cell<bool>,
57    abort_initiated: Cell<bool>,
58    abort_requested: Cell<bool>,
59    committing: Cell<bool>,
60    commit_started: Cell<bool>,
61    version_change_old_version: Cell<Option<u64>>,
62    // https://w3c.github.io/IndexedDB/#abort-upgrade-transaction
63    // Step 4. NOTE: This reverts the value of objectStoreNames returned by the IDBDatabase object.
64    version_change_old_object_store_names: DomRefCell<Option<Vec<DOMString>>>,
65    // https://w3c.github.io/IndexedDB/#transaction-concept
66    // “A transaction optionally has a cleanup event loop which is an event loop.”
67    #[no_trace]
68    cleanup_event_loop: Cell<Option<ScriptEventLoopId>>,
69    registered_in_global: Cell<bool>,
70    // Tracks how many IDBRequest instances are still pending for this
71    // transaction. The value is incremented when a request is added to the
72    // transaction’s request list and decremented once the request has
73    // finished.
74    pending_request_count: Cell<usize>,
75    next_request_id: Cell<u64>,
76    // Smallest request_id that has not yet been marked handled (all < this are handled).
77    next_unhandled_request_id: Cell<u64>,
78    handled_pending: DomRefCell<HashSet<u64>>,
79
80    // An unique identifier, used to commit and revert this transaction
81    // FIXME:(rasviitanen) Replace this with a channel
82    serial_number: u64,
83}
84
85impl IDBTransaction {
86    fn new_inherited(
87        connection: &IDBDatabase,
88        mode: IDBTransactionMode,
89        scope: &DOMStringList,
90        serial_number: u64,
91    ) -> IDBTransaction {
92        IDBTransaction {
93            eventtarget: EventTarget::new_inherited(),
94            object_store_names: Dom::from_ref(scope),
95            mode,
96            db: Dom::from_ref(connection),
97            error: Default::default(),
98
99            store_handles: Default::default(),
100            requests: Default::default(),
101            active: Cell::new(true),
102            finished: Cell::new(false),
103            abort_initiated: Cell::new(false),
104            abort_requested: Cell::new(false),
105            committing: Cell::new(false),
106            commit_started: Cell::new(false),
107            version_change_old_version: Cell::new(None),
108            version_change_old_object_store_names: DomRefCell::new(
109                (mode == IDBTransactionMode::Versionchange)
110                    .then(|| connection.object_store_names_snapshot()),
111            ),
112            cleanup_event_loop: Cell::new(None),
113            registered_in_global: Cell::new(false),
114            pending_request_count: Cell::new(0),
115            next_request_id: Cell::new(0),
116            next_unhandled_request_id: Cell::new(0),
117            handled_pending: Default::default(),
118            serial_number,
119        }
120    }
121
122    /// Does a blocking call to create a backend transaction and get its id.
123    pub fn new(
124        global: &GlobalScope,
125        connection: &IDBDatabase,
126        mode: IDBTransactionMode,
127        scope: &DOMStringList,
128        can_gc: CanGc,
129    ) -> DomRoot<IDBTransaction> {
130        let serial_number =
131            IDBTransaction::create_transaction(global, connection.get_name(), mode, scope);
132        IDBTransaction::new_with_serial(global, connection, mode, scope, serial_number, can_gc)
133    }
134
135    pub(crate) fn new_with_serial(
136        global: &GlobalScope,
137        connection: &IDBDatabase,
138        mode: IDBTransactionMode,
139        scope: &DOMStringList,
140        serial_number: u64,
141        can_gc: CanGc,
142    ) -> DomRoot<IDBTransaction> {
143        reflect_dom_object(
144            Box::new(IDBTransaction::new_inherited(
145                connection,
146                mode,
147                scope,
148                serial_number,
149            )),
150            global,
151            can_gc,
152        )
153    }
154
155    fn create_transaction(
156        global: &GlobalScope,
157        db_name: DOMString,
158        mode: IDBTransactionMode,
159        scope: &DOMStringList,
160    ) -> u64 {
161        let backend_mode = match mode {
162            IDBTransactionMode::Readonly => IndexedDBTxnMode::Readonly,
163            IDBTransactionMode::Readwrite => IndexedDBTxnMode::Readwrite,
164            IDBTransactionMode::Versionchange => IndexedDBTxnMode::Versionchange,
165        };
166        let scope: Vec<String> = (0..scope.Length())
167            .filter_map(|i| scope.Item(i))
168            .map(|name| name.to_string())
169            .collect();
170        let (sender, receiver) = channel(global.time_profiler_chan().clone()).unwrap();
171
172        global
173            .storage_threads()
174            .send(IndexedDBThreadMsg::Sync(SyncOperation::CreateTransaction {
175                sender,
176                origin: global.origin().immutable().clone(),
177                db_name: db_name.to_string(),
178                mode: backend_mode,
179                scope,
180            }))
181            .expect("Failed to send IndexedDBThreadMsg::Sync");
182
183        receiver.recv().unwrap().expect("CreateTransaction failed")
184    }
185
186    /// <https://w3c.github.io/IndexedDB/#transaction-lifecycle>
187    pub fn set_active_flag(&self, status: bool) {
188        // inactive
189        // A transaction is in this state after control returns to the event loop after its creation,
190        //  and when events are not being dispatched.
191        // No requests can be made against the transaction when it is in this state.
192        self.active.set(status);
193    }
194
195    pub fn is_active(&self) -> bool {
196        self.active.get()
197    }
198
199    /// <https://w3c.github.io/IndexedDB/#transaction-lifetime>
200    pub(crate) fn is_usable(&self) -> bool {
201        // A transaction can be aborted at any time before it is finished,
202        //  even if the transaction isn’t currently active or hasn’t yet started.
203        // An explicit call to abort() will initiate an abort.
204        // An abort will also be initiated following a failed request that is not handled by script.
205        !self.finished.get() && !self.abort_initiated.get() && !self.committing.get()
206    }
207
208    pub(crate) fn is_inactive(&self) -> bool {
209        !self.active.get() &&
210            !self.finished.get() &&
211            !self.abort_initiated.get() &&
212            !self.committing.get()
213    }
214
215    pub(crate) fn is_committing(&self) -> bool {
216        self.committing.get()
217    }
218
219    pub(crate) fn is_finished(&self) -> bool {
220        self.finished.get()
221    }
222
223    pub(crate) fn set_cleanup_event_loop(&self) {
224        // https://w3c.github.io/IndexedDB/#transaction-concept
225        // A transaction optionally has a cleanup event loop which is an event loop.
226        self.cleanup_event_loop.set(ScriptEventLoopId::installed());
227    }
228
229    pub(crate) fn clear_cleanup_event_loop(&self) {
230        // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions
231        // Clear transaction’s cleanup event loop.
232        self.cleanup_event_loop.set(None);
233    }
234
235    pub(crate) fn cleanup_event_loop_matches_current(&self) -> bool {
236        match ScriptEventLoopId::installed() {
237            Some(current) => self.cleanup_event_loop.get() == Some(current),
238            None => false,
239        }
240    }
241
242    pub(crate) fn set_registered_in_global(&self) {
243        self.registered_in_global.set(true);
244    }
245
246    pub(crate) fn clear_registered_in_global(&self) {
247        self.registered_in_global.set(false);
248    }
249
250    pub(crate) fn set_versionchange_old_version(&self, version: u64) {
251        self.version_change_old_version.set(Some(version));
252    }
253
254    fn attempt_commit(&self) -> bool {
255        if self.commit_started.get() {
256            return true;
257        }
258        let this = Trusted::new(self);
259        let global = self.global();
260        let task_source = global
261            .task_manager()
262            .dom_manipulation_task_source()
263            .to_sendable();
264
265        // TODO: Reuse a shared transaction callback path (similar to IDBFactory
266        // connection callbacks) instead of creating one per transaction operation.
267        let callback = GenericCallback::new(
268            global.time_profiler_chan().clone(),
269            move |message: Result<TxnCompleteMsg, ipc_channel::IpcError>| {
270                let this = this.clone();
271                let task_source = task_source.clone();
272                task_source.queue(task!(handle_commit_result: move || {
273                    let this = this.root();
274                    let message = message.expect("Could not unwrap message");
275                    match message.result {
276                        Ok(()) => {
277                            this.finalize_commit();
278                        }
279                        Err(_err) => {
280                             // TODO: Map backend commit/rollback failure to an appropriate DOMException
281                            this.initiate_abort(Error::Operation(None), CanGc::note());
282
283                            this.finalize_abort();
284                        }
285                    }
286                    // TODO: https://w3c.github.io/IndexedDB/#commit-a-transaction
287                    // Backend commit/rollback is not yet atomic.
288                }));
289            },
290        )
291        .expect("Could not create callback");
292
293        let commit_operation = SyncOperation::Commit(
294            callback,
295            global.origin().immutable().clone(),
296            self.db.get_name().to_string(),
297            self.serial_number,
298        );
299
300        // https://w3c.github.io/IndexedDB/#transaction-lifecycle
301        // When committing, the transaction state is set to committing.
302        let send_result = self
303            .get_idb_thread()
304            .send(IndexedDBThreadMsg::Sync(commit_operation));
305        if send_result.is_err() {
306            return false;
307        }
308
309        self.committing.set(true);
310        self.commit_started.set(true);
311        true
312    }
313
314    pub(crate) fn maybe_commit(&self) {
315        // https://w3c.github.io/IndexedDB/#transaction-lifetime
316        // Step 5: transaction when all requests
317        //  placed against the transaction have completed and their returned results handled,
318        //  no new requests have been placed against the transaction, and the transaction has
319        //  not been aborted.
320        let finished = self.finished.get();
321        let abort_initiated = self.abort_initiated.get();
322        let commit_started = self.commit_started.get();
323        let active = self.active.get();
324        let pending_request_count = self.pending_request_count.get();
325        let next_unhandled_request_id = self.next_unhandled_request_id.get();
326        let issued_count = self.issued_count();
327        if finished || abort_initiated || commit_started {
328            return;
329        }
330        if active || pending_request_count != 0 {
331            return;
332        }
333        if next_unhandled_request_id != issued_count {
334            return;
335        }
336        if !self.attempt_commit() {
337            // We failed to initiate the commit algorithm (backend task could not be queued),
338            // so the transaction cannot progress to a successful "complete".
339            // Choose the most appropriate DOMException mapping for Servo here.
340            self.initiate_abort(Error::InvalidState(None), CanGc::note());
341            self.finalize_abort();
342        }
343    }
344
345    fn force_commit(&self) {
346        // https://w3c.github.io/IndexedDB/#transaction-lifetime
347        // An explicit call to commit() will initiate a commit without waiting for request results
348        //  to be handled by script.
349        //
350        // This differs from automatic commit:
351        // The implementation must attempt to commit an inactive transaction when all requests
352        // placed against the transaction have completed and their returned results handled,
353        // no new requests have been placed against the transaction, and the transaction has not been aborted
354        if self.finished.get() || self.abort_initiated.get() || self.commit_started.get() {
355            return;
356        }
357        if self.active.get() || self.pending_request_count.get() != 0 {
358            return;
359        }
360        self.attempt_commit();
361    }
362
363    pub fn get_mode(&self) -> IDBTransactionMode {
364        self.mode
365    }
366
367    pub fn get_db_name(&self) -> DOMString {
368        self.db.get_name()
369    }
370
371    pub(crate) fn get_db(&self) -> &IDBDatabase {
372        &self.db
373    }
374
375    pub fn get_serial_number(&self) -> u64 {
376        self.serial_number
377    }
378
379    pub(crate) fn issued_count(&self) -> u64 {
380        self.next_request_id.get()
381    }
382
383    /// request_id is only required to be unique within this transaction.
384    /// The backend keys “handled” state by (txn, request_id).
385    pub(crate) fn allocate_request_id(&self) -> u64 {
386        let id = self.next_request_id.get();
387        self.next_request_id.set(id + 1);
388        id
389    }
390
391    pub(crate) fn mark_request_handled(&self, request_id: u64) {
392        let current = self.next_unhandled_request_id.get();
393        if request_id == current {
394            let mut next = current + 1;
395            {
396                let mut pending = self.handled_pending.borrow_mut();
397                while pending.remove(&next) {
398                    next += 1;
399                }
400            }
401            self.next_unhandled_request_id.set(next);
402        } else if request_id > current {
403            self.handled_pending.borrow_mut().insert(request_id);
404        }
405    }
406
407    pub fn add_request(&self, request: &IDBRequest) {
408        self.requests.borrow_mut().push(Dom::from_ref(request));
409        // Increase the number of outstanding requests so that we can detect when
410        // the transaction is allowed to finish.
411        self.pending_request_count
412            .set(self.pending_request_count.get() + 1);
413    }
414
415    pub fn request_finished(&self) {
416        // https://w3c.github.io/IndexedDB/#transaction-lifecycle
417        // finished
418        // Once a transaction has committed or aborted, it enters this state.
419        // No requests can be made against the transaction when it is in this state.
420        if self.pending_request_count.get() == 0 {
421            return;
422        }
423        let remaining = self.pending_request_count.get() - 1;
424        self.pending_request_count.set(remaining);
425    }
426
427    pub(crate) fn initiate_abort(&self, error: Error, can_gc: CanGc) {
428        // https://w3c.github.io/IndexedDB/#transaction-lifetime
429        // Step 4: An abort will also be initiated following a failed request that is not handled by script.
430        // A transaction can be aborted at any time before it is finished,
431        // even if the transaction isn’t currently active or hasn’t yet started.
432        if self.finished.get() || self.abort_initiated.get() {
433            return;
434        }
435        if self.mode == IDBTransactionMode::Versionchange {
436            // https://w3c.github.io/IndexedDB/#abort-upgrade-transaction
437            // Step 4. Set connection’s object store set to the set of object stores in database if database previously existed,
438            // or the empty set if database was newly created.
439            if let Some(names) = self
440                .version_change_old_object_store_names
441                .borrow()
442                .as_ref()
443                .cloned()
444            {
445                self.db.restore_object_store_names(names);
446            }
447        }
448        self.abort_initiated.set(true);
449        // https://w3c.github.io/IndexedDB/#transaction-concept
450        // A transaction has a error which is set if the transaction is aborted.
451        // NOTE: Implementors need to keep in mind that the value "null" is considered an error, as it is set from abort()
452        if self.error.get().is_none() {
453            if let Ok(exception) = create_dom_exception(&self.global(), error, can_gc) {
454                self.error.set(Some(&exception));
455            }
456        }
457    }
458
459    pub(crate) fn request_backend_abort(&self) {
460        if self.abort_requested.get() {
461            return;
462        }
463        self.abort_requested.set(true);
464        let this = Trusted::new(self);
465        let global = self.global();
466        let task_source = global
467            .task_manager()
468            .dom_manipulation_task_source()
469            .to_sendable();
470        let callback = GenericCallback::new(
471            global.time_profiler_chan().clone(),
472            move |message: Result<TxnCompleteMsg, ipc_channel::IpcError>| {
473                let this = this.clone();
474                let task_source = task_source.clone();
475                task_source.queue(task!(handle_abort_result: move || {
476                    let this = this.root();
477                    let _ = message.expect("Could not unwrap message");
478                    this.finalize_abort();
479                }));
480            },
481        )
482        .expect("Could not create callback");
483        let operation = SyncOperation::Abort(
484            callback,
485            global.origin().immutable().clone(),
486            self.db.get_name().to_string(),
487            self.serial_number,
488        );
489        let _ = self
490            .get_idb_thread()
491            .send(IndexedDBThreadMsg::Sync(operation));
492    }
493
494    fn notify_backend_transaction_finished(&self) {
495        let global = self.global();
496        let _ = self.get_idb_thread().send(IndexedDBThreadMsg::Sync(
497            SyncOperation::TransactionFinished {
498                origin: global.origin().immutable().clone(),
499                db_name: self.db.get_name().to_string(),
500                txn: self.serial_number,
501            },
502        ));
503    }
504
505    pub(crate) fn finalize_abort(&self) {
506        if self.finished.get() {
507            return;
508        }
509        self.committing.set(false);
510        self.commit_started.set(false);
511        let this = Trusted::new(self);
512        self.global()
513            .task_manager()
514            .dom_manipulation_task_source()
515            .queue(task!(send_abort_notification: move || {
516                let this = this.root();
517                this.active.set(false);
518                if this.mode == IDBTransactionMode::Versionchange {
519                    if let Some(old_version) = this.version_change_old_version.get() {
520                        // IndexedDB §5.8 "Aborting an upgrade transaction":
521                        // set connection's version to database's version (or 0 if newly created).
522                        // Spec note: this reverts the value of `IDBDatabase.version`.
523                        // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
524                        this.db.set_version(old_version);
525                    }
526                    this.db.clear_upgrade_transaction(&this);
527                }
528                let global = this.global();
529                let event = Event::new(
530                    &global,
531                    Atom::from("abort"),
532                    EventBubbles::DoesNotBubble,
533                    EventCancelable::NotCancelable,
534                    CanGc::note(),
535                );
536                event.fire(this.upcast(), CanGc::note());
537                if this.mode == IDBTransactionMode::Versionchange {
538                    this.global()
539                        .get_indexeddb()
540                        .clear_open_request_transaction_for_txn(&this);
541                    let origin = this.global().origin().immutable().clone();
542                    let db_name = this.db.get_name().to_string();
543                    let txn = this.serial_number;
544                    let _ = this.get_idb_thread().send(IndexedDBThreadMsg::Sync(
545                        SyncOperation::UpgradeTransactionFinished {
546                            origin,
547                            db_name,
548                            txn,
549                            committed: false,
550                        },
551                    ));
552                }
553                // https://w3c.github.io/IndexedDB/#transaction-lifetime
554                // Step 6: When a transaction is committed or aborted, its state is set to finished.
555                this.finished.set(true);
556                this.version_change_old_version.set(None);
557                this.version_change_old_object_store_names.borrow_mut().take();
558                this.notify_backend_transaction_finished();
559                if this.registered_in_global.get() {
560                    this.global().get_indexeddb().unregister_indexeddb_transaction(&this);
561                }
562            }));
563    }
564
565    pub(crate) fn finalize_commit(&self) {
566        if self.finished.get() {
567            return;
568        }
569        self.committing.set(false);
570        self.commit_started.set(false);
571        self.version_change_old_version.set(None);
572        self.version_change_old_object_store_names
573            .borrow_mut()
574            .take();
575        // https://w3c.github.io/IndexedDB/#transaction-lifetime
576        // Step 6: When a transaction is committed or aborted, its state is set to finished.
577        self.finished.set(true);
578        if self.mode == IDBTransactionMode::Versionchange {
579            self.db.clear_upgrade_transaction(self);
580        }
581        // Queue the "complete" event before unblocking later transactions in the backend.
582        // This preserves event ordering for overlapping transactions created on the same connection
583        self.dispatch_complete();
584        self.notify_backend_transaction_finished();
585        if self.registered_in_global.get() {
586            self.global()
587                .get_indexeddb()
588                .unregister_indexeddb_transaction(self);
589        }
590    }
591
592    fn dispatch_complete(&self) {
593        let global = self.global();
594        let this = Trusted::new(self);
595        global.task_manager().database_access_task_source().queue(
596            task!(send_complete_notification: move || {
597                let this = this.root();
598                let global = this.global();
599                let event = Event::new(
600                    &global,
601                    Atom::from("complete"),
602                    EventBubbles::DoesNotBubble,
603                    EventCancelable::NotCancelable,
604                    CanGc::note()
605                );
606                event.fire(this.upcast(), CanGc::note());
607                if this.mode == IDBTransactionMode::Versionchange {
608                    this.global()
609                        .get_indexeddb()
610                        .clear_open_request_transaction_for_txn(&this);
611                    let origin = this.global().origin().immutable().clone();
612                    let db_name = this.db.get_name().to_string();
613                    let txn = this.serial_number;
614                    let _ = this.get_idb_thread().send(IndexedDBThreadMsg::Sync(
615                        SyncOperation::UpgradeTransactionFinished {
616                            origin,
617                            db_name,
618                            txn,
619                            committed: true,
620                        },
621                    ));
622                }
623            }),
624        );
625    }
626
627    fn get_idb_thread(&self) -> GenericSender<IndexedDBThreadMsg> {
628        self.global().storage_threads().sender()
629    }
630
631    fn object_store_parameters(
632        &self,
633        object_store_name: &DOMString,
634    ) -> Option<(IDBObjectStoreParameters, Vec<IndexedDBIndex>, Option<i32>)> {
635        let global = self.global();
636        let idb_sender = global.storage_threads().sender();
637        let (sender, receiver) =
638            channel(global.time_profiler_chan().clone()).expect("failed to create channel");
639
640        let origin = global.origin().immutable().clone();
641        let db_name = self.db.get_name().to_string();
642        let object_store_name = object_store_name.to_string();
643
644        let operation = SyncOperation::GetObjectStore(sender, origin, db_name, object_store_name);
645
646        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
647
648        // First unwrap for ipc
649        // Second unwrap will never happen unless this db gets manually deleted somehow
650        let object_store = receiver.recv().ok()?.ok()?;
651
652        // First unwrap for ipc
653        // Second unwrap will never happen unless this db gets manually deleted somehow
654        let key_path = object_store.key_path.map(|key_path| match key_path {
655            KeyPath::String(string) => StringOrStringSequence::String(string.into()),
656            KeyPath::Sequence(seq) => {
657                StringOrStringSequence::StringSequence(seq.into_iter().map(Into::into).collect())
658            },
659        });
660        Some((
661            IDBObjectStoreParameters {
662                autoIncrement: object_store.has_key_generator,
663                keyPath: key_path,
664            },
665            object_store.indexes,
666            object_store.key_generator_current_number,
667        ))
668    }
669}
670
671impl IDBTransactionMethods<crate::DomTypeHolder> for IDBTransaction {
672    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-db>
673    fn Db(&self) -> DomRoot<IDBDatabase> {
674        DomRoot::from_ref(&*self.db)
675    }
676
677    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-objectstore>
678    fn ObjectStore(&self, name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<IDBObjectStore>> {
679        // Step 1: If transaction has finished, throw an "InvalidStateError" DOMException.
680        if self.finished.get() || self.abort_initiated.get() {
681            return Err(Error::InvalidState(None));
682        }
683
684        // Step 2: Check that the object store exists in this transaction's scope.
685        // For versionchange transactions, the scope tracks object store changes
686        // performed during the upgrade.
687        let in_scope = if self.mode == IDBTransactionMode::Versionchange {
688            self.db.object_store_exists(&name)
689        } else {
690            self.object_store_names.Contains(name.clone())
691        };
692        if !in_scope {
693            return Err(Error::NotFound(None));
694        }
695
696        // Step 3: Each call to this method on the same
697        // IDBTransaction instance with the same name
698        // returns the same IDBObjectStore instance.
699        if let Some(store) = self.store_handles.borrow().get(&*name.str()) {
700            return Ok(DomRoot::from_ref(store));
701        }
702
703        let parameters = self.object_store_parameters(&name);
704        let store = IDBObjectStore::new(
705            &self.global(),
706            self.db.get_name(),
707            name.clone(),
708            parameters.as_ref().map(|(params, _, _)| params),
709            parameters
710                .as_ref()
711                .and_then(|(_, _, key_generator_current_number)| *key_generator_current_number),
712            can_gc,
713            self,
714        );
715        if let Some(indexes) = parameters.map(|(_, indexes, _)| indexes) {
716            for index in indexes {
717                store.add_index(
718                    index.name.into(),
719                    &IDBIndexParameters {
720                        multiEntry: index.multi_entry,
721                        unique: index.unique,
722                    },
723                    index.key_path.into(),
724                    can_gc,
725                );
726            }
727        }
728        self.store_handles
729            .borrow_mut()
730            .insert(name.to_string(), Dom::from_ref(&*store));
731        Ok(store)
732    }
733
734    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-commit>
735    fn Commit(&self) -> Fallible<()> {
736        // Step 1. If this’s state is not active, then throw an "InvalidStateError" DOMException.
737        if !self.active.get() {
738            return Err(Error::InvalidState(None));
739        }
740
741        // Step 2. Run commit a transaction with this.
742        self.set_active_flag(false);
743        self.committing.set(true);
744        self.force_commit();
745
746        Ok(())
747    }
748
749    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-abort>
750    fn Abort(&self) -> Fallible<()> {
751        if self.finished.get() || self.committing.get() {
752            return Err(Error::InvalidState(None));
753        }
754        self.active.set(false);
755        self.initiate_abort(Error::Abort(None), CanGc::note());
756        self.request_backend_abort();
757
758        Ok(())
759    }
760
761    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-objectstorenames>
762    fn ObjectStoreNames(&self) -> DomRoot<DOMStringList> {
763        if self.mode == IDBTransactionMode::Versionchange {
764            self.db.object_stores()
765        } else {
766            self.object_store_names.as_rooted()
767        }
768    }
769
770    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-mode>
771    fn Mode(&self) -> IDBTransactionMode {
772        self.mode
773    }
774
775    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-mode
776    // fn Durability(&self) -> IDBTransactionDurability {
777    //     // FIXME:(arihant2math) Durability is not implemented at all
778    //     unimplemented!();
779    // }
780
781    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-error>
782    fn GetError(&self) -> Option<DomRoot<DOMException>> {
783        self.error.get()
784    }
785
786    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-onabort
787    event_handler!(abort, GetOnabort, SetOnabort);
788
789    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-oncomplete
790    event_handler!(complete, GetOncomplete, SetOncomplete);
791
792    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-onerror
793    event_handler!(error, GetOnerror, SetOnerror);
794}