mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
fix(indexeddb): queue versionchange before rechecking blocked requests
This commit is contained in:
@@ -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;
|
||||
|
||||
+2
-12
@@ -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);
|
||||
}
|
||||
|
||||
+11
-1
@@ -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;
|
||||
|
||||
+47
@@ -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,
|
||||
);
|
||||
}
|
||||
+4
-19
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user