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, IDBObjectStoreAbortState};
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-an-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(String::from)
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: String::from(db_name),
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    pub(crate) fn register_object_store_handle(&self, name: &DOMString, store: &IDBObjectStore) {
272        self.store_handles
273            .borrow_mut()
274            .insert(name.to_string(), Dom::from_ref(store));
275    }
276
277    pub(crate) fn rename_object_store_handle_cache(
278        &self,
279        old_name: &DOMString,
280        new_name: &DOMString,
281        store: &IDBObjectStore,
282    ) {
283        let mut store_handles = self.store_handles.borrow_mut();
284        store_handles.remove(&old_name.to_string());
285        store_handles.insert(new_name.to_string(), Dom::from_ref(store));
286    }
287
288    /// <https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction>
289    fn restore_associated_object_store_handles_after_abort(&self, can_gc: CanGc) {
290        // Step 5. For each object store handle handle associated with transaction,
291        // including those for object stores that were created or deleted during
292        // transaction:
293        let stores = self
294            .store_handles
295            .borrow()
296            .values()
297            .map(|store| DomRoot::from_ref(&**store))
298            .collect::<Vec<_>>();
299        for store in stores {
300            store.restore_metadata_after_abort(can_gc);
301        }
302    }
303
304    fn attempt_commit(&self) -> bool {
305        if self.commit_started.get() {
306            return true;
307        }
308        let this = Trusted::new(self);
309        let global = self.global();
310        let task_source = global
311            .task_manager()
312            .dom_manipulation_task_source()
313            .to_sendable();
314
315        // TODO: Reuse a shared transaction callback path (similar to IDBFactory
316        // connection callbacks) instead of creating one per transaction operation.
317        let callback = GenericCallback::new(
318            global.time_profiler_chan().clone(),
319            move |message: Result<TxnCompleteMsg, ipc_channel::IpcError>| {
320                let this = this.clone();
321                let task_source = task_source.clone();
322                task_source.queue(task!(handle_commit_result: move || {
323                    let this = this.root();
324                    let message = message.expect("Could not unwrap message");
325                    match message.result {
326                        Ok(()) => {
327                            this.finalize_commit();
328                        }
329                        Err(_err) => {
330                             // TODO: Map backend commit/rollback failure to an appropriate DOMException
331                            this.initiate_abort(Error::Operation(None), CanGc::deprecated_note());
332
333                            this.finalize_abort();
334                        }
335                    }
336                    // TODO: https://w3c.github.io/IndexedDB/#commit-a-transaction
337                    // Backend commit/rollback is not yet atomic.
338                }));
339            },
340        )
341        .expect("Could not create callback");
342
343        let commit_operation = SyncOperation::Commit(
344            callback,
345            global.origin().immutable().clone(),
346            String::from(self.db.get_name()),
347            self.serial_number,
348        );
349
350        // https://w3c.github.io/IndexedDB/#transaction-lifecycle
351        // When committing, the transaction state is set to committing.
352        let send_result = self
353            .get_idb_thread()
354            .send(IndexedDBThreadMsg::Sync(commit_operation));
355        if send_result.is_err() {
356            return false;
357        }
358
359        self.committing.set(true);
360        self.commit_started.set(true);
361        true
362    }
363
364    pub(crate) fn maybe_commit(&self) {
365        // https://w3c.github.io/IndexedDB/#transaction-lifetime
366        // Step 5: transaction when all requests
367        //  placed against the transaction have completed and their returned results handled,
368        //  no new requests have been placed against the transaction, and the transaction has
369        //  not been aborted.
370        let finished = self.finished.get();
371        let abort_initiated = self.abort_initiated.get();
372        let commit_started = self.commit_started.get();
373        let active = self.active.get();
374        let pending_request_count = self.pending_request_count.get();
375        let next_unhandled_request_id = self.next_unhandled_request_id.get();
376        let issued_count = self.issued_count();
377        if finished || abort_initiated || commit_started {
378            return;
379        }
380        if active || pending_request_count != 0 {
381            return;
382        }
383        if next_unhandled_request_id != issued_count {
384            return;
385        }
386        if !self.attempt_commit() {
387            // We failed to initiate the commit algorithm (backend task could not be queued),
388            // so the transaction cannot progress to a successful "complete".
389            // Choose the most appropriate DOMException mapping for Servo here.
390            self.initiate_abort(Error::InvalidState(None), CanGc::deprecated_note());
391            self.finalize_abort();
392        }
393    }
394
395    fn force_commit(&self) {
396        // https://w3c.github.io/IndexedDB/#transaction-lifetime
397        // An explicit call to commit() will initiate a commit without waiting for request results
398        //  to be handled by script.
399        //
400        // This differs from automatic commit:
401        // The implementation must attempt to commit an inactive transaction when all requests
402        // placed against the transaction have completed and their returned results handled,
403        // no new requests have been placed against the transaction, and the transaction has not been aborted
404        if self.finished.get() || self.abort_initiated.get() || self.commit_started.get() {
405            return;
406        }
407        if self.active.get() || self.pending_request_count.get() != 0 {
408            return;
409        }
410        self.attempt_commit();
411    }
412
413    pub fn get_mode(&self) -> IDBTransactionMode {
414        self.mode
415    }
416
417    pub fn get_db_name(&self) -> DOMString {
418        self.db.get_name()
419    }
420
421    pub(crate) fn get_db(&self) -> &IDBDatabase {
422        &self.db
423    }
424
425    pub fn get_serial_number(&self) -> u64 {
426        self.serial_number
427    }
428
429    pub(crate) fn issued_count(&self) -> u64 {
430        self.next_request_id.get()
431    }
432
433    /// request_id is only required to be unique within this transaction.
434    /// The backend keys “handled” state by (txn, request_id).
435    pub(crate) fn allocate_request_id(&self) -> u64 {
436        let id = self.next_request_id.get();
437        self.next_request_id.set(id + 1);
438        id
439    }
440
441    pub(crate) fn mark_request_handled(&self, request_id: u64) {
442        let current = self.next_unhandled_request_id.get();
443        if request_id == current {
444            let mut next = current + 1;
445            {
446                let mut pending = self.handled_pending.borrow_mut();
447                while pending.remove(&next) {
448                    next += 1;
449                }
450            }
451            self.next_unhandled_request_id.set(next);
452        } else if request_id > current {
453            self.handled_pending.borrow_mut().insert(request_id);
454        }
455    }
456
457    pub fn add_request(&self, request: &IDBRequest) {
458        self.requests.borrow_mut().push(Dom::from_ref(request));
459        // Increase the number of outstanding requests so that we can detect when
460        // the transaction is allowed to finish.
461        self.pending_request_count
462            .set(self.pending_request_count.get() + 1);
463    }
464
465    pub fn request_finished(&self) {
466        // https://w3c.github.io/IndexedDB/#transaction-lifecycle
467        // finished
468        // Once a transaction has committed or aborted, it enters this state.
469        // No requests can be made against the transaction when it is in this state.
470        if self.pending_request_count.get() == 0 {
471            return;
472        }
473        let remaining = self.pending_request_count.get() - 1;
474        self.pending_request_count.set(remaining);
475    }
476
477    pub(crate) fn initiate_abort(&self, error: Error, can_gc: CanGc) {
478        // https://w3c.github.io/IndexedDB/#transaction-lifetime
479        // Step 4: An abort will also be initiated following a failed request that is not handled by script.
480        // A transaction can be aborted at any time before it is finished,
481        // even if the transaction isn’t currently active or hasn’t yet started.
482        if self.finished.get() || self.abort_initiated.get() {
483            return;
484        }
485        if self.mode == IDBTransactionMode::Versionchange {
486            // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
487            // Step 4. Set connection’s object store set to the set of object stores in database if database previously existed,
488            // or the empty set if database was newly created.
489            if let Some(names) = self
490                .version_change_old_object_store_names
491                .borrow()
492                .as_ref()
493                .cloned()
494            {
495                self.db.restore_object_store_names(names);
496            }
497            self.restore_associated_object_store_handles_after_abort(can_gc);
498        }
499        self.abort_initiated.set(true);
500        // https://w3c.github.io/IndexedDB/#transaction-concept
501        // A transaction has a error which is set if the transaction is aborted.
502        // NOTE: Implementors need to keep in mind that the value "null" is considered an error, as it is set from abort()
503        if self.error.get().is_none() &&
504            let Ok(exception) = create_dom_exception(&self.global(), error, can_gc)
505        {
506            self.error.set(Some(&exception));
507        }
508    }
509
510    pub(crate) fn request_backend_abort(&self) {
511        if self.abort_requested.get() {
512            return;
513        }
514        self.abort_requested.set(true);
515        let this = Trusted::new(self);
516        let global = self.global();
517        let task_source = global
518            .task_manager()
519            .dom_manipulation_task_source()
520            .to_sendable();
521        let callback = GenericCallback::new(
522            global.time_profiler_chan().clone(),
523            move |message: Result<TxnCompleteMsg, ipc_channel::IpcError>| {
524                let this = this.clone();
525                let task_source = task_source.clone();
526                task_source.queue(task!(handle_abort_result: move || {
527                    let this = this.root();
528                    let _ = message.expect("Could not unwrap message");
529                    this.finalize_abort();
530                }));
531            },
532        )
533        .expect("Could not create callback");
534        let operation = SyncOperation::Abort(
535            callback,
536            global.origin().immutable().clone(),
537            String::from(self.db.get_name()),
538            self.serial_number,
539        );
540        let _ = self
541            .get_idb_thread()
542            .send(IndexedDBThreadMsg::Sync(operation));
543    }
544
545    fn notify_backend_transaction_finished(&self) {
546        let global = self.global();
547        let _ = self.get_idb_thread().send(IndexedDBThreadMsg::Sync(
548            SyncOperation::TransactionFinished {
549                origin: global.origin().immutable().clone(),
550                db_name: String::from(self.db.get_name()),
551                txn: self.serial_number,
552            },
553        ));
554    }
555
556    pub(crate) fn finalize_abort(&self) {
557        if self.finished.get() {
558            return;
559        }
560        self.committing.set(false);
561        self.commit_started.set(false);
562        let this = Trusted::new(self);
563        self.global()
564            .task_manager()
565            .dom_manipulation_task_source()
566            .queue(task!(send_abort_notification: move |cx| {
567                let this = this.root();
568                this.active.set(false);
569                if this.mode == IDBTransactionMode::Versionchange {
570                    if let Some(old_version) = this.version_change_old_version.get() {
571                        // IndexedDB §5.8 "Aborting an upgrade transaction":
572                        // set connection's version to database's version (or 0 if newly created).
573                        // Spec note: this reverts the value of `IDBDatabase.version`.
574                        // https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
575                        this.db.set_version(old_version);
576                    }
577                    this.db.clear_upgrade_transaction(&this);
578                }
579                let global = this.global();
580                let event = Event::new(
581                    &global,
582                    Atom::from("abort"),
583                    EventBubbles::DoesNotBubble,
584                    EventCancelable::NotCancelable,
585                    CanGc::from_cx(cx),
586                );
587                event.fire(cx, this.upcast());
588                if this.mode == IDBTransactionMode::Versionchange {
589                    this.global()
590                        .get_indexeddb()
591                        .clear_open_request_transaction_for_txn(&this);
592                    let origin = this.global().origin().immutable().clone();
593                    let db_name = String::from(this.db.get_name());
594                    let txn = this.serial_number;
595                    let _ = this.get_idb_thread().send(IndexedDBThreadMsg::Sync(
596                        SyncOperation::UpgradeTransactionFinished {
597                            origin,
598                            db_name,
599                            txn,
600                            committed: false,
601                        },
602                    ));
603                }
604                // https://w3c.github.io/IndexedDB/#transaction-lifetime
605                // Step 6: When a transaction is committed or aborted, its state is set to finished.
606                this.finished.set(true);
607                this.version_change_old_version.set(None);
608                this.version_change_old_object_store_names.borrow_mut().take();
609                this.notify_backend_transaction_finished();
610                if this.registered_in_global.get() {
611                    this.global().get_indexeddb().unregister_indexeddb_transaction(&this);
612                }
613            }));
614    }
615
616    pub(crate) fn finalize_commit(&self) {
617        if self.finished.get() {
618            return;
619        }
620        self.dispatch_complete();
621    }
622
623    fn dispatch_complete(&self) {
624        let global = self.global();
625        let this = Trusted::new(self);
626        global.task_manager().database_access_task_source().queue(
627            task!(send_complete_notification: move |cx| {
628                let this = this.root();
629                this.committing.set(false);
630                this.commit_started.set(false);
631                this.version_change_old_version.set(None);
632                this.version_change_old_object_store_names
633                    .borrow_mut()
634                    .take();
635                if this.mode == IDBTransactionMode::Versionchange {
636                    // https://w3c.github.io/IndexedDB/#commit-transaction
637                    // Step 5.1: If transaction is an upgrade transaction, then set transaction’s connection’s
638                    // associated database’s upgrade transaction to null.
639                    this.db.clear_upgrade_transaction(&this);
640                }
641                // https://w3c.github.io/IndexedDB/#commit-transaction
642                // Step 5.2: Set transaction’s state to finished.
643                this.finished.set(true);
644                let global = this.global();
645                let event = Event::new(
646                    &global,
647                    Atom::from("complete"),
648                    EventBubbles::DoesNotBubble,
649                    EventCancelable::NotCancelable,
650                    CanGc::from_cx(cx)
651                );
652                // https://w3c.github.io/IndexedDB/#commit-transaction
653                // Step 5.3: Fire an event named complete at transaction.
654                event.fire(cx, this.upcast());
655                if this.mode == IDBTransactionMode::Versionchange {
656                    // https://w3c.github.io/IndexedDB/#commit-transaction
657                    //  Step 5.1: If transaction is an upgrade transaction, then let request be the request
658                    // associated with transaction and set request’s transaction to null.
659                    this.global()
660                        .get_indexeddb()
661                        .clear_open_request_transaction_for_txn(&this);
662                    let origin = this.global().origin().immutable().clone();
663                    let db_name = String::from(this.db.get_name());
664                    let txn = this.serial_number;
665                    let _ = this.get_idb_thread().send(IndexedDBThreadMsg::Sync(
666                        SyncOperation::UpgradeTransactionFinished {
667                            origin,
668                            db_name,
669                            txn,
670                            committed: true,
671                        },
672                    ));
673                }
674                this.notify_backend_transaction_finished();
675                if this.registered_in_global.get() {
676                    this.global().get_indexeddb().unregister_indexeddb_transaction(&this);
677                }
678            }),
679        );
680    }
681
682    fn get_idb_thread(&self) -> GenericSender<IndexedDBThreadMsg> {
683        self.global().storage_threads().sender()
684    }
685
686    fn object_store_parameters(
687        &self,
688        object_store_name: &DOMString,
689    ) -> Option<(IDBObjectStoreParameters, Vec<IndexedDBIndex>, Option<i64>)> {
690        let global = self.global();
691        let idb_sender = global.storage_threads().sender();
692        let (sender, receiver) =
693            channel(global.time_profiler_chan().clone()).expect("failed to create channel");
694
695        let origin = global.origin().immutable().clone();
696        let db_name = String::from(self.db.get_name());
697        let object_store_name = object_store_name.to_string();
698
699        let operation = SyncOperation::GetObjectStore(sender, origin, db_name, object_store_name);
700
701        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
702
703        // First unwrap for ipc
704        // Second unwrap will never happen unless this db gets manually deleted somehow
705        let object_store = receiver.recv().ok()?.ok()?;
706
707        // First unwrap for ipc
708        // Second unwrap will never happen unless this db gets manually deleted somehow
709        let key_path = object_store.key_path.map(|key_path| match key_path {
710            KeyPath::String(string) => StringOrStringSequence::String(string.into()),
711            KeyPath::Sequence(seq) => {
712                StringOrStringSequence::StringSequence(seq.into_iter().map(Into::into).collect())
713            },
714        });
715        Some((
716            IDBObjectStoreParameters {
717                autoIncrement: object_store.has_key_generator,
718                keyPath: key_path,
719            },
720            object_store.indexes,
721            object_store.key_generator_current_number,
722        ))
723    }
724}
725
726impl IDBTransactionMethods<crate::DomTypeHolder> for IDBTransaction {
727    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-db>
728    fn Db(&self) -> DomRoot<IDBDatabase> {
729        DomRoot::from_ref(&*self.db)
730    }
731
732    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-objectstore>
733    fn ObjectStore(&self, name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<IDBObjectStore>> {
734        // Step 1: If transaction has finished, throw an "InvalidStateError" DOMException.
735        if self.finished.get() || self.abort_initiated.get() {
736            return Err(Error::InvalidState(None));
737        }
738
739        // Step 2: Check that the object store exists in this transaction's scope.
740        // For versionchange transactions, the scope tracks object store changes
741        // performed during the upgrade.
742        let in_scope = if self.mode == IDBTransactionMode::Versionchange {
743            self.db.object_store_exists(&name)
744        } else {
745            self.object_store_names.Contains(name.clone())
746        };
747        if !in_scope {
748            return Err(Error::NotFound(None));
749        }
750
751        // Step 3: Each call to this method on the same
752        // IDBTransaction instance with the same name
753        // returns the same IDBObjectStore instance.
754        if let Some(store) = self.store_handles.borrow().get(&*name.str()) {
755            return Ok(DomRoot::from_ref(store));
756        }
757
758        let parameters = self.object_store_parameters(&name);
759        let store = IDBObjectStore::new(
760            &self.global(),
761            self.db.get_name(),
762            name.clone(),
763            parameters.as_ref().map(|(params, _, _)| params),
764            IDBObjectStoreAbortState {
765                newly_created_during_transaction: false,
766                rollback_indexes_on_abort: if self.mode == IDBTransactionMode::Versionchange {
767                    parameters
768                        .as_ref()
769                        .map(|(_, indexes, _)| indexes.clone())
770                        .unwrap_or_default()
771                } else {
772                    Vec::new()
773                },
774                key_generator_current_number: parameters
775                    .as_ref()
776                    .and_then(|(_, _, key_generator_current_number)| *key_generator_current_number),
777            },
778            can_gc,
779            self,
780        );
781        if let Some(indexes) = parameters.map(|(_, indexes, _)| indexes) {
782            for index in indexes {
783                store.add_index(
784                    index.name.into(),
785                    &IDBIndexParameters {
786                        multiEntry: index.multi_entry,
787                        unique: index.unique,
788                    },
789                    index.key_path.into(),
790                    can_gc,
791                );
792            }
793        }
794        self.register_object_store_handle(&name, &store);
795        Ok(store)
796    }
797
798    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-commit>
799    fn Commit(&self) -> Fallible<()> {
800        // Step 1. If this’s state is not active, then throw an "InvalidStateError" DOMException.
801        if !self.active.get() {
802            return Err(Error::InvalidState(None));
803        }
804
805        // Step 2. Run commit a transaction with this.
806        self.set_active_flag(false);
807        self.committing.set(true);
808        self.force_commit();
809
810        Ok(())
811    }
812
813    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-abort>
814    fn Abort(&self) -> Fallible<()> {
815        if self.finished.get() || self.committing.get() {
816            return Err(Error::InvalidState(None));
817        }
818        self.active.set(false);
819        self.initiate_abort(Error::Abort(None), CanGc::deprecated_note());
820        self.request_backend_abort();
821
822        Ok(())
823    }
824
825    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-objectstorenames>
826    fn ObjectStoreNames(&self) -> DomRoot<DOMStringList> {
827        if self.mode == IDBTransactionMode::Versionchange {
828            self.db.object_stores()
829        } else {
830            self.object_store_names.as_rooted()
831        }
832    }
833
834    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-mode>
835    fn Mode(&self) -> IDBTransactionMode {
836        self.mode
837    }
838
839    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-durability>
840    fn Durability(&self) -> IDBTransactionDurability {
841        self.durability
842    }
843
844    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-error>
845    fn GetError(&self) -> Option<DomRoot<DOMException>> {
846        self.error.get()
847    }
848
849    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-onabort
850    event_handler!(abort, GetOnabort, SetOnabort);
851
852    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-oncomplete
853    event_handler!(complete, GetOncomplete, SetOncomplete);
854
855    // https://www.w3.org/TR/IndexedDB-3/#dom-idbtransaction-onerror
856    event_handler!(error, GetOnerror, SetOnerror);
857}