servo/components/script/dom/idbcursor.rs
Kingsley Yung 250c4cda00
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>
2025-09-12 16:54:07 +00:00

497 lines
19 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
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, 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;
use crate::dom::idbrequest::IDBRequest;
use crate::dom::idbtransaction::IDBTransaction;
use crate::indexed_db::key_type_to_jsval;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
#[derive(JSTraceable, MallocSizeOf)]
#[expect(unused)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) enum ObjectStoreOrIndex {
ObjectStore(Dom<IDBObjectStore>),
Index(Dom<IDBIndex>),
}
#[dom_struct]
pub(crate) struct IDBCursor {
reflector_: Reflector,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-transaction>
transaction: Dom<IDBTransaction>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-range>
#[no_trace]
range: IndexedDBKeyRange,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-source>
source: ObjectStoreOrIndex,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-direction>
direction: IDBCursorDirection,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-position>
#[no_trace]
position: DomRefCell<Option<IndexedDBKeyType>>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-key>
#[no_trace]
key: DomRefCell<Option<IndexedDBKeyType>>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-value>
#[ignore_malloc_size_of = "mozjs"]
value: Heap<JSVal>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-got-value-flag>
got_value: Cell<bool>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-object-store-position>
#[no_trace]
object_store_position: DomRefCell<Option<IndexedDBKeyType>>,
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-key-only-flag>
key_only: bool,
/// <https://w3c.github.io/IndexedDB/#cursor-request>
request: MutNullableDom<IDBRequest>,
}
impl IDBCursor {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_inherited(
transaction: &IDBTransaction,
direction: IDBCursorDirection,
got_value: bool,
source: ObjectStoreOrIndex,
range: IndexedDBKeyRange,
key_only: bool,
) -> IDBCursor {
IDBCursor {
reflector_: Reflector::new(),
transaction: Dom::from_ref(transaction),
range,
source,
direction,
position: DomRefCell::new(None),
key: DomRefCell::new(None),
value: Heap::default(),
got_value: Cell::new(got_value),
object_store_position: DomRefCell::new(None),
key_only,
request: Default::default(),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
global: &GlobalScope,
transaction: &IDBTransaction,
direction: IDBCursorDirection,
got_value: bool,
source: ObjectStoreOrIndex,
range: IndexedDBKeyRange,
key_only: bool,
can_gc: CanGc,
) -> DomRoot<IDBCursor> {
reflect_dom_object(
Box::new(IDBCursor::new_inherited(
transaction,
direction,
got_value,
source,
range,
key_only,
)),
global,
can_gc,
)
}
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());
}
/// <https://www.w3.org/TR/IndexedDB-2/#cursor-effective-key>
pub(crate) fn effective_key(&self) -> Option<IndexedDBKeyType> {
match &self.source {
ObjectStoreOrIndex::ObjectStore(_) => self.position.borrow().clone(),
ObjectStoreOrIndex::Index(_) => self.object_store_position.borrow().clone(),
}
}
}
impl IDBCursorMethods<crate::DomTypeHolder> for IDBCursor {
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbcursor-source>
fn Source(&self) -> IDBObjectStoreOrIDBIndex {
match &self.source {
ObjectStoreOrIndex::ObjectStore(source) => {
IDBObjectStoreOrIDBIndex::IDBObjectStore(source.as_rooted())
},
ObjectStoreOrIndex::Index(source) => {
IDBObjectStoreOrIDBIndex::IDBIndex(source.as_rooted())
},
}
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbcursor-direction>
fn Direction(&self) -> IDBCursorDirection {
self.direction
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbcursor-key>
fn Key(&self, cx: SafeJSContext, mut value: MutableHandleValue) {
match self.key.borrow().as_ref() {
Some(key) => key_type_to_jsval(cx, key, value),
None => value.set(UndefinedValue()),
}
}
/// <https://www.w3.org/TR/IndexedDB-2/#dom-idbcursor-primarykey>
fn PrimaryKey(&self, cx: SafeJSContext, mut value: MutableHandleValue) {
match self.effective_key() {
Some(effective_key) => key_type_to_jsval(cx, &effective_key, value),
None => value.set(UndefinedValue()),
}
}
/// <https://w3c.github.io/IndexedDB/#dom-idbcursor-request>
fn Request(&self) -> DomRoot<IDBRequest> {
self.request
.get()
.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))
}