Files
servo/components/script/dom/indexeddb/idbcursor.rs
Taym Haddadi a7ef47a46e Indexeddb: Cache IDBCursor.key conversion to preserve object identity (#44161)
Testing: Covered by idbcursor-key.any.html test.
part of https://github.com/servo/servo/issues/40983

---------

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
2026-04-13 18:14:26 +00:00

559 lines
22 KiB
Rust
Raw 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::context::JSContext;
use js::jsapi::Heap;
use js::jsval::{JSVal, UndefinedValue};
use js::rust::MutableHandleValue;
use script_bindings::script_runtime::CanGc;
use storage_traits::indexeddb::{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::indexeddb::idbindex::IDBIndex;
use crate::dom::indexeddb::idbobjectstore::IDBObjectStore;
use crate::dom::indexeddb::idbrequest::IDBRequest;
use crate::dom::indexeddb::idbtransaction::IDBTransaction;
use crate::indexeddb::key_type_to_jsval;
#[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-3/#cursor-transaction>
transaction: Dom<IDBTransaction>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-range>
#[no_trace]
range: IndexedDBKeyRange,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-source>
source: ObjectStoreOrIndex,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-direction>
direction: IDBCursorDirection,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-position>
#[no_trace]
position: DomRefCell<Option<IndexedDBKeyType>>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-key>
#[no_trace]
key: DomRefCell<Option<IndexedDBKeyType>>,
#[ignore_malloc_size_of = "mozjs"]
cached_key: DomRefCell<Option<Heap<JSVal>>>,
#[ignore_malloc_size_of = "mozjs"]
cached_primary_key: DomRefCell<Option<Heap<JSVal>>>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-value>
#[ignore_malloc_size_of = "mozjs"]
value: Heap<JSVal>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-got-value-flag>
got_value: Cell<bool>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-object-store-position>
#[no_trace]
object_store_position: DomRefCell<Option<IndexedDBKeyType>>,
/// <https://www.w3.org/TR/IndexedDB-3/#cursor-key-only-flag>
key_only: bool,
/// <https://w3c.github.io/IndexedDB/#cursor-request>
request: MutNullableDom<IDBRequest>,
}
impl IDBCursor {
#[cfg_attr(crown, expect(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),
cached_key: DomRefCell::new(None),
cached_primary_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, expect(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>) {
let changed = *self.position.borrow() != position;
*self.position.borrow_mut() = position;
if changed {
*self.cached_primary_key.borrow_mut() = None;
}
}
fn set_key(&self, key: Option<IndexedDBKeyType>) {
let key_changed = {
let current_key = self.key.borrow();
current_key.as_ref() != key.as_ref()
};
*self.key.borrow_mut() = key;
if key_changed {
*self.cached_key.borrow_mut() = None;
}
}
fn set_object_store_position(&self, object_store_position: Option<IndexedDBKeyType>) {
let changed = *self.object_store_position.borrow() != object_store_position;
*self.object_store_position.borrow_mut() = object_store_position;
if changed {
*self.cached_primary_key.borrow_mut() = None;
}
}
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-3/#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-3/#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-3/#dom-idbcursor-direction>
fn Direction(&self) -> IDBCursorDirection {
self.direction
}
/// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-key>
fn Key(&self, cx: &mut JSContext, mut value: MutableHandleValue) {
// The key getter steps are to return the result of converting a key to a value with the cursors current key.
//
// NOTE: If key returns an object (e.g. a Date or Array), it returns the
// same object instance every time it is inspected, until the cursors key is changed.
// This means that if the object is modified, those modifications will be seen by
// anyone inspecting the value of the cursor. However modifying such an object does not
// modify the contents of the database.
if let Some(cached) = &*self.cached_key.borrow() {
value.set(cached.get());
return;
}
match self.key.borrow().as_ref() {
Some(key) => key_type_to_jsval(cx, key, value.reborrow()),
None => value.set(UndefinedValue()),
}
*self.cached_key.borrow_mut() = Some(Heap::default());
self.cached_key.borrow().as_ref().unwrap().set(value.get());
}
/// <https://www.w3.org/TR/IndexedDB-3/#dom-idbcursor-primarykey>
fn PrimaryKey(&self, cx: &mut JSContext, mut value: MutableHandleValue) {
// NOTE: If primaryKey returns an object (e.g. a Date or Array),
// it returns the same object instance every time it is inspected,
// until the cursors effective key is changed. This means that if the object is modified,
// those modifications will be seen by anyone inspecting the value of the cursor.
// However modifying such an object does not modify the contents of the database.
if let Some(cached) = &*self.cached_primary_key.borrow() {
value.set(cached.get());
return;
}
match self.effective_key() {
Some(effective_key) => key_type_to_jsval(cx, &effective_key, value.reborrow()),
None => value.set(UndefinedValue()),
}
*self.cached_primary_key.borrow_mut() = Some(Heap::default());
self.cached_primary_key
.borrow()
.as_ref()
.unwrap()
.set(value.get());
}
/// <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-3/#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-3/#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: &mut JSContext,
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());
postcard::from_bytes(&found_record.value)
.map_err(|_| Error::Data(None))
.and_then(|data| {
structuredclone::read(
global,
data,
new_cursor_value.handle_mut(),
CanGc::from_cx(cx),
)
})?;
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))
}