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