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