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