Skip to main content

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