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