Switch indexeddb backend to sqlite and improve IPC messaging (#38187)

- Use sqlite instead of heed. (one indexed database = one sqlite
database)
- Implement the backend for indexes
- Use keyranges where needed (as specified by the spec)
- Implement `getKey`
- Fix channel error messaging (led to a bunch of changes to how async
requests are handled)

Note: `components/net/indexeddb/engines/sqlite/serialize.rs` is unused;
I can delete it if needed.

Testing: Switching to sqlite eliminated many panics (exposing some new
failures).
Fixes: #38040

---------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
This commit is contained in:
Ashwin Naren 2025-08-16 00:27:17 -07:00 committed by GitHub
parent f4bbdf8010
commit fc3feceee5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 2002 additions and 818 deletions

View file

@ -7,7 +7,7 @@ use std::cell::Cell;
use dom_struct::dom_struct;
use ipc_channel::ipc::IpcSender;
use net_traits::IpcSend;
use net_traits::indexeddb_thread::{IndexedDBThreadMsg, SyncOperation};
use net_traits::indexeddb_thread::{IndexedDBThreadMsg, KeyPath, SyncOperation};
use profile_traits::ipc;
use stylo_atoms::Atom;
@ -104,7 +104,10 @@ impl IDBDatabase {
.get_idb_thread()
.send(IndexedDBThreadMsg::Sync(operation));
receiver.recv().unwrap()
receiver.recv().unwrap().unwrap_or_else(|e| {
error!("{e:?}");
u64::MAX
})
}
pub fn set_transaction(&self, transaction: &IDBTransaction) {
@ -185,7 +188,6 @@ impl IDBDatabaseMethods<crate::DomTypeHolder> for IDBDatabase {
name: DOMString,
options: &IDBObjectStoreParameters,
) -> Fallible<DomRoot<IDBObjectStore>> {
// FIXME:(arihant2math) ^^ Change idl to match above.
// Step 2
let upgrade_transaction = match self.upgrade_transaction.get() {
Some(txn) => txn,
@ -247,11 +249,18 @@ impl IDBDatabaseMethods<crate::DomTypeHolder> for IDBDatabase {
let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
let key_paths = key_path.map(|p| match p {
StringOrStringSequence::String(s) => KeyPath::String(s.to_string()),
StringOrStringSequence::StringSequence(s) => {
KeyPath::Sequence(s.iter().map(|s| s.to_string()).collect())
},
});
let operation = SyncOperation::CreateObjectStore(
sender,
self.global().origin().immutable().clone(),
self.name.to_string(),
name.to_string(),
key_paths,
auto_increment,
);

View file

@ -26,7 +26,8 @@ use crate::dom::domstringlist::DOMStringList;
use crate::dom::globalscope::GlobalScope;
use crate::dom::idbrequest::IDBRequest;
use crate::dom::idbtransaction::IDBTransaction;
use crate::indexed_db::{convert_value_to_key, extract_key};
use crate::indexed_db;
use crate::indexed_db::{convert_value_to_key, convert_value_to_key_range, extract_key};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
#[derive(JSTraceable, MallocSizeOf)]
@ -125,9 +126,43 @@ impl IDBObjectStore {
.send(IndexedDBThreadMsg::Sync(operation))
.unwrap();
receiver.recv().unwrap()
// First unwrap for ipc
// Second unwrap will never happen unless this db gets manually deleted somehow
receiver.recv().unwrap().unwrap()
}
// fn get_stored_key_path(&mut self) -> Option<KeyPath> {
// let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
//
// let operation = SyncOperation::KeyPath(
// sender,
// self.global().origin().immutable().clone(),
// self.db_name.to_string(),
// self.name.borrow().to_string(),
// );
//
// self.global()
// .resource_threads()
// .sender()
// .send(IndexedDBThreadMsg::Sync(operation))
// .unwrap();
//
// // First unwrap for ipc
// // Second unwrap will never happen unless this db gets manually deleted somehow
// let key_path = receiver.recv().unwrap().unwrap();
// key_path.map(|p| {
// // TODO: have separate storage for string sequence of len 1 and signle string
// if p.len() == 1 {
// KeyPath::String(DOMString::from_string(p[0].clone()))
// } else {
// let strings: Vec<_> = p.into_iter().map(|s| {
// DOMString::from_string(s)
// }).collect();
// KeyPath::StringSequence(strings)
// }
// })
// }
// https://www.w3.org/TR/IndexedDB-2/#object-store-in-line-keys
fn uses_inline_keys(&self) -> bool {
self.key_path.is_some()
@ -200,30 +235,34 @@ impl IDBObjectStore {
serialized_key = convert_value_to_key(cx, key, None)?;
} else {
// Step 11: We should use in-line keys instead
if let Ok(kpk) = extract_key(
cx,
value,
self.key_path.as_ref().expect("No key path"),
None,
) {
if let Some(Ok(kpk)) = self
.key_path
.as_ref()
.map(|p| extract_key(cx, value, p, None))
{
serialized_key = kpk;
} else {
// FIXME:(rasviitanen)
// Check if store has a key generator
// Check if we can inject a key
return Err(Error::Data);
if !self.has_key_generator() {
return Err(Error::Data);
}
// FIXME:(arihant2math)
return Err(Error::NotSupported);
}
}
let serialized_value = structuredclone::write(cx, value, None)?;
let (sender, receiver) = indexed_db::create_channel(self.global());
IDBRequest::execute_async(
self,
AsyncOperation::ReadWrite(AsyncReadWriteOperation::PutItem(
serialized_key,
serialized_value.serialized,
overwrite,
)),
AsyncOperation::ReadWrite(AsyncReadWriteOperation::PutItem {
sender,
key: serialized_key,
value: serialized_value.serialized,
should_overwrite: overwrite,
}),
receiver,
None,
can_gc,
)
@ -262,10 +301,12 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// TODO: Convert to key range instead
let serialized_query = convert_value_to_key(cx, query, None);
// Step 7
let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| {
IDBRequest::execute_async(
self,
AsyncOperation::ReadWrite(AsyncReadWriteOperation::RemoveItem(q)),
AsyncOperation::ReadWrite(AsyncReadWriteOperation::RemoveItem { sender, key: q }),
receiver,
None,
CanGc::note(),
)
@ -279,9 +320,12 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// TODO: Step 3
// Steps 4-5
self.check_readwrite_transaction_active()?;
let (sender, receiver) = indexed_db::create_channel(self.global());
IDBRequest::execute_async(
self,
AsyncOperation::ReadWrite(AsyncReadWriteOperation::Clear),
AsyncOperation::ReadWrite(AsyncReadWriteOperation::Clear(sender)),
receiver,
None,
CanGc::note(),
)
@ -295,13 +339,17 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// Step 4
self.check_transaction_active()?;
// Step 5
// TODO: Convert to key range instead
let serialized_query = convert_value_to_key(cx, query, None);
let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6
let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| {
IDBRequest::execute_async(
self,
AsyncOperation::ReadOnly(AsyncReadOnlyOperation::GetItem(q)),
AsyncOperation::ReadOnly(AsyncReadOnlyOperation::GetItem {
sender,
key_range: q,
}),
receiver,
None,
CanGc::note(),
)
@ -309,17 +357,29 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
}
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getkey
// fn GetKey(&self, _cx: SafeJSContext, _query: HandleValue) -> DomRoot<IDBRequest> {
// // Step 1: Unneeded, handled by self.check_transaction_active()
// // TODO: Step 2
// // TODO: Step 3
// // Step 4
// self.check_transaction_active()?;
// // Step 5
// // TODO: Convert to key range instead
// let serialized_query = IDBObjectStore::convert_value_to_key(cx, query, None);
// unimplemented!();
// }
fn GetKey(&self, cx: SafeJSContext, query: HandleValue) -> Result<DomRoot<IDBRequest>, Error> {
// Step 1: Unneeded, handled by self.check_transaction_active()
// TODO: Step 2
// TODO: Step 3
// Step 4
self.check_transaction_active()?;
// Step 5
let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6
let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| {
IDBRequest::execute_async(
self,
AsyncOperation::ReadOnly(AsyncReadOnlyOperation::GetKey {
sender,
key_range: q,
}),
receiver,
None,
CanGc::note(),
)
})
}
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getall
// fn GetAll(
@ -350,13 +410,18 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
self.check_transaction_active()?;
// Step 5
let serialized_query = convert_value_to_key(cx, query, None);
let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6
let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| {
IDBRequest::execute_async(
self,
AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Count(q)),
AsyncOperation::ReadOnly(AsyncReadOnlyOperation::Count {
sender,
key_range: q,
}),
receiver,
None,
CanGc::note(),
)

View file

@ -7,7 +7,7 @@ use ipc_channel::router::ROUTER;
use js::jsval::UndefinedValue;
use js::rust::HandleValue;
use net_traits::IpcSend;
use net_traits::indexeddb_thread::{IndexedDBThreadMsg, SyncOperation};
use net_traits::indexeddb_thread::{BackendResult, IndexedDBThreadMsg, SyncOperation};
use profile_traits::ipc;
use script_bindings::conversions::SafeToJSValConvertible;
use stylo_atoms::Atom;
@ -84,7 +84,7 @@ impl OpenRequestListener {
}
}
fn handle_delete_db(&self, result: Result<(), ()>, can_gc: CanGc) {
fn handle_delete_db(&self, result: BackendResult<()>, can_gc: CanGc) {
let open_request = self.open_request.root();
let global = open_request.global();
open_request.idbrequest.set_ready_state_done();
@ -108,7 +108,7 @@ impl OpenRequestListener {
event.fire(open_request.upcast(), can_gc);
},
Err(_e) => {
// FIXME(rasviitanen) Set the error of request to the
// FIXME(arihant2math) Set the error of request to the
// appropriate error
let event = Event::new(

View file

@ -8,13 +8,15 @@ use constellation_traits::StructuredSerializedData;
use dom_struct::dom_struct;
use ipc_channel::router::ROUTER;
use js::jsapi::Heap;
use js::jsval::{JSVal, UndefinedValue};
use js::jsval::{DoubleValue, JSVal, UndefinedValue};
use js::rust::HandleValue;
use net_traits::IpcSend;
use net_traits::indexeddb_thread::{
AsyncOperation, IdbResult, IndexedDBThreadMsg, IndexedDBTxnMode,
AsyncOperation, BackendResult, IndexedDBKeyType, IndexedDBThreadMsg, IndexedDBTxnMode,
PutItemResult,
};
use profile_traits::ipc;
use profile_traits::ipc::IpcReceiver;
use serde::{Deserialize, Serialize};
use stylo_atoms::Atom;
use crate::dom::bindings::codegen::Bindings::IDBRequestBinding::{
@ -42,8 +44,61 @@ struct RequestListener {
request: Trusted<IDBRequest>,
}
pub enum IdbResult {
Key(IndexedDBKeyType),
Data(Vec<u8>),
Count(u64),
Error(Error),
None,
}
impl From<IndexedDBKeyType> for IdbResult {
fn from(value: IndexedDBKeyType) -> Self {
IdbResult::Key(value)
}
}
impl From<Vec<u8>> for IdbResult {
fn from(value: Vec<u8>) -> Self {
IdbResult::Data(value)
}
}
impl From<PutItemResult> for IdbResult {
fn from(value: PutItemResult) -> Self {
match value {
PutItemResult::Success => Self::None,
PutItemResult::CannotOverwrite => Self::Error(Error::Constraint),
}
}
}
impl From<()> for IdbResult {
fn from(_value: ()) -> Self {
Self::None
}
}
impl<T> From<Option<T>> for IdbResult
where
T: Into<IdbResult>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(value) => value.into(),
None => IdbResult::None,
}
}
}
impl From<u64> for IdbResult {
fn from(value: u64) -> Self {
IdbResult::Count(value)
}
}
impl RequestListener {
fn handle_async_request_finished(&self, result: Result<Option<IdbResult>, ()>) {
fn handle_async_request_finished(&self, result: BackendResult<IdbResult>) {
let request = self.request.root();
let global = request.global();
let cx = GlobalScope::get_cx();
@ -53,7 +108,7 @@ impl RequestListener {
let _ac = enter_realm(&*request);
rooted!(in(*cx) let mut answer = UndefinedValue());
if let Ok(Some(data)) = result {
if let Ok(data) = result {
match data {
IdbResult::Key(key) => {
key_type_to_jsval(GlobalScope::get_cx(), &key, answer.handle_mut())
@ -68,6 +123,17 @@ impl RequestListener {
warn!("Error reading structuredclone data");
}
},
IdbResult::Count(count) => {
answer.handle_mut().set(DoubleValue(count as f64));
},
IdbResult::None => {
// no-op
},
IdbResult::Error(_err) => {
request.set_result(answer.handle());
Self::handle_async_request_error(request, &global);
return;
},
}
request.set_result(answer.handle());
@ -92,30 +158,33 @@ impl RequestListener {
transaction.set_active_flag(false);
} else {
request.set_result(answer.handle());
// FIXME:(rasviitanen)
// Set the error of request to result
let transaction = request
.transaction
.get()
.expect("Request has no transaction");
let event = Event::new(
&global,
Atom::from("error"),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
CanGc::note(),
);
transaction.set_active_flag(true);
event
.upcast::<Event>()
.fire(request.upcast(), CanGc::note());
transaction.set_active_flag(false);
Self::handle_async_request_error(request, &global);
}
}
fn handle_async_request_error(request: DomRoot<IDBRequest>, global: &GlobalScope) {
// FIXME:(rasviitanen)
// Set the error of request to result
let transaction = request
.transaction
.get()
.expect("Request has no transaction");
let event = Event::new(
global,
Atom::from("error"),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
CanGc::note(),
);
transaction.set_active_flag(true);
event
.upcast::<Event>()
.fire(request.upcast(), CanGc::note());
transaction.set_active_flag(false);
}
}
#[dom_struct]
@ -174,12 +243,16 @@ impl IDBRequest {
}
// https://www.w3.org/TR/IndexedDB-2/#asynchronously-execute-a-request
pub fn execute_async(
pub fn execute_async<T>(
source: &IDBObjectStore,
operation: AsyncOperation,
receiver: IpcReceiver<BackendResult<T>>,
request: Option<DomRoot<IDBRequest>>,
can_gc: CanGc,
) -> Fallible<DomRoot<IDBRequest>> {
) -> Fallible<DomRoot<IDBRequest>>
where
T: Into<IdbResult> + for<'a> Deserialize<'a> + Serialize + Send + Sync + 'static,
{
// Step 1: Let transaction be the transaction associated with source.
let transaction = source.transaction().expect("Store has no transaction");
let global = transaction.global();
@ -208,10 +281,6 @@ impl IDBRequest {
IDBTransactionMode::Versionchange => IndexedDBTxnMode::Versionchange,
};
let (sender, receiver) =
ipc::channel::<Result<Option<IdbResult>, ()>>(global.time_profiler_chan().clone())
.unwrap();
let response_listener = RequestListener {
request: Trusted::new(&request),
};
@ -227,7 +296,7 @@ impl IDBRequest {
let response_listener = response_listener.clone();
task_source.queue(task!(request_callback: move || {
response_listener.handle_async_request_finished(
message.expect("Could not unwrap message"));
message.expect("Could not unwrap message").map(|t| t.into()));
}));
}),
);
@ -237,7 +306,6 @@ impl IDBRequest {
.resource_threads()
.sender()
.send(IndexedDBThreadMsg::Async(
sender,
global.origin().immutable().clone(),
transaction.get_db_name().to_string(),
source.get_name().to_string(),

View file

@ -229,6 +229,7 @@ impl IDBTransactionMethods<crate::DomTypeHolder> for IDBTransaction {
// returns the same IDBObjectStore instance.
let mut store_handles = self.store_handles.borrow_mut();
let store = store_handles.entry(name.to_string()).or_insert_with(|| {
// TODO: get key path from backend
let store = IDBObjectStore::new(
&self.global(),
self.db.get_name(),

View file

@ -5,6 +5,7 @@
use std::iter::repeat;
use std::ptr;
use ipc_channel::ipc::IpcSender;
use js::conversions::jsstr_to_string;
use js::gc::MutableHandle;
use js::jsapi::{
@ -14,17 +15,31 @@ use js::jsapi::{
};
use js::jsval::{DoubleValue, UndefinedValue};
use js::rust::{HandleValue, MutableHandleValue};
use net_traits::indexeddb_thread::{IndexedDBKeyRange, IndexedDBKeyType};
use net_traits::indexeddb_thread::{BackendResult, IndexedDBKeyRange, IndexedDBKeyType};
use profile_traits::ipc;
use profile_traits::ipc::IpcReceiver;
use script_bindings::conversions::{SafeToJSValConvertible, root_from_object};
use script_bindings::root::DomRoot;
use script_bindings::str::DOMString;
use serde::{Deserialize, Serialize};
use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::import::module::SafeJSContext;
use crate::dom::bindings::structuredclone;
use crate::dom::globalscope::GlobalScope;
use crate::dom::idbkeyrange::IDBKeyRange;
use crate::dom::idbobjectstore::KeyPath;
pub fn create_channel<T>(
global: DomRoot<GlobalScope>,
) -> (IpcSender<BackendResult<T>>, IpcReceiver<BackendResult<T>>)
where
T: for<'a> Deserialize<'a> + Serialize,
{
ipc::channel::<BackendResult<T>>(global.time_profiler_chan().clone()).unwrap()
}
// https://www.w3.org/TR/IndexedDB-2/#convert-key-to-value
#[allow(unsafe_code)]
pub fn key_type_to_jsval(
@ -147,7 +162,6 @@ pub fn convert_value_to_key(
// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key-range
#[allow(unsafe_code)]
#[expect(unused)]
pub fn convert_value_to_key_range(
cx: SafeJSContext,
input: HandleValue,