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,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))
}