From f9830e18512667b013a154a08109ff5fbe3dc2ea Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 16 Sep 2026 23:23:18 +0800 Subject: [PATCH] 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. --- .../context_bootstrap/indexed_db/core/env.rs | 2 +- .../indexed_db/core/env/dom_string_list.rs | 9 + .../core/objects/creation/database.rs | 4 +- .../core/objects/creation/transaction.rs | 4 +- .../core/objects/metadata/database.rs | 8 +- .../objects/metadata/descriptors/create.rs | 2 +- .../core/objects/metadata/descriptors/sync.rs | 2 +- .../indexed_db/core/objects/metadata/index.rs | 12 +- .../core/objects/surface/database.rs | 20 +- .../database/transactions/lifecycle.rs | 2 + .../context_bootstrap/indexed_db/stores.rs | 9 +- .../indexed_db/stores/index/common.rs | 12 + .../indexed_db/stores/index/cursor/open.rs | 6 - .../stores/index/read/collection/get_all.rs | 6 - .../index/read/collection/get_all_keys.rs | 6 - .../indexed_db/stores/index/read/count.rs | 6 - .../stores/index/read/single/get.rs | 6 - .../stores/index/read/single/get_key.rs | 6 - .../indexed_db/stores/object_store/common.rs | 16 ++ .../stores/object_store/cursor/open.rs | 6 - .../object_store/read/collection/get_all.rs | 6 - .../read/collection/get_all_keys.rs | 6 - .../stores/object_store/read/count.rs | 6 - .../stores/object_store/read/single/get.rs | 6 - .../object_store/read/single/get_key.rs | 6 - .../stores/object_store/schema/common.rs | 4 + .../object_store/schema/create_index.rs | 4 - .../object_store/schema/delete_index.rs | 4 - .../stores/object_store/schema/lookup.rs | 16 ++ .../stores/object_store/write/delete_clear.rs | 12 - .../object_store/write/put_add/operation.rs | 6 - .../indexed_db/tasks/dispatch/open.rs | 4 +- .../indexed_db/tasks/dispatch/open/abort.rs | 12 +- .../indexed_db/tasks/dispatch/open/commit.rs | 2 + .../tasks/dispatch/transaction/abort.rs | 14 + .../indexed_db/tasks/open_delete/open/info.rs | 7 +- .../tasks/open_delete/open/upgrade.rs | 2 + .../indexed_db/typed_state.rs | 64 ++++- .../indexed_db/typed_state/upgrade.rs | 261 ++++++++++++++++++ .../src/script_vm/tests/indexed_db.rs | 171 ++++++++++++ 40 files changed, 626 insertions(+), 131 deletions(-) create mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state/upgrade.rs diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs index 84424aaf66..d9a5b442ff 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/dom_string_list.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/dom_string_list.rs index 1f43bea348..1d250164d9 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/dom_string_list.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/dom_string_list.rs @@ -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>, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs index 40fd91fdad..1c88c290a5 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs @@ -36,7 +36,7 @@ pub(in crate::context_bootstrap::indexed_db) fn create_database_object<'s>( ) -> Option> { 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) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/transaction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/transaction.rs index adcdd584ba..7c8fee271d 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/transaction.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/transaction.rs @@ -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) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/database.rs index 200974b229..f02a5051ef 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/database.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/database.rs @@ -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(), diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/create.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/create.rs index 0a20c0285e..200e4b7f67 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/create.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/create.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/sync.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/sync.rs index 5feb1dcafa..a834f8312f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/sync.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/descriptors/sync.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/index.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/index.rs index 7a073080bd..94ebae8bc8 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/index.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/metadata/index.rs @@ -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(()) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/surface/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/surface/database.rs index 11cd7fecb5..edbcb3def6 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/surface/database.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/surface/database.rs @@ -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(()) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs index 6b1f6a90d1..bcca3e4ead 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs index 2c0906e5d5..53b7534cff 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/common.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/common.rs index 198bc5e2d2..b5c7d55167 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/common.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/common.rs @@ -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)?; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/cursor/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/cursor/open.rs index 789251ec99..e7efc42b86 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/cursor/open.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/cursor/open.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all.rs index f08ec963ca..617d216632 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all.rs @@ -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") { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all_keys.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all_keys.rs index 639497c0e5..d47bf81062 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all_keys.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/collection/get_all_keys.rs @@ -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") { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/count.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/count.rs index b0487960c4..b8ae82137e 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/count.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/count.rs @@ -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()); diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get.rs index 1d84b952f3..bb4a1b54f7 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get.rs @@ -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) { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get_key.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get_key.rs index dd560b3db1..a1fb86f10f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get_key.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/index/read/single/get_key.rs @@ -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) { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs index 8ada561ece..6598a76a6b 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/common.rs @@ -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)?; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/cursor/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/cursor/open.rs index 99c47fb70a..5ab45692c1 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/cursor/open.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/cursor/open.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all.rs index d72656ad29..a7b079b5f3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all.rs @@ -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") { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all_keys.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all_keys.rs index 1e0fcb5bdd..fefa629864 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all_keys.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/collection/get_all_keys.rs @@ -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") { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/count.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/count.rs index 584651ed42..f7b734a917 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/count.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/count.rs @@ -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()); diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get.rs index 42802ee890..5de53c29ed 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get.rs @@ -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) { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get_key.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get_key.rs index 7e8239a157..884df4ed2c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get_key.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/read/single/get_key.rs @@ -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) { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/common.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/common.rs index 0f42d568af..ea09d5ca83 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/common.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/common.rs @@ -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) diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs index 17bc7d0a7a..c501d1ff6b 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/delete_index.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/delete_index.rs index 2a8b614648..09b363f68e 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/delete_index.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/delete_index.rs @@ -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) => { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/lookup.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/lookup.rs index c5bf17f7f9..766751905f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/lookup.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/lookup.rs @@ -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"); diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/delete_clear.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/delete_clear.rs index aca4f459ef..37001ea187 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/delete_clear.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/delete_clear.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs index cb1482d227..681c043295 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/write/put_add/operation.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs index fddeabf151..8a5ffcb252 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs @@ -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; } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs index b0aa9d9101..873abb719a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs @@ -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); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs index 704a0758a2..efe09524ad 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs @@ -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( diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs index c2f925ffcf..19d9b5b815 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs @@ -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); + } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/info.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/info.rs index dff0f10f59..607b0a6dce 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/info.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/info.rs @@ -6,7 +6,12 @@ pub(super) fn opened_database_info( opened: &moli_indexeddb::OpenResult, ) -> Result { 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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/upgrade.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/upgrade.rs index 3498a1d2ba..736b1dd38f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/upgrade.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open/upgrade.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs index c50655119b..9c4d41573e 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs @@ -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>, + aborted_open_request: Option>, handle: Option, active: bool, finished: bool, @@ -367,8 +376,15 @@ struct IndexedDbTransactionLifecycleState { } impl IndexedDbTransactionLifecycleState { - fn new(handle: Option, started: bool, db_key: Option) -> Self { + fn new( + database: v8::Global, + handle: Option, + started: bool, + db_key: Option, + ) -> 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, + created_in_upgrade: bool, + created_indexes: BTreeSet, } 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, upgrade_transaction: Option>, + upgrade_metadata: Option, } 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, transaction: v8::Global, database: v8::Global, 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::::from(transaction)), database: v8::Global::new(scope, v8::Local::::from(database)), name, metadata, + deleted: false, } } } @@ -509,6 +540,8 @@ struct IndexedDbIndexLifecycleState { object_store: v8::Global, 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::::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, started: bool, db_key: Option, @@ -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)) diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state/upgrade.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state/upgrade.rs new file mode 100644 index 0000000000..89c32d1d7f --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state/upgrade.rs @@ -0,0 +1,261 @@ +use super::*; + +pub(super) struct IndexedDbUpgradeMetadata { + version: u64, + stores: BTreeMap, +} + +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::>(); + 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::::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::>(); + for index in table.indexes.values_mut() { + let store = v8::Local::::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::::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) +} diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 385af58287..30d2888ef1 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -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/");