fix(indexeddb): queue versionchange before rechecking blocked requests

This commit is contained in:
ldm0
2026-10-04 10:00:42 +08:00
parent 7106a4e4b6
commit efaecfc678
15 changed files with 418 additions and 73 deletions
@@ -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};
@@ -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,
@@ -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<u64>,
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(
@@ -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,
@@ -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),
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -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,
);
}
@@ -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,
);
}
@@ -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<u64>,
) {
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, '_>,
@@ -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<u64>,
old_version: u64,
new_version: Option<u64>,
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<u64>,
pub(super) old_version: u64,
pub(super) new_version: Option<u64>,
pub(super) notifications_pending: bool,
}
struct IndexedDbVersionChangeTaskPayload {
database: v8::Global<v8::Object>,
old_version: u64,
new_version: Option<u64>,
}
struct IndexedDbTransactionTaskPayload {
@@ -545,6 +558,8 @@ pub(super) struct IndexedDbRuntimeStateTable {
request_dispatch_tasks: BTreeMap<IndexedDbTaskId, IndexedDbRequestDispatchTaskPayload>,
open_tasks: BTreeMap<IndexedDbTaskId, IndexedDbOpenTaskPayload>,
blocked_tasks: BTreeMap<IndexedDbTaskId, IndexedDbBlockedTaskPayload>,
version_change_tasks: BTreeMap<IndexedDbTaskId, IndexedDbVersionChangeTaskPayload>,
blocked_recheck_tasks: BTreeMap<IndexedDbTaskId, v8::Global<v8::Object>>,
transaction_tasks: BTreeMap<IndexedDbTaskId, IndexedDbTransactionTaskPayload>,
requests: BTreeMap<IndexedDbObjectId, IndexedDbRequestLifecycleState>,
transactions: BTreeMap<IndexedDbObjectId, IndexedDbTransactionLifecycleState>,
@@ -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<u64>,
) {
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<u64>)> {
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<v8::Local<'s, v8::Object>> {
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>(
@@ -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");
}
@@ -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");
}
}