diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs index 9fd7de11b4..4e850be3c3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs @@ -11,21 +11,21 @@ use super::{ IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbValue, IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite, PreparedObjectStoreWriteError, TransactionHandle, TransactionMode, - context_host_ptr_from_global_bridge, dispatch_version_change_event, - global_constructor_prototype, indexed_db_database_store_metadata, - indexed_db_object_store_metadata, indexed_db_runtime_array, indexed_db_typed_execution_owner, - indexed_db_typed_storage_scope, new_null_prototype_object, object_bool_property, - object_hidden_value, object_number_property, object_property_as_object, object_string_property, - push_unique_object_to_indexed_db_runtime_array, register_indexed_db_cursor_lifecycle, - register_indexed_db_database_lifecycle, register_indexed_db_index_lifecycle, - register_indexed_db_key_range_lifecycle, register_indexed_db_object_store_lifecycle, - register_indexed_db_request_lifecycle, register_indexed_db_transaction_lifecycle, - register_indexed_db_wrapper, register_indexed_db_wrapper_with_owner, - remove_indexed_db_database_index_metadata, remove_indexed_db_database_store_metadata, - replace_indexed_db_database_metadata, replace_indexed_db_runtime_array, - set_indexed_db_database_index_metadata, set_indexed_db_database_store_metadata, - set_indexed_db_internal_object_property, set_indexed_db_object_store_metadata, - set_indexed_db_request_surface_value, set_indexed_db_slot_value, v8_string, v8str, + context_host_ptr_from_global_bridge, global_constructor_prototype, + indexed_db_database_store_metadata, indexed_db_object_store_metadata, indexed_db_runtime_array, + indexed_db_typed_execution_owner, indexed_db_typed_storage_scope, new_null_prototype_object, + object_bool_property, object_hidden_value, object_number_property, object_property_as_object, + object_string_property, push_unique_object_to_indexed_db_runtime_array, + register_indexed_db_cursor_lifecycle, register_indexed_db_database_lifecycle, + register_indexed_db_index_lifecycle, register_indexed_db_key_range_lifecycle, + register_indexed_db_object_store_lifecycle, register_indexed_db_request_lifecycle, + register_indexed_db_transaction_lifecycle, register_indexed_db_wrapper, + register_indexed_db_wrapper_with_owner, remove_indexed_db_database_index_metadata, + remove_indexed_db_database_store_metadata, replace_indexed_db_database_metadata, + replace_indexed_db_runtime_array, set_indexed_db_database_index_metadata, + set_indexed_db_database_store_metadata, set_indexed_db_internal_object_property, + set_indexed_db_object_store_metadata, set_indexed_db_request_surface_value, + set_indexed_db_slot_value, v8_string, v8str, }; use std::collections::BTreeSet; use v8::{ValueDeserializerHelper, ValueSerializerHelper}; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs index d4080aee55..a075081e77 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs @@ -4,7 +4,7 @@ mod connections; mod readwrite; pub(in crate::context_bootstrap::indexed_db) use self::connections::{ - database_registry_key, dispatch_version_change_to_open_connections, + database_registry_key, enqueue_version_change_to_open_connections, has_open_database_connections_for_key, open_database_connection_version_for_key, register_blocked_database_context, register_open_database_connection, unregister_blocked_database_context, unregister_open_database_connection, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/connections.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/connections.rs index 461f5f4031..609150b712 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/connections.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/connections.rs @@ -1,4 +1,8 @@ use super::*; +use crate::context_bootstrap::indexed_db::{ + enqueue_blocked_recheck_task, enqueue_version_change_task, + set_indexed_db_blocked_notifications_pending, +}; pub(in crate::context_bootstrap::indexed_db) fn database_registry_key( origin: &str, @@ -33,28 +37,25 @@ pub(in crate::context_bootstrap::indexed_db) fn open_database_connection_version .max() } -pub(in crate::context_bootstrap::indexed_db) fn dispatch_version_change_to_open_connections( - scope: &mut v8::PinScope<'_, '_>, +pub(in crate::context_bootstrap::indexed_db) fn enqueue_version_change_to_open_connections<'s>( + scope: &mut v8::PinScope<'s, '_>, key: &str, old_version: u64, new_version: Option, + blocked_task: v8::Local<'s, v8::Object>, ) { + set_indexed_db_blocked_notifications_pending(scope, blocked_task, true); let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { for database in local_open_database_connections_for_key(scope, key) { - let _ = dispatch_version_change_event( - scope, - database, - "versionchange", - old_version, - new_version, - ); + enqueue_version_change_task(scope, database, old_version, new_version); } + enqueue_blocked_recheck_task(scope, blocked_task); return; }; // Connections share backend coordination but retain the V8 realm that - // created each IDBDatabase wrapper. Snapshot roots before invoking script - // so close()/navigation can mutate the coordinator during dispatch. + // created each IDBDatabase wrapper. Each connection gets its own task and + // checkpoint; its callback may close other connections before their turn. let connections = unsafe { &*host_ptr }.indexed_db_open_connection_snapshots(scope, key); for connection in connections { if !unsafe { &*host_ptr } @@ -70,14 +71,9 @@ pub(in crate::context_bootstrap::indexed_db) fn dispatch_version_change_to_open_ continue; } let database = v8::Local::new(target_scope, &connection.database); - let _ = dispatch_version_change_event( - target_scope, - database, - "versionchange", - old_version, - new_version, - ); + enqueue_version_change_task(target_scope, database, old_version, new_version); } + enqueue_blocked_recheck_task(scope, blocked_task); } pub(in crate::context_bootstrap::indexed_db) fn register_open_database_connection( diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs index 294d158357..21d9b835e8 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs @@ -17,8 +17,8 @@ use super::{ close_indexed_db_database_connection, create_database_object, create_transaction_object, database_handle_from_value, database_registry_key, define_non_enumerable_value_property, deserialize_js_value, dispatch_idb_named_event, dispatch_version_change_event, - dispatch_version_change_to_open_connections, dom_exception_value, dom_string_list_values, - enforce_object_store_unique_constraints, flush_databases_settle_task, + dom_exception_value, dom_string_list_values, enforce_object_store_unique_constraints, + enqueue_version_change_to_open_connections, flush_databases_settle_task, has_open_database_connections_for_key, index_cursor_snapshot, indexed_db_blocked_task_payload, indexed_db_index_info, indexed_db_open_task_payload, indexed_db_request_dispatch_task_request, indexed_db_request_transaction_object, indexed_db_runtime_array, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs index adbda889b9..306ab2d3ec 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs @@ -53,6 +53,8 @@ fn flush_indexed_db_task<'s>( IndexedDbTaskKind::Open => flush_open_task(scope, task), IndexedDbTaskKind::OpenBlocked => flush_open_blocked_task(scope, task), IndexedDbTaskKind::DeleteBlocked => flush_delete_blocked_task(scope, task), + IndexedDbTaskKind::VersionChange => flush_version_change_task(scope, task), + IndexedDbTaskKind::BlockedRecheck => flush_blocked_recheck_task(scope, task), IndexedDbTaskKind::DrainBlockedOpens => flush_drain_blocked_open_requests_task(scope), IndexedDbTaskKind::DatabasesSettle => flush_databases_settle_task(scope, task), IndexedDbTaskKind::TransactionStart => flush_transaction_start_task(scope, task), diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete.rs index 49282f4132..2a888197c5 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete.rs @@ -5,7 +5,8 @@ mod delete; mod open; pub(in crate::context_bootstrap::indexed_db) use self::blocked::{ - flush_delete_blocked_task, flush_drain_blocked_open_requests_task, flush_open_blocked_task, + flush_blocked_recheck_task, flush_delete_blocked_task, flush_drain_blocked_open_requests_task, + flush_open_blocked_task, flush_version_change_task, }; pub(in crate::context_bootstrap::indexed_db) use self::delete::execute_delete_database_request; pub(in crate::context_bootstrap::indexed_db) use self::open::execute_open_request; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs index 8e68d87a53..24abc8c9e1 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs @@ -3,6 +3,7 @@ use super::*; mod delete_task; mod drain; mod event; +mod notifications; mod open_task; pub(super) fn blocked_task_storage_scope<'s>( @@ -14,4 +15,7 @@ pub(super) fn blocked_task_storage_scope<'s>( pub(in crate::context_bootstrap::indexed_db) use self::delete_task::flush_delete_blocked_task; pub(in crate::context_bootstrap::indexed_db) use self::drain::flush_drain_blocked_open_requests_task; +pub(in crate::context_bootstrap::indexed_db) use self::notifications::{ + flush_blocked_recheck_task, flush_version_change_task, +}; pub(in crate::context_bootstrap::indexed_db) use self::open_task::flush_open_blocked_task; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs index f1b4be2399..8341ba6b01 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs @@ -20,16 +20,6 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_delete_blocked_task<'s>( ); return; } - dispatch_version_change_to_open_connections(scope, &key, payload.old_version, None); - if !has_open_database_connections_for_key(scope, &key) { - delete::execute_delete_database_request( - scope, - payload.request, - storage_scope, - payload.name, - ); - return; - } push_unique_object_to_indexed_db_runtime_array( scope, IndexedDbRuntimeArray::BlockedOpenQueue, @@ -37,6 +27,6 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_delete_blocked_task<'s>( ); let owner = indexed_db_typed_task_execution_owner(scope, task) .expect("blocked delete task must retain its IndexedDB execution owner"); - register_blocked_database_context(scope, key, owner); - event::dispatch_blocked_once(scope, payload.request, payload.old_version, None); + register_blocked_database_context(scope, key.clone(), owner); + enqueue_version_change_to_open_connections(scope, &key, payload.old_version, None, task); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs index 19d5fece6b..7b1795bd0a 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs @@ -8,6 +8,7 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_drain_blocked_open_request return; }; let next = v8::Array::new(scope, 0); + let mut waiting_keys = std::collections::BTreeSet::new(); for index in 0..queue.length() { let Some(value) = queue.get_index(scope, index) else { continue; @@ -17,7 +18,13 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_drain_blocked_open_request }; let database_key = indexed_db_blocked_task_payload(scope, task) .map(|payload| database_registry_key(&payload.origin, &payload.name)); - if !try_execute_unblocked_request(scope, task) { + let waiting_for_earlier = database_key + .as_ref() + .is_some_and(|key| waiting_keys.contains(key)); + if waiting_for_earlier || !try_execute_unblocked_request(scope, task) { + if let Some(key) = database_key { + waiting_keys.insert(key); + } let _ = next.set_index(scope, next.length(), task.into()); } else { if let Some(database_key) = database_key.as_deref() { @@ -50,6 +57,9 @@ fn try_execute_unblocked_request_in_owner_scope<'s>( let Some(payload) = indexed_db_blocked_task_payload(scope, task) else { return false; }; + if payload.notifications_pending { + return false; + } let key = database_registry_key(&payload.origin, &payload.name); if has_open_database_connections_for_key(scope, &key) { return false; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/notifications.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/notifications.rs new file mode 100644 index 0000000000..f51b6adfcc --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/notifications.rs @@ -0,0 +1,47 @@ +use super::*; +use crate::context_bootstrap::indexed_db::{ + INDEXED_DB_DATABASE_CLOSED_SLOT, indexed_db_blocked_recheck_task_payload, + indexed_db_version_change_task_payload, set_indexed_db_blocked_notifications_pending, +}; + +pub(in crate::context_bootstrap::indexed_db) fn flush_version_change_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, +) { + let Some((database, old_version, new_version)) = + indexed_db_version_change_task_payload(scope, task) + else { + return; + }; + if object_bool_property(scope, database, INDEXED_DB_DATABASE_CLOSED_SLOT).unwrap_or(false) { + return; + } + let _ = + dispatch_version_change_event(scope, database, "versionchange", old_version, new_version); +} + +pub(in crate::context_bootstrap::indexed_db) fn flush_blocked_recheck_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, +) { + let Some(blocked_task) = indexed_db_blocked_recheck_task_payload(scope, task) else { + return; + }; + let Some(payload) = indexed_db_blocked_task_payload(scope, blocked_task) else { + return; + }; + // All connection notifications precede this task in the same IndexedDB + // source, including stale tickets discarded when their realm is retired. + set_indexed_db_blocked_notifications_pending(scope, blocked_task, false); + let key = database_registry_key(&payload.origin, &payload.name); + if !has_open_database_connections_for_key(scope, &key) { + enqueue_drain_blocked_open_requests_task(scope); + return; + } + event::dispatch_blocked_once( + scope, + payload.request, + payload.old_version, + payload.new_version, + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs index 7cf6996c2e..a5e174f9f4 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs @@ -24,22 +24,6 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_blocked_task<'s>( ); return; } - dispatch_version_change_to_open_connections( - scope, - &key, - payload.old_version, - Some(new_version), - ); - if !has_open_database_connections_for_key(scope, &key) { - open::execute_open_request( - scope, - payload.request, - storage_scope, - payload.name, - payload.version, - ); - return; - } push_unique_object_to_indexed_db_runtime_array( scope, IndexedDbRuntimeArray::BlockedOpenQueue, @@ -47,11 +31,12 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_blocked_task<'s>( ); let owner = indexed_db_typed_task_execution_owner(scope, task) .expect("blocked open task must retain its IndexedDB execution owner"); - register_blocked_database_context(scope, key, owner); - event::dispatch_blocked_once( + register_blocked_database_context(scope, key.clone(), owner); + enqueue_version_change_to_open_connections( scope, - payload.request, + &key, payload.old_version, Some(new_version), + task, ); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs index 00140e8633..f6b21ca38e 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs @@ -1,4 +1,35 @@ use super::*; +use crate::context_bootstrap::indexed_db::{ + register_indexed_db_blocked_recheck_task, register_indexed_db_version_change_task, +}; + +pub(in crate::context_bootstrap::indexed_db) fn enqueue_version_change_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + database: v8::Local<'s, v8::Object>, + old_version: u64, + new_version: Option, +) { + let Some(context) = database.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + let task = v8::Object::new(scope); + register_indexed_db_version_change_task(scope, task, database, old_version, new_version); + enqueue_indexed_db_task(scope, task); +} + +pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_recheck_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + blocked_task: v8::Local<'s, v8::Object>, +) { + let Some(context) = blocked_task.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + let task = v8::Object::new(scope); + register_indexed_db_blocked_recheck_task(scope, task, blocked_task); + enqueue_indexed_db_task(scope, task); +} pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_open_task<'s>( scope: &mut v8::PinScope<'s, '_>, 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 2a3db2941f..1f1249d68a 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 @@ -35,6 +35,8 @@ pub(super) enum IndexedDbTaskKind { Open, OpenBlocked, DeleteBlocked, + VersionChange, + BlockedRecheck, DrainBlockedOpens, DatabasesSettle, TransactionStart, @@ -187,6 +189,8 @@ impl IndexedDbTaskState { | IndexedDbTaskKind::Open | IndexedDbTaskKind::OpenBlocked | IndexedDbTaskKind::DeleteBlocked + | IndexedDbTaskKind::VersionChange + | IndexedDbTaskKind::BlockedRecheck | IndexedDbTaskKind::DrainBlockedOpens | IndexedDbTaskKind::DatabasesSettle | IndexedDbTaskKind::TransactionStart @@ -268,6 +272,7 @@ struct IndexedDbBlockedTaskPayload { version: Option, old_version: u64, new_version: Option, + notifications_pending: bool, } impl IndexedDbBlockedTaskPayload { @@ -288,6 +293,7 @@ impl IndexedDbBlockedTaskPayload { version, old_version, new_version, + notifications_pending: false, } } } @@ -299,6 +305,13 @@ pub(super) struct IndexedDbBlockedTaskPayloadLocals<'s> { pub(super) version: Option, pub(super) old_version: u64, pub(super) new_version: Option, + pub(super) notifications_pending: bool, +} + +struct IndexedDbVersionChangeTaskPayload { + database: v8::Global, + old_version: u64, + new_version: Option, } struct IndexedDbTransactionTaskPayload { @@ -545,6 +558,8 @@ pub(super) struct IndexedDbRuntimeStateTable { request_dispatch_tasks: BTreeMap, open_tasks: BTreeMap, blocked_tasks: BTreeMap, + version_change_tasks: BTreeMap, + blocked_recheck_tasks: BTreeMap>, transaction_tasks: BTreeMap, requests: BTreeMap, transactions: BTreeMap, @@ -1190,9 +1205,99 @@ pub(super) fn indexed_db_blocked_task_payload<'s>( version: payload.version, old_version: payload.old_version, new_version: payload.new_version, + notifications_pending: payload.notifications_pending, }) } +pub(super) fn set_indexed_db_blocked_notifications_pending<'s>( + scope: &mut v8::PinScope<'s, '_>, + blocked_task: v8::Local<'s, v8::Object>, + pending: bool, +) { + let Some(id) = indexed_db_typed_task_id(scope, blocked_task) else { + return; + }; + let table = indexed_db_runtime_state_table_for_object(scope, blocked_task); + if let Some(payload) = table.borrow_mut().blocked_tasks.get_mut(&id) { + payload.notifications_pending = pending; + } +} + +pub(super) fn register_indexed_db_version_change_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, + database: v8::Local<'s, v8::Object>, + old_version: u64, + new_version: Option, +) { + let owner = indexed_db_typed_execution_owner(scope, database) + .expect("versionchange task retains its database owner"); + let storage_scope = indexed_db_typed_storage_scope(scope, database); + let id = register_indexed_db_task_with_owner( + scope, + task, + IndexedDbTaskKind::VersionChange, + owner, + storage_scope, + ); + let table = indexed_db_runtime_state_table_for_object(scope, task); + table.borrow_mut().version_change_tasks.insert( + id, + IndexedDbVersionChangeTaskPayload { + database: v8::Global::new(scope, database), + old_version, + new_version, + }, + ); +} + +pub(super) fn indexed_db_version_change_task_payload<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, +) -> Option<(v8::Local<'s, v8::Object>, u64, Option)> { + let id = indexed_db_typed_task_id(scope, task)?; + let table = indexed_db_runtime_state_table_for_object(scope, task); + let table = table.borrow(); + let payload = table.version_change_tasks.get(&id)?; + Some(( + v8::Local::new(scope, &payload.database), + payload.old_version, + payload.new_version, + )) +} + +pub(super) fn register_indexed_db_blocked_recheck_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, + blocked_task: v8::Local<'s, v8::Object>, +) { + let owner = indexed_db_typed_task_execution_owner(scope, blocked_task) + .expect("blocked recheck retains the requesting task owner"); + let storage_scope = indexed_db_typed_task_storage_scope(scope, blocked_task); + let id = register_indexed_db_task_with_owner( + scope, + task, + IndexedDbTaskKind::BlockedRecheck, + owner, + storage_scope, + ); + let table = indexed_db_runtime_state_table_for_object(scope, task); + table + .borrow_mut() + .blocked_recheck_tasks + .insert(id, v8::Global::new(scope, blocked_task)); +} + +pub(super) fn indexed_db_blocked_recheck_task_payload<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, +) -> Option> { + let id = indexed_db_typed_task_id(scope, task)?; + let table = indexed_db_runtime_state_table_for_object(scope, task); + let table = table.borrow(); + Some(v8::Local::new(scope, table.blocked_recheck_tasks.get(&id)?)) +} + pub(super) fn register_indexed_db_transaction_task<'s>( scope: &mut v8::PinScope<'s, '_>, task: v8::Local<'s, v8::Object>, @@ -1248,6 +1353,8 @@ pub(super) fn unregister_indexed_db_task<'s>( table.open_tasks.remove(&id); table.blocked_tasks.remove(&id); table.transaction_tasks.remove(&id); + table.blocked_recheck_tasks.remove(&id); + table.version_change_tasks.remove(&id); } pub(super) fn replace_indexed_db_database_metadata<'s>( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/indexed_db.rs b/moli-renderer-v8/src/runtime/page_vm/tests/indexed_db.rs index da348bd420..6227e77b30 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/indexed_db.rs @@ -632,3 +632,72 @@ fn indexed_db_rejects_a_real_page_vm_replacement_identity_collision() { }, ); } + +#[tokio::test(flavor = "current_thread")] +async fn indexed_db_retired_versionchange_notification_does_not_strand_open_request() { + run_page_vm_async_test(async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()) + .expect("loader"); + let manager = crate::new_indexed_db_manager(None).expect("IndexedDB manager"); + let document_url = Url::parse("https://example.com/indexed-db-retired-notification")?; + let (mut page_vm, _resource_source, _owner_wake_rx) = + page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url); + install_indexed_db_manager(&mut page_vm, &manager); + page_vm.vm_mut().eval(r#" + globalThis.notificationEvents = []; + const frame = document.createElement('iframe'); + frame.id = 'notification-owner'; + document.body.appendChild(frame); + "#)?; + let child_handle = page_vm.vm().element_handle_by_id_for_test("notification-owner") + .expect("notification owner frame should exist"); + let child_context = materialize_only_child_realm_execution_context_through_page_turn_for_test( + &mut page_vm, "notification-owner", + )?; + page_vm.vm_mut().eval_in_child_default_context(child_context, r#" + const open = indexedDB.open('retired-notification', 1); + open.onupgradeneeded = () => open.result.createObjectStore('records'); + open.onsuccess = () => { + globalThis.notificationDatabase = open.result; + notificationDatabase.onversionchange = () => top.notificationEvents.push('unexpected-retired-callback'); + }; + "#)?; + for _ in 0..8 { + if page_vm.vm_mut().eval_in_child_default_context( + child_context, "typeof globalThis.notificationDatabase", + )? == "object" { + break; + } + assert!(run_selected_indexed_db_task_for_test(&mut page_vm, &loader) + .await?.is_some(), "opening the initial connection must leave a runnable task"); + } + assert_eq!(page_vm.vm_mut().eval_in_child_default_context( + child_context, "String(notificationDatabase.version)", + )?, "1"); + page_vm.vm_mut().eval(r#" + const upgrade = indexedDB.open('retired-notification', 2); + upgrade.onupgradeneeded = () => notificationEvents.push('upgrade'); + upgrade.onblocked = () => notificationEvents.push('unexpected-blocked'); + upgrade.onerror = () => notificationEvents.push(`error:${upgrade.error.name}`); + upgrade.onsuccess = () => { + notificationEvents.push('success'); + upgrade.result.close(); + }; + "#)?; + // Queue the notification in the connection's realm, then retire that + // realm before its task can run. The requesting realm must still recheck. + assert!(run_selected_indexed_db_task_for_test(&mut page_vm, &loader) + .await?.is_some()); + page_vm.vm_mut().retire_child_frame_realm_for_test(child_handle); + for _ in 0..16 { + if page_vm.vm_mut().eval("notificationEvents.includes('success')")? == "true" { + break; + } + assert!(run_selected_indexed_db_task_for_test(&mut page_vm, &loader) + .await?.is_some(), "a discarded notification must leave a runnable continuation"); + } + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(notificationEvents)")?, + r#"["upgrade","success"]"#); + Ok::<_, anyhow::Error>(()) + }).await.expect("retiring a versionchange recipient should unblock the requesting realm"); +} 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 6608d73e16..bf459e7d2d 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -1829,10 +1829,12 @@ async fn indexed_db_blocked_upgrade_result_database_keeps_opener_owner() { request.onsuccess = () => { globalThis.__blockedUpgradeOwnerTopDb = request.result; globalThis.__blockedUpgradeOwnerTopDb.onversionchange = () => { - const sender = new BroadcastChannel("blocked-upgrade-top-versionchange-owner"); - sender.postMessage("top-versionchange"); - globalThis.__blockedUpgradeOwnerTopVersionChange = "closed"; - globalThis.__blockedUpgradeOwnerTopDb.close(); + Promise.resolve().then(() => { + const sender = new BroadcastChannel("blocked-upgrade-top-versionchange-owner"); + sender.postMessage("top-versionchange"); + globalThis.__blockedUpgradeOwnerTopVersionChange = "closed"; + globalThis.__blockedUpgradeOwnerTopDb.close(); + }); }; globalThis.__blockedUpgradeOwnerTopReady = "ok"; }; @@ -5860,3 +5862,104 @@ fn indexed_db_roundtrips_image_data_with_graph_identity() { r#"[true,true,true,2,1,"display-p3",[1,2,3,4,5,6,7,8],true,true]"# ); } + +#[test] +fn indexed_db_versionchange_waits_for_connection_microtasks_before_blocked() { + for operation in ["upgrade", "delete"] { + let mut vm = + new_storage_page_task_executor_test_vm("https://indexeddb-notification-tasks.test/"); + vm.eval(&format!("globalThis.operation = {operation:?};")) + .expect("connection operation should be set"); + vm.eval(r#" +globalThis.notificationEvents = []; +const first = indexedDB.open(`notifications-${operation}`, 1); +first.onupgradeneeded = () => first.result.createObjectStore('records'); +first.onsuccess = () => { + const db1 = first.result; + const second = indexedDB.open(db1.name); + second.onsuccess = () => { + const db2 = second.result; + const request = operation === 'upgrade' ? indexedDB.open(db1.name, 2) : indexedDB.deleteDatabase(db1.name); + db1.onversionchange = event => { + notificationEvents.push(`first:${event.oldVersion}:${event.newVersion}`); + Promise.resolve().then(() => { + notificationEvents.push('first-microtask'); + request.onblocked = () => { + notificationEvents.push('blocked'); + db1.close(); + db2.close(); + }; + }); + }; + db2.onversionchange = () => { + notificationEvents.push('second'); + queueMicrotask(() => notificationEvents.push('second-microtask')); + }; + request.onupgradeneeded = () => notificationEvents.push('upgrade'); + request.onerror = () => notificationEvents.push(`error:${request.error.name}`); + request.onsuccess = () => { + notificationEvents.push('success'); + if (operation === 'upgrade') request.result.close(); + }; + }; +}; +"#).expect("connection notifications should be scheduled"); + let result = vm + .eval_after_selected_page_tasks("JSON.stringify(notificationEvents)") + .expect("connection notification tasks should finish"); + let expected = if operation == "upgrade" { + r#"["first:1:2","first-microtask","second","second-microtask","blocked","upgrade","success"]"# + } else { + r#"["first:1:null","first-microtask","second","second-microtask","blocked","success"]"# + }; + assert_eq!(result, expected, "{operation} notification order"); + } +} + +#[test] +fn indexed_db_versionchange_skips_connections_closed_by_an_earlier_microtask() { + for operation in ["upgrade", "delete"] { + let mut vm = + new_storage_page_task_executor_test_vm("https://indexeddb-notification-close.test/"); + vm.eval(&format!("globalThis.operation = {operation:?};")) + .expect("connection operation should be set"); + vm.eval(r#" +globalThis.notificationEvents = []; +const first = indexedDB.open(`notifications-close-${operation}`, 1); +first.onupgradeneeded = () => first.result.createObjectStore('records'); +first.onsuccess = () => { + const db1 = first.result; + const second = indexedDB.open(db1.name); + second.onsuccess = () => { + const db2 = second.result; + db1.onversionchange = () => { + notificationEvents.push('first'); + Promise.resolve().then(() => { + notificationEvents.push('close-microtask'); + db1.close(); + db2.close(); + }); + }; + db2.onversionchange = () => notificationEvents.push('unexpected-second'); + const request = operation === 'upgrade' ? indexedDB.open(db1.name, 2) : indexedDB.deleteDatabase(db1.name); + request.onblocked = () => notificationEvents.push('unexpected-blocked'); + request.onupgradeneeded = () => notificationEvents.push('upgrade'); + request.onerror = () => notificationEvents.push(`error:${request.error.name}`); + request.onsuccess = () => { + notificationEvents.push('success'); + if (operation === 'upgrade') request.result.close(); + }; + }; +}; +"#).expect("closing notification should be scheduled"); + let result = vm + .eval_after_selected_page_tasks("JSON.stringify(notificationEvents)") + .expect("closing notification tasks should finish"); + let expected = if operation == "upgrade" { + r#"["first","close-microtask","upgrade","success"]"# + } else { + r#"["first","close-microtask","success"]"# + }; + assert_eq!(result, expected, "{operation} must skip closed connections"); + } +}