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 <kingsley@kkoyung.dev>
This commit is contained in:
Kingsley Yung 2025-09-13 00:54:07 +08:00 committed by GitHub
parent 1f63116bdd
commit 250c4cda00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 580 additions and 279 deletions

View file

@ -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<(Vec<u8>, Vec<u8>)>, 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;

View file

@ -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<IndexedDBKeyType>) {
*self.position.borrow_mut() = position;
}
fn set_key(&self, key: Option<IndexedDBKeyType>) {
*self.key.borrow_mut() = key;
}
fn set_object_store_position(&self, object_store_position: Option<IndexedDBKeyType>) {
*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<crate::DomTypeHolder> for IDBCursor {
.expect("IDBCursor.request should be set when cursor is opened")
}
}
/// A struct containing parameters for
/// <https://www.w3.org/TR/IndexedDB-2/#iterate-a-cursor>
#[derive(Clone)]
pub(crate) struct IterationParam {
pub(crate) cursor: Trusted<IDBCursor>,
pub(crate) key: Option<IndexedDBKeyType>,
pub(crate) primary_key: Option<IndexedDBKeyType>,
pub(crate) count: Option<u32>,
}
/// <https://www.w3.org/TR/IndexedDB-2/#iterate-a-cursor>
///
/// NOTE: Be cautious: this part of the specification seems to assume the cursors source is an
/// index. Therefore,
/// "records key" means the key of the record,
/// "records value" means the primary key of the record, and
/// "records referenced value" means the value of the record.
pub(crate) fn iterate_cursor(
global: &GlobalScope,
cx: SafeJSContext,
param: &IterationParam,
records: Vec<IndexedDBRecord>,
) -> Result<Option<DomRoot<IDBCursor>>, 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 cursors source.
let source = &cursor.source;
// Step 2. Let direction be cursors 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 cursors range.
let range = &cursor.range;
// Step 6. Let position be cursors position.
let mut position = cursor.position.borrow().clone();
// Step 7. Let object store position be cursors 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 records key is greater than or equal to key.
let requirement1 = || match &key {
Some(key) => &record.key >= key,
None => true,
};
// If primaryKey is defined, the records key is equal to key and the records
// value is greater than or equal to primaryKey, or the records 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 records 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 records key is equal to
// position and the records value is greater than object store position or the
// records 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 records 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 records key is greater than or equal to key.
let requirement1 = || match &key {
Some(key) => &record.key >= key,
None => true,
};
// If position is defined, the records key is greater than position.
let requirement2 = || match &position {
Some(position) => &record.key > position,
None => true,
};
// The records 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 records key is less than or equal to key.
let requirement1 = || match &key {
Some(key) => &record.key <= key,
None => true,
};
// If primaryKey is defined, the records key is equal to key and the records
// value is less than or equal to primaryKey, or the records 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 records 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 records key is equal to
// position and the records value is less than object store position or the
// records 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 records 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 records key is less than or equal to key.
let requirement1 = || match &key {
Some(key) => &record.key <= key,
None => true,
};
// If position is defined, the records key is less than position.
let requirement2 = || match &position {
Some(position) => &record.key < position,
None => true,
};
// The records 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 records 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 cursors key to undefined.
cursor.set_key(None);
// Step 9.2.2. If source is an index, set cursors object store position to undefined.
if matches!(source, ObjectStoreOrIndex::Index(_)) {
cursor.set_object_store_position(None);
}
// Step 9.2.3. If cursors key only flag is unset, set cursors 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 records key.
position = Some(found_record.key.clone());
// Step 9.4. If source is an index, let object store position be found records 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 cursors position to position.
cursor.set_position(position);
// Step 11. If source is an index, set cursors object store position to object store position.
if let ObjectStoreOrIndex::Index(_) = source {
cursor.set_object_store_position(object_store_position);
}
// Step 12. Set cursors key to found records key.
cursor.set_key(Some(found_record.key.clone()));
// Step 13. If cursors key only flag is unset, then:
if !cursor.key_only {
// Step 13.1. Let serialized be found records referenced value.
// Step 13.2. Set cursors 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 cursors got value flag.
cursor.got_value.set(true);
// Step 15. Return cursor.
Ok(Some(cursor))
}

