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;
7
8use base::IpcSend;
9use dom_struct::dom_struct;
10use ipc_channel::ipc::IpcSender;
11use profile_traits::ipc;
12use script_bindings::codegen::GenericUnionTypes::StringOrStringSequence;
13use storage_traits::indexeddb::{IndexedDBThreadMsg, KeyPath, SyncOperation};
14use stylo_atoms::Atom;
15
16use crate::dom::bindings::cell::DomRefCell;
17use crate::dom::bindings::codegen::Bindings::DOMStringListBinding::DOMStringListMethods;
18use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters;
19use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{
20    IDBTransactionMethods, IDBTransactionMode,
21};
22use crate::dom::bindings::error::{Error, Fallible};
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::Trusted;
25use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
26use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
27use crate::dom::bindings::str::DOMString;
28use crate::dom::domexception::DOMException;
29use crate::dom::domstringlist::DOMStringList;
30use crate::dom::event::{Event, EventBubbles, EventCancelable};
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::indexeddb::idbdatabase::IDBDatabase;
34use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
35use crate::dom::indexeddb::idbopendbrequest::IDBOpenDBRequest;
36use crate::dom::indexeddb::idbrequest::IDBRequest;
37use crate::script_runtime::CanGc;
38
39#[dom_struct]
40pub struct IDBTransaction {
41    eventtarget: EventTarget,
42    object_store_names: Dom<DOMStringList>,
43    mode: IDBTransactionMode,
44    db: Dom<IDBDatabase>,
45    error: MutNullableDom<DOMException>,
46
47    store_handles: DomRefCell<HashMap<String, Dom<IDBObjectStore>>>,
48    // https://www.w3.org/TR/IndexedDB-2/#transaction-request-list
49    requests: DomRefCell<Vec<Dom<IDBRequest>>>,
50    // https://www.w3.org/TR/IndexedDB-2/#transaction-active-flag
51    active: Cell<bool>,
52    // https://www.w3.org/TR/IndexedDB-2/#transaction-finish
53    finished: Cell<bool>,
54    // Tracks how many IDBRequest instances are still pending for this
55    // transaction. The value is incremented when a request is added to the
56    // transaction’s request list and decremented once the request has
57    // finished.
58    pending_request_count: Cell<usize>,
59    // When the transaction belongs to a database open request (i.e. during an
60    // upgrade), the corresponding IDBOpenDBRequest is stored so we can fire its
61    // "success" event after the transaction is fully finished.
62    open_request: MutNullableDom<IDBOpenDBRequest>,
63
64    // An unique identifier, used to commit and revert this transaction
65    // FIXME:(rasviitanen) Replace this with a channel
66    serial_number: u64,
67}
68
69impl IDBTransaction {
70    fn new_inherited(
71        connection: &IDBDatabase,
72        mode: IDBTransactionMode,
73        scope: &DOMStringList,
74        serial_number: u64,
75    ) -> IDBTransaction {
76        IDBTransaction {
77            eventtarget: EventTarget::new_inherited(),
78            object_store_names: Dom::from_ref(scope),
79            mode,
80            db: Dom::from_ref(connection),
81            error: Default::default(),
82
83            store_handles: Default::default(),
84            requests: Default::default(),
85            active: Cell::new(true),
86            finished: Cell::new(false),
87            pending_request_count: Cell::new(0),
88            open_request: Default::default(),
89            serial_number,
90        }
91    }
92
93    pub fn new(
94        global: &GlobalScope,
95        connection: &IDBDatabase,
96        mode: IDBTransactionMode,
97        scope: &DOMStringList,
98        can_gc: CanGc,
99    ) -> DomRoot<IDBTransaction> {
100        let serial_number = IDBTransaction::register_new(global, connection.get_name());
101        reflect_dom_object(
102            Box::new(IDBTransaction::new_inherited(
103                connection,
104                mode,
105                scope,
106                serial_number,
107            )),
108            global,
109            can_gc,
110        )
111    }
112
113    // Registers a new transaction in the idb thread, and gets an unique serial number in return.
114    // The serial number is used when placing requests against a transaction
115    // and allows us to commit/abort transactions running in our idb thread.
116    // FIXME:(rasviitanen) We could probably replace this with a channel instead,
117    // and queue requests directly to that channel.
118    fn register_new(global: &GlobalScope, db_name: DOMString) -> u64 {
119        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
120
121        global
122            .storage_threads()
123            .send(IndexedDBThreadMsg::Sync(SyncOperation::RegisterNewTxn(
124                sender,
125                global.origin().immutable().clone(),
126                db_name.to_string(),
127            )))
128            .unwrap();
129
130        receiver.recv().unwrap()
131    }
132
133    // Runs the transaction and waits for it to finish
134    pub fn wait(&self) {
135        // Start the transaction
136        let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
137
138        let start_operation = SyncOperation::StartTransaction(
139            sender,
140            self.global().origin().immutable().clone(),
141            self.db.get_name().to_string(),
142            self.serial_number,
143        );
144
145        self.get_idb_thread()
146            .send(IndexedDBThreadMsg::Sync(start_operation))
147            .unwrap();
148
149        // Wait for the transaction to complete
150        let _ = receiver.recv();
151        // Mark the transaction as finished and dispatch the complete event.
152        // This also takes care of firing the open request "success" event
153        // (if any) in the correct order.
154        self.finished.set(true);
155        self.dispatch_complete();
156    }
157
158    pub fn set_active_flag(&self, status: bool) {
159        self.active.set(status);
160        // When the transaction becomes inactive and no requests are pending,
161        // it can transition to the finished state.
162        if !status && self.pending_request_count.get() == 0 && !self.finished.get() {
163            self.finished.set(true);
164            self.dispatch_complete();
165        }
166    }
167
168    pub fn is_active(&self) -> bool {
169        self.active.get()
170    }
171
172    pub fn get_mode(&self) -> IDBTransactionMode {
173        self.mode
174    }
175
176    pub fn get_db_name(&self) -> DOMString {
177        self.db.get_name()
178    }
179
180    pub fn get_serial_number(&self) -> u64 {
181        self.serial_number
182    }
183
184    /// Associate an `IDBOpenDBRequest` with this transaction so that its
185    /// "success" event is dispatched only once the transaction has truly
186    /// finished (after the "complete" event).
187    pub fn set_open_request(&self, request: &IDBOpenDBRequest) {
188        self.open_request.set(Some(request));
189    }
190
191    pub fn add_request(&self, request: &IDBRequest) {
192        self.requests.borrow_mut().push(Dom::from_ref(request));
193        // Increase the number of outstanding requests so that we can detect when
194        // the transaction is allowed to finish.
195        self.pending_request_count
196            .set(self.pending_request_count.get() + 1);
197    }
198
199    /// Must be called by an `IDBRequest` when it finishes (either success or
200    /// error). When the last pending request has completed and the transaction
201    /// is no longer active, the `"complete"` event is dispatched and any
202    /// associated `IDBOpenDBRequest` `"success"` event is fired afterwards.
203    pub fn request_finished(&self) {
204        if self.pending_request_count.get() == 0 {
205            return;
206        }
207        let remaining = self.pending_request_count.get() - 1;
208        self.pending_request_count.set(remaining);
209
210        if remaining == 0 && !self.active.get() && !self.finished.get() {
211            self.finished.set(true);
212            self.dispatch_complete();
213        }
214    }
215
216    pub fn upgrade_db_version(&self, version: u64) {
217        // Runs the previous request and waits for them to finish
218        self.wait();
219        // Queue a request to upgrade the db version
220        let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
221        let upgrade_version_operation = SyncOperation::UpgradeVersion(
222            sender,
223            self.global().origin().immutable().clone(),
224            self.db.get_name().to_string(),
225            self.serial_number,
226            version,
227        );
228        self.get_idb_thread()
229            .send(IndexedDBThreadMsg::Sync(upgrade_version_operation))
230            .unwrap();
231        // Wait for the version to be updated
232        // TODO(jdm): This returns a Result; what do we do with an error?
233        let _ = receiver.recv().unwrap();
234    }
235
236    fn dispatch_complete(&self) {
237        let global = self.global();
238        let this = Trusted::new(self);
239        global.task_manager().database_access_task_source().queue(
240            task!(send_complete_notification: move || {
241                let this = this.root();
242                let global = this.global();
243                let event = Event::new(
244                    &global,
245                    Atom::from("complete"),
246                    EventBubbles::DoesNotBubble,
247                    EventCancelable::NotCancelable,
248                    CanGc::note()
249                );
250                event.fire(this.upcast(), CanGc::note());
251
252                // If this transaction was created as part of an IDBOpenDBRequest,
253                // fire the "success" event for that
254                // request after the complete event to respect spec ordering.
255                if let Some(open_req) = this.open_request.get() {
256                    open_req.dispatch_success(&this.db);
257                    this.open_request.set(None);
258                }
259            }),
260        );
261    }
262
263    fn get_idb_thread(&self) -> IpcSender<IndexedDBThreadMsg> {
264        self.global().storage_threads().sender()
265    }
266
267    fn object_store_parameters(
268        &self,
269        object_store_name: &DOMString,
270    ) -> Option<IDBObjectStoreParameters> {
271        let global = self.global();
272        let idb_sender = global.storage_threads().sender();
273        let (sender, receiver) =
274            ipc::channel(global.time_profiler_chan().clone()).expect("failed to create channel");
275
276        let origin = global.origin().immutable().clone();
277        let db_name = self.db.get_name().to_string();
278        let object_store_name = object_store_name.to_string();
279
280        let operation = SyncOperation::HasKeyGenerator(
281            sender,
282            origin.clone(),
283            db_name.clone(),
284            object_store_name.clone(),
285        );
286
287        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
288
289        // First unwrap for ipc
290        // Second unwrap will never happen unless this db gets manually deleted somehow
291        let auto_increment = receiver.recv().ok()?.ok()?;
292
293        let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).ok()?;
294        let operation = SyncOperation::KeyPath(sender, origin, db_name, object_store_name);
295
296        let _ = idb_sender.send(IndexedDBThreadMsg::Sync(operation));
297
298        // First unwrap for ipc
299        // Second unwrap will never happen unless this db gets manually deleted somehow
300        let key_path = receiver.recv().unwrap().ok()?;
301        let key_path = key_path.map(|key_path| match key_path {
302            KeyPath::String(s) => StringOrStringSequence::String(DOMString::from_string(s)),
303            KeyPath::Sequence(seq) => StringOrStringSequence::StringSequence(
304                seq.into_iter().map(DOMString::from_string).collect(),
305            ),
306        });
307        Some(IDBObjectStoreParameters {
308            autoIncrement: auto_increment,
309            keyPath: key_path,
310        })
311    }
312}
313
314impl IDBTransactionMethods<crate::DomTypeHolder> for IDBTransaction {
315    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-db>
316    fn Db(&self) -> DomRoot<IDBDatabase> {
317        DomRoot::from_ref(&*self.db)
318    }
319
320    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-objectstore>
321    fn ObjectStore(&self, name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<IDBObjectStore>> {
322        // Step 1: If transaction has finished, throw an "InvalidStateError" DOMException.
323        if self.finished.get() {
324            return Err(Error::InvalidState(None));
325        }
326
327        // Step 2: Check that the object store exists
328        if !self.object_store_names.Contains(name.clone()) {
329            return Err(Error::NotFound(None));
330        }
331
332        // Step 3: Each call to this method on the same
333        // IDBTransaction instance with the same name
334        // returns the same IDBObjectStore instance.
335        if let Some(store) = self.store_handles.borrow().get(&*name.str()) {
336            return Ok(DomRoot::from_ref(store));
337        }
338
339        let parameters = self.object_store_parameters(&name);
340        let store = IDBObjectStore::new(
341            &self.global(),
342            self.db.get_name(),
343            name.clone(),
344            parameters.as_ref(),
345            can_gc,
346            self,
347        );
348        self.store_handles
349            .borrow_mut()
350            .insert(name.to_string(), Dom::from_ref(&*store));
351        Ok(store)
352    }
353
354    /// <https://www.w3.org/TR/IndexedDB-2/#commit-transaction>
355    fn Commit(&self) -> Fallible<()> {
356        // Step 1
357        let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
358        let start_operation = SyncOperation::Commit(
359            sender,
360            self.global().origin().immutable().clone(),
361            self.db.get_name().to_string(),
362            self.serial_number,
363        );
364
365        self.get_idb_thread()
366            .send(IndexedDBThreadMsg::Sync(start_operation))
367            .unwrap();
368
369        let result = receiver.recv().unwrap();
370
371        // Step 2
372        if let Err(_result) = result {
373            // FIXME:(rasviitanen) also support Unknown error
374            return Err(Error::QuotaExceeded {
375                quota: None,
376                requested: None,
377            });
378        }
379
380        // Step 3
381        // FIXME:(rasviitanen) https://www.w3.org/TR/IndexedDB-2/#commit-a-transaction
382
383        // Steps 3.1 and 3.3
384        self.dispatch_complete();
385
386        Ok(())
387    }
388
389    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-abort>
390    fn Abort(&self) -> Fallible<()> {
391        // FIXME:(rasviitanen)
392        // This only sets the flags, and does not abort the transaction
393        // see https://www.w3.org/TR/IndexedDB-2/#abort-a-transaction
394        if self.finished.get() {
395            return Err(Error::InvalidState(None));
396        }
397
398        self.active.set(false);
399
400        Ok(())
401    }
402
403    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-objectstorenames>
404    fn ObjectStoreNames(&self) -> DomRoot<DOMStringList> {
405        self.object_store_names.as_rooted()
406    }
407
408    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-mode>
409    fn Mode(&self) -> IDBTransactionMode {
410        self.mode
411    }
412
413    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-mode
414    // fn Durability(&self) -> IDBTransactionDurability {
415    //     // FIXME:(arihant2math) Durability is not implemented at all
416    //     unimplemented!();
417    // }
418
419    /// <https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-error>
420    fn GetError(&self) -> Option<DomRoot<DOMException>> {
421        self.error.get()
422    }
423
424    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-onabort
425    event_handler!(abort, GetOnabort, SetOnabort);
426
427    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-oncomplete
428    event_handler!(complete, GetOncomplete, SetOncomplete);
429
430    // https://www.w3.org/TR/IndexedDB-2/#dom-idbtransaction-onerror
431    event_handler!(error, GetOnerror, SetOnerror);
432}