Skip to main content

script/dom/indexeddb/
idbrequest.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
5#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
6
7use std::cell::Cell;
8
9use dom_struct::dom_struct;
10use js::context::JSContext;
11use js::conversions::ToJSValConvertible;
12use js::jsapi::Heap;
13use js::jsval::{DoubleValue, JSVal, NullValue, ObjectValue, UndefinedValue};
14use js::rust::HandleValue;
15use profile_traits::generic_callback::GenericCallback;
16use script_bindings::reflector::{DomObject, reflect_dom_object};
17use serde::{Deserialize, Serialize};
18use servo_base::generic_channel::{GenericSend, SendError};
19use storage_traits::indexeddb::{
20    AsyncOperation, AsyncReadOnlyOperation, BackendError, BackendResult, IndexedDBKeyType,
21    IndexedDBRecord, IndexedDBThreadMsg, IndexedDBTxnMode, PutItemResult, SyncOperation,
22};
23use stylo_atoms::Atom;
24
25use crate::dom::bindings::codegen::Bindings::IDBRequestBinding::{
26    IDBRequestMethods, IDBRequestReadyState,
27};
28use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::IDBTransactionMode;
29use crate::dom::bindings::error::{Error, Fallible, create_dom_exception};
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::DomGlobal;
33use crate::dom::bindings::root::{DomRoot, MutNullableDom};
34use crate::dom::bindings::structuredclone;
35use crate::dom::domexception::DOMException;
36use crate::dom::event::{Event, EventBubbles, EventCancelable};
37use crate::dom::eventtarget::EventTarget;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::indexeddb::idbcursor::{IterationParam, iterate_cursor};
40use crate::dom::indexeddb::idbcursorwithvalue::IDBCursorWithValue;
41use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
42use crate::dom::indexeddb::idbtransaction::IDBTransaction;
43use crate::dom::indexeddb::key::key_type_to_jsval;
44use crate::realms::enter_auto_realm;
45
46#[derive(Clone)]
47struct RequestListener {
48    request: Trusted<IDBRequest>,
49    iteration_param: Option<IterationParam>,
50    request_id: u64,
51}
52
53pub enum IdbResult {
54    Key(IndexedDBKeyType),
55    Keys(Vec<IndexedDBKeyType>),
56    Value(Vec<u8>),
57    Values(Vec<Vec<u8>>),
58    Count(u64),
59    Iterate(Vec<IndexedDBRecord>),
60    Error(Error),
61    None,
62}
63
64impl From<IndexedDBKeyType> for IdbResult {
65    fn from(value: IndexedDBKeyType) -> Self {
66        IdbResult::Key(value)
67    }
68}
69
70impl From<Vec<IndexedDBKeyType>> for IdbResult {
71    fn from(value: Vec<IndexedDBKeyType>) -> Self {
72        IdbResult::Keys(value)
73    }
74}
75
76impl From<Vec<u8>> for IdbResult {
77    fn from(value: Vec<u8>) -> Self {
78        IdbResult::Value(value)
79    }
80}
81
82impl From<Vec<Vec<u8>>> for IdbResult {
83    fn from(value: Vec<Vec<u8>>) -> Self {
84        IdbResult::Values(value)
85    }
86}
87
88impl From<PutItemResult> for IdbResult {
89    fn from(value: PutItemResult) -> Self {
90        match value {
91            PutItemResult::Key(key) => Self::Key(key),
92            PutItemResult::CannotOverwrite => Self::Error(Error::Constraint(None)),
93        }
94    }
95}
96
97impl From<Vec<IndexedDBRecord>> for IdbResult {
98    fn from(value: Vec<IndexedDBRecord>) -> Self {
99        Self::Iterate(value)
100    }
101}
102
103impl From<()> for IdbResult {
104    fn from(_value: ()) -> Self {
105        Self::None
106    }
107}
108
109impl<T> From<Option<T>> for IdbResult
110where
111    T: Into<IdbResult>,
112{
113    fn from(value: Option<T>) -> Self {
114        match value {
115            Some(value) => value.into(),
116            None => IdbResult::None,
117        }
118    }
119}
120
121impl From<u64> for IdbResult {
122    fn from(value: u64) -> Self {
123        IdbResult::Count(value)
124    }
125}
126
127impl RequestListener {
128    fn send_request_handled(cx: &mut JSContext, transaction: &IDBTransaction, request_id: u64) {
129        let global = transaction.global();
130        // https://w3c.github.io/IndexedDB/#transaction-lifecycle
131        // A transaction is inactive after control returns to the event loop and
132        // when events are not being dispatched. We call this after dispatching
133        // the request event, so the backend can reevaluate commit eligibility.
134        let send_result = global.storage_threads().send(IndexedDBThreadMsg::Sync(
135            SyncOperation::RequestHandled {
136                origin: global.origin().immutable().clone(),
137                db_name: String::from(transaction.get_db_name()),
138                txn: transaction.get_serial_number(),
139                request_id,
140            },
141        ));
142        if send_result.is_err() {
143            error!("Failed to send SyncOperation::RequestHandled");
144        }
145        transaction.mark_request_handled(request_id);
146
147        // This request's result has been handled by script, the
148        // transaction might finally be ready to auto-commit.
149        transaction.maybe_commit(cx);
150    }
151
152    // https://www.w3.org/TR/IndexedDB-3/#async-execute-request
153    // Implements Step 5.4
154    fn handle_async_request_finished(&self, cx: &mut JSContext, result: BackendResult<IdbResult>) {
155        let request = self.request.root();
156        let global = request.global();
157
158        let transaction = request
159            .transaction
160            .get()
161            .expect("Request unexpectedly has no transaction");
162        // Substep 1: Set the result of request to result.
163        request.set_ready_state_done();
164
165        let mut realm = enter_auto_realm(cx, &*request);
166        let cx: &mut JSContext = &mut realm;
167        rooted!(&in(cx) let mut answer = UndefinedValue());
168
169        if let Ok(data) = result {
170            match data {
171                IdbResult::Key(key) => key_type_to_jsval(cx, &key, answer.handle_mut()),
172                IdbResult::Keys(keys) => {
173                    rooted!(&in(cx) let mut array = vec![JSVal::default(); keys.len()]);
174                    for (i, key) in keys.into_iter().enumerate() {
175                        key_type_to_jsval(cx, &key, array.handle_mut_at(i));
176                    }
177                    array.to_jsval(cx, answer.handle_mut());
178                },
179                IdbResult::Value(serialized_data) => {
180                    let result = postcard::from_bytes(&serialized_data)
181                        .map_err(|_| Error::Data(None))
182                        .and_then(|data| {
183                            structuredclone::read(cx, &global, data, answer.handle_mut())
184                        });
185                    if let Err(e) = result {
186                        warn!("Error reading structuredclone data");
187                        Self::handle_async_request_error(&global, cx, request, e, self.request_id);
188                        return;
189                    };
190                },
191                IdbResult::Values(serialized_values) => {
192                    rooted!(&in(cx) let mut values = vec![JSVal::default(); serialized_values.len()]);
193                    for (i, serialized_data) in serialized_values.into_iter().enumerate() {
194                        let result = postcard::from_bytes(&serialized_data)
195                            .map_err(|_| Error::Data(None))
196                            .and_then(|data| {
197                                structuredclone::read(cx, &global, data, values.handle_mut_at(i))
198                            });
199                        if let Err(e) = result {
200                            warn!("Error reading structuredclone data");
201                            Self::handle_async_request_error(
202                                &global,
203                                cx,
204                                request,
205                                e,
206                                self.request_id,
207                            );
208                            return;
209                        };
210                    }
211                    values.to_jsval(cx, answer.handle_mut());
212                },
213                IdbResult::Count(count) => {
214                    answer.handle_mut().set(DoubleValue(count as f64));
215                },
216                IdbResult::Iterate(records) => {
217                    let param = self.iteration_param.as_ref().expect(
218                        "iteration_param must be provided by IDBRequest::execute_async for Iterate",
219                    );
220                    let cursor = match iterate_cursor(&global, cx, param, records) {
221                        Ok(cursor) => cursor,
222                        Err(e) => {
223                            warn!("Error reading structuredclone data");
224                            Self::handle_async_request_error(
225                                &global,
226                                cx,
227                                request,
228                                e,
229                                self.request_id,
230                            );
231                            return;
232                        },
233                    };
234                    if let Some(cursor) = cursor {
235                        match cursor.downcast::<IDBCursorWithValue>() {
236                            Some(cursor_with_value) => {
237                                answer.handle_mut().set(ObjectValue(
238                                    *cursor_with_value.reflector().get_jsobject(),
239                                ));
240                            },
241                            None => {
242                                answer
243                                    .handle_mut()
244                                    .set(ObjectValue(*cursor.reflector().get_jsobject()));
245                            },
246                        }
247                    } else {
248                        answer.handle_mut().set(NullValue());
249                    }
250                },
251                IdbResult::None => {
252                    // no-op
253                },
254                IdbResult::Error(error) => {
255                    // Substep 2
256                    Self::handle_async_request_error(&global, cx, request, error, self.request_id);
257                    return;
258                },
259            }
260
261            // Substep 3.1: Set the result of request to answer.
262            request.set_result(answer.handle());
263
264            // Substep 3.2: Set the error of request to undefined
265            request.set_error(cx, None);
266
267            // https://w3c.github.io/IndexedDB/#fire-success-event
268            // Step 1: Let event be the result of creating an event using Event.
269            // Step 2: Set event’s type attribute to "success".
270            // Step 3: Set event’s bubbles and cancelable attributes to false.
271            let event = Event::new(
272                cx,
273                &global,
274                Atom::from("success"),
275                EventBubbles::DoesNotBubble,
276                EventCancelable::NotCancelable,
277            );
278
279            // Step 5: Let legacyOutputDidListenersThrowFlag be initially false.
280            let did_listeners_throw = Cell::new(false);
281            // Step 6: If transaction’s state is inactive, then set transaction’s state to active.
282            if transaction.is_inactive() {
283                transaction.set_active_flag(true);
284            }
285            // Step 7: Dispatch event at request with legacyOutputDidListenersThrowFlag.
286            event
287                .upcast::<Event>()
288                .fire_with_legacy_output_did_listeners_throw(
289                    cx,
290                    request.upcast(),
291                    &did_listeners_throw,
292                );
293            // Step 8: If transaction’s state is active, then:
294            if transaction.is_active() {
295                // Step 8.1: Set transaction’s state to inactive.
296                transaction.set_active_flag(false);
297                // Step 8.2: If legacyOutputDidListenersThrowFlag is true, then run abort a
298                // transaction with transaction and a newly created "AbortError" DOMException.
299                if did_listeners_throw.get() {
300                    transaction.initiate_abort(cx, Error::Abort(None));
301                    transaction.request_backend_abort();
302                }
303            }
304            transaction.request_finished();
305
306            Self::send_request_handled(cx, &transaction, self.request_id);
307        } else {
308            // FIXME:(arihant2math) dispatch correct error
309            // Substep 2
310            Self::handle_async_request_error(
311                &global,
312                cx,
313                request,
314                Error::Data(None),
315                self.request_id,
316            );
317        }
318    }
319
320    // https://www.w3.org/TR/IndexedDB-3/#async-execute-request
321    // Implements Step 5.4.2
322    fn handle_async_request_error(
323        global: &GlobalScope,
324        cx: &mut JSContext,
325        request: DomRoot<IDBRequest>,
326        error: Error,
327        request_id: u64,
328    ) {
329        let transaction = request
330            .transaction
331            .get()
332            .expect("Request has no transaction");
333        // Substep 1: Set the result of request to undefined.
334        rooted!(&in(cx) let undefined = UndefinedValue());
335        request.set_result(undefined.handle());
336
337        // Substep 2: Set the error of request to result.
338        request.set_error(cx, Some(error.clone()));
339
340        // https://w3c.github.io/IndexedDB/#fire-error-event
341        // Step 1: Let event be the result of creating an event using Event.
342        // Step 2: Set event’s type attribute to "error".
343        // Step 3: Set event’s bubbles and cancelable attributes to true.
344        let event = Event::new(
345            cx,
346            global,
347            Atom::from("error"),
348            EventBubbles::Bubbles,
349            EventCancelable::Cancelable,
350        );
351
352        // If result is an error and transaction’s state is committing, then run abort a
353        // transaction with transaction and result, and terminate these steps.
354        if transaction.is_committing() {
355            transaction.initiate_abort(cx, error.clone());
356            transaction.request_backend_abort();
357        }
358        // Step 5: Let legacyOutputDidListenersThrowFlag be initially false.
359        let did_listeners_throw = Cell::new(false);
360        // Step 6: If transaction’s state is inactive, then set transaction’s state to active.
361        if transaction.is_inactive() {
362            transaction.set_active_flag(true);
363        }
364        // Step 7: Dispatch event at request with legacyOutputDidListenersThrowFlag.
365        let default_not_prevented = event
366            .upcast::<Event>()
367            .fire_with_legacy_output_did_listeners_throw(
368                cx,
369                request.upcast(),
370                &did_listeners_throw,
371            );
372        // Step 8: If transaction’s state is active, then:
373        if transaction.is_active() {
374            // Step 8.1: Set transaction’s state to inactive.
375            transaction.set_active_flag(false);
376            // Step 8.2: If legacyOutputDidListenersThrowFlag is true, then run abort a transaction
377            // with transaction and a newly created "AbortError" DOMException and terminate these steps.
378            // NOTE: This is done even if event’s canceled flag is false.
379            // NOTE: This means that if an error event is fired and any of the event handlers throw an
380            // exception, transaction’s error property is set to an AbortError rather than request’s
381            // error, even if preventDefault() is never called.
382            if did_listeners_throw.get() {
383                transaction.initiate_abort(cx, Error::Abort(None));
384                transaction.request_backend_abort();
385            } else if default_not_prevented {
386                // Step 8.3: If event’s canceled flag is false, then run abort a transaction
387                // using transaction and request’s error, and terminate these steps.
388                transaction.initiate_abort(cx, error);
389                transaction.request_backend_abort();
390            }
391        }
392        transaction.request_finished();
393        Self::send_request_handled(cx, &transaction, request_id);
394    }
395}
396
397#[dom_struct]
398pub struct IDBRequest {
399    eventtarget: EventTarget,
400    #[ignore_malloc_size_of = "mozjs"]
401    result: Heap<JSVal>,
402    error: MutNullableDom<DOMException>,
403    source: MutNullableDom<IDBObjectStore>,
404    transaction: MutNullableDom<IDBTransaction>,
405    ready_state: Cell<IDBRequestReadyState>,
406}
407
408impl IDBRequest {
409    pub fn new_inherited() -> IDBRequest {
410        IDBRequest {
411            eventtarget: EventTarget::new_inherited(),
412
413            result: Heap::default(),
414            error: Default::default(),
415            source: Default::default(),
416            transaction: Default::default(),
417            ready_state: Cell::new(IDBRequestReadyState::Pending),
418        }
419    }
420
421    pub fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<IDBRequest> {
422        reflect_dom_object(cx, Box::new(IDBRequest::new_inherited()), global)
423    }
424
425    pub fn set_source(&self, source: Option<&IDBObjectStore>) {
426        self.source.set(source);
427    }
428
429    pub fn set_ready_state_done(&self) {
430        self.ready_state.set(IDBRequestReadyState::Done);
431    }
432
433    pub fn set_result(&self, result: HandleValue) {
434        self.result.set(result.get());
435    }
436
437    pub fn set_error(&self, cx: &mut JSContext, error: Option<Error>) {
438        if let Some(error) = error {
439            if let Ok(exception) = create_dom_exception(cx, &self.global(), error) {
440                self.error.set(Some(&exception));
441            }
442        } else {
443            self.error.set(None);
444        }
445    }
446
447    pub fn set_transaction(&self, transaction: &IDBTransaction) {
448        self.transaction.set(Some(transaction));
449    }
450
451    pub fn clear_transaction(&self) {
452        self.transaction.set(None);
453    }
454
455    pub fn unset_done_flag(&self) {
456        self.ready_state.set(IDBRequestReadyState::Pending);
457    }
458
459    fn is_done(&self) -> bool {
460        self.ready_state.get() == IDBRequestReadyState::Done
461    }
462
463    pub(crate) fn transaction(&self) -> Option<DomRoot<IDBTransaction>> {
464        self.transaction.get()
465    }
466
467    // https://www.w3.org/TR/IndexedDB-3/#asynchronously-execute-a-request
468    pub fn execute_async<T, F>(
469        cx: &mut JSContext,
470        source: &IDBObjectStore,
471        operation_fn: F,
472        request: Option<DomRoot<IDBRequest>>,
473        iteration_param: Option<IterationParam>,
474    ) -> Fallible<DomRoot<IDBRequest>>
475    where
476        T: Into<IdbResult> + for<'a> Deserialize<'a> + Serialize + Send + Sync + 'static,
477        F: FnOnce(GenericCallback<BackendResult<T>>) -> AsyncOperation,
478    {
479        // Step 1: Let transaction be the transaction associated with source.
480        let transaction = source.transaction();
481        let global = transaction.global();
482        // Step 2: Assert: transaction is active.
483        if !transaction.is_active() || !transaction.is_usable() {
484            return Err(Error::TransactionInactive(None));
485        }
486
487        let request_id = transaction.allocate_request_id();
488
489        // Step 3: If request was not given, let request be a new request with source as source.
490        let request = request.unwrap_or_else(|| {
491            let new_request = IDBRequest::new(cx, &global);
492            new_request.set_source(Some(source));
493            new_request.set_transaction(&transaction);
494            new_request
495        });
496
497        // Step 4: Add request to the end of transaction’s request list.
498        transaction.add_request(&request);
499
500        // Step 5: Run the operation, and queue a returning task in parallel
501        // the result will be put into `receiver`
502        let transaction_mode = match transaction.get_mode() {
503            IDBTransactionMode::Readonly => IndexedDBTxnMode::Readonly,
504            IDBTransactionMode::Readwrite => IndexedDBTxnMode::Readwrite,
505            IDBTransactionMode::Versionchange => IndexedDBTxnMode::Versionchange,
506        };
507
508        let response_listener = RequestListener {
509            request: Trusted::new(&request),
510            iteration_param: iteration_param.clone(),
511            request_id,
512        };
513
514        let task_source = global
515            .task_manager()
516            .database_access_task_source()
517            .to_sendable();
518
519        let closure = move |message: Result<BackendResult<T>, SendError>| {
520            let response_listener = response_listener.clone();
521            task_source.queue(task!(request_callback: move |cx| {
522                response_listener.handle_async_request_finished(
523                    cx,
524                    message.expect("Could not unwrap message").inspect_err(|e| {
525                        if let BackendError::DbErr(e) = e {
526                            error!("Error in IndexedDB operation: {}", e);
527                        }
528                    }).map(|t| t.into()),
529                );
530            }));
531        };
532        let callback = GenericCallback::new(closure).expect("Could not create callback");
533        let operation = operation_fn(callback);
534
535        if matches!(
536            operation,
537            AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Iterate { .. })
538        ) {
539            assert!(
540                iteration_param.is_some(),
541                "iteration_param must be provided for Iterate"
542            );
543        } else {
544            assert!(
545                iteration_param.is_none(),
546                "iteration_param should not be provided for operation other than Iterate"
547            );
548        }
549
550        // Start is a backend database task (spec). Script does not model it with a
551        // separate queued task, backend scheduling decides when requests begin.
552        transaction
553            .global()
554            .storage_threads()
555            .send(IndexedDBThreadMsg::Async(
556                global.origin().immutable().clone(),
557                String::from(transaction.get_db_name()),
558                String::from(source.get_name()),
559                transaction.get_serial_number(),
560                request_id,
561                transaction_mode,
562                operation,
563            ))
564            .unwrap();
565
566        // Step 6
567        Ok(request)
568    }
569}
570
571impl IDBRequestMethods<crate::DomTypeHolder> for IDBRequest {
572    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-result>
573    fn GetResult(
574        &self,
575        _cx: &mut JSContext,
576        mut val: js::rust::MutableHandle<'_, js::jsapi::Value>,
577    ) -> Fallible<()> {
578        // Step 1. If this's done flag is false, then throw an "InvalidStateError" DOMException.
579        if !self.is_done() {
580            return Err(Error::InvalidState(Some(
581                "Cannot get result on a request that is still pending.".into(),
582            )));
583        }
584
585        // Step 2. Return this's result, or undefined if the request resulted in an error.
586        val.set(self.result.get());
587        Ok(())
588    }
589
590    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-error>
591    fn GetError(&self) -> Fallible<Option<DomRoot<DOMException>>> {
592        // Step 1. If this's done flag is false, then throw an "InvalidStateError" DOMException.
593        if !self.is_done() {
594            return Err(Error::InvalidState(Some(
595                "Cannot get error on a request that is still pending.".into(),
596            )));
597        }
598
599        // Step 2. Return this's error, or null if no error occurred.
600        Ok(self.error.get())
601    }
602
603    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-source>
604    fn GetSource(&self) -> Option<DomRoot<IDBObjectStore>> {
605        self.source.get()
606    }
607
608    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-transaction>
609    fn GetTransaction(&self) -> Option<DomRoot<IDBTransaction>> {
610        self.transaction.get()
611    }
612
613    /// <https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-readystate>
614    fn ReadyState(&self) -> IDBRequestReadyState {
615        self.ready_state.get()
616    }
617
618    // https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-onsuccess
619    event_handler!(success, GetOnsuccess, SetOnsuccess);
620
621    // https://www.w3.org/TR/IndexedDB-3/#dom-idbrequest-onerror
622    event_handler!(error, GetOnerror, SetOnerror);
623}