View file

@ -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(

View file

@ -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,
)
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-opencursor>
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-openkeycursor>
fn open_cursor(
&self,
cx: SafeJSContext,
query: HandleValue,
direction: IDBCursorDirection,
key_only: bool,
can_gc: CanGc,
) -> Fallible<DomRoot<IDBRequest>> {
// 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<crate::DomTypeHolder> for IDBObjectStore {
@ -297,6 +383,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
AsyncOperation::ReadWrite(AsyncReadWriteOperation::RemoveItem { sender, key: q }),
receiver,
None,
None,
CanGc::note(),
)
})
@ -322,6 +409,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
AsyncOperation::ReadWrite(AsyncReadWriteOperation::Clear(sender)),
receiver,
None,
None,
CanGc::note(),
)
}
@ -351,6 +439,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}),
receiver,
None,
None,
CanGc::note(),
)
})
@ -382,6 +471,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}),
receiver,
None,
None,
CanGc::note(),
)
})
@ -419,6 +509,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}),
receiver,
None,
None,
CanGc::note(),
)
})
@ -456,6 +547,7 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}),
receiver,
None,
None,
CanGc::note(),
)
})
@ -486,11 +578,32 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}),
receiver,
None,
None,
CanGc::note(),
)
})
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-opencursor>
fn OpenCursor(
&self,
cx: SafeJSContext,
query: HandleValue,
direction: IDBCursorDirection,
) -> Fallible<DomRoot<IDBRequest>> {
self.open_cursor(cx, query, direction, false, CanGc::note())
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-openkeycursor>
fn OpenKeyCursor(
&self,
cx: SafeJSContext,
query: HandleValue,
direction: IDBCursorDirection,
) -> Fallible<DomRoot<IDBRequest>> {
self.open_cursor(cx, query, direction, true, CanGc::note())
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-name>
fn Name(&self) -> DOMString {
self.name.borrow().clone()

View file

@ -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<IDBRequest>,
iteration_param: Option<IterationParam>,
}
pub enum IdbResult {
@ -51,6 +54,7 @@ pub enum IdbResult {
Value(Vec<u8>),
Values(Vec<Vec<u8>>),
Count(u64),
Iterate(Vec<IndexedDBRecord>),
Error(Error),
None,
}
@ -88,6 +92,12 @@ impl From<PutItemResult> for IdbResult {
}
}
impl From<Vec<IndexedDBRecord>> for IdbResult {
fn from(value: Vec<IndexedDBRecord>) -> 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::<IDBCursorWithValue>() {
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<BackendResult<T>>,
request: Option<DomRoot<IDBRequest>>,
iteration_param: Option<IterationParam>,
can_gc: CanGc,
) -> Fallible<DomRoot<IDBRequest>>
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

View file

@ -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
/// <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<bool>,
) -> Result<IndexedDBKeyRange, Error> {
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))
}

View file

@ -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);

View file

@ -233,6 +233,14 @@ impl IndexedDBKeyRange {
}
}
/// <https://w3c.github.io/IndexedDB/#record-snapshot>
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct IndexedDBRecord {
pub key: IndexedDBKeyType,
pub primary_key: IndexedDBKeyType,
pub value: Vec<u8>,
}
#[test]
fn test_as_singleton() {
let key = IndexedDBKeyType::Number(1.0);
@ -281,6 +289,10 @@ pub enum AsyncReadOnlyOperation {
sender: IpcSender<BackendResult<u64>>,
key_range: IndexedDBKeyRange,
},
Iterate {
sender: IpcSender<BackendResult<Vec<IndexedDBRecord>>>,
key_range: IndexedDBKeyRange,
},
}
#[derive(Debug, Deserialize, Serialize)]