From 250c4cda00abce73e8168dc04eaed068ed494500 Mon Sep 17 00:00:00 2001 From: Kingsley Yung Date: Sat, 13 Sep 2025 00:54:07 +0800 Subject: [PATCH] indexeddb: Implement `openCursor` and `openKeyCursor` for object store (#39080) Continue on implementing indexeddb's cursor. This patch focuses on implementing the `openCursor` [1] and `openKeyCursor` [2] methods of the `IDBObjectStore` interface, which create and initialize cursors by running the iterate-a-cursor algorithm [3]. It also adds struct `IndexedDBRecord` to `components/shared/net/indexeddb_thread.rs`. This struct can later be used to implement the new `IDBRecord` interface [4]. [1] https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-opencursor [2] https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-openkeycursor [3] https://www.w3.org/TR/IndexedDB-2/#iterate-a-cursor [4] https://w3c.github.io/IndexedDB/#record-interface Testing: Pass WPT tests that were expected to fail. Fixes: Part of #38111 --------- Signed-off-by: Kingsley Yung --- components/net/indexeddb/engines/sqlite.rs | 36 +- components/script/dom/idbcursor.rs | 325 +++++++++++++++++- components/script/dom/idbcursorwithvalue.rs | 1 - components/script/dom/idbobjectstore.rs | 113 ++++++ components/script/dom/idbrequest.rs | 62 +++- components/script/indexed_db.rs | 32 +- .../webidls/IDBObjectStore.webidl | 8 +- components/shared/net/indexeddb_thread.rs | 12 + ...bcursor-advance-exception-order.any.js.ini | 6 - .../IndexedDB/idbcursor-direction.any.js.ini | 30 -- .../meta/IndexedDB/idbcursor-key.any.js.ini | 12 - .../idbcursor_advance_objectstore.any.js.ini | 6 - .../idbcursor_update_objectstore.any.js.ini | 6 - ...dbobjectstore-cross-realm-methods.html.ini | 15 - ...jectstore-query-exception-order.any.js.ini | 12 - .../idbobjectstore-request-source.any.js.ini | 29 -- .../IndexedDB/idbobjectstore_count.any.js.ini | 6 - .../IndexedDB/idbobjectstore_get.any.js.ini | 5 + .../idbobjectstore_getAll.any.js.ini | 36 -- ...re_getAllKeys-options.tentative.any.js.ini | 12 - .../idbobjectstore_getAllKeys.any.js.ini | 30 -- .../idbobjectstore_getKey.any.js.ini | 6 + .../wpt/meta/IndexedDB/idlharness.any.js.ini | 12 - .../IndexedDB/nested-cloning-basic.any.js.ini | 11 - .../IndexedDB/nested-cloning-large.any.js.ini | 18 - .../IndexedDB/nested-cloning-small.any.js.ini | 18 - 26 files changed, 580 insertions(+), 279 deletions(-) diff --git a/components/net/indexeddb/engines/sqlite.rs b/components/net/indexeddb/engines/sqlite.rs index b5c66762b06..02d510e7b13 100644 --- a/components/net/indexeddb/engines/sqlite.rs +++ b/components/net/indexeddb/engines/sqlite.rs @@ -8,8 +8,8 @@ use ipc_channel::ipc::IpcSender; use log::{error, info}; use net_traits::indexeddb_thread::{ AsyncOperation, AsyncReadOnlyOperation, AsyncReadWriteOperation, BackendError, BackendResult, - CreateObjectResult, IndexedDBKeyRange, IndexedDBKeyType, IndexedDBTxnMode, KeyPath, - PutItemResult, + CreateObjectResult, IndexedDBKeyRange, IndexedDBKeyType, IndexedDBRecord, IndexedDBTxnMode, + KeyPath, PutItemResult, }; use rusqlite::{Connection, Error, OptionalExtension, params}; use sea_query::{Condition, Expr, ExprTrait, IntoCondition, SqliteQueryBuilder}; @@ -225,6 +225,16 @@ impl SqliteEngine { .map(|models| models.into_iter().map(|m| m.data).collect()) } + #[allow(clippy::type_complexity)] + fn get_all_records( + connection: &Connection, + store: object_store_model::Model, + key_range: IndexedDBKeyRange, + ) -> Result, Vec)>, Error> { + Self::get_all(connection, store, key_range, None) + .map(|models| models.into_iter().map(|m| (m.key, m.data)).collect()) + } + fn put_item( connection: &Connection, store: object_store_model::Model, @@ -507,6 +517,28 @@ impl KvsEngine for SqliteEngine { .map_err(|e| BackendError::DbErr(format!("{:?}", e))), ); }, + AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Iterate { + sender, + key_range, + }) => { + let Ok(object_store) = process_object_store(object_store, &sender) else { + continue; + }; + let _ = sender.send( + Self::get_all_records(&connection, object_store, key_range) + .map(|records| { + records + .into_iter() + .map(|(key, data)| IndexedDBRecord { + key: bincode::deserialize(&key).unwrap(), + primary_key: bincode::deserialize(&key).unwrap(), + value: data, + }) + .collect() + }) + .map_err(|e| BackendError::DbErr(format!("{:?}", e))), + ); + }, AsyncOperation::ReadWrite(AsyncReadWriteOperation::Clear(sender)) => { let Ok(object_store) = process_object_store(object_store, &sender) else { continue; diff --git a/components/script/dom/idbcursor.rs b/components/script/dom/idbcursor.rs index c3ae488298b..39a3cb2e963 100644 --- a/components/script/dom/idbcursor.rs +++ b/components/script/dom/idbcursor.rs @@ -8,15 +8,18 @@ use dom_struct::dom_struct; use js::jsapi::Heap; use js::jsval::{JSVal, UndefinedValue}; use js::rust::MutableHandleValue; -use net_traits::indexeddb_thread::{IndexedDBKeyRange, IndexedDBKeyType}; +use net_traits::indexeddb_thread::{IndexedDBKeyRange, IndexedDBKeyType, IndexedDBRecord}; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::IDBCursorBinding::{ IDBCursorDirection, IDBCursorMethods, }; use crate::dom::bindings::codegen::UnionTypes::IDBObjectStoreOrIDBIndex; +use crate::dom::bindings::error::Error; +use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; +use crate::dom::bindings::structuredclone; use crate::dom::globalscope::GlobalScope; use crate::dom::idbindex::IDBIndex; use crate::dom::idbobjectstore::IDBObjectStore; @@ -93,7 +96,6 @@ impl IDBCursor { } } - #[expect(unused)] #[cfg_attr(crown, allow(crown::unrooted_must_root))] #[allow(clippy::too_many_arguments)] pub(crate) fn new( @@ -120,6 +122,22 @@ impl IDBCursor { ) } + fn set_position(&self, position: Option) { + *self.position.borrow_mut() = position; + } + + fn set_key(&self, key: Option) { + *self.key.borrow_mut() = key; + } + + fn set_object_store_position(&self, object_store_position: Option) { + *self.object_store_position.borrow_mut() = object_store_position; + } + + pub(crate) fn set_request(&self, request: &IDBRequest) { + self.request.set(Some(request)); + } + pub(crate) fn value(&self, mut out: MutableHandleValue) { out.set(self.value.get()); } @@ -174,3 +192,306 @@ impl IDBCursorMethods for IDBCursor { .expect("IDBCursor.request should be set when cursor is opened") } } + +/// A struct containing parameters for +/// +#[derive(Clone)] +pub(crate) struct IterationParam { + pub(crate) cursor: Trusted, + pub(crate) key: Option, + pub(crate) primary_key: Option, + pub(crate) count: Option, +} + +/// +/// +/// NOTE: Be cautious: this part of the specification seems to assume the cursor’s source is an +/// index. Therefore, +/// "record’s key" means the key of the record, +/// "record’s value" means the primary key of the record, and +/// "record’s referenced value" means the value of the record. +pub(crate) fn iterate_cursor( + global: &GlobalScope, + cx: SafeJSContext, + param: &IterationParam, + records: Vec, +) -> Result>, Error> { + // Unpack IterationParam + let cursor = param.cursor.root(); + let key = param.key.clone(); + let primary_key = param.primary_key.clone(); + let count = param.count; + + // Step 1. Let source be cursor’s source. + let source = &cursor.source; + + // Step 2. Let direction be cursor’s direction. + let direction = cursor.direction; + + // Step 3. Assert: if primaryKey is given, source is an index and direction is "next" or "prev". + if primary_key.is_some() { + assert!(matches!(source, ObjectStoreOrIndex::Index(..))); + assert!(matches!( + direction, + IDBCursorDirection::Next | IDBCursorDirection::Prev + )); + } + + // Step 4. Let records be the list of records in source. + // NOTE: It is given as a function parameter. + + // Step 5. Let range be cursor’s range. + let range = &cursor.range; + + // Step 6. Let position be cursor’s position. + let mut position = cursor.position.borrow().clone(); + + // Step 7. Let object store position be cursor’s object store position. + let object_store_position = cursor.object_store_position.borrow().clone(); + + // Step 8. If count is not given, let count be 1. + let mut count = count.unwrap_or(1); + + let mut found_record: Option<&IndexedDBRecord> = None; + + // Step 9. While count is greater than 0: + while count > 0 { + // Step 9.1. Switch on direction: + found_record = match direction { + // "next" + IDBCursorDirection::Next => records.iter().find(|record| { + // Let found record be the first record in records which satisfy all of the + // following requirements: + + // If key is defined, the record’s key is greater than or equal to key. + let requirement1 = || match &key { + Some(key) => &record.key >= key, + None => true, + }; + + // If primaryKey is defined, the record’s key is equal to key and the record’s + // value is greater than or equal to primaryKey, or the record’s key is greater + // than key. + let requirement2 = || match &primary_key { + Some(primary_key) => key.as_ref().is_some_and(|key| { + (&record.key == key && &record.primary_key >= primary_key) || + &record.key > key + }), + _ => true, + }; + + // If position is defined, and source is an object store, the record’s key is + // greater than position. + let requirement3 = || match (&position, source) { + (Some(position), ObjectStoreOrIndex::ObjectStore(_)) => &record.key > position, + _ => true, + }; + + // If position is defined, and source is an index, the record’s key is equal to + // position and the record’s value is greater than object store position or the + // record’s key is greater than position. + let requirement4 = || match (&position, source) { + (Some(position), ObjectStoreOrIndex::Index(_)) => { + (&record.key == position && + object_store_position.as_ref().is_some_and( + |object_store_position| &record.primary_key > object_store_position, + )) || + &record.key > position + }, + _ => true, + }; + + // The record’s key is in range. + let requirement5 = || range.contains(&record.key); + + // NOTE: Use closures here for lazy computation on requirements. + requirement1() && + requirement2() && + requirement3() && + requirement4() && + requirement5() + }), + // "nextunique" + IDBCursorDirection::Nextunique => records.iter().find(|record| { + // Let found record be the first record in records which satisfy all of the + // following requirements: + + // If key is defined, the record’s key is greater than or equal to key. + let requirement1 = || match &key { + Some(key) => &record.key >= key, + None => true, + }; + + // If position is defined, the record’s key is greater than position. + let requirement2 = || match &position { + Some(position) => &record.key > position, + None => true, + }; + + // The record’s key is in range. + let requirement3 = || range.contains(&record.key); + + // NOTE: Use closures here for lazy computation on requirements. + requirement1() && requirement2() && requirement3() + }), + // "prev" + IDBCursorDirection::Prev => { + records.iter().rev().find(|&record| { + // Let found record be the last record in records which satisfy all of the + // following requirements: + + // If key is defined, the record’s key is less than or equal to key. + let requirement1 = || match &key { + Some(key) => &record.key <= key, + None => true, + }; + + // If primaryKey is defined, the record’s key is equal to key and the record’s + // value is less than or equal to primaryKey, or the record’s key is less than + // key. + let requirement2 = || match &primary_key { + Some(primary_key) => key.as_ref().is_some_and(|key| { + (&record.key == key && &record.primary_key <= primary_key) || + &record.key < key + }), + _ => true, + }; + + // If position is defined, and source is an object store, the record’s key is + // less than position. + let requirement3 = || match (&position, source) { + (Some(position), ObjectStoreOrIndex::ObjectStore(_)) => { + &record.key < position + }, + _ => true, + }; + + // If position is defined, and source is an index, the record’s key is equal to + // position and the record’s value is less than object store position or the + // record’s key is less than position. + let requirement4 = || match (&position, source) { + (Some(position), ObjectStoreOrIndex::Index(_)) => { + (&record.key == position && + object_store_position.as_ref().is_some_and( + |object_store_position| { + &record.primary_key < object_store_position + }, + )) || + &record.key < position + }, + _ => true, + }; + + // The record’s key is in range. + let requirement5 = || range.contains(&record.key); + + // NOTE: Use closures here for lazy computation on requirements. + requirement1() && + requirement2() && + requirement3() && + requirement4() && + requirement5() + }) + }, + // "prevunique" + IDBCursorDirection::Prevunique => records + .iter() + .rev() + .find(|&record| { + // Let temp record be the last record in records which satisfy all of the + // following requirements: + + // If key is defined, the record’s key is less than or equal to key. + let requirement1 = || match &key { + Some(key) => &record.key <= key, + None => true, + }; + + // If position is defined, the record’s key is less than position. + let requirement2 = || match &position { + Some(position) => &record.key < position, + None => true, + }; + + // The record’s key is in range. + let requirement3 = || range.contains(&record.key); + + // NOTE: Use closures here for lazy computation on requirements. + requirement1() && requirement2() && requirement3() + }) + // If temp record is defined, let found record be the first record in records + // whose key is equal to temp record’s key. + .map(|temp_record| { + records + .iter() + .find(|&record| record.key == temp_record.key) + .expect( + "Record with key equal to temp record's key should exist in records", + ) + }), + }; + + match found_record { + // Step 9.2. If found record is not defined, then: + None => { + // Step 9.2.1. Set cursor’s key to undefined. + cursor.set_key(None); + + // Step 9.2.2. If source is an index, set cursor’s object store position to undefined. + if matches!(source, ObjectStoreOrIndex::Index(_)) { + cursor.set_object_store_position(None); + } + + // Step 9.2.3. If cursor’s key only flag is unset, set cursor’s value to undefined. + if !cursor.key_only { + cursor.value.set(UndefinedValue()); + } + + // Step 9.2.4. Return null. + return Ok(None); + }, + Some(found_record) => { + // Step 9.3. Let position be found record’s key. + position = Some(found_record.key.clone()); + + // Step 9.4. If source is an index, let object store position be found record’s value. + if matches!(source, ObjectStoreOrIndex::Index(_)) { + cursor.set_object_store_position(Some(found_record.primary_key.clone())); + } + + // Step 9.5. Decrease count by 1. + count -= 1; + }, + } + } + let found_record = + found_record.expect("The while loop above guarantees found_record is defined"); + + // Step 10. Set cursor’s position to position. + cursor.set_position(position); + + // Step 11. If source is an index, set cursor’s object store position to object store position. + if let ObjectStoreOrIndex::Index(_) = source { + cursor.set_object_store_position(object_store_position); + } + + // Step 12. Set cursor’s key to found record’s key. + cursor.set_key(Some(found_record.key.clone())); + + // Step 13. If cursor’s key only flag is unset, then: + if !cursor.key_only { + // Step 13.1. Let serialized be found record’s referenced value. + // Step 13.2. Set cursor’s value to ! StructuredDeserialize(serialized, targetRealm) + rooted!(in(*cx) let mut new_cursor_value = UndefinedValue()); + bincode::deserialize(&found_record.value) + .map_err(|_| Error::Data) + .and_then(|data| structuredclone::read(global, data, new_cursor_value.handle_mut()))?; + cursor.value.set(new_cursor_value.get()); + } + + // Step 14. Set cursor’s got value flag. + cursor.got_value.set(true); + + // Step 15. Return cursor. + Ok(Some(cursor)) +} diff --git a/components/script/dom/idbcursorwithvalue.rs b/components/script/dom/idbcursorwithvalue.rs index 818a974402f..ab89f8391fb 100644 --- a/components/script/dom/idbcursorwithvalue.rs +++ b/components/script/dom/idbcursorwithvalue.rs @@ -42,7 +42,6 @@ impl IDBCursorWithValue { } } - #[expect(unused)] #[cfg_attr(crown, allow(crown::unrooted_must_root))] #[allow(clippy::too_many_arguments)] pub(crate) fn new( diff --git a/components/script/dom/idbobjectstore.rs b/components/script/dom/idbobjectstore.rs index f7563ace040..7188344b443 100644 --- a/components/script/dom/idbobjectstore.rs +++ b/components/script/dom/idbobjectstore.rs @@ -16,6 +16,7 @@ use script_bindings::conversions::SafeToJSValConvertible; use script_bindings::error::ErrorResult; use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::IDBCursorBinding::IDBCursorDirection; use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters; use crate::dom::bindings::codegen::Bindings::IDBObjectStoreBinding::IDBObjectStoreMethods; use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{ @@ -24,12 +25,15 @@ use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{ // We need to alias this name, otherwise test-tidy complains at &String reference. use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence; use crate::dom::bindings::error::{Error, Fallible}; +use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::bindings::structuredclone; use crate::dom::domstringlist::DOMStringList; use crate::dom::globalscope::GlobalScope; +use crate::dom::idbcursor::{IDBCursor, IterationParam, ObjectStoreOrIndex}; +use crate::dom::idbcursorwithvalue::IDBCursorWithValue; use crate::dom::idbrequest::IDBRequest; use crate::dom::idbtransaction::IDBTransaction; use crate::indexed_db::{ @@ -248,9 +252,91 @@ impl IDBObjectStore { }), receiver, None, + None, can_gc, ) } + + /// + /// + fn open_cursor( + &self, + cx: SafeJSContext, + query: HandleValue, + direction: IDBCursorDirection, + key_only: bool, + can_gc: CanGc, + ) -> Fallible> { + // Step 1. Let transaction be this object store handle's transaction. + // Step 2. Let store be this object store handle's object store. + + // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException. + self.verify_not_deleted()?; + + // Step 4. If transaction is not active, throw a "TransactionInactiveError" DOMException. + self.check_transaction_active()?; + + // Step 5. Let range be the result of running the steps to convert a value to a key range + // with query. Rethrow any exceptions. + // + // The query parameter may be a key or an IDBKeyRange to use as the cursor's range. If null + // or not given, an unbounded key range is used. + let range = convert_value_to_key_range(cx, query, Some(false))?; + + // Step 6. Let cursor be a new cursor with transaction set to transaction, an undefined + // position, direction set to direction, got value flag unset, and undefined key and value. + // The source of cursor is store. The range of cursor is range. + // + // NOTE: A cursor that has the key only flag unset implements the IDBCursorWithValue + // interface as well. + let cursor = if key_only { + IDBCursor::new( + &self.global(), + &self.transaction, + direction, + false, + ObjectStoreOrIndex::ObjectStore(Dom::from_ref(self)), + range.clone(), + key_only, + can_gc, + ) + } else { + DomRoot::upcast(IDBCursorWithValue::new( + &self.global(), + &self.transaction, + direction, + false, + ObjectStoreOrIndex::ObjectStore(Dom::from_ref(self)), + range.clone(), + key_only, + can_gc, + )) + }; + + // Step 7. Run the steps to asynchronously execute a request and return the IDBRequest + // created by these steps. The steps are run with this object store handle as source and + // the steps to iterate a cursor as operation, using the current Realm as targetRealm, and + // cursor. + let iteration_param = IterationParam { + cursor: Trusted::new(&cursor), + key: None, + primary_key: None, + count: None, + }; + let (sender, receiver) = indexed_db::create_channel(self.global()); + IDBRequest::execute_async( + self, + AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Iterate { + sender, + key_range: range, + }), + receiver, + None, + Some(iteration_param), + can_gc, + ) + .inspect(|request| cursor.set_request(request)) + } } impl IDBObjectStoreMethods for IDBObjectStore { @@ -297,6 +383,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { AsyncOperation::ReadWrite(AsyncReadWriteOperation::RemoveItem { sender, key: q }), receiver, None, + None, CanGc::note(), ) }) @@ -322,6 +409,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { AsyncOperation::ReadWrite(AsyncReadWriteOperation::Clear(sender)), receiver, None, + None, CanGc::note(), ) } @@ -351,6 +439,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { }), receiver, None, + None, CanGc::note(), ) }) @@ -382,6 +471,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { }), receiver, None, + None, CanGc::note(), ) }) @@ -419,6 +509,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { }), receiver, None, + None, CanGc::note(), ) }) @@ -456,6 +547,7 @@ impl IDBObjectStoreMethods for IDBObjectStore { }), receiver, None, + None, CanGc::note(), ) }) @@ -486,11 +578,32 @@ impl IDBObjectStoreMethods for IDBObjectStore { }), receiver, None, + None, CanGc::note(), ) }) } + /// + fn OpenCursor( + &self, + cx: SafeJSContext, + query: HandleValue, + direction: IDBCursorDirection, + ) -> Fallible> { + self.open_cursor(cx, query, direction, false, CanGc::note()) + } + + /// + fn OpenKeyCursor( + &self, + cx: SafeJSContext, + query: HandleValue, + direction: IDBCursorDirection, + ) -> Fallible> { + self.open_cursor(cx, query, direction, true, CanGc::note()) + } + /// fn Name(&self) -> DOMString { self.name.borrow().clone() diff --git a/components/script/dom/idbrequest.rs b/components/script/dom/idbrequest.rs index 9c051cbd0ba..0890588b307 100644 --- a/components/script/dom/idbrequest.rs +++ b/components/script/dom/idbrequest.rs @@ -8,12 +8,12 @@ use std::iter::repeat_n; use dom_struct::dom_struct; use ipc_channel::router::ROUTER; use js::jsapi::Heap; -use js::jsval::{DoubleValue, JSVal, UndefinedValue}; +use js::jsval::{DoubleValue, JSVal, ObjectValue, UndefinedValue}; use js::rust::HandleValue; use net_traits::IpcSend; use net_traits::indexeddb_thread::{ - AsyncOperation, BackendError, BackendResult, IndexedDBKeyType, IndexedDBThreadMsg, - IndexedDBTxnMode, PutItemResult, + AsyncOperation, AsyncReadOnlyOperation, BackendError, BackendResult, IndexedDBKeyType, + IndexedDBRecord, IndexedDBThreadMsg, IndexedDBTxnMode, PutItemResult, }; use profile_traits::ipc::IpcReceiver; use script_bindings::conversions::SafeToJSValConvertible; @@ -27,13 +27,15 @@ use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::IDBTransacti use crate::dom::bindings::error::{Error, Fallible, create_dom_exception}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; -use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; +use crate::dom::bindings::reflector::{DomGlobal, DomObject, reflect_dom_object}; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::structuredclone; use crate::dom::domexception::DOMException; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; +use crate::dom::idbcursor::{IterationParam, iterate_cursor}; +use crate::dom::idbcursorwithvalue::IDBCursorWithValue; use crate::dom::idbobjectstore::IDBObjectStore; use crate::dom::idbtransaction::IDBTransaction; use crate::indexed_db::key_type_to_jsval; @@ -43,6 +45,7 @@ use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; #[derive(Clone)] struct RequestListener { request: Trusted, + iteration_param: Option, } pub enum IdbResult { @@ -51,6 +54,7 @@ pub enum IdbResult { Value(Vec), Values(Vec>), Count(u64), + Iterate(Vec), Error(Error), None, } @@ -88,6 +92,12 @@ impl From for IdbResult { } } +impl From> for IdbResult { + fn from(value: Vec) -> Self { + Self::Iterate(value) + } +} + impl From<()> for IdbResult { fn from(_value: ()) -> Self { Self::None @@ -171,6 +181,33 @@ impl RequestListener { IdbResult::Count(count) => { answer.handle_mut().set(DoubleValue(count as f64)); }, + IdbResult::Iterate(records) => { + let param = self.iteration_param.as_ref().expect( + "iteration_param must be provided by IDBRequest::execute_async for Iterate", + ); + let cursor = match iterate_cursor(&global, cx, param, records) { + Ok(cursor) => cursor, + Err(e) => { + warn!("Error reading structuredclone data"); + Self::handle_async_request_error(&global, cx, request, e); + return; + }, + }; + if let Some(cursor) = cursor { + match cursor.downcast::() { + Some(cursor_with_value) => { + answer.handle_mut().set(ObjectValue( + *cursor_with_value.reflector().get_jsobject(), + )); + }, + None => { + answer + .handle_mut() + .set(ObjectValue(*cursor.reflector().get_jsobject())); + }, + } + } + }, IdbResult::None => { // no-op }, @@ -313,6 +350,7 @@ impl IDBRequest { operation: AsyncOperation, receiver: IpcReceiver>, request: Option>, + iteration_param: Option, can_gc: CanGc, ) -> Fallible> where @@ -346,8 +384,24 @@ impl IDBRequest { IDBTransactionMode::Versionchange => IndexedDBTxnMode::Versionchange, }; + if matches!( + operation, + AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Iterate { .. }) + ) { + assert!( + iteration_param.is_some(), + "iteration_param must be provided for Iterate" + ); + } else { + assert!( + iteration_param.is_none(), + "iteration_param should not be provided for operation other than Iterate" + ); + } + let response_listener = RequestListener { request: Trusted::new(&request), + iteration_param, }; let task_source = global diff --git a/components/script/indexed_db.rs b/components/script/indexed_db.rs index b73ea8203ad..60d58884b6f 100644 --- a/components/script/indexed_db.rs +++ b/components/script/indexed_db.rs @@ -222,15 +222,14 @@ pub fn convert_value_to_key( Ok(ConversionResult::Invalid) } -// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key-range +/// #[allow(unsafe_code)] pub fn convert_value_to_key_range( cx: SafeJSContext, input: HandleValue, null_disallowed: Option, ) -> Result { - let null_disallowed = null_disallowed.unwrap_or(false); - // Step 1. + // Step 1. If value is a key range, return value. if input.is_object() { rooted!(in(*cx) let object = input.to_object()); unsafe { @@ -240,11 +239,30 @@ pub fn convert_value_to_key_range( } } } - // Step 2. - if (input.get().is_undefined() || input.get().is_null()) && null_disallowed { - return Err(Error::Data); + + // Step 2. If value is undefined or is null, then throw a "DataError" DOMException if null + // disallowed flag is set, or return an unbounded key range otherwise. + if input.get().is_undefined() || input.get().is_null() { + if null_disallowed.is_some_and(|flag| flag) { + return Err(Error::Data); + } else { + return Ok(IndexedDBKeyRange { + lower: None, + upper: None, + lower_open: Default::default(), + upper_open: Default::default(), + }); + } } - let key = convert_value_to_key(cx, input, None)?.into_result()?; + + // Step 3. Let key be the result of running the steps to convert a value to a key with value. + // Rethrow any exceptions. + let key = convert_value_to_key(cx, input, None)?; + + // Step 4. If key is invalid, throw a "DataError" DOMException. + let key = key.into_result()?; + + // Step 5. Return a key range containing only key. Ok(IndexedDBKeyRange::only(key)) } diff --git a/components/script_bindings/webidls/IDBObjectStore.webidl b/components/script_bindings/webidls/IDBObjectStore.webidl index 9ad3541ef52..8d1312fa8ff 100644 --- a/components/script_bindings/webidls/IDBObjectStore.webidl +++ b/components/script_bindings/webidls/IDBObjectStore.webidl @@ -28,10 +28,10 @@ interface IDBObjectStore { optional [EnforceRange] unsigned long count); [NewObject, Throws] IDBRequest count(optional any query); - // [NewObject] IDBRequest openCursor(optional any query, - // optional IDBCursorDirection direction = "next"); - // [NewObject] IDBRequest openKeyCursor(optional any query, - // optional IDBCursorDirection direction = "next"); + [NewObject, Throws] IDBRequest openCursor(optional any query, + optional IDBCursorDirection direction = "next"); + [NewObject, Throws] IDBRequest openKeyCursor(optional any query, + optional IDBCursorDirection direction = "next"); // IDBIndex index(DOMString name); diff --git a/components/shared/net/indexeddb_thread.rs b/components/shared/net/indexeddb_thread.rs index 8d5d721b536..5f35b051d2c 100644 --- a/components/shared/net/indexeddb_thread.rs +++ b/components/shared/net/indexeddb_thread.rs @@ -233,6 +233,14 @@ impl IndexedDBKeyRange { } } +/// +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct IndexedDBRecord { + pub key: IndexedDBKeyType, + pub primary_key: IndexedDBKeyType, + pub value: Vec, +} + #[test] fn test_as_singleton() { let key = IndexedDBKeyType::Number(1.0); @@ -281,6 +289,10 @@ pub enum AsyncReadOnlyOperation { sender: IpcSender>, key_range: IndexedDBKeyRange, }, + Iterate { + sender: IpcSender>>, + key_range: IndexedDBKeyRange, + }, } #[derive(Debug, Deserialize, Serialize)] diff --git a/tests/wpt/meta/IndexedDB/idbcursor-advance-exception-order.any.js.ini b/tests/wpt/meta/IndexedDB/idbcursor-advance-exception-order.any.js.ini index 330de32cb28..6e10ed113d2 100644 --- a/tests/wpt/meta/IndexedDB/idbcursor-advance-exception-order.any.js.ini +++ b/tests/wpt/meta/IndexedDB/idbcursor-advance-exception-order.any.js.ini @@ -1,7 +1,4 @@ [idbcursor-advance-exception-order.any.worker.html] - [IDBCursor.advance exception order: TypeError vs. TransactionInactiveError] - expected: FAIL - [IDBCursor.advance exception order: TransactionInactiveError vs. InvalidStateError #1] expected: FAIL @@ -13,9 +10,6 @@ expected: ERROR [idbcursor-advance-exception-order.any.html] - [IDBCursor.advance exception order: TypeError vs. TransactionInactiveError] - expected: FAIL - [IDBCursor.advance exception order: TransactionInactiveError vs. InvalidStateError #1] expected: FAIL diff --git a/tests/wpt/meta/IndexedDB/idbcursor-direction.any.js.ini b/tests/wpt/meta/IndexedDB/idbcursor-direction.any.js.ini index 46a53ba31e9..ba47513afff 100644 --- a/tests/wpt/meta/IndexedDB/idbcursor-direction.any.js.ini +++ b/tests/wpt/meta/IndexedDB/idbcursor-direction.any.js.ini @@ -1,39 +1,9 @@ [idbcursor-direction.any.worker.html] - [IDBCursor.direction - undefined] - expected: FAIL - - [IDBCursor.direction - next] - expected: FAIL - - [IDBCursor.direction - prev] - expected: FAIL - - [IDBCursor.direction - nextunique] - expected: FAIL - - [IDBCursor.direction - prevunique] - expected: FAIL - [idbcursor-direction.any.serviceworker.html] expected: ERROR [idbcursor-direction.any.html] - [IDBCursor.direction - undefined] - expected: FAIL - - [IDBCursor.direction - next] - expected: FAIL - - [IDBCursor.direction - prev] - expected: FAIL - - [IDBCursor.direction - nextunique] - expected: FAIL - - [IDBCursor.direction - prevunique] - expected: FAIL - [idbcursor-direction.any.sharedworker.html] expected: ERROR diff --git a/tests/wpt/meta/IndexedDB/idbcursor-key.any.js.ini b/tests/wpt/meta/IndexedDB/idbcursor-key.any.js.ini index 55628adf510..883870a4eea 100644 --- a/tests/wpt/meta/IndexedDB/idbcursor-key.any.js.ini +++ b/tests/wpt/meta/IndexedDB/idbcursor-key.any.js.ini @@ -2,23 +2,11 @@ expected: ERROR [idbcursor-key.any.worker.html] - [IDBCursor.key] - expected: FAIL - - [IDBCursor.key 1] - expected: FAIL - [IDBCursor.key 2] expected: FAIL [idbcursor-key.any.html] - [IDBCursor.key] - expected: FAIL - - [IDBCursor.key 1] - expected: FAIL - [IDBCursor.key 2] expected: FAIL diff --git a/tests/wpt/meta/IndexedDB/idbcursor_advance_objectstore.any.js.ini b/tests/wpt/meta/IndexedDB/idbcursor_advance_objectstore.any.js.ini index 7b17df9461a..08ad4f8806f 100644 --- a/tests/wpt/meta/IndexedDB/idbcursor_advance_objectstore.any.js.ini +++ b/tests/wpt/meta/IndexedDB/idbcursor_advance_objectstore.any.js.ini @@ -5,9 +5,6 @@ [object store - iterate cursor number of times specified by count] expected: FAIL - [Calling advance() with count argument 0 should throw TypeError.] - expected: FAIL - [Calling advance() should throws an exception TransactionInactiveError when the transaction is not active] expected: FAIL @@ -22,9 +19,6 @@ [object store - iterate cursor number of times specified by count] expected: FAIL - [Calling advance() with count argument 0 should throw TypeError.] - expected: FAIL - [Calling advance() should throws an exception TransactionInactiveError when the transaction is not active] expected: FAIL diff --git a/tests/wpt/meta/IndexedDB/idbcursor_update_objectstore.any.js.ini b/tests/wpt/meta/IndexedDB/idbcursor_update_objectstore.any.js.ini index 3730c9c3bcb..1357201b076 100644 --- a/tests/wpt/meta/IndexedDB/idbcursor_update_objectstore.any.js.ini +++ b/tests/wpt/meta/IndexedDB/idbcursor_update_objectstore.any.js.ini @@ -20,9 +20,6 @@ [Throw DataCloneError] expected: FAIL - [No argument] - expected: FAIL - [Throw DataError] expected: FAIL @@ -49,9 +46,6 @@ [Throw DataCloneError] expected: FAIL - [No argument] - expected: FAIL - [Throw DataError] expected: FAIL diff --git a/tests/wpt/meta/IndexedDB/idbobjectstore-cross-realm-methods.html.ini b/tests/wpt/meta/IndexedDB/idbobjectstore-cross-realm-methods.html.ini index 44e6fabbe39..e96cfa3239e 100644 --- a/tests/wpt/meta/IndexedDB/idbobjectstore-cross-realm-methods.html.ini +++ b/tests/wpt/meta/IndexedDB/idbobjectstore-cross-realm-methods.html.ini @@ -5,18 +5,3 @@ [Cross-realm IDBObjectStore::add() method from detached