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