fix(indexeddb): restore upgrade metadata and handle state on abort

Expose the requested version on provisional connections, snapshot the prior
schema, and synchronously restore database/transaction/store metadata on abort.
Track created and deleted store/index handles so replacements with the same
name are not revived. Use native transaction ownership during rollback.

Keep the open request transaction through its abort event, then queue the open
failure. Share deletion checks across store/index requests and sort IDB name
lists by UTF-16 code units.

Cover initial and existing upgrades, schema/data rollback, retained handles,
same-name replacements, author property getters, and abort event ordering.
This commit is contained in:
ldm0
2026-09-23 00:14:07 +08:00
parent e429e35217
commit f9830e1851
40 changed files with 626 additions and 131 deletions
@@ -7,7 +7,7 @@ mod origin;
pub(in crate::context_bootstrap::indexed_db) use self::dom_string_list::{
idb_dom_string_list_backing_values, install_dom_string_list_template_bindings,
new_idb_dom_string_list,
new_idb_dom_string_list, new_idb_name_list,
};
pub(in crate::context_bootstrap::indexed_db) use self::errors::{
dom_exception_value, request_error_object,
@@ -69,6 +69,15 @@ pub(in crate::context_bootstrap::indexed_db) fn new_idb_dom_string_list<'s>(
object
}
pub(in crate::context_bootstrap::indexed_db) fn new_idb_name_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
names: &[String],
) -> v8::Local<'s, v8::Object> {
let mut names = names.to_vec();
names.sort_by(|left, right| left.encode_utf16().cmp(right.encode_utf16()));
new_idb_dom_string_list(scope, &names)
}
pub(in crate::context_bootstrap::indexed_db) fn install_dom_string_list_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
prototype: v8::Local<'s, v8::ObjectTemplate>,
@@ -36,7 +36,7 @@ pub(in crate::context_bootstrap::indexed_db) fn create_database_object<'s>(
) -> Option<v8::Local<'s, v8::Object>> {
let storage_key = storage_scope.storage_key().to_owned();
let database_key = database_registry_key(&storage_key, &info.name);
let object_store_names = new_idb_dom_string_list(scope, &info.object_store_names);
let object_store_names = new_idb_name_list(scope, &info.object_store_names);
let database =
IdbDatabaseObjectDeclaration::new(&info.name, info.version as f64, object_store_names)
.bind(scope)
@@ -55,7 +55,7 @@ pub(in crate::context_bootstrap::indexed_db) fn create_database_object<'s>(
database_key.clone(),
storage_scope,
);
let _ = refresh_database_surface(scope, database);
let _ = refresh_database_metadata(scope, database, info);
register_open_database_connection(scope, owner, handle, database_key, info.version, database);
Some(database)
}
@@ -36,7 +36,7 @@ pub(in crate::context_bootstrap::indexed_db) fn create_transaction_object<'s>(
let handle_raw = handle.map(|handle| handle.into_raw() as f64);
let mode: &'static str = mode.into();
let db_key = object_string_property(scope, db, INDEXED_DB_DATABASE_KEY_SLOT);
let object_store_names = new_idb_dom_string_list(scope, store_names);
let object_store_names = new_idb_name_list(scope, store_names);
let tx = IdbTransactionObjectDeclaration::new(db, mode, object_store_names)
.bind(scope)
.ok()?;
@@ -50,6 +50,6 @@ pub(in crate::context_bootstrap::indexed_db) fn create_transaction_object<'s>(
owner,
storage_scope,
);
register_indexed_db_transaction_lifecycle(scope, tx, handle, handle_raw.is_some(), db_key);
register_indexed_db_transaction_lifecycle(scope, tx, db, handle, handle_raw.is_some(), db_key);
Some(tx)
}
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::sync_indexed_db_store_handles;
pub(in crate::context_bootstrap::indexed_db) fn sync_transaction_object_store_names_from_database<
's,
@@ -10,7 +11,7 @@ pub(in crate::context_bootstrap::indexed_db) fn sync_transaction_object_store_na
let store_names = object_property_as_object(scope, database, "objectStoreNames")
.map(|value| dom_string_list_values(scope, value))
.unwrap_or_default();
let object_store_names = new_idb_dom_string_list(scope, &store_names);
let object_store_names = new_idb_name_list(scope, &store_names);
let _ = transaction.set(
scope,
v8str(scope, "objectStoreNames").into(),
@@ -32,7 +33,7 @@ pub(in crate::context_bootstrap::indexed_db) fn set_database_store_metadata<'s>(
if !store_names.iter().any(|name| name == &info.name) {
store_names.push(info.name.clone());
}
let object_store_names = new_idb_dom_string_list(scope, &store_names);
let object_store_names = new_idb_name_list(scope, &store_names);
let _ = database.set(
scope,
v8str(scope, "objectStoreNames").into(),
@@ -47,11 +48,12 @@ pub(in crate::context_bootstrap::indexed_db) fn remove_database_store_metadata<'
store_name: &str,
) -> Option<()> {
remove_indexed_db_database_store_metadata(scope, database, store_name)?;
sync_indexed_db_store_handles(scope, database, store_name);
let mut store_names = object_property_as_object(scope, database, "objectStoreNames")
.map(|value| dom_string_list_values(scope, value))
.unwrap_or_default();
store_names.retain(|name| name != store_name);
let object_store_names = new_idb_dom_string_list(scope, &store_names);
let object_store_names = new_idb_name_list(scope, &store_names);
let _ = database.set(
scope,
v8str(scope, "objectStoreNames").into(),
@@ -58,7 +58,7 @@ pub(in crate::context_bootstrap::indexed_db) fn create_object_store_descriptor_o
} else {
indexes.iter().map(|index| index.name.clone()).collect()
};
let index_names_list = new_idb_dom_string_list(scope, &index_names);
let index_names_list = new_idb_name_list(scope, &index_names);
set_indexed_db_internal_object_property(
scope,
descriptor,
@@ -26,7 +26,7 @@ pub(in crate::context_bootstrap::indexed_db) fn sync_store_surface_from_metadata
Some(value) => key_path_to_js_value(scope, value)?,
None => v8::null(scope).into(),
};
let index_names = new_idb_dom_string_list(scope, &info.index_names);
let index_names = new_idb_name_list(scope, &info.index_names);
set_indexed_db_object_store_metadata(scope, store, metadata)?;
IdbObjectStoreSurfaceDeclaration::new(
&info.name,
@@ -1,4 +1,7 @@
use super::*;
use crate::context_bootstrap::indexed_db::{
mark_indexed_db_index_handles_deleted, sync_indexed_db_store_handles,
};
pub(in crate::context_bootstrap::indexed_db) fn index_info_from_store_metadata<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -16,7 +19,9 @@ pub(in crate::context_bootstrap::indexed_db) fn set_database_index_metadata<'s>(
store_name: &str,
info: &IndexInfo,
) -> Option<()> {
set_indexed_db_database_index_metadata(scope, database, store_name, info.clone())
set_indexed_db_database_index_metadata(scope, database, store_name, info.clone())?;
sync_indexed_db_store_handles(scope, database, store_name);
Some(())
}
pub(in crate::context_bootstrap::indexed_db) fn remove_database_index_metadata<'s>(
@@ -25,5 +30,8 @@ pub(in crate::context_bootstrap::indexed_db) fn remove_database_index_metadata<'
store_name: &str,
index_name: &str,
) -> Option<()> {
remove_indexed_db_database_index_metadata(scope, database, store_name, index_name)
remove_indexed_db_database_index_metadata(scope, database, store_name, index_name)?;
mark_indexed_db_index_handles_deleted(scope, database, store_name, index_name);
sync_indexed_db_store_handles(scope, database, store_name);
Some(())
}
@@ -23,6 +23,22 @@ pub(in crate::context_bootstrap::indexed_db) fn refresh_database_surface<'s>(
return Ok(());
};
let info = with_indexed_db_manager(scope, |manager| manager.database_info(handle))?;
refresh_database_metadata(scope, database, &info)?;
let object_store_names = new_idb_name_list(scope, &info.object_store_names);
IdbDatabaseSurfaceDeclaration::new(&info.name, info.version as f64, object_store_names)
.initialize(scope, database)
.map_err(|error| IndexedDbError::InvalidState(error.to_string()))?;
Ok(())
}
pub(in crate::context_bootstrap::indexed_db) fn refresh_database_metadata<'s>(
scope: &mut v8::PinScope<'s, '_>,
database: v8::Local<'s, v8::Object>,
info: &DatabaseInfo,
) -> std::result::Result<(), IndexedDbError> {
let Some(handle) = database_handle_from_value(scope, database.into()) else {
return Ok(());
};
let mut metadata = Vec::with_capacity(info.object_store_names.len());
for store_name in &info.object_store_names {
let store = with_indexed_db_manager(scope, |manager| {
@@ -36,11 +52,7 @@ pub(in crate::context_bootstrap::indexed_db) fn refresh_database_surface<'s>(
}
metadata.push(IndexedDbObjectStoreMetadata::new(store, indexes));
}
let object_store_names = new_idb_dom_string_list(scope, &info.object_store_names);
let _ = replace_indexed_db_database_metadata(scope, database, metadata);
IdbDatabaseSurfaceDeclaration::new(&info.name, info.version as f64, object_store_names)
.initialize(scope, database)
.map_err(|error| IndexedDbError::InvalidState(error.to_string()))?;
Ok(())
}
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::restore_indexed_db_upgrade_metadata;
use crate::context_bootstrap::indexed_db::schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint;
pub(in crate::context_bootstrap::indexed_db) fn finish_transaction_abort<'s>(
@@ -6,6 +7,7 @@ pub(in crate::context_bootstrap::indexed_db) fn finish_transaction_abort<'s>(
transaction: v8::Local<'s, v8::Object>,
error: v8::Local<'s, v8::Value>,
) {
restore_indexed_db_upgrade_metadata(scope, transaction);
let _ = transaction.set(scope, v8str(scope, "error").into(), error);
set_indexed_db_slot_value(
scope,
@@ -7,9 +7,8 @@ use super::{
create_index_object, create_key_range_object, create_request_object, cursor_direction_to_value,
deserialize_js_value, dom_exception_value, enforce_object_store_unique_constraints,
enqueue_transaction_operation, execute_object_store_clear_request,
execute_object_store_delete_request, index_info_from_store_metadata,
indexed_db_database_store_metadata, indexed_db_index_info, indexed_db_index_object_store,
indexed_db_object_store_database, indexed_db_object_store_name,
execute_object_store_delete_request, index_info_from_store_metadata, indexed_db_index_info,
indexed_db_index_object_store, indexed_db_object_store_database, indexed_db_object_store_name,
indexed_db_object_store_transaction, key_in_range, key_to_js_value, object_bool_property,
object_string_property, optional_count_to_value, parse_cursor_direction,
parse_cursor_direction_with_context, parse_idb_key, parse_idb_key_path, parse_key_or_range,
@@ -17,8 +16,8 @@ use super::{
queue_transaction_request, remove_database_index_metadata, request_error_object,
scan_index_entries, scan_object_store_entries, serialize_js_value, set_database_index_metadata,
storage_bucket_quota_check_for_object_store, store_request_error, store_request_success,
submit_cursor_open_operation, sync_store_surface_from_metadata, throw_type_error,
transaction_handle_from_value, with_indexed_db_manager,
submit_cursor_open_operation, throw_type_error, transaction_handle_from_value,
with_indexed_db_manager,
};
use crate::webidl;
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::indexed_db_index_is_deleted;
pub(in crate::context_bootstrap::indexed_db) fn create_index_request<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -9,11 +10,22 @@ pub(in crate::context_bootstrap::indexed_db) fn create_index_request<'s>(
String,
IndexInfo,
)> {
if indexed_db_index_is_deleted(scope, index) {
let error = dom_exception_value(scope, "The index has been deleted.", "InvalidStateError");
scope.throw_exception(error);
return None;
}
let store = indexed_db_index_object_store(scope, index)?;
let transaction = indexed_db_object_store_transaction(scope, store)?;
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
.unwrap_or(false)
{
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return None;
}
let request = create_request_object(scope, index.into(), transaction)?;
@@ -10,12 +10,6 @@ fn idb_index_open_cursor_impl<'s>(
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let operation_name = if key_only {
@@ -9,12 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_index_get_all_callback<'s>(
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let parsed = match parse_collection_request_args(scope, &args, "IDBIndex.getAll") {
@@ -9,12 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_index_get_all_keys_callback<
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let parsed = match parse_collection_request_args(scope, &args, "IDBIndex.getAllKeys") {
@@ -19,12 +19,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_index_count_callback<'s>(
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query_value = parsed.query.unwrap_or_else(|| v8::undefined(scope).into());
@@ -18,12 +18,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_index_get_callback<'s>(
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query = match parse_key_or_range(scope, parsed.query) {
@@ -18,12 +18,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_index_get_key_callback<'s>(
let index = args.this();
let Some((request, transaction, store_name, index_info)) = create_index_request(scope, index)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query = match parse_key_or_range(scope, parsed.query) {
@@ -1,13 +1,29 @@
use super::*;
use crate::context_bootstrap::indexed_db::indexed_db_object_store_is_deleted;
pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>(
scope: &mut v8::PinScope<'s, '_>,
source: v8::Local<'s, v8::Object>,
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
if indexed_db_object_store_is_deleted(scope, source) {
let error = dom_exception_value(
scope,
"The object store has been deleted.",
"InvalidStateError",
);
scope.throw_exception(error);
return None;
}
let transaction = indexed_db_object_store_transaction(scope, source)?;
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
.unwrap_or(false)
{
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return None;
}
let request = create_request_object(scope, source.into(), transaction)?;
@@ -10,12 +10,6 @@ fn idb_object_store_open_cursor_impl<'s>(
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let operation_name = if key_only {
@@ -9,12 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_get_all_callbac
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let parsed = match parse_collection_request_args(scope, &args, "IDBObjectStore.getAll") {
@@ -9,12 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_get_all_keys_ca
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let parsed = match parse_collection_request_args(scope, &args, "IDBObjectStore.getAllKeys") {
@@ -19,12 +19,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_count_callback<
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query_value = parsed.query.unwrap_or_else(|| v8::undefined(scope).into());
@@ -18,12 +18,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_get_callback<'s
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query = match parse_key_or_range(scope, parsed.query) {
@@ -18,12 +18,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_get_key_callbac
let store = args.this();
let Some((request, transaction, store_name)) = object_store_operation_common(scope, store)
else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let query = match parse_key_or_range(scope, parsed.query) {
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::indexed_db_object_store_is_deleted;
pub(in crate::context_bootstrap::indexed_db) fn object_store_versionchange_common<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -9,6 +10,9 @@ pub(in crate::context_bootstrap::indexed_db) fn object_store_versionchange_commo
TransactionHandle,
String,
)> {
if indexed_db_object_store_is_deleted(scope, store) {
return None;
}
let transaction = indexed_db_object_store_transaction(scope, store)?;
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
.unwrap_or(false)
@@ -69,10 +69,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_create_index_ca
}) {
Ok(info) => {
let _ = set_database_index_metadata(scope, database, &store_name, &info);
if let Some(metadata) = indexed_db_database_store_metadata(scope, database, &store_name)
{
let _ = sync_store_surface_from_metadata(scope, store, metadata);
}
if let Some(index) = create_index_object(scope, store, &info) {
rv.set(index.into());
} else {
@@ -34,10 +34,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_delete_index_ca
}) {
Ok(()) => {
let _ = remove_database_index_metadata(scope, database, &store_name, &index_name);
if let Some(metadata) = indexed_db_database_store_metadata(scope, database, &store_name)
{
let _ = sync_store_surface_from_metadata(scope, store, metadata);
}
rv.set_undefined();
}
Err(error) => {
@@ -1,4 +1,7 @@
use super::*;
use crate::context_bootstrap::indexed_db::{
INDEXED_DB_TRANSACTION_FINISHED_SLOT, indexed_db_object_store_is_deleted,
};
use crate::webidl;
#[derive(webidl::WebIdlArgs)]
@@ -18,6 +21,19 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_index_callback<
};
let index_name = parsed.index_name;
let store = args.this();
let finished = indexed_db_object_store_transaction(scope, store).is_none_or(|transaction| {
object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT)
.unwrap_or(false)
});
if indexed_db_object_store_is_deleted(scope, store) || finished {
let error = dom_exception_value(
scope,
"The object store has been deleted or its transaction has finished.",
"InvalidStateError",
);
scope.throw_exception(error);
return;
}
let Some(info) = index_info_from_store_metadata(scope, store, &index_name) else {
let error =
dom_exception_value(scope, "The requested index was not found.", "NotFoundError");
@@ -17,12 +17,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_delete_callback
};
let store = args.this();
let Some((request, transaction)) = create_store_request(scope, store) else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
@@ -70,12 +64,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_clear_callback<
) {
let store = args.this();
let Some((request, transaction)) = create_store_request(scope, store) else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
@@ -13,12 +13,6 @@ pub(super) fn object_store_write_callback<'s>(
) {
let store = args.this();
let Some((request, transaction)) = create_store_request(scope, store) else {
let error = dom_exception_value(
scope,
"The transaction is not active.",
"TransactionInactiveError",
);
scope.throw_exception(error);
return;
};
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
@@ -1,8 +1,10 @@
use super::*;
use crate::context_bootstrap::indexed_db::defer_indexed_db_aborted_open;
mod abort;
mod commit;
mod success;
pub(in crate::context_bootstrap::indexed_db::tasks::dispatch) use abort::finish_aborted_upgrade_open;
pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -40,7 +42,7 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
if object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ABORTED_SLOT)
.unwrap_or(false)
{
abort::finish_aborted_upgrade_open(scope, request);
defer_indexed_db_aborted_open(scope, transaction, request);
return;
}
@@ -1,6 +1,6 @@
use super::*;
pub(super) fn finish_aborted_upgrade_open<'s>(
pub(in crate::context_bootstrap::indexed_db::tasks::dispatch) fn finish_aborted_upgrade_open<'s>(
scope: &mut v8::PinScope<'s, '_>,
request: v8::Local<'s, v8::Object>,
) {
@@ -37,6 +37,12 @@ pub(super) fn finish_aborted_upgrade_open<'s>(
"result",
undefined,
);
let _ = dispatch_idb_named_event(scope, request, "error", |_, _| {});
release_request_dispatch_refs(scope, request);
set_indexed_db_request_surface_value(
scope,
request,
INDEXED_DB_REQUEST_TRANSACTION_SLOT,
"transaction",
v8::null(scope).into(),
);
enqueue_request_task(scope, "request-error", request);
}
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::restore_indexed_db_upgrade_metadata;
pub(super) fn commit_upgrade_transaction<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -58,6 +59,7 @@ fn finish_failed_upgrade_commit<'s>(
transaction: v8::Local<'s, v8::Object>,
error: IndexedDbError,
) -> bool {
restore_indexed_db_upgrade_metadata(scope, transaction);
let error_value = request_error_object(scope, &error);
let _ = transaction.set(scope, v8str(scope, "error").into(), error_value);
set_indexed_db_request_surface_value(
@@ -1,4 +1,6 @@
use super::super::open::finish_aborted_upgrade_open;
use super::*;
use crate::context_bootstrap::indexed_db::take_indexed_db_aborted_open;
pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_abort_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -7,6 +9,18 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_abort_task<'s>
let Some(transaction) = indexed_db_transaction_task_transaction(scope, task) else {
return;
};
let aborted_open = take_indexed_db_aborted_open(scope, transaction);
if let Some((_, database)) = aborted_open {
set_indexed_db_slot_value(
scope,
database,
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
v8::null(scope).into(),
);
}
let _ = dispatch_idb_named_event(scope, transaction, "abort", |_, _| {});
release_indexed_db_transaction_dispatch_refs(scope, transaction);
if let Some((request, _)) = aborted_open {
finish_aborted_upgrade_open(scope, request);
}
}
@@ -6,7 +6,12 @@ pub(super) fn opened_database_info(
opened: &moli_indexeddb::OpenResult,
) -> Result<DatabaseInfo, moli_indexeddb::IndexedDbError> {
match with_indexed_db_manager(scope, |manager| manager.database_info(opened.database)) {
Ok(info) => Ok(info),
Ok(mut info) => {
if let OpenDisposition::UpgradeNeeded { new_version, .. } = opened.disposition {
info.version = new_version;
}
Ok(info)
}
Err(error) => match &opened.disposition {
OpenDisposition::UpgradeNeeded {
old_version: 0,
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::indexed_db::save_indexed_db_upgrade_metadata;
pub(super) fn enqueue_upgrade_needed_open_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -34,6 +35,7 @@ pub(super) fn enqueue_upgrade_needed_open_task<'s>(
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
transaction.into(),
);
save_indexed_db_upgrade_metadata(scope, database, old_version);
enqueue_open_task(
scope,
request,
@@ -1,6 +1,13 @@
use super::*;
use moli_storage_service::StorageBucketIdentity;
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
use std::{
cell::RefCell,
collections::{BTreeMap, BTreeSet},
rc::Rc,
};
mod upgrade;
pub(super) use upgrade::*;
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(super) struct IndexedDbObjectId(u64);
@@ -352,6 +359,8 @@ impl IndexedDbRequestLifecycleState {
}
struct IndexedDbTransactionLifecycleState {
database: Option<v8::Global<v8::Object>>,
aborted_open_request: Option<v8::Global<v8::Object>>,
handle: Option<TransactionHandle>,
active: bool,
finished: bool,
@@ -367,8 +376,15 @@ struct IndexedDbTransactionLifecycleState {
}
impl IndexedDbTransactionLifecycleState {
fn new(handle: Option<TransactionHandle>, started: bool, db_key: Option<String>) -> Self {
fn new(
database: v8::Global<v8::Object>,
handle: Option<TransactionHandle>,
started: bool,
db_key: Option<String>,
) -> Self {
Self {
database: Some(database),
aborted_open_request: None,
handle,
active: true,
finished: false,
@@ -389,6 +405,8 @@ impl IndexedDbTransactionLifecycleState {
pub(super) struct IndexedDbObjectStoreMetadata {
info: ObjectStoreInfo,
indexes: BTreeMap<String, IndexInfo>,
created_in_upgrade: bool,
created_indexes: BTreeSet<String>,
}
impl IndexedDbObjectStoreMetadata {
@@ -397,7 +415,12 @@ impl IndexedDbObjectStoreMetadata {
.into_iter()
.map(|index| (index.name.clone(), index))
.collect();
Self { info, indexes }
Self {
info,
indexes,
created_in_upgrade: false,
created_indexes: BTreeSet::new(),
}
}
pub(super) fn info(&self) -> &ObjectStoreInfo {
@@ -417,6 +440,7 @@ impl IndexedDbObjectStoreMetadata {
}
fn set_index(&mut self, info: IndexInfo) {
self.created_indexes.insert(info.name.clone());
if !self.info.index_names.iter().any(|name| name == &info.name) {
self.info.index_names.push(info.name.clone());
}
@@ -436,6 +460,7 @@ struct IndexedDbDatabaseLifecycleState {
closed: bool,
metadata: BTreeMap<String, IndexedDbObjectStoreMetadata>,
upgrade_transaction: Option<v8::Global<v8::Value>>,
upgrade_metadata: Option<IndexedDbUpgradeMetadata>,
}
impl IndexedDbDatabaseLifecycleState {
@@ -451,6 +476,7 @@ impl IndexedDbDatabaseLifecycleState {
closed: false,
metadata: BTreeMap::new(),
upgrade_transaction: None,
upgrade_metadata: None,
}
}
}
@@ -482,25 +508,30 @@ impl IndexedDbCursorLifecycleState {
}
struct IndexedDbObjectStoreLifecycleState {
wrapper: v8::Weak<v8::Object>,
transaction: v8::Global<v8::Value>,
database: v8::Global<v8::Value>,
name: String,
metadata: IndexedDbObjectStoreMetadata,
deleted: bool,
}
impl IndexedDbObjectStoreLifecycleState {
fn new(
scope: &mut v8::PinScope<'_, '_>,
wrapper: v8::Local<'_, v8::Object>,
transaction: v8::Local<'_, v8::Object>,
database: v8::Local<'_, v8::Object>,
metadata: IndexedDbObjectStoreMetadata,
) -> Self {
let name = metadata.info.name.clone();
Self {
wrapper: v8::Weak::new(scope, wrapper),
transaction: v8::Global::new(scope, v8::Local::<v8::Value>::from(transaction)),
database: v8::Global::new(scope, v8::Local::<v8::Value>::from(database)),
name,
metadata,
deleted: false,
}
}
}
@@ -509,6 +540,8 @@ struct IndexedDbIndexLifecycleState {
object_store: v8::Global<v8::Value>,
info: IndexInfo,
marker: bool,
deleted: bool,
created_in_upgrade: bool,
}
impl IndexedDbIndexLifecycleState {
@@ -516,11 +549,14 @@ impl IndexedDbIndexLifecycleState {
scope: &mut v8::PinScope<'_, '_>,
object_store: v8::Local<'_, v8::Object>,
info: IndexInfo,
created_in_upgrade: bool,
) -> Self {
Self {
object_store: v8::Global::new(scope, v8::Local::<v8::Value>::from(object_store)),
info,
marker: true,
deleted: false,
created_in_upgrade,
}
}
}
@@ -685,6 +721,7 @@ pub(super) fn release_indexed_db_request_dispatch_refs<'s>(
pub(super) fn register_indexed_db_transaction_lifecycle<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
database: v8::Local<'s, v8::Object>,
handle: Option<TransactionHandle>,
started: bool,
db_key: Option<String>,
@@ -692,7 +729,12 @@ pub(super) fn register_indexed_db_transaction_lifecycle<'s>(
let Some(id) = indexed_db_typed_state_id(scope, transaction) else {
return;
};
let state = IndexedDbTransactionLifecycleState::new(handle, started, db_key);
let state = IndexedDbTransactionLifecycleState::new(
v8::Global::new(scope, database),
handle,
started,
db_key,
);
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
table.borrow_mut().transactions.insert(id, state);
}
@@ -760,6 +802,7 @@ pub(super) fn release_indexed_db_transaction_dispatch_refs<'s>(
let Some(transaction) = table.transactions.get_mut(&id) else {
return;
};
transaction.database = None;
transaction.operations_waiting_for_start.clear();
}
@@ -838,7 +881,8 @@ pub(super) fn register_indexed_db_object_store_lifecycle<'s>(
let Some(id) = indexed_db_typed_state_id(scope, store) else {
return;
};
let state = IndexedDbObjectStoreLifecycleState::new(scope, transaction, database, metadata);
let state =
IndexedDbObjectStoreLifecycleState::new(scope, store, transaction, database, metadata);
let table = indexed_db_runtime_state_table_for_object(scope, store);
table.borrow_mut().object_stores.insert(id, state);
}
@@ -852,10 +896,14 @@ pub(super) fn register_indexed_db_index_lifecycle<'s>(
let Some(id) = indexed_db_typed_state_id(scope, index) else {
return;
};
let created_in_upgrade =
indexed_db_object_store_metadata(scope, object_store).is_some_and(|metadata| {
metadata.created_in_upgrade || metadata.created_indexes.contains(&info.name)
});
let table = indexed_db_runtime_state_table_for_object(scope, index);
table.borrow_mut().indexes.insert(
id,
IndexedDbIndexLifecycleState::new(scope, object_store, info),
IndexedDbIndexLifecycleState::new(scope, object_store, info, created_in_upgrade),
);
}
@@ -1268,12 +1316,13 @@ pub(super) fn indexed_db_database_store_metadata<'s>(
pub(super) fn set_indexed_db_database_store_metadata<'s>(
scope: &mut v8::PinScope<'s, '_>,
database: v8::Local<'s, v8::Object>,
metadata: IndexedDbObjectStoreMetadata,
mut metadata: IndexedDbObjectStoreMetadata,
) -> Option<()> {
let id = indexed_db_typed_state_id(scope, database)?;
let table = indexed_db_runtime_state_table_for_object(scope, database);
let mut table = table.borrow_mut();
let database = table.databases.get_mut(&id)?;
metadata.created_in_upgrade = database.upgrade_metadata.is_some();
database
.metadata
.insert(metadata.info.name.clone(), metadata);
@@ -1826,6 +1875,7 @@ fn set_indexed_db_typed_database_slot_value(
INDEXED_DB_DATABASE_METADATA_SLOT => true,
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT => {
database.upgrade_transaction = if value.is_null_or_undefined() {
database.upgrade_metadata = None;
None
} else {
Some(v8::Global::new(scope, value))
@@ -0,0 +1,261 @@
use super::*;
pub(super) struct IndexedDbUpgradeMetadata {
version: u64,
stores: BTreeMap<String, IndexedDbObjectStoreMetadata>,
}
pub(in crate::context_bootstrap::indexed_db) fn defer_indexed_db_aborted_open<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
request: v8::Local<'s, v8::Object>,
) {
let Some(id) = indexed_db_typed_state_id(scope, transaction) else {
return;
};
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
if let Some(state) = table.borrow_mut().transactions.get_mut(&id) {
state.aborted_open_request = Some(v8::Global::new(scope, request));
}
}
pub(in crate::context_bootstrap::indexed_db) fn take_indexed_db_aborted_open<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
let id = indexed_db_typed_state_id(scope, transaction)?;
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
let mut table = table.borrow_mut();
let state = table.transactions.get_mut(&id)?;
let request = state.aborted_open_request.take()?;
Some((
v8::Local::new(scope, &request),
v8::Local::new(scope, state.database.as_ref()?),
))
}
pub(in crate::context_bootstrap::indexed_db) fn save_indexed_db_upgrade_metadata(
scope: &mut v8::PinScope<'_, '_>,
database: v8::Local<'_, v8::Object>,
old_version: u64,
) {
let Some(id) = indexed_db_typed_state_id(scope, database) else {
return;
};
let table = indexed_db_runtime_state_table_for_object(scope, database);
let mut table = table.borrow_mut();
let Some(database) = table.databases.get_mut(&id) else {
return;
};
database.upgrade_metadata = Some(IndexedDbUpgradeMetadata {
version: old_version,
stores: database.metadata.clone(),
});
}
pub(in crate::context_bootstrap::indexed_db) fn restore_indexed_db_upgrade_metadata<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
) {
let Some(transaction_id) = indexed_db_typed_state_id(scope, transaction) else {
return;
};
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
let database = {
let table = table.borrow();
let Some(state) = table.transactions.get(&transaction_id) else {
return;
};
let Some(database) = state.database.as_ref() else {
return;
};
v8::Local::new(scope, database)
};
let Some(id) = indexed_db_typed_state_id(scope, database) else {
return;
};
let table = indexed_db_runtime_state_table_for_object(scope, database);
let (version, names, stores) = {
let mut table = table.borrow_mut();
let Some(state) = table.databases.get_mut(&id) else {
return;
};
if !state
.upgrade_transaction
.as_ref()
.is_some_and(|value| v8::Local::new(scope, value) == transaction)
{
return;
}
let Some(snapshot) = state.upgrade_metadata.take() else {
return;
};
state.metadata = snapshot.stores.clone();
let names = snapshot.stores.keys().cloned().collect::<Vec<_>>();
let mut stores = Vec::new();
let mut store_ids = BTreeSet::new();
for (id, store) in &mut table.object_stores {
if v8::Local::new(scope, &store.transaction) != transaction {
continue;
}
store_ids.insert(*id);
// A replacement with the same name is a different object store.
// Only handles for pre-upgrade stores are restored from the snapshot.
store.deleted = store.metadata.created_in_upgrade;
let metadata = if store.deleted {
let mut metadata = store.metadata.clone();
metadata.info.index_names.clear();
metadata.indexes.clear();
metadata
} else {
snapshot
.stores
.get(&store.name)
.cloned()
.expect("existing store has upgrade snapshot")
};
store.metadata = metadata.clone();
if let Some(wrapper) = store.wrapper.to_local(scope) {
stores.push((wrapper, metadata));
}
}
for index in table.indexes.values_mut() {
let store =
v8::Local::<v8::Object>::try_from(v8::Local::new(scope, &index.object_store))
.expect("index retains its object store");
if indexed_db_typed_state_id(scope, store).is_some_and(|id| store_ids.contains(&id)) {
index.deleted = index.created_in_upgrade;
}
}
(snapshot.version, names, stores)
};
let version = v8::Number::new(scope, version as f64);
let _ = database.define_own_property(
scope,
v8str(scope, "version").into(),
version.into(),
v8::PropertyAttribute::NONE,
);
let database_names = new_idb_name_list(scope, &names);
let transaction_names = new_idb_name_list(scope, &names);
let _ = database.define_own_property(
scope,
v8str(scope, "objectStoreNames").into(),
database_names.into(),
v8::PropertyAttribute::NONE,
);
let _ = transaction.define_own_property(
scope,
v8str(scope, "objectStoreNames").into(),
transaction_names.into(),
v8::PropertyAttribute::NONE,
);
for (store, metadata) in stores {
let _ = sync_store_surface_from_metadata(scope, store, metadata);
}
}
// Update every handle for this store, including separately obtained wrappers.
// V8 surface writes happen after releasing the runtime table's borrow.
pub(in crate::context_bootstrap::indexed_db) fn sync_indexed_db_store_handles<'s>(
scope: &mut v8::PinScope<'s, '_>,
database: v8::Local<'s, v8::Object>,
store_name: &str,
) {
let metadata = indexed_db_database_store_metadata(scope, database, store_name);
let table = indexed_db_runtime_state_table_for_object(scope, database);
let stores = {
let mut table = table.borrow_mut();
let mut stores = Vec::new();
for store in table.object_stores.values_mut() {
if store.deleted
|| store.name != store_name
|| v8::Local::new(scope, &store.database) != database
{
continue;
}
let metadata = match &metadata {
Some(metadata) => metadata.clone(),
None => {
store.deleted = true;
let mut metadata = store.metadata.clone();
metadata.info.index_names.clear();
metadata.indexes.clear();
metadata
}
};
store.metadata = metadata.clone();
if let Some(wrapper) = store.wrapper.to_local(scope) {
stores.push((wrapper, metadata));
}
}
stores
};
for (store, metadata) in stores {
let _ = sync_store_surface_from_metadata(scope, store, metadata);
}
}
pub(in crate::context_bootstrap::indexed_db) fn mark_indexed_db_index_handles_deleted(
scope: &mut v8::PinScope<'_, '_>,
database: v8::Local<'_, v8::Object>,
store_name: &str,
index_name: &str,
) {
let table = indexed_db_runtime_state_table_for_object(scope, database);
let mut table = table.borrow_mut();
let store_ids = table
.object_stores
.iter()
.filter_map(|(id, store)| {
(!store.deleted
&& store.name == store_name
&& v8::Local::new(scope, &store.database) == database)
.then_some(*id)
})
.collect::<BTreeSet<_>>();
for index in table.indexes.values_mut() {
let store = v8::Local::<v8::Object>::try_from(v8::Local::new(scope, &index.object_store))
.expect("index retains its object store");
if index.info.name == index_name
&& indexed_db_typed_state_id(scope, store).is_some_and(|id| store_ids.contains(&id))
{
index.deleted = true;
}
}
}
pub(in crate::context_bootstrap::indexed_db) fn indexed_db_object_store_is_deleted(
scope: &mut v8::PinScope<'_, '_>,
store: v8::Local<'_, v8::Object>,
) -> bool {
let Some(id) = indexed_db_typed_state_id(scope, store) else {
return true;
};
let table = indexed_db_runtime_state_table_for_object(scope, store);
table
.borrow()
.object_stores
.get(&id)
.is_none_or(|store| store.deleted)
}
pub(in crate::context_bootstrap::indexed_db) fn indexed_db_index_is_deleted(
scope: &mut v8::PinScope<'_, '_>,
index: v8::Local<'_, v8::Object>,
) -> bool {
let Some(id) = indexed_db_typed_state_id(scope, index) else {
return true;
};
let table = indexed_db_runtime_state_table_for_object(scope, index);
let table = table.borrow();
let Some(index) = table.indexes.get(&id) else {
return true;
};
let store = v8::Local::<v8::Object>::try_from(v8::Local::new(scope, &index.object_store))
.expect("index retains its object store");
index.deleted
|| indexed_db_typed_state_id(scope, store)
.and_then(|id| table.object_stores.get(&id))
.is_none_or(|store| store.deleted)
}
@@ -5465,6 +5465,177 @@ fn indexed_db_aborted_upgrade_closes_provisional_connection_before_reopen_and_de
assert_eq!(result, "AbortError|1|records|deleted");
}
#[test]
fn indexed_db_initial_upgrade_abort_restores_metadata_synchronously() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-initial-rollback.test/");
vm.eval(r#"
globalThis.rollback = [];
const open = indexedDB.open('initial-rollback', 7);
let db, tx, store, index;
function snapshot() {
return [db.version, Array.from(db.objectStoreNames),
Array.from(tx.objectStoreNames), Array.from(store.indexNames)];
}
open.onupgradeneeded = () => {
db = open.result;
tx = open.transaction;
store = db.createObjectStore('records');
index = store.createIndex('by-value', 'value');
for (const name of ['\uE000', '\u{10000}', 'z', 'a']) {
db.createObjectStore(name);
store.createIndex(name, 'value');
}
rollback.push(snapshot());
tx.onabort = () => rollback.push(['abort', open.transaction === tx, snapshot()]);
Object.defineProperty(tx, 'db', {get() { throw new Error('author db getter'); }});
tx.abort();
rollback.push(snapshot());
for (const source of [store, index]) {
for (const method of ['get', 'getKey', 'getAll', 'getAllKeys', 'count', 'openCursor', 'openKeyCursor']) {
try { source[method](1); rollback.push(method + ':accepted'); }
catch (error) { rollback.push(error.name); }
}
}
for (const method of ['add', 'put', 'delete', 'clear']) {
try { store[method](1, 1); rollback.push(method + ':accepted'); }
catch (error) { rollback.push(error.name); }
}
};
open.onerror = () => rollback.push(['error', open.error.name, open.result === undefined,
open.transaction === null, snapshot()]);
"#).expect("initial upgrade rollback should schedule");
let result = vm
.eval_after_selected_page_tasks("JSON.stringify(rollback)")
.expect("initial upgrade rollback should run");
let result: serde_json::Value = serde_json::from_str(&result).unwrap();
let entries = result.as_array().unwrap();
assert_eq!(
entries[0],
serde_json::json!([
7,
["a", "records", "z", "\u{10000}", "\u{e000}"],
["a", "records", "z", "\u{10000}", "\u{e000}"],
["a", "by-value", "z", "\u{10000}", "\u{e000}"]
])
);
assert_eq!(entries[1], serde_json::json!([0, [], [], []]));
assert_eq!(
&entries[2..20],
vec![serde_json::json!("InvalidStateError"); 18]
);
assert!(entries.contains(&serde_json::json!([
"error",
"AbortError",
true,
true,
[0, [], [], []]
])));
assert_eq!(
entries[20],
serde_json::json!(["abort", true, [0, [], [], []]])
);
assert_eq!(entries[21][0], "error");
assert_eq!(entries.len(), 22);
}
#[test]
fn indexed_db_upgrade_abort_restores_existing_handles_without_reviving_replacements() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-upgrade-rollback.test/");
vm.eval(r#"
globalThis.rollback = [];
const initial = indexedDB.open('upgrade-rollback', 9);
initial.onupgradeneeded = () => {
const store = initial.result.createObjectStore('records');
store.createIndex('by-value', 'value');
store.put({value: 'original'}, 1);
};
function failure(operation) {
try { operation(); return 'accepted'; } catch (error) { return error.name; }
}
initial.onsuccess = () => {
initial.result.close();
const open = indexedDB.open('upgrade-rollback', 10);
let db, tx, store, alias, index, replacement, replacementIndex;
function snapshot() {
return [db.version, Array.from(db.objectStoreNames), Array.from(tx.objectStoreNames),
Array.from(store.indexNames), Array.from(alias.indexNames), Array.from(replacement.indexNames),
failure(() => store.get(1)), failure(() => index.get(1)),
failure(() => replacement.get(1)), failure(() => replacementIndex.get(1))];
}
open.onupgradeneeded = () => {
db = open.result;
tx = open.transaction;
store = tx.objectStore('records');
alias = tx.objectStore('records');
index = store.index('by-value');
store.put({value: 'changed'}, 1);
store.createIndex('aa-transient', 'value');
rollback.push([db.version, Array.from(alias.indexNames)]);
store.deleteIndex('by-value');
const newIndex = alias.createIndex('by-value', 'other');
rollback.push(failure(() => index.get(1)));
db.deleteObjectStore('records');
rollback.push([Array.from(store.indexNames), Array.from(alias.indexNames), failure(() => store.get(1))]);
replacement = db.createObjectStore('records', {keyPath: 'id', autoIncrement: true});
replacementIndex = replacement.createIndex('by-value', 'other');
db.createObjectStore('temporary');
tx.abort();
rollback.push(snapshot());
rollback.push(failure(() => newIndex.get(1)));
tx.onabort = () => rollback.push(['abort', snapshot()]);
};
open.onerror = () => {
rollback.push(['error', snapshot()]);
const reopen = indexedDB.open('upgrade-rollback');
reopen.onsuccess = () => {
const reopened = reopen.result;
const st = reopened.transaction('records').objectStore('records');
rollback.push([reopened.version, Array.from(reopened.objectStoreNames), st.keyPath,
st.autoIncrement, Array.from(st.indexNames)]);
const get = st.get(1);
get.onsuccess = () => { rollback.push(get.result); reopened.close(); };
};
};
};
"#).expect("upgrade rollback should schedule");
let result = vm
.eval_after_selected_page_tasks("JSON.stringify(rollback)")
.expect("upgrade rollback should run");
let result: serde_json::Value = serde_json::from_str(&result).unwrap();
let entries = result.as_array().unwrap();
let restored = serde_json::json!([
9,
["records"],
["records"],
["by-value"],
["by-value"],
[],
"TransactionInactiveError",
"TransactionInactiveError",
"InvalidStateError",
"InvalidStateError"
]);
assert_eq!(
entries[0],
serde_json::json!([10, ["aa-transient", "by-value"]])
);
assert_eq!(entries[1], "InvalidStateError");
assert_eq!(entries[2], serde_json::json!([[], [], "InvalidStateError"]));
assert_eq!(entries[3], restored);
assert_eq!(entries[4], "InvalidStateError");
assert!(entries.contains(&serde_json::json!(["error", restored])));
assert!(entries.contains(&serde_json::json!(["abort", restored])));
assert!(entries.contains(&serde_json::json!([
9,
["records"],
null,
false,
["by-value"]
])));
assert!(entries.contains(&serde_json::json!({"value": "original"})));
assert_eq!(entries.len(), 9);
}
#[test]
fn indexed_db_upgrade_open_dispatches_versionchange_and_blocked_until_close() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-open-blocked.test/");