diff --git a/moli-cdp-smoke/README.md b/moli-cdp-smoke/README.md
index ce53ff11d..14836e894 100644
--- a/moli-cdp-smoke/README.md
+++ b/moli-cdp-smoke/README.md
@@ -178,6 +178,21 @@ external endpoint or on non-Linux systems, protocol churn/navigation still run;
the artifact explicitly reports that FD sampling was unavailable. CI uses the
managed Linux server, so the resource assertions are mandatory there.
+The Chromium-calibrated `svg-indexeddb-startup` group covers the JSXGraph-style
+SVG feature check and drives IndexedDB through asynchronous versionchange requests,
+request-callback/microtask schema migration, commit, reopen, abort rollback
+and FIFO admission across all open/delete calls. The shared connection lifecycle
+fixture also checks database versus open-request transaction attachment during
+complete/abort, concurrent initial opens, deferred version resolution, blocked
+head isolation, versionchange microtask close, and queue recovery after abort
+or close. Rust and CDP use the same Chromium-calibrated expected result. Its application
+fixture deliberately contains hidden error messages from the beginning;
+success requires completed database work and the ready panel, not a large
+text dump. This distinction comes from the live sketchometry investigation:
+the IndexedDB factory was present, while a missing SVG feature-detection
+method and prematurely completed upgrade transactions prevented startup.
+Each smoke group gets its own Moli process.
+
Covered well:
- The default raw `debugger-breakpoints`, `runtime-exception`, and
diff --git a/moli-cdp-smoke/moli_cdp_smoke/groups/svg_indexeddb_startup.py b/moli-cdp-smoke/moli_cdp_smoke/groups/svg_indexeddb_startup.py
new file mode 100644
index 000000000..1ba45f05a
--- /dev/null
+++ b/moli-cdp-smoke/moli_cdp_smoke/groups/svg_indexeddb_startup.py
@@ -0,0 +1,203 @@
+from __future__ import annotations
+
+import json
+from typing import Any
+
+from ..assertions import assert_equal, record_contract
+from ..config import REPO_ROOT
+
+
+async def run_svg_indexeddb_startup_group(
+ browser: Any, fixture: str, results: list[dict[str, Any]]
+) -> None:
+ context = await browser.new_context()
+ try:
+ page = await context.new_page()
+ await page.goto(f"{fixture}/plain", wait_until="load", timeout=10_000)
+ lifecycle_fixture = REPO_ROOT / "moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle"
+ observed = await page.evaluate(lifecycle_fixture.with_suffix(".js").read_text())
+ expected = json.loads(lifecycle_fixture.with_suffix(".json").read_text())
+ assert_equal(observed, expected, "connection queue admission and upgrade terminal lifecycle")
+ record_contract(
+ results, "indexeddb_connection_queue_lifecycle",
+ contract=("Database upgrade association clears before terminal events, while the open "
+ "request retains its transaction during dispatch. All open/delete calls share "
+ "FIFO admission, versions resolve at the head, and versionchange microtasks "
+ "finish before checking whether the request is blocked."),
+ source="Chromium IndexedDB connection coordinator and upgrade lifecycle WPT",
+ commands=["Runtime.evaluate"], observed=observed,
+ )
+ # A fresh browser context keeps the worker's database namespace empty.
+ # Worker task wakes use a different route from Window Page tasks, but
+ # must exercise the same admission/lifecycle implementation.
+ worker_context = await browser.new_context()
+ try:
+ worker_page = await worker_context.new_page()
+ await worker_page.goto(f"{fixture}/plain", wait_until="load", timeout=10_000)
+ observed = await worker_page.evaluate("""source => new Promise((resolve, reject) => {
+ const program = source + '.then(value => postMessage({value}), error => postMessage({error: String(error)}));';
+ const url = URL.createObjectURL(new Blob([program], {type: 'application/javascript'}));
+ const worker = new Worker(url);
+ const cleanup = () => { worker.terminate(); URL.revokeObjectURL(url); };
+ worker.onmessage = event => {
+ cleanup();
+ if (event.data.error) reject(new Error(event.data.error));
+ else resolve(event.data.value);
+ };
+ worker.onerror = event => { cleanup(); reject(new Error(event.message)); };
+ })""", lifecycle_fixture.with_suffix(".js").read_text().strip())
+ assert_equal(observed, expected, "worker connection queue and upgrade lifecycle")
+ record_contract(
+ results, "indexeddb_worker_connection_queue_lifecycle",
+ contract="Worker task wakes preserve the same connection admission, terminal ordering, and FIFO recovery as Window tasks.",
+ source="Shared Chromium-calibrated IndexedDB connection lifecycle fixture",
+ commands=["Runtime.evaluate"], observed=observed,
+ )
+ finally:
+ await worker_context.close()
+ upgrade_script = (REPO_ROOT / "moli-renderer-v8/tests/fixtures/indexeddb-upgrade.js").read_text()
+ observed = await page.evaluate(upgrade_script)
+ assert_equal(observed, {
+ "trace": ["upgrade:done", "seed", "read:original", "request-microtask",
+ "migration-write", "microtask-write", "complete", "complete-microtask", "open-success"],
+ "transactionCleared": True,
+ "records": [{"id": 1, "text": "original"}, {"id": 2, "text": "microtask write"}],
+ "microtaskValue": "value",
+ "abortTrace": ["request-success", "abort", "open-error:AbortError"],
+ "rollback": {"oldVersion": 0, "stores": []},
+ "closedResult": "AbortError",
+ }, "upgrade request callbacks/microtasks drain before commit; abort rolls back")
+ record_contract(
+ results, "indexeddb_upgrade_request_drain",
+ contract=("Upgrade request callbacks and their microtasks can migrate schema and data; "
+ "the open result follows transaction completion, while abort rolls back and closes the connection."),
+ source="Chromium 145.0.7632.116 executable calibration",
+ commands=["Runtime.evaluate"], observed=observed,
+ )
+
+ observed = await page.evaluate("""() => new Promise((resolve, reject) => {
+ const order = [];
+ const open = indexedDB.open('startup-delete-order', 1);
+ const remove = label => {
+ const request = indexedDB.deleteDatabase(open.result.name);
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ order.push(label);
+ if (order.length === 2) resolve(order);
+ };
+ };
+ open.onerror = () => reject(open.error);
+ open.onupgradeneeded = () => remove('delete1');
+ open.onsuccess = () => {
+ remove('delete2');
+ open.result.close();
+ };
+ })""")
+ assert_equal(observed, ["delete1", "delete2"], "blocked deletes preserve arrival order")
+ record_contract(
+ results, "indexeddb_upgrade_delete_order",
+ contract=("A delete queued during upgrade remains ahead of a delete requested by "
+ "open.success, even if both become runnable after the connection closes."),
+ source="Chromium executable calibration; renderer initial-upgrade deletion regression",
+ commands=["Runtime.evaluate"], observed=observed,
+ )
+
+ # Error strings deliberately exist in the DOM before startup. Neither
+ # their presence nor a large text dump establishes application failure
+ # or success: inspect the selected panel and await database completion.
+ await page.set_content("""
+
+ Your browser does not support IndexedDB
+ Your browser does not support SVG or Base64
+ The app could not be started
+ """)
+ observed = await page.evaluate("""async () => {
+ const trace = [];
+ const requestResult = request => new Promise((resolve, reject) => {
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ const transactionDone = tx => new Promise((resolve, reject) => {
+ tx.oncomplete = resolve;
+ tx.onabort = () => reject(tx.error || new Error('transaction aborted'));
+ });
+ const failPanel = id => { document.getElementById(id).classList.remove('off'); };
+ const supportsSVG = !!document.createElementNS &&
+ !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect;
+ if (!supportsSVG || !window.atob) { failPanel('not-supported'); return {trace}; }
+ if (!('indexedDB' in window)) { failPanel('no-indexeddb'); return {trace}; }
+ let db;
+ try {
+ const name = 'svg-indexeddb-startup';
+ let open = indexedDB.open(name, 1);
+ open.onupgradeneeded = event => {
+ trace.push(`seed:${event.oldVersion}->${event.newVersion}`);
+ const store = event.currentTarget.result.createObjectStore('legacy', {keyPath:'id'});
+ store.createIndex('id', 'id', {unique:true});
+ store.put({id:'drawing', content:'saved geometry'});
+ };
+ db = await requestResult(open);
+ db.close();
+ open = indexedDB.open(name, 4);
+ open.onupgradeneeded = event => {
+ trace.push(`upgrade:${event.oldVersion}->${event.newVersion}`);
+ const database = event.currentTarget.result;
+ const tx = event.currentTarget.transaction;
+ const read = tx.objectStore('legacy').getAll();
+ read.onsuccess = () => {
+ trace.push('migration-read');
+ const store = database.createObjectStore('drawings', {keyPath:'uuid'});
+ store.createIndex('uuid', 'uuid', {unique:true});
+ for (const record of read.result) store.put({uuid:record.id, content:record.content});
+ database.deleteObjectStore('legacy');
+ };
+ };
+ db = await requestResult(open);
+ trace.push('opened');
+ let tx = db.transaction('drawings', 'readwrite');
+ let done = transactionDone(tx);
+ tx.objectStore('drawings').put({uuid:'second', content:'new geometry'});
+ await done;
+ trace.push('written');
+ db.close();
+ db = await requestResult(indexedDB.open(name, 4));
+ tx = db.transaction('drawings', 'readonly');
+ done = transactionDone(tx);
+ const value = await requestResult(tx.objectStore('drawings').index('uuid').get('drawing'));
+ await done;
+ const version = db.version;
+ const stores = Array.from(db.objectStoreNames);
+ db.close();
+ trace.push('reopened-read');
+ document.getElementById('app').classList.remove('off');
+ document.getElementById('app').textContent = value.content;
+ return {trace, version, stores, value};
+ } catch (error) {
+ db?.close();
+ failPanel('internal-error');
+ return {trace, error:`${error.name}: ${error.message}`};
+ }
+ }""")
+ assert_equal(observed, {
+ "trace": ["seed:0->1", "upgrade:1->4", "migration-read", "opened", "written", "reopened-read"],
+ "version": 4, "stores": ["drawings"],
+ "value": {"uuid": "drawing", "content": "saved geometry"},
+ }, "SVG-gated IndexedDB startup, asynchronous upgrade, commit and reopen")
+ panels = await page.evaluate("""() => ({
+ hiddenErrors: ['no-indexeddb','not-supported','internal-error'].every(id =>
+ getComputedStyle(document.getElementById(id)).display === 'none'),
+ ready: !document.getElementById('app').classList.contains('off'),
+ text: document.getElementById('app').textContent,
+ })""")
+ assert_equal(panels, {"hiddenErrors": True, "ready": True, "text": "saved geometry"},
+ "application readiness, not hidden error template text")
+ record_contract(
+ results, "svg_indexeddb_application_startup",
+ contract=("SVG/Base64 feature admission and real IndexedDB upgrade/commit/reopen complete; "
+ "preexisting hidden error templates are not mistaken for a failed application."),
+ source="Reduced sketchometry/JSXGraph startup; Chromium executable calibration",
+ commands=["Runtime.evaluate", "Page.setDocumentContent"],
+ observed={"database": observed, "panels": panels},
+ )
+ finally:
+ await context.close()
diff --git a/moli-cdp-smoke/moli_cdp_smoke/runner.py b/moli-cdp-smoke/moli_cdp_smoke/runner.py
index e161ec874..872245556 100644
--- a/moli-cdp-smoke/moli_cdp_smoke/runner.py
+++ b/moli-cdp-smoke/moli_cdp_smoke/runner.py
@@ -66,6 +66,7 @@
from .groups.tracing import run_raw_tracing_group, run_tracing_group
from .groups.url_policy import run_url_policy_group
from .groups.webgl_viewport import run_webgl_viewport_group
+from .groups.svg_indexeddb_startup import run_svg_indexeddb_startup_group
from .groups.media_error import run_media_error_group
from .groups.workers import run_workers_group
from .groups.xhr_sync_semantics import run_xhr_sync_semantics_group
@@ -354,6 +355,12 @@ async def _await_group(group: SmokeGroup, awaitable: Awaitable[None]) -> None:
"browser",
run_svg_rect_group,
),
+ SmokeGroup(
+ "svg-indexeddb-startup",
+ "SVG feature detection plus database migration and application readiness.",
+ "browser",
+ run_svg_indexeddb_startup_group,
+ ),
SmokeGroup(
"media-error",
"HTMLMediaElement MediaError publication, identity, and reset lifecycle.",
diff --git a/moli-cdp-smoke/tests/test_group_selection.py b/moli-cdp-smoke/tests/test_group_selection.py
index fd40533aa..e89b5da3b 100644
--- a/moli-cdp-smoke/tests/test_group_selection.py
+++ b/moli-cdp-smoke/tests/test_group_selection.py
@@ -35,6 +35,7 @@ def test_default_runs_every_repository_managed_group(self) -> None:
self.assertIn("webgl-viewport", DEFAULT_GROUP_NAMES)
self.assertIn("svg-rect", DEFAULT_GROUP_NAMES)
self.assertIn("target-lifecycle", DEFAULT_GROUP_NAMES)
+ self.assertIn("svg-indexeddb-startup", DEFAULT_GROUP_NAMES)
self.assertIn("multi-page", DEFAULT_GROUP_NAMES)
self.assertIn("puppeteer", DEFAULT_GROUP_NAMES)
self.assertEqual(
diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs
index 61ef4d1a2..bdfed75f1 100644
--- a/moli-renderer-v8/src/context_bootstrap.rs
+++ b/moli-renderer-v8/src/context_bootstrap.rs
@@ -263,10 +263,10 @@ pub(crate) use self::image_data::{
image_data_clone_payload_from_object, is_image_data_object,
};
pub(crate) use self::indexed_db::{
- IndexedDbTaskId, discard_indexed_db_task_by_id, flush_blocked_indexed_db_requests,
- flush_indexed_db_task_by_id, flush_next_indexed_db_task, indexed_db_has_pending_tasks,
- install_worker_indexed_db_runtime_state, set_indexed_db_manager_for_context,
- set_worker_indexed_db_task_wake_for_context,
+ IndexedDbTaskId, SharedIndexedDbConnectionQueue, discard_indexed_db_task_by_id,
+ flush_indexed_db_connection_requests, flush_indexed_db_task_by_id, flush_next_indexed_db_task,
+ indexed_db_has_pending_tasks, install_worker_indexed_db_runtime_state,
+ set_indexed_db_manager_for_context, set_worker_indexed_db_task_wake_for_context,
};
pub use self::indexed_db::{
Key as IndexedDbKey, ObjectStoreOptions as IndexedDbObjectStoreOptions,
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue.rs
new file mode 100644
index 000000000..6e9f0bcfa
--- /dev/null
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue.rs
@@ -0,0 +1,395 @@
+//! One connection-admission queue for each storage-key/database-name pair.
+//!
+//! Like Chromium's ConnectionCoordinator, admission is unconditional: even an
+//! open at the current version, or a delete with no live connection, waits for
+//! earlier calls. Version checks happen at the head, never against an admission-
+//! time snapshot. A running upgrade keeps its head until its open result is
+//! dispatched; closing a connection alone cannot let the next request overtake.
+//!
+//! The Page shares this coordinator across its exact Window/realm owners.
+//! Workers use the same implementation with a context-local coordinator. V8
+//! callbacks and scheduler publication always happen outside the RefCell borrow.
+
+use super::*;
+use crate::native_bridge::WindowExecutionContextIdentity;
+use std::{cell::RefCell, rc::Rc};
+
+mod order;
+use order::ConnectionQueue;
+pub(crate) use order::ConnectionRequestId;
+
+pub(crate) type SharedIndexedDbConnectionQueue = Rc>;
+
+#[derive(Clone, Copy)]
+pub(super) enum ConnectionOperation {
+ Open(Option),
+ Delete,
+}
+
+#[derive(Clone, Copy)]
+enum ConnectionRequestState {
+ NotStarted,
+ WaitingForConnections {
+ old_version: u64,
+ new_version: Option,
+ },
+ WaitingForResult,
+}
+
+struct ConnectionRequest {
+ request: v8::Global,
+ owner: IndexedDbExecutionOwner,
+ storage_scope: IndexedDbStorageScope,
+ name: String,
+ operation: ConnectionOperation,
+ state: ConnectionRequestState,
+ manager: Option,
+ provisional_database: Option,
+ upgrade_transaction: Option,
+}
+
+impl Drop for ConnectionRequest {
+ fn drop(&mut self) {
+ // Realm retirement can cancel the head while its upgrade is pending.
+ // Only un-delivered opens own these handles. Normal terminal delivery
+ // disarms them before removal, transferring the connection to script.
+ if self.provisional_database.is_none() && self.upgrade_transaction.is_none() {
+ return;
+ }
+ let Some(manager) = self
+ .manager
+ .as_ref()
+ .and_then(WeakIndexedDbManager::upgrade)
+ else {
+ return;
+ };
+ let mut manager = manager.lock();
+ if let Some(transaction) = self.upgrade_transaction.take() {
+ let _ = manager.abort_transaction(transaction);
+ }
+ if let Some(database) = self.provisional_database.take() {
+ let _ = manager.close_database(database);
+ }
+ }
+}
+
+#[derive(Default)]
+pub(crate) struct IndexedDbConnectionQueue {
+ requests: ConnectionQueue,
+ local_drain_scheduled: bool,
+}
+
+impl IndexedDbConnectionQueue {
+ pub(crate) fn waiting_owner(&self, key: &str) -> Option {
+ let (_, request) = self.requests.head_for_key(key)?;
+ matches!(
+ request.state,
+ ConnectionRequestState::WaitingForConnections { .. }
+ )
+ .then(|| request.owner.execution_context())
+ .flatten()
+ }
+
+ pub(crate) fn retire_matching(
+ &mut self,
+ matches: impl Fn(WindowExecutionContextIdentity) -> bool,
+ ) -> Vec {
+ self.requests
+ .retire(|request| request.owner.execution_context().is_some_and(&matches))
+ .into_iter()
+ .filter_map(|id| self.requests.head(id)?.owner.execution_context())
+ .collect()
+ }
+}
+
+fn connection_queue(scope: &mut v8::PinScope<'_, '_>) -> SharedIndexedDbConnectionQueue {
+ if let Some(host) = crate::util::context_host_from_global_bridge(scope) {
+ // Clone just the independent owner before making any V8 call.
+ return host.indexed_db_connection_queue();
+ }
+ let context = scope.get_current_context();
+ if let Some(queue) = context.get_slot::>() {
+ return queue;
+ }
+ let queue = Rc::new(RefCell::new(IndexedDbConnectionQueue::default()));
+ let _ = context.set_slot(queue.clone());
+ queue
+}
+
+pub(super) fn enqueue_connection_request<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+ storage_scope: IndexedDbStorageScope,
+ name: String,
+ operation: ConnectionOperation,
+) {
+ let owner = indexed_db_typed_execution_owner(scope, request)
+ .expect("connection request must retain its accepting owner");
+ let key = database_registry_key(storage_scope.storage_key(), &name);
+ let entry = ConnectionRequest {
+ request: v8::Global::new(scope, request),
+ owner,
+ storage_scope,
+ name,
+ operation,
+ state: ConnectionRequestState::NotStarted,
+ manager: weak_indexed_db_manager_for_context(scope),
+ provisional_database: None,
+ upgrade_transaction: None,
+ };
+ let (id, is_head) = connection_queue(scope)
+ .borrow_mut()
+ .requests
+ .push(key, entry);
+ bind_indexed_db_connection_request(scope, request, id);
+ if is_head {
+ schedule_connection_drain(scope, owner);
+ }
+}
+
+fn schedule_connection_drain(scope: &mut v8::PinScope<'_, '_>, owner: IndexedDbExecutionOwner) {
+ if let Some(host) = crate::util::context_host_from_global_bridge(scope) {
+ let owner = owner
+ .execution_context()
+ .expect("Page connection request has an exact owner");
+ host.schedule_indexed_db_connection_drains([owner]);
+ return;
+ }
+ let queue = connection_queue(scope);
+ {
+ let mut queue = queue.borrow_mut();
+ if queue.local_drain_scheduled {
+ return;
+ }
+ queue.local_drain_scheduled = true;
+ }
+ let task = v8::Object::new(scope);
+ register_indexed_db_task(
+ scope,
+ task,
+ IndexedDbTaskKind::DrainConnectionRequests,
+ None,
+ );
+ enqueue_indexed_db_task(scope, task);
+}
+
+pub(super) fn wake_connection_requests(scope: &mut v8::PinScope<'_, '_>, key: &str) {
+ let queue = connection_queue(scope);
+ let owner = queue
+ .borrow()
+ .requests
+ .head_for_key(key)
+ .and_then(|(_, request)| {
+ matches!(
+ request.state,
+ ConnectionRequestState::WaitingForConnections { .. }
+ )
+ .then_some(request.owner)
+ });
+ if let Some(owner) = owner {
+ schedule_connection_drain(scope, owner);
+ }
+}
+
+pub(super) fn retain_provisional_connection<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+ database: DatabaseHandle,
+ transaction: Option,
+) {
+ let Some(id) = indexed_db_connection_request_id(scope, request) else {
+ return;
+ };
+ let queue = connection_queue(scope);
+ if let Some(entry) = queue.borrow_mut().requests.head_mut(id) {
+ entry.provisional_database = Some(database);
+ entry.upgrade_transaction = transaction;
+ }
+}
+
+pub(super) fn finish_connection_request<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+) {
+ let Some(id) = take_indexed_db_connection_request_id(scope, request) else {
+ return;
+ };
+ let successful = object_hidden_value(scope, request, INDEXED_DB_REQUEST_ERROR_SLOT)
+ .is_some_and(|error| error.is_null());
+ let queue = connection_queue(scope);
+ let (completed, next_owner) = {
+ let mut queue = queue.borrow_mut();
+ let Some((mut completed, next)) = queue.requests.finish(id) else {
+ return; // A retired owner must not finish a replacement head.
+ };
+ if successful {
+ completed.provisional_database = None;
+ completed.upgrade_transaction = None;
+ }
+ let next_owner =
+ next.and_then(|next| queue.requests.head(next).map(|request| request.owner));
+ (completed, next_owner)
+ };
+ // An error before wrapper construction still owns backend handles. Drop
+ // them before waking the next head, but outside the coordinator borrow.
+ drop(completed);
+ if let Some(owner) = next_owner {
+ schedule_connection_drain(scope, owner);
+ }
+}
+
+pub(crate) fn flush_indexed_db_connection_requests(
+ scope: &mut v8::PinScope<'_, '_>,
+ owner: Option,
+) {
+ let queue = connection_queue(scope);
+ let heads = {
+ let mut queue = queue.borrow_mut();
+ queue.local_drain_scheduled = false;
+ queue
+ .requests
+ .heads()
+ .filter_map(|(id, request)| {
+ (request.owner.execution_context() == owner
+ && !matches!(request.state, ConnectionRequestState::WaitingForResult))
+ .then_some(id)
+ })
+ .collect::>()
+ };
+ for id in heads {
+ advance_connection_request(scope, &queue, id);
+ }
+}
+
+fn advance_connection_request(
+ scope: &mut v8::PinScope<'_, '_>,
+ queue: &SharedIndexedDbConnectionQueue,
+ id: ConnectionRequestId,
+) {
+ let work = {
+ let mut queue = queue.borrow_mut();
+ let Some(entry) = queue.requests.head_mut(id) else {
+ return;
+ };
+ let state = entry.state;
+ if matches!(state, ConnectionRequestState::WaitingForResult) {
+ return;
+ }
+ entry.state = ConnectionRequestState::WaitingForResult;
+ ConnectionRequestWork {
+ request: v8::Local::new(scope, &entry.request),
+ owner: entry.owner,
+ storage_scope: entry.storage_scope.clone(),
+ name: entry.name.clone(),
+ operation: entry.operation,
+ state,
+ }
+ };
+ let owner = work.owner;
+ let previous_owner = owner.dispatch_scope().enter(scope);
+ advance_connection_request_in_owner(scope, queue, id, work);
+ owner.dispatch_scope().defer_restore(scope, previous_owner);
+}
+
+// A short-lived snapshot releases the queue borrow before any V8 callback can
+// close a connection, enqueue another request, or retire this exact owner.
+struct ConnectionRequestWork<'s> {
+ request: v8::Local<'s, v8::Object>,
+ owner: IndexedDbExecutionOwner,
+ storage_scope: IndexedDbStorageScope,
+ name: String,
+ operation: ConnectionOperation,
+ state: ConnectionRequestState,
+}
+
+fn advance_connection_request_in_owner<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ queue: &SharedIndexedDbConnectionQueue,
+ id: ConnectionRequestId,
+ work: ConnectionRequestWork<'s>,
+) {
+ let ConnectionRequestWork {
+ request,
+ owner,
+ storage_scope,
+ name,
+ operation,
+ state,
+ } = work;
+ if let Err(error) = validate_storage_bucket_scope(scope, &storage_scope) {
+ let error = request_error_object(scope, &error);
+ store_request_error(scope, request, error);
+ return;
+ }
+ let key = database_registry_key(storage_scope.storage_key(), &name);
+ match state {
+ ConnectionRequestState::NotStarted => {
+ let version = match with_indexed_db_manager(scope, |manager| {
+ manager.database_version(storage_scope.storage_key(), &name)
+ }) {
+ Ok(version) => version.unwrap_or(0),
+ Err(error) => {
+ let error = request_error_object(scope, &error);
+ store_request_error(scope, request, error);
+ return;
+ }
+ };
+ let new_version = match operation {
+ ConnectionOperation::Open(requested) => Some(requested.unwrap_or(version.max(1))),
+ ConnectionOperation::Delete => None,
+ };
+ let needs_exclusive_connection =
+ new_version.is_none_or(|requested| requested > version);
+ if needs_exclusive_connection && has_open_database_connections_for_key(scope, &key) {
+ if let Some(entry) = queue.borrow_mut().requests.head_mut(id) {
+ entry.state = ConnectionRequestState::WaitingForConnections {
+ old_version: version,
+ new_version,
+ };
+ }
+ dispatch_version_change_to_open_connections(scope, &key, version, new_version);
+ // Checking blocked in a subsequent task lets each dispatched
+ // versionchange callback's microtasks close its connection.
+ schedule_connection_drain(scope, owner);
+ return;
+ }
+ }
+ ConnectionRequestState::WaitingForConnections {
+ old_version,
+ new_version,
+ } => {
+ if has_open_database_connections_for_key(scope, &key) {
+ if let Some(entry) = queue.borrow_mut().requests.head_mut(id) {
+ entry.state = state;
+ }
+ if !object_bool_property(scope, request, INDEXED_DB_REQUEST_BLOCKED_DISPATCHED_SLOT)
+ .unwrap_or(false)
+ {
+ set_indexed_db_slot_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_BLOCKED_DISPATCHED_SLOT,
+ v8::Boolean::new(scope, true).into(),
+ );
+ let _ = dispatch_version_change_event(
+ scope,
+ request,
+ "blocked",
+ old_version,
+ new_version,
+ );
+ }
+ return;
+ }
+ }
+ ConnectionRequestState::WaitingForResult => return,
+ }
+ match operation {
+ ConnectionOperation::Open(version) => {
+ execute_open_request(scope, request, storage_scope, name, version)
+ }
+ ConnectionOperation::Delete => {
+ execute_delete_database_request(scope, request, storage_scope, name)
+ }
+ }
+}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue/order.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue/order.rs
new file mode 100644
index 000000000..8ac6a847a
--- /dev/null
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue/order.rs
@@ -0,0 +1,153 @@
+//! Native connection admission order, independent of V8 and task dispatch.
+
+use std::collections::{BTreeMap, VecDeque};
+
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub(crate) struct ConnectionRequestId(u64);
+
+pub(super) struct ConnectionQueue {
+ next_id: u64,
+ queues: BTreeMap>,
+ keys: BTreeMap,
+}
+
+impl Default for ConnectionQueue {
+ fn default() -> Self {
+ Self {
+ next_id: 0,
+ queues: BTreeMap::new(),
+ keys: BTreeMap::new(),
+ }
+ }
+}
+
+impl ConnectionQueue {
+ pub(super) fn push(&mut self, key: String, value: T) -> (ConnectionRequestId, bool) {
+ self.next_id = self
+ .next_id
+ .checked_add(1)
+ .expect("IDB connection request id overflow");
+ let id = ConnectionRequestId(self.next_id);
+ self.keys.insert(id, key.clone());
+ let queue = self.queues.entry(key).or_default();
+ let is_head = queue.is_empty();
+ queue.push_back((id, value));
+ (id, is_head)
+ }
+
+ pub(super) fn head(&self, id: ConnectionRequestId) -> Option<&T> {
+ let key = self.keys.get(&id)?;
+ let (head, value) = self.queues.get(key)?.front()?;
+ (*head == id).then_some(value)
+ }
+
+ pub(super) fn head_mut(&mut self, id: ConnectionRequestId) -> Option<&mut T> {
+ let key = self.keys.get(&id)?;
+ let (head, value) = self.queues.get_mut(key)?.front_mut()?;
+ (*head == id).then_some(value)
+ }
+
+ pub(super) fn head_for_key(&self, key: &str) -> Option<(ConnectionRequestId, &T)> {
+ self.queues
+ .get(key)?
+ .front()
+ .map(|(id, value)| (*id, value))
+ }
+
+ pub(super) fn heads(&self) -> impl Iterator- {
+ self.queues
+ .values()
+ .filter_map(|queue| queue.front())
+ .map(|(id, value)| (*id, value))
+ }
+
+ /// A callback may finish only its exact head, never a newer request whose
+ /// database name happens to be the same. Starting an upgrade does not pop
+ /// this entry: the open result still belongs to it.
+ pub(super) fn finish(
+ &mut self,
+ id: ConnectionRequestId,
+ ) -> Option<(T, Option)> {
+ let key = self.keys.get(&id)?.clone();
+ let queue = self.queues.get_mut(&key)?;
+ if queue.front()?.0 != id {
+ return None;
+ }
+ let (_, value) = queue
+ .pop_front()
+ .expect("checked nonempty connection queue");
+ self.keys.remove(&id);
+ let next = queue.front().map(|(id, _)| *id);
+ if queue.is_empty() {
+ self.queues.remove(&key);
+ }
+ Some((value, next))
+ }
+
+ /// Retiring a realm may remove both a running head and queued followers.
+ /// Return only newly exposed heads so unrelated databases are not woken.
+ pub(super) fn retire(
+ &mut self,
+ mut matches: impl FnMut(&T) -> bool,
+ ) -> Vec {
+ let mut ready = Vec::new();
+ self.queues.retain(|_, queue| {
+ let previous = queue.front().map(|(id, _)| *id);
+ queue.retain(|(id, value)| {
+ if matches(value) {
+ self.keys.remove(id);
+ false
+ } else {
+ true
+ }
+ });
+ if let Some((id, _)) = queue.front()
+ && Some(*id) != previous
+ {
+ ready.push(*id);
+ }
+ !queue.is_empty()
+ });
+ ready
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn only_the_exact_head_can_finish_and_empty_queues_are_removed() {
+ let mut queue = ConnectionQueue::default();
+ let (first, ready) = queue.push("db".into(), 'a');
+ assert!(ready);
+ let (second, ready) = queue.push("db".into(), 'b');
+ assert!(!ready);
+ assert!(queue.head(second).is_none());
+ assert!(queue.finish(second).is_none());
+ assert_eq!(queue.finish(first), Some(('a', Some(second))));
+ assert!(queue.finish(first).is_none());
+ assert_eq!(queue.head(second), Some(&'b'));
+ assert_eq!(queue.finish(second), Some(('b', None)));
+ assert!(queue.queues.is_empty());
+ assert!(queue.keys.is_empty());
+ }
+
+ #[test]
+ fn databases_have_independent_heads_and_retirement_preserves_survivor_order() {
+ let mut queue = ConnectionQueue::default();
+ let (first, _) = queue.push("db".into(), 1);
+ let (_, _) = queue.push("db".into(), 1);
+ let (survivor, _) = queue.push("db".into(), 2);
+ let (last, _) = queue.push("db".into(), 3);
+ let (independent, ready) = queue.push("other-origin-or-bucket".into(), 2);
+ assert!(ready);
+ assert_eq!(queue.retire(|owner| *owner == 1), [survivor]);
+ assert!(queue.finish(first).is_none());
+ assert_eq!(queue.head(independent), Some(&2));
+ assert_eq!(queue.finish(survivor), Some((2, Some(last))));
+ assert_eq!(queue.finish(last), Some((3, None)));
+ assert_eq!(queue.finish(independent), Some((2, None)));
+ assert!(queue.keys.is_empty());
+ }
+}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs
index 84424aaf6..4c2f11260 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env.rs
@@ -14,12 +14,14 @@ pub(in crate::context_bootstrap::indexed_db) use self::errors::{
};
pub(in crate::context_bootstrap) use self::manager::indexed_db_usage_bytes_for_storage_key;
pub(crate) use self::manager::set_indexed_db_manager_for_context;
-pub(in crate::context_bootstrap::indexed_db) use self::manager::with_indexed_db_manager;
#[cfg(test)]
pub(crate) use self::manager::{
indexed_db_manager_context_slot_present_for_test,
indexed_db_manager_isolate_slot_present_for_test,
};
+pub(in crate::context_bootstrap::indexed_db) use self::manager::{
+ weak_indexed_db_manager_for_context, with_indexed_db_manager,
+};
pub(in crate::context_bootstrap::indexed_db) use self::origin::{
current_storage_scope, origin_allows_indexed_db, storage_scope_for_current_partition,
storage_scope_for_window_execution_context,
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/manager.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/manager.rs
index 84394d545..e34a3143d 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/manager.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/env/manager.rs
@@ -6,6 +6,16 @@ use crate::context_bootstrap::indexed_db::WeakIndexedDbManager;
#[derive(Clone, Debug)]
pub(crate) struct IndexedDbManagerSlot(pub(crate) Option);
+pub(in crate::context_bootstrap::indexed_db) fn weak_indexed_db_manager_for_context(
+ scope: &mut v8::PinScope<'_, '_>,
+) -> Option {
+ scope
+ .get_current_context()
+ .get_slot::()?
+ .0
+ .clone()
+}
+
pub(crate) fn set_indexed_db_manager_for_context(
context: v8::Local<'_, v8::Context>,
manager: Option,
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs
index 05d76fe5c..1789fe34e 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/objects/creation/database.rs
@@ -55,6 +55,6 @@ pub(in crate::context_bootstrap::indexed_db) fn create_database_object<'s>(
storage_scope,
);
let _ = refresh_database_surface(scope, database);
- register_open_database_connection(scope, owner, handle, database_key, info.version, database);
+ register_open_database_connection(scope, owner, handle, database_key, database);
Some(database)
}
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 d4080aee5..0ef8a6bfb 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
@@ -5,9 +5,8 @@ mod readwrite;
pub(in crate::context_bootstrap::indexed_db) use self::connections::{
database_registry_key, dispatch_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,
+ has_open_database_connections_for_key, register_open_database_connection,
+ unregister_open_database_connection,
};
pub(in crate::context_bootstrap::indexed_db) use self::readwrite::{
has_unfinished_readwrite_transaction_for_db, readwrite_transaction_can_start,
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 461f5f403..116e3dfe1 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
@@ -19,20 +19,6 @@ pub(in crate::context_bootstrap::indexed_db) fn has_open_database_connections_fo
!local_open_database_connections_for_key(scope, key).is_empty()
}
-pub(in crate::context_bootstrap::indexed_db) fn open_database_connection_version_for_key(
- scope: &mut v8::PinScope<'_, '_>,
- key: &str,
-) -> Option {
- if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
- return unsafe { &*host_ptr }.indexed_db_open_connection_version(key);
- }
- local_open_database_connections_for_key(scope, key)
- .into_iter()
- .filter_map(|database| object_number_property(scope, database, "version"))
- .map(|version| version as u64)
- .max()
-}
-
pub(in crate::context_bootstrap::indexed_db) fn dispatch_version_change_to_open_connections(
scope: &mut v8::PinScope<'_, '_>,
key: &str,
@@ -85,7 +71,6 @@ pub(in crate::context_bootstrap::indexed_db) fn register_open_database_connectio
owner: IndexedDbExecutionOwner,
handle: DatabaseHandle,
database_key: String,
- version: u64,
database: v8::Local<'_, v8::Object>,
) {
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
@@ -99,7 +84,6 @@ pub(in crate::context_bootstrap::indexed_db) fn register_open_database_connectio
execution_context,
handle,
database_key,
- version,
database,
);
return;
@@ -111,8 +95,7 @@ pub(in crate::context_bootstrap::indexed_db) fn register_open_database_connectio
);
}
-/// Removes a connection and schedules blocked-request rechecks in every page
-/// realm that was waiting on the same database key.
+/// Removes a connection and wakes only the head connection request for its key.
///
/// Returns true when the page-owned coordinator handled the connection. Worker
/// and standalone contexts retain their realm-local fallback queue.
@@ -145,34 +128,6 @@ pub(in crate::context_bootstrap::indexed_db) fn unregister_open_database_connect
false
}
-pub(in crate::context_bootstrap::indexed_db) fn register_blocked_database_context(
- scope: &mut v8::PinScope<'_, '_>,
- database_key: String,
- owner: IndexedDbExecutionOwner,
-) {
- let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
- return;
- };
- let execution_context = owner
- .execution_context()
- .expect("Page blocked IndexedDB request must retain its exact accepting Window realm");
- unsafe { &*host_ptr }.register_indexed_db_blocked_context(database_key, execution_context);
-}
-
-pub(in crate::context_bootstrap::indexed_db) fn unregister_blocked_database_context(
- scope: &mut v8::PinScope<'_, '_>,
- database_key: &str,
- owner: IndexedDbExecutionOwner,
-) {
- let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
- return;
- };
- let execution_context = owner
- .execution_context()
- .expect("Page blocked IndexedDB request must retain its exact accepting Window realm");
- unsafe { &*host_ptr }.unregister_indexed_db_blocked_context(database_key, execution_context);
-}
-
fn local_open_database_connections_for_key<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: &str,
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs
index e825e1d42..eb2203c19 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs
@@ -1,3 +1,6 @@
+use super::connection_queue::{
+ ConnectionOperation, enqueue_connection_request, wake_connection_requests,
+};
use super::{
INDEXED_DB_DATABASE_CLOSED_SLOT, INDEXED_DB_DATABASE_KEY_SLOT,
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT,
@@ -6,24 +9,20 @@ use super::{
IndexedDbWrapperKind, KeyPath, ObjectStoreInfo, ObjectStoreOptions, TransactionMode,
abort_queued_transaction_requests, compare_idb_keys, context_host_ptr_from_global_bridge,
create_object_store_object, create_open_request_object, create_transaction_object,
- current_storage_scope, database_handle_from_value, database_registry_key, dom_exception_value,
- enqueue_blocked_delete_task, enqueue_blocked_open_task,
- enqueue_drain_blocked_open_requests_task, enqueue_indexed_db_task,
- enqueue_next_readwrite_transaction_start, enqueue_transaction_abort_task,
- enqueue_transaction_commit_task, execute_delete_database_request, execute_open_request,
- has_open_database_connections_for_key, has_unfinished_readwrite_transaction_for_db,
- indexed_db_databases_settle_task_payload, indexed_db_factory_storage_scope,
- indexed_db_runtime_factory, indexed_db_typed_execution_owner, indexed_db_typed_wrapper_is,
- object_bool_property, object_property_as_object, object_store_info_from_database_metadata,
- object_string_property, open_database_connection_version_for_key, origin_allows_indexed_db,
+ current_storage_scope, database_handle_from_value, dom_exception_value,
+ enqueue_indexed_db_task, enqueue_next_readwrite_transaction_start,
+ enqueue_transaction_abort_task, enqueue_transaction_commit_task,
+ has_unfinished_readwrite_transaction_for_db, indexed_db_databases_settle_task_payload,
+ indexed_db_factory_storage_scope, indexed_db_runtime_factory, indexed_db_typed_execution_owner,
+ indexed_db_typed_wrapper_is, object_bool_property, object_property_as_object,
+ object_store_info_from_database_metadata, object_string_property, origin_allows_indexed_db,
parse_idb_key, parse_optional_idb_key_path_member, register_indexed_db_databases_settle_task,
register_readwrite_transaction, remove_database_store_metadata, request_error_object,
set_database_store_metadata, set_indexed_db_slot_value,
- storage_scope_for_window_execution_context, store_request_error,
- sync_transaction_object_store_names_from_database, throw_type_error, transaction_db_key,
- transaction_handle_from_value, unregister_open_database_connection,
- unregister_readwrite_transaction, v8_string, v8str, validate_storage_bucket_scope,
- with_indexed_db_manager,
+ storage_scope_for_window_execution_context, sync_transaction_object_store_names_from_database,
+ throw_type_error, transaction_db_key, transaction_handle_from_value,
+ unregister_open_database_connection, unregister_readwrite_transaction, v8_string, v8str,
+ validate_storage_bucket_scope, with_indexed_db_manager,
};
mod factory;
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/delete_database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/delete_database.rs
index 718b02436..29c191f1f 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/delete_database.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/delete_database.rs
@@ -37,7 +37,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_delete_database_call
return;
};
let _ = ensure_indexed_db_runtime_state(scope);
- let origin = storage_scope.storage_key().to_owned();
let request_storage_scope = storage_scope.clone();
let Some(request) =
create_open_request_object(scope, args.this(), owner, request_storage_scope)
@@ -45,33 +44,12 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_delete_database_call
rv.set_undefined();
return;
};
- if let Err(error) = validate_storage_bucket_scope(scope, &storage_scope) {
- let error = request_error_object(scope, &error);
- store_request_error(scope, request, error);
- rv.set(request.into());
- return;
- }
- let registry_key = database_registry_key(&origin, &name);
- let has_open_connections = has_open_database_connections_for_key(scope, ®istry_key);
- let delete_blocked =
- match with_indexed_db_manager(scope, |manager| manager.database_version(&origin, &name)) {
- Ok(version) if has_open_connections => Some(
- version
- .or_else(|| open_database_connection_version_for_key(scope, ®istry_key))
- .unwrap_or(0),
- ),
- Ok(_) => None,
- Err(error) => {
- let error = request_error_object(scope, &error);
- store_request_error(scope, request, error);
- rv.set(request.into());
- return;
- }
- };
- if let Some(old_version) = delete_blocked {
- enqueue_blocked_delete_task(scope, request, &origin, &name, old_version);
- } else {
- execute_delete_database_request(scope, request, storage_scope, name);
- }
+ enqueue_connection_request(
+ scope,
+ request,
+ storage_scope,
+ name,
+ ConnectionOperation::Delete,
+ );
rv.set(request.into());
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/open.rs
index 0aaee2b41..82afa56c8 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/open.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/factory/open.rs
@@ -46,7 +46,6 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_open_callback<'s>(
scope.throw_exception(exception);
return;
};
- let origin = storage_scope.storage_key().to_owned();
let request_storage_scope = storage_scope.clone();
let _ = ensure_indexed_db_runtime_state(scope);
@@ -56,48 +55,12 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_open_callback<'s>(
rv.set_undefined();
return;
};
- if let Err(error) = validate_storage_bucket_scope(scope, &storage_scope) {
- let error = request_error_object(scope, &error);
- store_request_error(scope, request, error);
- rv.set(request.into());
- return;
- }
- let blocked_upgrade = match version {
- Some(requested_version) => {
- match with_indexed_db_manager(scope, |manager| manager.database_version(&origin, &name))
- {
- Ok(Some(existing_version))
- if requested_version > existing_version
- && has_open_database_connections_for_key(
- scope,
- &database_registry_key(&origin, &name),
- ) =>
- {
- Some((existing_version, requested_version))
- }
- Ok(_) => None,
- Err(error) => {
- let error = request_error_object(scope, &error);
- store_request_error(scope, request, error);
- rv.set(request.into());
- return;
- }
- }
- }
- None => None,
- };
- if let Some((old_version, new_version)) = blocked_upgrade {
- enqueue_blocked_open_task(
- scope,
- request,
- &origin,
- &name,
- version,
- old_version,
- new_version,
- );
- } else {
- execute_open_request(scope, request, storage_scope, name, version);
- }
+ enqueue_connection_request(
+ scope,
+ request,
+ storage_scope,
+ name,
+ ConnectionOperation::Open(version),
+ );
rv.set(request.into());
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs
index 52c93ee6f..199b783e1 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs
@@ -136,7 +136,9 @@ pub(in crate::context_bootstrap::indexed_db) fn close_indexed_db_database_connec
if let Some(handle) = database_handle_from_value(scope, database.into()) {
let _ = with_indexed_db_manager(scope, |manager| manager.close_database(handle));
}
- if !coordinated {
- enqueue_drain_blocked_open_requests_task(scope);
+ if !coordinated
+ && let Some(key) = object_string_property(scope, database, INDEXED_DB_DATABASE_KEY_SLOT)
+ {
+ wake_connection_requests(scope, &key);
}
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs
index 3651de691..b8ec4c615 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs
@@ -10,6 +10,7 @@ use super::{
use crate::util::{new_null_prototype_object, private_key, set_private_value};
mod backend;
+mod connection_queue;
mod core;
mod cursor;
mod database;
@@ -26,6 +27,7 @@ mod typed_state;
mod types;
use self::backend::*;
+use self::connection_queue::*;
use self::core::*;
use self::cursor::*;
use self::database::*;
@@ -56,9 +58,9 @@ pub(crate) use self::typed_state::IndexedDbTaskId;
pub(crate) use self::typed_state::deactivate_indexed_db_transaction_after_microtask_checkpoint;
pub(in crate::context_bootstrap::indexed_db) use self::typed_state::schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint;
-pub(crate) fn flush_blocked_indexed_db_requests(scope: &mut v8::PinScope<'_, '_>) {
- flush_drain_blocked_open_requests_task(scope);
-}
+pub(crate) use self::connection_queue::{
+ SharedIndexedDbConnectionQueue, flush_indexed_db_connection_requests,
+};
pub(in crate::context_bootstrap) use self::core::indexed_db_usage_bytes_for_storage_key;
pub(crate) use self::core::set_indexed_db_manager_for_context;
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs
index 2fe370c41..09b04e92c 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs
@@ -8,7 +8,6 @@ const INDEXED_DB_FACTORY_FIELD: &str = "moli.IndexedDb.runtime.factory";
const INDEXED_DB_FACTORY_INITIALIZED_FIELD: &str = "moli.IndexedDb.runtime.factoryInitialized";
const INDEXED_DB_TASK_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.taskQueue";
const INDEXED_DB_OPEN_DATABASES_FIELD: &str = "moli.IndexedDb.runtime.openDatabases";
-const INDEXED_DB_BLOCKED_OPEN_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.blockedOpenQueue";
const INDEXED_DB_READWRITE_TRANSACTION_QUEUE_FIELD: &str =
"moli.IndexedDb.runtime.readwriteTransactionQueue";
@@ -27,7 +26,6 @@ struct IndexedDbWorkerTaskWake {
pub(in crate::context_bootstrap::indexed_db) enum IndexedDbRuntimeArray {
TaskQueue,
OpenDatabases,
- BlockedOpenQueue,
ReadwriteTransactions,
}
@@ -36,7 +34,6 @@ impl IndexedDbRuntimeArray {
match self {
Self::TaskQueue => INDEXED_DB_TASK_QUEUE_FIELD,
Self::OpenDatabases => INDEXED_DB_OPEN_DATABASES_FIELD,
- Self::BlockedOpenQueue => INDEXED_DB_BLOCKED_OPEN_QUEUE_FIELD,
Self::ReadwriteTransactions => INDEXED_DB_READWRITE_TRANSACTION_QUEUE_FIELD,
}
}
@@ -87,16 +84,6 @@ pub(in crate::context_bootstrap::indexed_db) fn push_unique_object_to_indexed_db
}
}
-pub(in crate::context_bootstrap::indexed_db) fn indexed_db_runtime_array_contains_object(
- scope: &mut v8::PinScope<'_, '_>,
- array: IndexedDbRuntimeArray,
- object: v8::Local<'_, v8::Object>,
-) -> bool {
- indexed_db_runtime_array(scope, array)
- .map(|queue| array_contains_strict(scope, queue, object.into()))
- .unwrap_or(false)
-}
-
pub(crate) fn indexed_db_has_pending_tasks(scope: &mut v8::PinScope<'_, '_>) -> bool {
let global = scope.get_current_context().global(scope);
let Some(state) = get_private_object(scope, global, INDEXED_DB_RUNTIME_STATE_SLOT) else {
@@ -206,7 +193,6 @@ fn ensure_runtime_state_fields<'s>(
ensure_runtime_factory_field(scope, state)?;
ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::TaskQueue)?;
ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::OpenDatabases)?;
- ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::BlockedOpenQueue)?;
ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::ReadwriteTransactions)?;
Some(())
}
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 5ed6ce087..3ec4f1b6d 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs
@@ -2,44 +2,38 @@ use super::{
CursorDirection, CursorSnapshotEntry, DatabaseInfo,
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT, INDEXED_DB_PENDING_CURSOR_POSITION_SLOT,
INDEXED_DB_PENDING_CURSOR_SLOT, INDEXED_DB_PENDING_ERROR_SLOT, INDEXED_DB_PENDING_RESULT_SLOT,
- INDEXED_DB_REQUEST_BLOCKED_DISPATCHED_SLOT, INDEXED_DB_REQUEST_ERROR_SLOT,
- INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_RESULT_SLOT,
- INDEXED_DB_REQUEST_TRANSACTION_SLOT, INDEXED_DB_TRANSACTION_ABORT_DISPATCHED_SLOT,
- INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
- INDEXED_DB_TRANSACTION_COMMIT_SCHEDULED_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT,
- INDEXED_DB_TRANSACTION_HANDLE_SLOT, INDEXED_DB_TRANSACTION_PENDING_SLOT,
- INDEXED_DB_TRANSACTION_START_SCHEDULED_SLOT, INDEXED_DB_TRANSACTION_STARTED_SLOT, IndexInfo,
- IndexedDbCursorOpenOperation, IndexedDbCursorSource, IndexedDbError,
- IndexedDbPendingTransactionOperation, IndexedDbRuntimeArray, IndexedDbStorageScope,
- IndexedDbTransactionOperationInput, IndexedDbTransactionOperationKindLocals, OpenOptions,
- PreparedObjectStoreWriteError, TransactionHandle, TransactionMode,
- apply_index_collection_direction, apply_object_store_collection_direction,
- 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,
- has_open_database_connections_for_key, index_cursor_snapshot, indexed_db_blocked_task_payload,
+ INDEXED_DB_REQUEST_ERROR_SLOT, INDEXED_DB_REQUEST_READY_STATE_SLOT,
+ INDEXED_DB_REQUEST_RESULT_SLOT, INDEXED_DB_REQUEST_TRANSACTION_SLOT,
+ INDEXED_DB_TRANSACTION_ABORT_DISPATCHED_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT,
+ INDEXED_DB_TRANSACTION_ACTIVE_SLOT, INDEXED_DB_TRANSACTION_COMMIT_SCHEDULED_SLOT,
+ INDEXED_DB_TRANSACTION_FINISHED_SLOT, INDEXED_DB_TRANSACTION_HANDLE_SLOT,
+ INDEXED_DB_TRANSACTION_PENDING_SLOT, INDEXED_DB_TRANSACTION_START_SCHEDULED_SLOT,
+ INDEXED_DB_TRANSACTION_STARTED_SLOT, IndexInfo, IndexedDbCursorOpenOperation,
+ IndexedDbCursorSource, IndexedDbError, IndexedDbPendingTransactionOperation,
+ IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbTransactionOperationInput,
+ IndexedDbTransactionOperationKindLocals, OpenOptions, PreparedObjectStoreWriteError,
+ TransactionHandle, TransactionMode, apply_index_collection_direction,
+ apply_object_store_collection_direction, close_indexed_db_database_connection,
+ create_database_object, create_transaction_object, database_handle_from_value,
+ define_non_enumerable_value_property, deserialize_js_value, dispatch_idb_named_event,
+ dispatch_version_change_event, dom_exception_value, dom_string_list_values,
+ enforce_object_store_unique_constraints, flush_databases_settle_task, index_cursor_snapshot,
indexed_db_index_info, indexed_db_open_task_payload, indexed_db_request_dispatch_task_request,
indexed_db_request_transaction_object, indexed_db_runtime_array,
- indexed_db_runtime_array_contains_object, indexed_db_transaction_task_transaction,
- indexed_db_typed_execution_owner, indexed_db_typed_owner_scope,
- indexed_db_typed_task_execution_context, indexed_db_typed_task_execution_owner,
+ indexed_db_transaction_task_transaction, indexed_db_typed_execution_owner,
+ indexed_db_typed_owner_scope, indexed_db_typed_task_execution_context,
indexed_db_typed_task_id, indexed_db_typed_task_kind, indexed_db_typed_task_owner_scope,
- indexed_db_typed_task_storage_scope, key_to_js_value,
- materialize_cursor_result_in_request_realm, object_bool_property, object_hidden_value,
- object_number_property, object_property_as_object, object_store_cursor_snapshot,
- object_string_property, parse_idb_key, parse_key_or_range, parse_optional_count,
- pop_first_indexed_db_task, prepare_object_store_write,
+ key_to_js_value, materialize_cursor_result_in_request_realm, object_bool_property,
+ object_hidden_value, object_number_property, object_property_as_object,
+ object_store_cursor_snapshot, object_string_property, parse_idb_key, parse_key_or_range,
+ parse_optional_count, pop_first_indexed_db_task, prepare_object_store_write,
push_indexed_db_operation_waiting_for_start, push_object_to_indexed_db_runtime_array,
- push_unique_object_to_indexed_db_runtime_array, readwrite_transaction_can_start,
- refresh_cursor_surface, refresh_database_surface, register_blocked_database_context,
- replace_indexed_db_runtime_array, request_error_object, scan_index_entries,
- scan_object_store_entries, serialize_js_value, set_indexed_db_request_surface_value,
- set_indexed_db_slot_value, signal_worker_indexed_db_task_wake,
- storage_bucket_quota_check_for_object_store, storage_bucket_quota_check_for_transaction,
- take_indexed_db_operations_waiting_for_start, take_indexed_db_task_by_id, transaction_db_key,
- transaction_handle_from_value, unregister_blocked_database_context,
+ readwrite_transaction_can_start, refresh_cursor_surface, refresh_database_surface,
+ request_error_object, scan_index_entries, scan_object_store_entries, serialize_js_value,
+ set_indexed_db_request_surface_value, set_indexed_db_slot_value,
+ signal_worker_indexed_db_task_wake, storage_bucket_quota_check_for_object_store,
+ storage_bucket_quota_check_for_transaction, take_indexed_db_operations_waiting_for_start,
+ take_indexed_db_task_by_id, transaction_db_key, transaction_handle_from_value,
unregister_readwrite_transaction, v8_string, v8str, validate_storage_bucket_scope,
with_indexed_db_manager,
};
@@ -47,9 +41,7 @@ use crate::util::enqueue_host_microtask;
use moli_indexeddb::OpenDisposition;
use super::{
- IndexedDbTaskKind, register_indexed_db_blocked_delete_task,
- register_indexed_db_blocked_open_task, register_indexed_db_open_task,
- register_indexed_db_request_dispatch_task, register_indexed_db_task,
+ IndexedDbTaskKind, register_indexed_db_open_task, register_indexed_db_request_dispatch_task,
register_indexed_db_transaction_task, release_indexed_db_request_dispatch_refs,
release_indexed_db_transaction_dispatch_refs, unregister_indexed_db_task,
};
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs
index fddeabf15..9eba9ac95 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open.rs
@@ -1,8 +1,12 @@
use super::*;
-
-mod abort;
-mod commit;
-mod success;
+use crate::context_bootstrap::indexed_db::{
+ INDEXED_DB_DATABASE_CLOSED_SLOT,
+ schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint,
+ typed_state::{
+ IndexedDbWrapperKind, bind_indexed_db_upgrade_open, indexed_db_typed_wrapper_is,
+ take_indexed_db_upgrade_open,
+ },
+};
pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -13,6 +17,7 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
else {
return;
};
+ bind_indexed_db_upgrade_open(scope, transaction, request, database);
set_indexed_db_request_surface_value(
scope,
@@ -28,6 +33,14 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
"transaction",
transaction.into(),
);
+ let done = v8str(scope, "done").into();
+ set_indexed_db_request_surface_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_READY_STATE_SLOT,
+ "readyState",
+ done,
+ );
let _ = dispatch_version_change_event(
scope,
@@ -37,15 +50,105 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_open_task<'s>(
Some(new_version),
);
- if object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ABORTED_SLOT)
- .unwrap_or(false)
- {
- abort::finish_aborted_upgrade_open(scope, request);
- return;
+ // upgradeneeded may enqueue data requests, whose handlers can enqueue more
+ // requests or change schema. Use the ordinary transaction pending-request
+ // gate and end-of-microtask-checkpoint deactivation; committing here would
+ // publish open.success before an asynchronous migration had run.
+ schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint(scope, transaction);
+}
+
+pub(in crate::context_bootstrap::indexed_db) fn prepare_upgrade_open_result<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ transaction: v8::Local<'s, v8::Object>,
+ successful: bool,
+) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
+ let (request, database) = take_indexed_db_upgrade_open(scope, transaction)?;
+ // Blink's TransactionWillFinish boundary: the database is no longer in
+ // an upgrade before complete/abort handlers run. Keep request.transaction
+ // attached through that event; it has a different, later lifetime.
+ set_indexed_db_slot_value(
+ scope,
+ database,
+ INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
+ v8::null(scope).into(),
+ );
+ if !successful {
+ close_indexed_db_database_connection(scope, database);
}
+ Some((request, database))
+}
- if !commit::commit_upgrade_transaction(scope, request, database, transaction) {
- return;
+pub(in crate::context_bootstrap::indexed_db) fn finish_upgrade_open<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+ database: v8::Local<'s, v8::Object>,
+ successful: bool,
+) {
+ set_indexed_db_request_surface_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_TRANSACTION_SLOT,
+ "transaction",
+ v8::null(scope).into(),
+ );
+ let pending = v8str(scope, "pending").into();
+ set_indexed_db_request_surface_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_READY_STATE_SLOT,
+ "readyState",
+ pending,
+ );
+ // Publish the open result as a separate request task after transaction
+ // complete/abort, not inside that event's callback or before its microtasks.
+ if successful {
+ store_request_success(scope, request, database.into());
+ } else {
+ set_indexed_db_request_surface_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_RESULT_SLOT,
+ "result",
+ v8::undefined(scope).into(),
+ );
+ let error = dom_exception_value(
+ scope,
+ "The upgrade transaction was aborted or its connection closed.",
+ "AbortError",
+ );
+ store_request_error(scope, request, error);
+ }
+}
+
+pub(in crate::context_bootstrap::indexed_db) fn reject_closed_open_result<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+) -> bool {
+ if !indexed_db_typed_wrapper_is(scope, request, IndexedDbWrapperKind::OpenRequest) {
+ return false;
+ }
+ let Some(database) = object_hidden_value(scope, request, INDEXED_DB_PENDING_RESULT_SLOT)
+ .and_then(|value| v8::Local::::try_from(value).ok())
+ else {
+ return false;
+ };
+ if !object_bool_property(scope, database, INDEXED_DB_DATABASE_CLOSED_SLOT).unwrap_or(false) {
+ return false;
}
- success::finish_open_success(scope, request);
+ // A complete-event microtask can close the provisional connection after
+ // success is queued. Check at delivery, as IDBOpenDBRequest does in Blink.
+ set_indexed_db_request_surface_value(
+ scope,
+ request,
+ INDEXED_DB_REQUEST_RESULT_SLOT,
+ "result",
+ v8::undefined(scope).into(),
+ );
+ let error = dom_exception_value(
+ scope,
+ "The connection was closed before the open result.",
+ "AbortError",
+ );
+ store_request_error(scope, request, error);
+ true
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs
deleted file mode 100644
index b0aa9d910..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use super::*;
-
-pub(super) fn finish_aborted_upgrade_open<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
-) {
- if let Some(database) = object_property_as_object(scope, request, "result") {
- set_indexed_db_slot_value(
- scope,
- database,
- INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
- v8::null(scope).into(),
- );
- close_indexed_db_database_connection(scope, database);
- }
- let error = dom_exception_value(scope, "The upgrade transaction was aborted.", "AbortError");
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_ERROR_SLOT,
- "error",
- error,
- );
- let done = v8str(scope, "done").into();
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_READY_STATE_SLOT,
- "readyState",
- done,
- );
- let undefined = v8::undefined(scope).into();
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_RESULT_SLOT,
- "result",
- undefined,
- );
- let _ = dispatch_idb_named_event(scope, request, "error", |_, _| {});
- release_request_dispatch_refs(scope, request);
-}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs
deleted file mode 100644
index 704a0758a..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use super::*;
-
-pub(super) fn commit_upgrade_transaction<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
- database: v8::Local<'s, v8::Object>,
- transaction: v8::Local<'s, v8::Object>,
-) -> bool {
- let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else {
- return true;
- };
- let quota_commit = match storage_bucket_quota_check_for_transaction(scope, transaction) {
- Some(Ok(quota)) => Some(quota),
- Some(Err(error)) => {
- let _ = with_indexed_db_manager(scope, |manager| manager.abort_transaction(handle));
- return finish_failed_upgrade_commit(scope, request, transaction, error);
- }
- None => None,
- };
- match with_indexed_db_manager(scope, |manager| {
- if let Some(quota) = quota_commit {
- manager.commit_transaction_with_quota(handle, quota.quota_check)
- } else {
- manager.commit_transaction(handle)
- }
- }) {
- Ok(()) => {
- set_indexed_db_slot_value(
- scope,
- transaction,
- INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
- v8::Boolean::new(scope, false).into(),
- );
- set_indexed_db_slot_value(
- scope,
- transaction,
- INDEXED_DB_TRANSACTION_FINISHED_SLOT,
- v8::Boolean::new(scope, true).into(),
- );
- let _ = refresh_database_surface(scope, database);
- set_indexed_db_slot_value(
- scope,
- database,
- INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
- v8::null(scope).into(),
- );
- let _ = dispatch_idb_named_event(scope, transaction, "complete", |_, _| {});
- release_indexed_db_transaction_dispatch_refs(scope, transaction);
- true
- }
- Err(error) => finish_failed_upgrade_commit(scope, request, transaction, error),
- }
-}
-
-fn finish_failed_upgrade_commit<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
- transaction: v8::Local<'s, v8::Object>,
- error: IndexedDbError,
-) -> bool {
- let error_value = request_error_object(scope, &error);
- let _ = transaction.set(scope, v8str(scope, "error").into(), error_value);
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_ERROR_SLOT,
- "error",
- error_value,
- );
- let done = v8str(scope, "done").into();
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_READY_STATE_SLOT,
- "readyState",
- done,
- );
- let _ = dispatch_idb_named_event(scope, transaction, "error", |_, _| {});
- release_indexed_db_transaction_dispatch_refs(scope, transaction);
- let _ = dispatch_idb_named_event(scope, request, "error", |_, _| {});
- release_request_dispatch_refs(scope, request);
- false
-}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/success.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/success.rs
deleted file mode 100644
index 426127b32..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/success.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-use super::*;
-
-pub(super) fn finish_open_success<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
-) {
- let null = v8::null(scope).into();
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_ERROR_SLOT,
- "error",
- null,
- );
- let done = v8str(scope, "done").into();
- set_indexed_db_request_surface_value(
- scope,
- request,
- INDEXED_DB_REQUEST_READY_STATE_SLOT,
- "readyState",
- done,
- );
- let _ = dispatch_idb_named_event(scope, request, "success", |_, _| {});
- release_request_dispatch_refs(scope, request);
-}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request.rs
index c4725c78e..cbb2e893e 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request.rs
@@ -6,5 +6,4 @@ mod finish;
mod success;
pub(in crate::context_bootstrap::indexed_db) use self::error::flush_request_error_task;
-pub(in crate::context_bootstrap::indexed_db) use self::finish::release_request_dispatch_refs;
pub(in crate::context_bootstrap::indexed_db) use self::success::flush_request_success_task;
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/finish.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/finish.rs
index cc9478520..e327f45af 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/finish.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/finish.rs
@@ -18,6 +18,9 @@ pub(super) fn finish_request_dispatch<'s>(
) {
request_finished(scope, request);
release_request_dispatch_refs(scope, request);
+ crate::context_bootstrap::indexed_db::connection_queue::finish_connection_request(
+ scope, request,
+ );
}
fn request_finished<'s>(scope: &mut v8::PinScope<'s, '_>, request: v8::Local<'s, v8::Object>) {
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/success.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/success.rs
index 5025734cc..aaf38ff4d 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/success.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/request/success.rs
@@ -12,6 +12,9 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_request_success_task<'s>(
abort::finish_request_with_abort_error(scope, request, error);
return;
}
+ if reject_closed_open_result(scope, request) {
+ return;
+ }
let transaction = indexed_db_request_transaction_object(scope, request);
if let Some(transaction) = transaction {
set_transaction_active_for_request_event(scope, transaction);
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 adbda889b..05c7fe6db 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
@@ -51,21 +51,15 @@ fn flush_indexed_db_task<'s>(
IndexedDbTaskKind::RequestSuccess => flush_request_success_task(scope, task),
IndexedDbTaskKind::RequestError => flush_request_error_task(scope, task),
IndexedDbTaskKind::Open => flush_open_task(scope, task),
- IndexedDbTaskKind::OpenBlocked => flush_open_blocked_task(scope, task),
- IndexedDbTaskKind::DeleteBlocked => flush_delete_blocked_task(scope, task),
- IndexedDbTaskKind::DrainBlockedOpens => flush_drain_blocked_open_requests_task(scope),
+ IndexedDbTaskKind::DrainConnectionRequests => {
+ crate::context_bootstrap::indexed_db::flush_indexed_db_connection_requests(scope, None)
+ }
IndexedDbTaskKind::DatabasesSettle => flush_databases_settle_task(scope, task),
IndexedDbTaskKind::TransactionStart => flush_transaction_start_task(scope, task),
IndexedDbTaskKind::TransactionCommit => flush_transaction_commit_task(scope, task),
IndexedDbTaskKind::TransactionAbort => flush_transaction_abort_task(scope, task),
}
- if !indexed_db_runtime_array_contains_object(
- scope,
- IndexedDbRuntimeArray::BlockedOpenQueue,
- task,
- ) {
- unregister_indexed_db_task(scope, task);
- }
+ unregister_indexed_db_task(scope, task);
owner.defer_restore(scope, owner_restore);
true
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction.rs
index 7200a5dba..266d65916 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction.rs
@@ -5,3 +5,17 @@ mod commit;
pub(in crate::context_bootstrap::indexed_db) use self::abort::flush_transaction_abort_task;
pub(in crate::context_bootstrap::indexed_db) use self::commit::flush_transaction_commit_task;
+
+fn dispatch_transaction_terminal<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ transaction: v8::Local<'s, v8::Object>,
+ event: &str,
+ successful: bool,
+) {
+ let upgrade = prepare_upgrade_open_result(scope, transaction, successful);
+ let _ = dispatch_idb_named_event(scope, transaction, event, |_, _| {});
+ if let Some((request, database)) = upgrade {
+ finish_upgrade_open(scope, request, database, successful);
+ }
+ release_indexed_db_transaction_dispatch_refs(scope, transaction);
+}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs
index c2f925ffc..886f2d5bf 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/abort.rs
@@ -7,6 +7,5 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_abort_task<'s>
let Some(transaction) = indexed_db_transaction_task_transaction(scope, task) else {
return;
};
- let _ = dispatch_idb_named_event(scope, transaction, "abort", |_, _| {});
- release_indexed_db_transaction_dispatch_refs(scope, transaction);
+ dispatch_transaction_terminal(scope, transaction, "abort", false);
}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs
index 42d5dc923..4606c8c45 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs
@@ -63,8 +63,7 @@ fn finish_committed_transaction<'s>(
if let Some(db) = object_property_as_object(scope, transaction, "db") {
let _ = refresh_database_surface(scope, db);
}
- let _ = dispatch_idb_named_event(scope, transaction, "complete", |_, _| {});
- release_indexed_db_transaction_dispatch_refs(scope, transaction);
+ dispatch_transaction_terminal(scope, transaction, "complete", true);
}
fn finish_failed_commit<'s>(
@@ -72,8 +71,7 @@ fn finish_failed_commit<'s>(
transaction: v8::Local<'s, v8::Object>,
) {
finish_transaction(scope, transaction);
- let _ = dispatch_idb_named_event(scope, transaction, "error", |_, _| {});
- release_indexed_db_transaction_dispatch_refs(scope, transaction);
+ dispatch_transaction_terminal(scope, transaction, "error", false);
}
fn finish_transaction<'s>(
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 49282f413..ace070cbc 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
@@ -1,11 +1,7 @@
use super::*;
-mod blocked;
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,
-};
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
deleted file mode 100644
index 8e68d87a5..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-use super::*;
-
-mod delete_task;
-mod drain;
-mod event;
-mod open_task;
-
-pub(super) fn blocked_task_storage_scope<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) -> Option {
- indexed_db_typed_task_storage_scope(scope, task)
-}
-
-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::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
deleted file mode 100644
index f1b4be239..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use super::*;
-
-pub(in crate::context_bootstrap::indexed_db) fn flush_delete_blocked_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) {
- let Some(payload) = indexed_db_blocked_task_payload(scope, task) else {
- return;
- };
- let Some(storage_scope) = blocked_task_storage_scope(scope, task) else {
- return;
- };
- let key = database_registry_key(&payload.origin, &payload.name);
- if !has_open_database_connections_for_key(scope, &key) {
- delete::execute_delete_database_request(
- scope,
- payload.request,
- storage_scope,
- payload.name,
- );
- 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,
- task,
- );
- 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);
-}
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
deleted file mode 100644
index 19d5fece6..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use super::*;
-
-pub(in crate::context_bootstrap::indexed_db) fn flush_drain_blocked_open_requests_task(
- scope: &mut v8::PinScope<'_, '_>,
-) {
- let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::BlockedOpenQueue)
- else {
- return;
- };
- let next = v8::Array::new(scope, 0);
- for index in 0..queue.length() {
- let Some(value) = queue.get_index(scope, index) else {
- continue;
- };
- let Ok(task) = v8::Local::::try_from(value) else {
- continue;
- };
- 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 _ = next.set_index(scope, next.length(), task.into());
- } else {
- if let Some(database_key) = database_key.as_deref() {
- let owner = indexed_db_typed_task_execution_owner(scope, task)
- .expect("drained blocked task must retain its IndexedDB execution owner");
- unregister_blocked_database_context(scope, database_key, owner);
- }
- unregister_indexed_db_task(scope, task);
- }
- }
- replace_indexed_db_runtime_array(scope, IndexedDbRuntimeArray::BlockedOpenQueue, next);
-}
-
-fn try_execute_unblocked_request<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) -> bool {
- let owner = indexed_db_typed_task_owner_scope(scope, task)
- .expect("IDB blocked open/delete task should have typed owner state");
- let owner_restore = owner.enter(scope);
- let executed = try_execute_unblocked_request_in_owner_scope(scope, task);
- owner.defer_restore(scope, owner_restore);
- executed
-}
-
-fn try_execute_unblocked_request_in_owner_scope<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) -> bool {
- let Some(payload) = indexed_db_blocked_task_payload(scope, task) else {
- return false;
- };
- let key = database_registry_key(&payload.origin, &payload.name);
- if has_open_database_connections_for_key(scope, &key) {
- return false;
- }
- let Some(kind) = indexed_db_typed_task_kind(scope, task) else {
- return false;
- };
- match kind {
- IndexedDbTaskKind::OpenBlocked => {
- let Some(storage_scope) = blocked_task_storage_scope(scope, task) else {
- return false;
- };
- open::execute_open_request(
- scope,
- payload.request,
- storage_scope,
- payload.name,
- payload.version,
- );
- }
- IndexedDbTaskKind::DeleteBlocked => {
- let Some(storage_scope) = blocked_task_storage_scope(scope, task) else {
- return false;
- };
- delete::execute_delete_database_request(
- scope,
- payload.request,
- storage_scope,
- payload.name,
- );
- }
- _ => {}
- }
- true
-}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/event.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/event.rs
deleted file mode 100644
index 0328d2285..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/event.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use super::*;
-
-pub(super) fn dispatch_blocked_once<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
- old_version: u64,
- new_version: Option,
-) {
- if object_bool_property(scope, request, INDEXED_DB_REQUEST_BLOCKED_DISPATCHED_SLOT)
- .unwrap_or(false)
- {
- return;
- }
- set_indexed_db_slot_value(
- scope,
- request,
- INDEXED_DB_REQUEST_BLOCKED_DISPATCHED_SLOT,
- v8::Boolean::new(scope, true).into(),
- );
- let _ = dispatch_version_change_event(scope, request, "blocked", old_version, 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
deleted file mode 100644
index 7cf6996c2..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use super::*;
-
-pub(in crate::context_bootstrap::indexed_db) fn flush_open_blocked_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) {
- let Some(payload) = indexed_db_blocked_task_payload(scope, task) else {
- return;
- };
- let Some(new_version) = payload.new_version else {
- return;
- };
- let Some(storage_scope) = blocked_task_storage_scope(scope, task) else {
- return;
- };
- let key = database_registry_key(&payload.origin, &payload.name);
- if !has_open_database_connections_for_key(scope, &key) {
- open::execute_open_request(
- scope,
- payload.request,
- storage_scope,
- payload.name,
- payload.version,
- );
- 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,
- task,
- );
- 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(
- scope,
- payload.request,
- payload.old_version,
- Some(new_version),
- );
-}
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open.rs
index 0fda9c896..7887f872d 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/open.rs
@@ -25,6 +25,12 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_open_request<'s>(
})
}) {
Ok(opened) => {
+ crate::context_bootstrap::indexed_db::connection_queue::retain_provisional_connection(
+ scope,
+ request,
+ opened.database,
+ opened.upgrade_transaction,
+ );
let info = match info::opened_database_info(scope, &database_name, &opened) {
Ok(info) => info,
Err(error) => {
diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue.rs
index 86467b5e4..61beca55b 100644
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue.rs
+++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue.rs
@@ -1,12 +1,10 @@
use super::*;
-mod blocked;
mod open;
mod request;
mod scheduler;
mod transaction;
-pub(in crate::context_bootstrap::indexed_db) use self::blocked::*;
pub(in crate::context_bootstrap::indexed_db) use self::open::*;
pub(in crate::context_bootstrap::indexed_db) use self::request::*;
pub(in crate::context_bootstrap::indexed_db) use self::scheduler::*;
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
deleted file mode 100644
index 00140e863..000000000
--- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use super::*;
-
-pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_open_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
- origin: &str,
- name: &str,
- version: Option,
- old_version: u64,
- new_version: u64,
-) {
- let task = v8::Object::new(scope);
- register_indexed_db_blocked_open_task(
- scope,
- task,
- request,
- origin,
- name,
- version,
- old_version,
- new_version,
- );
- enqueue_indexed_db_task(scope, task);
-}
-
-pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_delete_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- request: v8::Local<'s, v8::Object>,
- origin: &str,
- name: &str,
- old_version: u64,
-) {
- let task = v8::Object::new(scope);
- register_indexed_db_blocked_delete_task(scope, task, request, origin, name, old_version);
- enqueue_indexed_db_task(scope, task);
-}
-
-pub(in crate::context_bootstrap::indexed_db) fn enqueue_drain_blocked_open_requests_task(
- scope: &mut v8::PinScope<'_, '_>,
-) {
- let task = v8::Object::new(scope);
- register_indexed_db_task(scope, task, IndexedDbTaskKind::DrainBlockedOpens, None);
- enqueue_indexed_db_task(scope, task);
-}
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 c50655119..572495fc3 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
@@ -33,9 +33,7 @@ pub(super) enum IndexedDbTaskKind {
RequestSuccess,
RequestError,
Open,
- OpenBlocked,
- DeleteBlocked,
- DrainBlockedOpens,
+ DrainConnectionRequests,
DatabasesSettle,
TransactionStart,
TransactionCommit,
@@ -49,7 +47,7 @@ pub(super) enum IndexedDbExecutionOwner {
}
impl IndexedDbExecutionOwner {
- fn dispatch_scope(self) -> crate::native_bridge::OwnerDispatchScope {
+ pub(super) fn dispatch_scope(self) -> crate::native_bridge::OwnerDispatchScope {
match self {
Self::PendingWindow(dispatch_scope) => dispatch_scope,
Self::Window(execution_context) => execution_context.dispatch_scope(),
@@ -185,9 +183,7 @@ impl IndexedDbTaskState {
IndexedDbTaskKind::RequestSuccess
| IndexedDbTaskKind::RequestError
| IndexedDbTaskKind::Open
- | IndexedDbTaskKind::OpenBlocked
- | IndexedDbTaskKind::DeleteBlocked
- | IndexedDbTaskKind::DrainBlockedOpens
+ | IndexedDbTaskKind::DrainConnectionRequests
| IndexedDbTaskKind::DatabasesSettle
| IndexedDbTaskKind::TransactionStart
| IndexedDbTaskKind::TransactionCommit
@@ -261,46 +257,6 @@ impl IndexedDbOpenTaskPayload {
}
}
-struct IndexedDbBlockedTaskPayload {
- request: v8::Global,
- origin: String,
- name: String,
- version: Option,
- old_version: u64,
- new_version: Option,
-}
-
-impl IndexedDbBlockedTaskPayload {
- fn new(
- scope: &mut v8::PinScope<'_, '_>,
- request: v8::Local<'_, v8::Object>,
- origin: impl Into,
- name: impl Into,
- version: Option,
- old_version: u64,
- new_version: Option,
- ) -> Self {
- let request: v8::Local<'_, v8::Value> = request.into();
- Self {
- request: v8::Global::new(scope, request),
- origin: origin.into(),
- name: name.into(),
- version,
- old_version,
- new_version,
- }
- }
-}
-
-pub(super) struct IndexedDbBlockedTaskPayloadLocals<'s> {
- pub(super) request: v8::Local<'s, v8::Object>,
- pub(super) origin: String,
- pub(super) name: String,
- pub(super) version: Option,
- pub(super) old_version: u64,
- pub(super) new_version: Option,
-}
-
struct IndexedDbTransactionTaskPayload {
transaction: v8::Global,
}
@@ -315,6 +271,7 @@ impl IndexedDbTransactionTaskPayload {
}
struct IndexedDbRequestLifecycleState {
+ connection_request: Option,
source: v8::Global,
transaction: v8::Global,
ready_state: String,
@@ -337,6 +294,7 @@ impl IndexedDbRequestLifecycleState {
let result: v8::Local<'_, v8::Value> = v8::undefined(scope).into();
let error: v8::Local<'_, v8::Value> = v8::null(scope).into();
Self {
+ connection_request: None,
source: v8::Global::new(scope, source),
transaction: v8::Global::new(scope, transaction),
ready_state: "pending".to_owned(),
@@ -364,6 +322,10 @@ struct IndexedDbTransactionLifecycleState {
deactivation_scheduled: bool,
operations_waiting_for_start: Vec,
db_key: Option,
+ // An upgrade open is a continuation of the versionchange transaction,
+ // not of the upgradeneeded event. Keep exact request/database identities
+ // until all request callbacks (including their microtasks) have drained.
+ upgrade_open: Option<(v8::Global, v8::Global)>,
}
impl IndexedDbTransactionLifecycleState {
@@ -381,6 +343,7 @@ impl IndexedDbTransactionLifecycleState {
deactivation_scheduled: false,
operations_waiting_for_start: Vec::new(),
db_key,
+ upgrade_open: None,
}
}
}
@@ -537,7 +500,6 @@ pub(super) struct IndexedDbRuntimeStateTable {
databases_settle_tasks: BTreeMap,
request_dispatch_tasks: BTreeMap,
open_tasks: BTreeMap,
- blocked_tasks: BTreeMap,
transaction_tasks: BTreeMap,
requests: BTreeMap,
transactions: BTreeMap,
@@ -682,6 +644,47 @@ pub(super) fn release_indexed_db_request_dispatch_refs<'s>(
request.pending_cursor_position = None;
}
+pub(super) fn bind_indexed_db_connection_request<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+ connection: ConnectionRequestId,
+) {
+ let id = indexed_db_typed_state_id(scope, request).expect("connection request has typed state");
+ let table = indexed_db_runtime_state_table_for_object(scope, request);
+ let mut table = table.borrow_mut();
+ let request = table
+ .requests
+ .get_mut(&id)
+ .expect("connection request has lifecycle state");
+ assert!(
+ request.connection_request.replace(connection).is_none(),
+ "connection request admitted twice"
+ );
+}
+
+pub(super) fn indexed_db_connection_request_id<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+) -> Option {
+ let id = indexed_db_typed_state_id(scope, request)?;
+ let table = indexed_db_runtime_state_table_for_object(scope, request);
+ table.borrow().requests.get(&id)?.connection_request
+}
+
+pub(super) fn take_indexed_db_connection_request_id<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ request: v8::Local<'s, v8::Object>,
+) -> Option {
+ let id = indexed_db_typed_state_id(scope, request)?;
+ let table = indexed_db_runtime_state_table_for_object(scope, request);
+ table
+ .borrow_mut()
+ .requests
+ .get_mut(&id)?
+ .connection_request
+ .take()
+}
+
pub(super) fn register_indexed_db_transaction_lifecycle<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
@@ -697,6 +700,45 @@ pub(super) fn register_indexed_db_transaction_lifecycle<'s>(
table.borrow_mut().transactions.insert(id, state);
}
+pub(in crate::context_bootstrap::indexed_db) fn bind_indexed_db_upgrade_open<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ transaction: v8::Local<'s, v8::Object>,
+ request: v8::Local<'s, v8::Object>,
+ database: v8::Local<'s, v8::Object>,
+) {
+ let id = indexed_db_typed_state_id(scope, transaction)
+ .expect("upgrade transaction must have typed state");
+ let table = indexed_db_runtime_state_table_for_object(scope, transaction);
+ let mut table = table.borrow_mut();
+ let state = table
+ .transactions
+ .get_mut(&id)
+ .expect("upgrade transaction must have lifecycle state");
+ debug_assert!(state.upgrade_open.is_none());
+ state.upgrade_open = Some((
+ v8::Global::new(scope, request),
+ v8::Global::new(scope, database),
+ ));
+}
+
+pub(in crate::context_bootstrap::indexed_db) fn take_indexed_db_upgrade_open<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ transaction: v8::Local<'s, v8::Object>,
+) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
+ let id = indexed_db_typed_state_id(scope, transaction)?;
+ let table = indexed_db_runtime_state_table_for_object(scope, transaction);
+ let (request, database) = table
+ .borrow_mut()
+ .transactions
+ .get_mut(&id)?
+ .upgrade_open
+ .take()?;
+ Some((
+ v8::Local::new(scope, &request),
+ v8::Local::new(scope, &database),
+ ))
+}
+
pub(in crate::context_bootstrap::indexed_db) fn schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint<
's,
>(
@@ -761,6 +803,7 @@ pub(super) fn release_indexed_db_transaction_dispatch_refs<'s>(
return;
};
transaction.operations_waiting_for_start.clear();
+ transaction.upgrade_open = None;
}
pub(super) fn push_indexed_db_operation_waiting_for_start<'s>(
@@ -931,13 +974,6 @@ pub(super) fn indexed_db_typed_task_execution_context<'s>(
.execution_context()
}
-pub(super) fn indexed_db_typed_task_execution_owner<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) -> Option {
- indexed_db_typed_task_state(scope, task).map(|state| state.owner)
-}
-
pub(super) fn indexed_db_typed_task_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
task: v8::Local<'s, v8::Object>,
@@ -1081,101 +1117,6 @@ pub(super) fn indexed_db_open_task_payload<'s>(
))
}
-fn register_indexed_db_blocked_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
- kind: IndexedDbTaskKind,
- request: v8::Local<'s, v8::Object>,
- origin: &str,
- name: &str,
- version: Option,
- old_version: u64,
- new_version: Option,
-) {
- debug_assert!(matches!(
- kind,
- IndexedDbTaskKind::OpenBlocked | IndexedDbTaskKind::DeleteBlocked
- ));
- let owner = indexed_db_typed_execution_owner(scope, request)
- .expect("IDB blocked task should have typed owner state");
- let storage_scope = indexed_db_typed_storage_scope(scope, request);
- let id = register_indexed_db_task_with_owner(scope, task, kind, owner, storage_scope);
- let payload = IndexedDbBlockedTaskPayload::new(
- scope,
- request,
- origin,
- name,
- version,
- old_version,
- new_version,
- );
- let table = indexed_db_runtime_state_table_for_object(scope, task);
- table.borrow_mut().blocked_tasks.insert(id, payload);
-}
-
-pub(super) fn register_indexed_db_blocked_open_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
- request: v8::Local<'s, v8::Object>,
- origin: &str,
- name: &str,
- version: Option,
- old_version: u64,
- new_version: u64,
-) {
- register_indexed_db_blocked_task(
- scope,
- task,
- IndexedDbTaskKind::OpenBlocked,
- request,
- origin,
- name,
- version,
- old_version,
- Some(new_version),
- );
-}
-
-pub(super) fn register_indexed_db_blocked_delete_task<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
- request: v8::Local<'s, v8::Object>,
- origin: &str,
- name: &str,
- old_version: u64,
-) {
- register_indexed_db_blocked_task(
- scope,
- task,
- IndexedDbTaskKind::DeleteBlocked,
- request,
- origin,
- name,
- None,
- old_version,
- None,
- );
-}
-
-pub(super) fn indexed_db_blocked_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();
- let payload = table.blocked_tasks.get(&id)?;
- let request = v8::Local::new(scope, &payload.request);
- Some(IndexedDbBlockedTaskPayloadLocals {
- request: v8::Local::::try_from(request).ok()?,
- origin: payload.origin.clone(),
- name: payload.name.clone(),
- version: payload.version,
- old_version: payload.old_version,
- new_version: payload.new_version,
- })
-}
-
pub(super) fn register_indexed_db_transaction_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
task: v8::Local<'s, v8::Object>,
@@ -1209,13 +1150,6 @@ pub(super) fn indexed_db_transaction_task_transaction<'s>(
v8::Local::::try_from(transaction).ok()
}
-pub(super) fn indexed_db_typed_task_storage_scope<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- task: v8::Local<'s, v8::Object>,
-) -> Option {
- indexed_db_typed_task_state(scope, task).and_then(|state| state.storage_scope)
-}
-
pub(super) fn unregister_indexed_db_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
task: v8::Local<'s, v8::Object>,
@@ -1229,7 +1163,6 @@ pub(super) fn unregister_indexed_db_task<'s>(
table.databases_settle_tasks.remove(&id);
table.request_dispatch_tasks.remove(&id);
table.open_tasks.remove(&id);
- table.blocked_tasks.remove(&id);
table.transaction_tasks.remove(&id);
}
@@ -2019,7 +1952,7 @@ mod tests {
let _ = table.upsert_task(
None,
IndexedDbTaskState::new(
- IndexedDbTaskKind::OpenBlocked,
+ IndexedDbTaskKind::Open,
IndexedDbExecutionOwner::without_execution_context(
crate::native_bridge::OwnerDispatchScope::Top,
),
@@ -2089,7 +2022,7 @@ mod tests {
let id = table.upsert_task(
None,
IndexedDbTaskState::new(
- IndexedDbTaskKind::OpenBlocked,
+ IndexedDbTaskKind::Open,
IndexedDbExecutionOwner::without_execution_context(
crate::native_bridge::OwnerDispatchScope::Top,
),
diff --git a/moli-renderer-v8/src/native_bridge/context_host/indexed_db_tasks.rs b/moli-renderer-v8/src/native_bridge/context_host/indexed_db_tasks.rs
index 7862c3b1e..bdf906ce9 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/indexed_db_tasks.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/indexed_db_tasks.rs
@@ -5,7 +5,7 @@ use super::{
use moli_indexeddb::DatabaseHandle;
use std::{
cell::RefCell,
- collections::{BTreeMap, HashMap, HashSet},
+ collections::{BTreeMap, HashSet},
};
struct IndexedDbOpenConnection {
@@ -13,7 +13,6 @@ struct IndexedDbOpenConnection {
context: v8::Global,
database: v8::Global,
database_key: String,
- version: u64,
}
pub(crate) struct IndexedDbOpenConnectionSnapshot {
@@ -31,8 +30,8 @@ pub(super) struct IndexedDbContextRetirement {
#[derive(Default)]
pub(super) struct IndexedDbContextState {
open_connections: RefCell>,
- blocked_contexts: RefCell>,
- pending_blocked_drains: RefCell>,
+ connection_queue: crate::context_bootstrap::SharedIndexedDbConnectionQueue,
+ pending_connection_drains: RefCell>,
}
impl IndexedDbContextState {
@@ -42,7 +41,6 @@ impl IndexedDbContextState {
execution_context: WindowExecutionContextIdentity,
handle: DatabaseHandle,
database_key: String,
- version: u64,
database: v8::Local<'_, v8::Object>,
) {
let previous = self.open_connections.borrow_mut().insert(
@@ -52,7 +50,6 @@ impl IndexedDbContextState {
context: v8::Global::new(scope, scope.get_current_context()),
database: v8::Global::new(scope, database),
database_key,
- version,
},
);
assert!(
@@ -78,57 +75,6 @@ impl IndexedDbContextState {
.collect()
}
- fn open_connection_version(&self, database_key: &str) -> Option {
- self.open_connections
- .borrow()
- .values()
- .filter_map(|connection| {
- (connection.database_key == database_key).then_some(connection.version)
- })
- .max()
- }
-
- fn register_blocked_context(
- &self,
- database_key: String,
- execution_context: WindowExecutionContextIdentity,
- ) {
- let mut blocked_contexts = self.blocked_contexts.borrow_mut();
- let count = blocked_contexts
- .entry((database_key, execution_context))
- .or_default();
- *count = count
- .checked_add(1)
- .expect("IndexedDB blocked request count overflow");
- }
-
- fn unregister_blocked_context(
- &self,
- database_key: &str,
- execution_context: WindowExecutionContextIdentity,
- ) {
- let key = (database_key.to_owned(), execution_context);
- let mut blocked_contexts = self.blocked_contexts.borrow_mut();
- let Some(count) = blocked_contexts.get_mut(&key) else {
- return;
- };
- assert!(*count > 0, "IndexedDB blocked request count underflow");
- *count -= 1;
- if *count == 0 {
- blocked_contexts.remove(&key);
- }
- }
-
- fn blocked_contexts_for_key(&self, database_key: &str) -> Vec {
- self.blocked_contexts
- .borrow()
- .keys()
- .filter_map(|(candidate_key, execution_context)| {
- (candidate_key == database_key).then_some(*execution_context)
- })
- .collect()
- }
-
fn unregister_open_connection(
&self,
handle: DatabaseHandle,
@@ -138,23 +84,27 @@ impl IndexedDbContextState {
};
(
true,
- self.blocked_contexts_for_key(&connection.database_key),
+ self.connection_queue
+ .borrow()
+ .waiting_owner(&connection.database_key)
+ .into_iter()
+ .collect(),
)
}
- fn reserve_blocked_drains(
+ fn reserve_connection_drains(
&self,
execution_contexts: impl IntoIterator
- ,
) -> Vec {
- let mut pending = self.pending_blocked_drains.borrow_mut();
+ let mut pending = self.pending_connection_drains.borrow_mut();
execution_contexts
.into_iter()
.filter(|execution_context| pending.insert(*execution_context))
.collect()
}
- fn finish_blocked_drain(&self, execution_context: WindowExecutionContextIdentity) {
- self.pending_blocked_drains
+ fn finish_connection_drain(&self, execution_context: WindowExecutionContextIdentity) {
+ self.pending_connection_drains
.borrow_mut()
.remove(&execution_context);
}
@@ -163,10 +113,11 @@ impl IndexedDbContextState {
&self,
should_retire: impl Fn(WindowExecutionContextIdentity) -> bool,
) -> IndexedDbContextRetirement {
- self.blocked_contexts
+ let next_owners = self
+ .connection_queue
.borrow_mut()
- .retain(|(_, candidate), _| !should_retire(*candidate));
- self.pending_blocked_drains
+ .retire_matching(&should_retire);
+ self.pending_connection_drains
.borrow_mut()
.retain(|candidate| !should_retire(*candidate));
@@ -178,7 +129,7 @@ impl IndexedDbContextState {
should_retire(connection.execution_context).then_some(*handle)
})
.collect::>();
- let mut scheduled_drains = HashSet::new();
+ let mut scheduled_drains = next_owners.into_iter().collect::>();
for handle in &retired_connections {
let (_, drain_contexts) = self.unregister_open_connection(*handle);
scheduled_drains.extend(drain_contexts);
@@ -203,20 +154,20 @@ impl IndexedDbContextState {
}
impl JsContextHost {
- fn schedule_indexed_db_blocked_drains(
+ pub(crate) fn schedule_indexed_db_connection_drains(
&self,
execution_contexts: impl IntoIterator
- ,
) -> usize {
let drains = self
.indexed_db_context_tasks
- .reserve_blocked_drains(execution_contexts);
+ .reserve_connection_drains(execution_contexts);
let mut scheduled = 0;
for execution_context in drains {
if self
.page_indexed_db_task_sender()
.send(
execution_context,
- crate::page_task_queue::RendererPageIndexedDbTaskKind::DrainBlockedOpenRequests,
+ crate::page_task_queue::RendererPageIndexedDbTaskKind::DrainConnectionRequests,
)
.is_ok()
{
@@ -227,7 +178,11 @@ impl JsContextHost {
// can ever execute; teardown must not fall back to legacy
// dispatch.
self.indexed_db_context_tasks
- .finish_blocked_drain(execution_context);
+ .finish_connection_drain(execution_context);
+ self.indexed_db_context_tasks
+ .connection_queue
+ .borrow_mut()
+ .retire_matching(|_| true);
}
}
scheduled
@@ -239,7 +194,6 @@ impl JsContextHost {
execution_context: WindowExecutionContextIdentity,
handle: DatabaseHandle,
database_key: String,
- version: u64,
database: v8::Local<'_, v8::Object>,
) {
self.indexed_db_context_tasks.register_open_connection(
@@ -247,7 +201,6 @@ impl JsContextHost {
execution_context,
handle,
database_key,
- version,
database,
);
}
@@ -261,43 +214,26 @@ impl JsContextHost {
.open_connection_snapshots(scope, database_key)
}
- pub(crate) fn indexed_db_open_connection_version(&self, database_key: &str) -> Option {
- self.indexed_db_context_tasks
- .open_connection_version(database_key)
+ pub(crate) fn indexed_db_connection_queue(
+ &self,
+ ) -> crate::context_bootstrap::SharedIndexedDbConnectionQueue {
+ self.indexed_db_context_tasks.connection_queue.clone()
}
pub(crate) fn unregister_indexed_db_open_connection(&self, handle: DatabaseHandle) -> bool {
let (removed, drain_contexts) = self
.indexed_db_context_tasks
.unregister_open_connection(handle);
- self.schedule_indexed_db_blocked_drains(drain_contexts);
+ self.schedule_indexed_db_connection_drains(drain_contexts);
removed
}
- pub(crate) fn register_indexed_db_blocked_context(
- &self,
- database_key: String,
- execution_context: WindowExecutionContextIdentity,
- ) {
- self.indexed_db_context_tasks
- .register_blocked_context(database_key, execution_context);
- }
-
- pub(crate) fn unregister_indexed_db_blocked_context(
- &self,
- database_key: &str,
- execution_context: WindowExecutionContextIdentity,
- ) {
- self.indexed_db_context_tasks
- .unregister_blocked_context(database_key, execution_context);
- }
-
- pub(crate) fn finish_indexed_db_blocked_drain(
+ pub(crate) fn finish_indexed_db_connection_drain(
&self,
execution_context: WindowExecutionContextIdentity,
) {
self.indexed_db_context_tasks
- .finish_blocked_drain(execution_context);
+ .finish_connection_drain(execution_context);
}
pub(super) fn retire_indexed_db_context(
@@ -306,7 +242,7 @@ impl JsContextHost {
) -> IndexedDbContextRetirement {
let mut retirement = self.indexed_db_context_tasks.retire_context(context_token);
let drain_contexts = std::mem::take(&mut retirement.scheduled_drains);
- self.schedule_indexed_db_blocked_drains(drain_contexts);
+ self.schedule_indexed_db_connection_drains(drain_contexts);
self.signal_page_indexed_db_task_reconsideration_if_installed();
retirement
}
@@ -317,7 +253,7 @@ impl JsContextHost {
) -> IndexedDbContextRetirement {
let mut retirement = self.indexed_db_context_tasks.retire_owner(owner);
let drain_contexts = std::mem::take(&mut retirement.scheduled_drains);
- self.schedule_indexed_db_blocked_drains(drain_contexts);
+ self.schedule_indexed_db_connection_drains(drain_contexts);
self.signal_page_indexed_db_task_reconsideration_if_installed();
retirement
}
@@ -355,14 +291,14 @@ mod tests {
}
#[test]
- fn blocked_drain_is_coalesced_per_context() {
+ fn connection_drain_is_coalesced_per_context() {
let state = IndexedDbContextState::default();
let context = identity(21);
- assert_eq!(state.reserve_blocked_drains([context]), vec![context]);
- assert!(state.reserve_blocked_drains([context]).is_empty());
- state.finish_blocked_drain(context);
- assert_eq!(state.reserve_blocked_drains([context]), vec![context]);
+ assert_eq!(state.reserve_connection_drains([context]), vec![context]);
+ assert!(state.reserve_connection_drains([context]).is_empty());
+ state.finish_connection_drain(context);
+ assert_eq!(state.reserve_connection_drains([context]), vec![context]);
}
#[test]
@@ -370,17 +306,14 @@ mod tests {
let state = IndexedDbContextState::default();
let opener = identity(31);
let popup = popup_identity(7, 9, opener.realm_token().as_u64());
- state.register_blocked_context("shared".to_owned(), opener);
- state.register_blocked_context("shared".to_owned(), popup);
assert_eq!(
- state.reserve_blocked_drains([opener, popup]),
+ state.reserve_connection_drains([opener, popup]),
vec![opener, popup]
);
let retirement = state.retire_owner(popup.owner());
assert!(retirement.retired_connections.is_empty());
- assert_eq!(state.blocked_contexts_for_key("shared"), vec![opener]);
- assert!(state.reserve_blocked_drains([opener]).is_empty());
- assert_eq!(state.reserve_blocked_drains([popup]), vec![popup]);
+ assert!(state.reserve_connection_drains([opener]).is_empty());
+ assert_eq!(state.reserve_connection_drains([popup]), vec![popup]);
}
}
diff --git a/moli-renderer-v8/src/page_task_queue/indexed_db_task.rs b/moli-renderer-v8/src/page_task_queue/indexed_db_task.rs
index a67af52da..3ed76c6b5 100644
--- a/moli-renderer-v8/src/page_task_queue/indexed_db_task.rs
+++ b/moli-renderer-v8/src/page_task_queue/indexed_db_task.rs
@@ -17,7 +17,7 @@ use super::{RendererOwnerWakeSender, RendererOwnerWakeSource, RendererPageTaskRe
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum RendererPageIndexedDbTaskKind {
RuntimeQueue(IndexedDbTaskId),
- DrainBlockedOpenRequests,
+ DrainConnectionRequests,
}
/// Exact Window/realm owner of a Page-side IndexedDB task.
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 da348bd42..2becb4358 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
@@ -22,6 +22,7 @@ fn schedule_open(page_vm: &mut PageVm, database_name: &str, marker: &str) {
const request = indexedDB.open({database_name:?}, 1);
request.onupgradeneeded = () => {{
globalThis[{marker:?}].push("upgrade");
+ Promise.resolve().then(() => globalThis[{marker:?}].push("upgrade-microtask"));
}};
request.onerror = () => {{
globalThis[{marker:?}].push(`error:${{request.error && request.error.name}}`);
@@ -53,7 +54,10 @@ fn schedule_child_open(
(() => {{
globalThis[{marker:?}] = [];
const request = indexedDB.open({database_name:?}, 1);
- request.onupgradeneeded = () => globalThis[{marker:?}].push("upgrade");
+ request.onupgradeneeded = () => {{
+ globalThis[{marker:?}].push("upgrade");
+ Promise.resolve().then(() => globalThis[{marker:?}].push("upgrade-microtask"));
+ }};
request.onerror = () => {{
globalThis[{marker:?}].push(`error:${{request.error && request.error.name}}`);
}};
@@ -112,6 +116,46 @@ async fn run_selected_indexed_db_task_for_test(
Ok(Some(IndexedDbSelectedTaskObservation { owner, kind }))
}
+/// Admission is now its own task. Advance it explicitly so tests of callback
+/// checkpoints still select exactly one callback, rather than draining work.
+async fn admit_indexed_db_connections_for_test(
+ page_vm: &mut PageVm,
+ loader: &crate::network::ResourceRequestClient,
+) -> anyhow::Result<()> {
+ let admission = run_selected_indexed_db_task_for_test(page_vm, loader)
+ .await?
+ .expect("connection admission should have a selected task");
+ assert_eq!(
+ admission.kind,
+ RendererPageIndexedDbTaskKind::DrainConnectionRequests
+ );
+ Ok(())
+}
+
+/// Set up an open-success task without treating upgrade/commit/success as one
+/// task. The upgrade's complete listener is the boundary, not a queue length
+/// or a timed wait. The caller still executes and checks exactly one task.
+async fn complete_fixture_upgrade_before_open_success(
+ page_vm: &mut PageVm,
+ loader: &crate::network::ResourceRequestClient,
+) -> anyhow::Result<()> {
+ for _ in 0..16 {
+ if page_vm
+ .vm_mut()
+ .eval_without_microtask_checkpoint_for_test(
+ "globalThis.__indexedDbFixtureUpgradeComplete === true",
+ )?
+ == "true"
+ {
+ return Ok(());
+ }
+ run_selected_indexed_db_task_for_test(page_vm, loader)
+ .await?
+ .expect("upgrade must retain a ready IndexedDB task until complete");
+ }
+ anyhow::bail!("fixture upgrade did not complete within 16 selected IndexedDB tasks")
+}
+
#[tokio::test(flavor = "current_thread")]
async fn indexed_db_task_body_leaves_reactions_and_transaction_deactivation_for_selected_completion()
{
@@ -131,6 +175,7 @@ async fn indexed_db_task_body_leaves_reactions_and_transaction_deactivation_for_
open.onupgradeneeded = () => {
open.result.createObjectStore("kv").put("value", 1);
__indexedDbTaskBodyBoundary.push("upgrade");
+ open.transaction.oncomplete = () => globalThis.__indexedDbFixtureUpgradeComplete = true;
};
open.onsuccess = () => {
__indexedDbTaskBodyBoundary.push("success");
@@ -149,6 +194,7 @@ async fn indexed_db_task_body_leaves_reactions_and_transaction_deactivation_for_
})()
"#,
)?;
+ complete_fixture_upgrade_before_open_success(&mut page_vm, &loader).await?;
page_vm
.vm_mut()
.enqueue_test_ready_runtime_script_followup();
@@ -226,6 +272,7 @@ async fn indexed_db_current_ticket_without_realm_payload_owns_only_a_checkpoint(
"missing-current-payload",
"__indexedDbMissingCurrentPayload",
);
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let task = take_indexed_db_page_task_for_test(&mut page_vm);
assert_eq!(
@@ -285,7 +332,10 @@ async fn indexed_db_selected_callback_completion_reconciles_a_created_child() {
r#"
(() => {
const open = indexedDB.open("selected-child-follow-up", 1);
- open.onupgradeneeded = () => open.result.createObjectStore("kv");
+ open.onupgradeneeded = () => {
+ open.result.createObjectStore("kv");
+ open.transaction.oncomplete = () => globalThis.__indexedDbFixtureUpgradeComplete = true;
+ };
open.onsuccess = () => {
const frame = document.createElement("iframe");
frame.id = "indexed-db-selected-child";
@@ -297,7 +347,11 @@ async fn indexed_db_selected_callback_completion_reconciles_a_created_child() {
})()
"#,
)?;
-
+ complete_fixture_upgrade_before_open_success(&mut page_vm, &loader).await?;
+ assert!(
+ !page_vm.vm().has_pending_child_navigation_commit_for_test(),
+ "upgrade completion must not execute the open-success listener"
+ );
assert!(
run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
.await?
@@ -325,6 +379,7 @@ async fn indexed_db_task_applies_real_producer_work_and_one_microtask_checkpoint
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
install_indexed_db_manager(&mut page_vm, &manager);
schedule_open(&mut page_vm, "current-owner", "__indexedDbOwnerTurn");
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let selected = run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
.await?
@@ -338,9 +393,28 @@ async fn indexed_db_task_applies_real_producer_work_and_one_microtask_checkpoint
page_vm
.vm_mut()
.eval("globalThis.__indexedDbOwnerTurn.join('|')")?,
- "upgrade|success|microtask",
+ "upgrade|upgrade-microtask",
"one authorized IDB turn must include its host-task microtask checkpoint"
);
+ run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
+ .await?
+ .expect("upgrade commit should consume a separate turn");
+ assert_eq!(
+ page_vm
+ .vm_mut()
+ .eval("globalThis.__indexedDbOwnerTurn.join('|')")?,
+ "upgrade|upgrade-microtask",
+ "commit must not dispatch open success in its own task"
+ );
+ run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
+ .await?
+ .expect("open success should consume a separate turn");
+ assert_eq!(
+ page_vm
+ .vm_mut()
+ .eval("globalThis.__indexedDbOwnerTurn.join('|')")?,
+ "upgrade|upgrade-microtask|success|microtask"
+ );
Ok::<_, anyhow::Error>(())
})
.await
@@ -359,6 +433,7 @@ async fn indexed_db_source_consumes_exactly_one_runtime_task_per_turn() {
install_indexed_db_manager(&mut page_vm, &manager);
schedule_open(&mut page_vm, "first", "__firstIndexedDbTurn");
schedule_open(&mut page_vm, "second", "__secondIndexedDbTurn");
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let first = run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
.await?
@@ -368,7 +443,7 @@ async fn indexed_db_source_consumes_exactly_one_runtime_task_per_turn() {
page_vm
.vm_mut()
.eval("JSON.stringify([__firstIndexedDbTurn, __secondIndexedDbTurn])")?,
- r#"[["upgrade","success","microtask"],[]]"#
+ r#"[["upgrade","upgrade-microtask"],[]]"#
);
let second = run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
@@ -381,7 +456,7 @@ async fn indexed_db_source_consumes_exactly_one_runtime_task_per_turn() {
page_vm
.vm_mut()
.eval("JSON.stringify([__firstIndexedDbTurn, __secondIndexedDbTurn])")?,
- r#"[["upgrade","success","microtask"],["upgrade","success","microtask"]]"#
+ r#"[["upgrade","upgrade-microtask"],["upgrade","upgrade-microtask"]]"#
);
Ok::<_, anyhow::Error>(())
})
@@ -400,6 +475,7 @@ async fn indexed_db_task_survives_document_open_in_the_same_window_realm() {
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
install_indexed_db_manager(&mut page_vm, &manager);
schedule_open(&mut page_vm, "document-open", "__documentOpenIndexedDbTurn");
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
page_vm.vm_mut().eval(
"document.open(); document.write('replacement'); \
document.close(); 'replaced'",
@@ -413,7 +489,7 @@ async fn indexed_db_task_survives_document_open_in_the_same_window_realm() {
page_vm
.vm_mut()
.eval("globalThis.__documentOpenIndexedDbTurn.join('|')")?,
- "upgrade|success|microtask",
+ "upgrade|upgrade-microtask",
"document.open() must not retire work owned by its preserved Window realm"
);
Ok::<_, anyhow::Error>(())
@@ -468,6 +544,7 @@ async fn indexed_db_rejects_a_replaced_child_realm_without_stealing_its_task() {
"retired-child-realm",
"__retiredRealmIndexedDbTurn",
);
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
page_vm
.vm_mut()
@@ -497,6 +574,7 @@ async fn indexed_db_rejects_a_replaced_child_realm_without_stealing_its_task() {
"discarding the retired realm must not consume the replacement realm's local task"
);
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let current = run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
.await?
.expect("replacement child-realm task should consume the next turn");
@@ -523,7 +601,7 @@ async fn indexed_db_rejects_a_replaced_child_realm_without_stealing_its_task() {
current_execution_context_id,
"globalThis.__replacementRealmIndexedDbTurn.join('|')",
)?,
- "upgrade|success|microtask",
+ "upgrade|upgrade-microtask",
"the central Agent checkpoint must drain reactions queued by the exact child realm"
);
Ok::<_, anyhow::Error>(())
@@ -532,6 +610,85 @@ async fn indexed_db_rejects_a_replaced_child_realm_without_stealing_its_task() {
.expect("IndexedDB child-realm replacement should use exact realm ownership");
}
+#[tokio::test(flavor = "current_thread")]
+async fn indexed_db_retired_connection_head_releases_the_next_live_realm() {
+ run_page_vm_async_test(async move {
+ // Cancel before admission, before upgradeneeded, with migration work
+ // queued, and after commit but before open.success. No sleeps: each
+ // boundary is a selected task or an observed transaction event.
+ for phase in 0..4 {
+ let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
+ let manager = crate::new_indexed_db_manager(None).expect("IndexedDB manager");
+ let (mut page_vm, _resource_source, _owner_wake_rx) =
+ page_vm_with_bound_task_sources_and_owner_wake(
+ &loader, Url::parse("https://example.com/retired-connection-head")?,
+ );
+ install_indexed_db_manager(&mut page_vm, &manager);
+ page_vm.vm_mut().eval(
+ "const frame = document.createElement('iframe'); frame.id = 'idb-head'; document.body.append(frame);",
+ )?;
+ let handle = page_vm.vm().element_handle_by_id_for_test("idb-head").unwrap();
+ let child_context = materialize_only_child_realm_execution_context_through_page_turn_for_test(
+ &mut page_vm, "idb-head",
+ )?;
+ page_vm.vm_mut().eval("globalThis.__headEvents = []; globalThis.__survivor = 'pending';")?;
+ page_vm.vm_mut().eval_in_child_default_context(child_context, r#"
+ const head = indexedDB.open('retired-head', 1);
+ head.onupgradeneeded = () => {
+ parent.__headEvents.push('upgrade');
+ head.result.createObjectStore('child').put('committed', 1);
+ head.transaction.oncomplete = () => parent.__headEvents.push('complete');
+ };
+ head.onsuccess = () => parent.__headEvents.push('unexpected-success');
+ head.onerror = () => parent.__headEvents.push('unexpected-error');
+ "#)?;
+ // Accept this in the top realm before retiring the child; it must
+ // share the child's FIFO instead of racing another initial upgrade.
+ page_vm.vm_mut().eval(r#"
+ const tail = indexedDB.open('retired-head');
+ tail.onupgradeneeded = () => tail.result.createObjectStore('parent');
+ tail.onsuccess = () => {
+ __survivor = Array.from(tail.result.objectStoreNames).join(',');
+ tail.result.close();
+ };
+ tail.onerror = () => __survivor = 'error:' + tail.error.name;
+ "#)?;
+ if phase >= 1 {
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
+ }
+ if phase >= 2 {
+ let upgrade = run_selected_indexed_db_task_for_test(&mut page_vm, &loader).await?.unwrap();
+ assert!(matches!(upgrade.kind, RendererPageIndexedDbTaskKind::RuntimeQueue(_)));
+ assert_eq!(page_vm.vm_mut().eval("__headEvents.join('|')")?, "upgrade");
+ }
+ if phase == 3 {
+ for _ in 0..8 {
+ if page_vm.vm_mut().eval("__headEvents.includes('complete')")? == "true" {
+ break;
+ }
+ run_selected_indexed_db_task_for_test(&mut page_vm, &loader).await?
+ .expect("upgrade requests must progress to a commit event");
+ }
+ assert_eq!(page_vm.vm_mut().eval("__headEvents.join('|')")?, "upgrade|complete");
+ }
+ let retired_events = page_vm.vm_mut().eval("__headEvents.join('|')")?;
+ page_vm.vm_mut().retire_child_frame_realm_for_test(handle);
+ for _ in 0..16 {
+ if page_vm.vm_mut().eval("__survivor !== 'pending'")? == "true" {
+ break;
+ }
+ run_selected_indexed_db_task_for_test(&mut page_vm, &loader).await?
+ .expect("retirement must wake the next live owner without a close() callback");
+ }
+ assert_eq!(page_vm.vm_mut().eval("__survivor")?, if phase == 3 { "child" } else { "parent" },
+ "phase {phase}: only a committed upgrade may survive retirement");
+ assert_eq!(page_vm.vm_mut().eval("__headEvents.join('|')")?, retired_events,
+ "phase {phase}: stale tasks must not dispatch the retired head's callbacks");
+ }
+ Ok::<_, anyhow::Error>(())
+ }).await.expect("retiring a connection head must preserve FIFO progress in another realm");
+}
+
#[test]
fn indexed_db_rejects_a_real_page_vm_replacement_identity_collision() {
run_page_vm_large_stack_async_test(
@@ -560,6 +717,7 @@ fn indexed_db_rejects_a_real_page_vm_replacement_identity_collision() {
"retired-page-vm",
"__retiredIndexedDbTurn",
);
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let retired_root = page_vm.document_lifecycle.identity().document;
let replacement_url = format!("{base_url}/replacement.html");
@@ -602,6 +760,7 @@ fn indexed_db_rejects_a_real_page_vm_replacement_identity_collision() {
"discarding the old root namespace must not steal the colliding local task"
);
+ admit_indexed_db_connections_for_test(&mut page_vm, &loader).await?;
let current = run_selected_indexed_db_task_for_test(&mut page_vm, &loader)
.await?
.expect("replacement producer task should consume the next turn");
@@ -620,7 +779,7 @@ fn indexed_db_rejects_a_real_page_vm_replacement_identity_collision() {
page_vm
.vm_mut()
.eval("globalThis.__replacementIndexedDbTurn.join('|')")?,
- "upgrade|success|microtask"
+ "upgrade|upgrade-microtask"
);
Ok::<_, anyhow::Error>(())
})
diff --git a/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs b/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs
index 1ddd27dfd..c9f871403 100644
--- a/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs
+++ b/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs
@@ -74,9 +74,12 @@ impl ScriptVm {
RendererPageIndexedDbTaskKind::RuntimeQueue(task_id) => {
crate::context_bootstrap::flush_indexed_db_task_by_id(scope, task_id)
}
- RendererPageIndexedDbTaskKind::DrainBlockedOpenRequests => {
- unsafe { &*host_ptr }.finish_indexed_db_blocked_drain(execution_context);
- crate::context_bootstrap::flush_blocked_indexed_db_requests(scope);
+ RendererPageIndexedDbTaskKind::DrainConnectionRequests => {
+ unsafe { &*host_ptr }.finish_indexed_db_connection_drain(execution_context);
+ crate::context_bootstrap::flush_indexed_db_connection_requests(
+ scope,
+ Some(execution_context),
+ );
true
}
};
@@ -109,8 +112,8 @@ impl ScriptVm {
RendererPageIndexedDbTaskKind::RuntimeQueue(task_id) => {
crate::context_bootstrap::discard_indexed_db_task_by_id(scope, task_id)
}
- RendererPageIndexedDbTaskKind::DrainBlockedOpenRequests => {
- unsafe { &*host_ptr }.finish_indexed_db_blocked_drain(execution_context);
+ RendererPageIndexedDbTaskKind::DrainConnectionRequests => {
+ unsafe { &*host_ptr }.finish_indexed_db_connection_drain(execution_context);
true
}
})
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 2bc59862d..693a445d6 100644
--- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs
+++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs
@@ -1,6 +1,58 @@
use super::*;
use moli_url::origin_ascii_serialization;
+#[test]
+fn indexed_db_connection_queue_and_upgrade_terminal_match_chromium() {
+ let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-connection-queue.test/");
+ vm.eval(&format!(
+ "globalThis.__connectionLifecycle = 'pending';\n({}).then(\
+ value => globalThis.__connectionLifecycle = JSON.stringify(value), \
+ error => globalThis.__connectionLifecycle = error.name + ': ' + error.message);",
+ include_str!("../../../tests/fixtures/indexeddb-connection-lifecycle.js")
+ ))
+ .expect("connection lifecycle regression should schedule");
+ let actual = vm
+ .eval_after_selected_page_tasks("globalThis.__connectionLifecycle")
+ .expect("connection lifecycle regression should drain");
+ let actual: serde_json::Value = serde_json::from_str(&actual)
+ .unwrap_or_else(|error| panic!("connection lifecycle did not finish: {actual}: {error}"));
+ let expected: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../tests/fixtures/indexeddb-connection-lifecycle.json"
+ ))
+ .expect("connection lifecycle expectation should be valid JSON");
+ assert_eq!(actual, expected);
+}
+
+#[test]
+fn indexed_db_upgrade_waits_for_requests_and_microtasks_before_open_success() {
+ let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-upgrade-drain.test/");
+ vm.eval(&format!(
+ "globalThis.__upgradeDrain = 'pending';\n({}).then(\
+ value => globalThis.__upgradeDrain = JSON.stringify(value), \
+ error => globalThis.__upgradeDrain = error.name + ': ' + error.message);",
+ include_str!("../../../tests/fixtures/indexeddb-upgrade.js")
+ ))
+ .expect("upgrade regression should schedule");
+ let result = vm
+ .eval_after_selected_page_tasks("globalThis.__upgradeDrain")
+ .expect("upgrade regression should finish");
+ let result: serde_json::Value = serde_json::from_str(&result)
+ .unwrap_or_else(|error| panic!("upgrade did not finish correctly: {result}: {error}"));
+ assert_eq!(
+ result,
+ serde_json::json!({
+ "trace": ["upgrade:done", "seed", "read:original", "request-microtask",
+ "migration-write", "microtask-write", "complete", "complete-microtask", "open-success"],
+ "transactionCleared": true,
+ "records": [{"id": 1, "text": "original"}, {"id": 2, "text": "microtask write"}],
+ "microtaskValue": "value",
+ "abortTrace": ["request-success", "abort", "open-error:AbortError"],
+ "rollback": {"oldVersion": 0, "stores": []},
+ "closedResult": "AbortError"
+ })
+ );
+}
+
#[test]
fn indexed_db_runtime_state_is_created_on_first_use_without_window_slots() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-lazy-runtime.test/");
@@ -156,18 +208,17 @@ async fn indexed_db_databases_returns_committed_name_version_snapshot() {
)
.expect("indexedDB databases list workflow should schedule");
- for _ in 0..8 {
- if vm
- .eval("String(globalThis.__indexedDbDatabasesResult !== 'pending')")
- .expect("IndexedDB databases list state should evaluate")
- == "true"
- {
- break;
- }
- wait_for_one_selected_page_task_executor_test_turn(&mut vm, &loader)
- .await
- .expect("IndexedDB databases list should advance");
- }
+ // Two sequential opens each have admission, upgrade, commit, and result
+ // tasks. Wait for the actual databases() result, not the old eight-turn
+ // assumption which stops before its separate Promise settlement task.
+ advance_page_task_executor_until_eval_equals(
+ &mut vm,
+ &loader,
+ "String(globalThis.__indexedDbDatabasesResult !== 'pending')",
+ "true",
+ "IndexedDB databases list should settle after the two committed opens",
+ )
+ .await;
let result = vm
.eval("String(globalThis.__indexedDbDatabasesResult)")
diff --git a/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.js b/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.js
new file mode 100644
index 000000000..21b07bf83
--- /dev/null
+++ b/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.js
@@ -0,0 +1,170 @@
+(async () => {
+ const requestResult = request => new Promise((resolve, reject) => {
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = event => { event.preventDefault(); reject(request.error); };
+ });
+ const exceptionName = action => {
+ try { action(); return 'unexpected-success'; }
+ catch (error) { return error.name; }
+ };
+ const openDatabase = (name, version, upgrade) => {
+ const request = version === undefined ? indexedDB.open(name) : indexedDB.open(name, version);
+ if (upgrade) request.onupgradeneeded = event => upgrade(request, event);
+ return requestResult(request);
+ };
+
+ const terminal = {};
+ for (const abort of [false, true]) {
+ const label = abort ? 'abort' : 'complete';
+ const request = indexedDB.open('connection-terminal-' + label, 1);
+ const trace = [];
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ const transaction = request.transaction;
+ db.createObjectStore('original');
+ transaction['on' + label] = () => {
+ trace.push(label);
+ terminal[label] = {
+ transactionAttachedDuringEvent: request.transaction === transaction,
+ create: exceptionName(() => db.createObjectStore('late')),
+ remove: exceptionName(() => db.deleteObjectStore('original')),
+ trace,
+ };
+ queueMicrotask(() => trace.push(label + '-microtask'));
+ };
+ if (abort) transaction.abort();
+ };
+ try {
+ const db = await requestResult(request);
+ trace.push('open-success');
+ db.close();
+ } catch (error) {
+ trace.push('open-error:' + error.name);
+ }
+ terminal[label].transactionClearedAfterResult = request.transaction === null;
+ }
+
+ const db = await openDatabase('connection-delete-order', 1);
+ const deletionOrder = [];
+ const deletion = label => requestResult(indexedDB.deleteDatabase(db.name))
+ .then(() => deletionOrder.push(label));
+ const firstDelete = deletion('delete1');
+ db.close();
+ const secondDelete = deletion('delete2');
+ await Promise.all([firstDelete, secondDelete]);
+
+ // Both calls are accepted before any event runs. The second must see the
+ // first upgrade's committed schema, not start another initial upgrade.
+ let initialUpgrades = 0;
+ const initial = openDatabase('connection-concurrent-open', 1, request => {
+ initialUpgrades++;
+ request.result.createObjectStore('records').put('retained', 1);
+ });
+ const joined = openDatabase('connection-concurrent-open', 1, () => initialUpgrades++);
+ const [first, second] = await Promise.all([initial, joined]);
+ const joinedValue = await requestResult(second.transaction('records').objectStore('records').get(1));
+ first.close();
+ second.close();
+
+ // Resolve versions at the queue head, after an earlier upgrade/delete, not
+ // at API admission while the database still has its old committed version.
+ (await openDatabase('connection-version-order', 1)).close();
+ const upgrades = [];
+ const upgraded = openDatabase('connection-version-order', 2, (_, event) => {
+ upgrades.push([event.oldVersion, event.newVersion]);
+ });
+ const implicit = openDatabase('connection-version-order');
+ const obsolete = openDatabase('connection-version-order', 1)
+ .then(db => { db.close(); return 'unexpected-success'; }, error => error.name);
+ const [upgradedDb, implicitDb, obsoleteResult] = await Promise.all([upgraded, implicit, obsolete]);
+ const implicitVersion = implicitDb.version;
+ upgradedDb.close();
+ implicitDb.close();
+
+ // A versionchange callback can close in a microtask. The blocked check must
+ // happen after that checkpoint, not immediately after the callback returns.
+ const blocker = await openDatabase('connection-microtask-close', 1);
+ const notifications = [];
+ blocker.onversionchange = event => {
+ notifications.push('versionchange:' + event.oldVersion + ':' + event.newVersion);
+ queueMicrotask(() => { notifications.push('close-microtask'); blocker.close(); });
+ };
+ const upgrade = indexedDB.open(blocker.name, 2);
+ upgrade.onblocked = () => notifications.push('unexpected-blocked');
+ upgrade.onupgradeneeded = () => notifications.push('upgrade');
+ (await requestResult(upgrade)).close();
+ notifications.push('success');
+
+ // A blocked head must not block a different database, but later requests
+ // for its own key (including opens at the old version) cannot pass it.
+ const held = await openDatabase('connection-blocked-head', 1);
+ const blockedOrder = [];
+ const head = indexedDB.open(held.name, 2);
+ head.onblocked = () => {
+ blockedOrder.push('blocked');
+ openDatabase('connection-independent', 1).then(other => {
+ blockedOrder.push('independent');
+ other.close();
+ held.close();
+ });
+ };
+ const headResult = requestResult(head).then(db => {
+ blockedOrder.push('head:2'); db.close();
+ });
+ const oldVersion = openDatabase(held.name, 1).then(
+ db => { db.close(); blockedOrder.push('unexpected-old-success'); },
+ error => blockedOrder.push('old:' + error.name),
+ );
+ const tail = openDatabase(held.name).then(db => {
+ blockedOrder.push('tail:' + db.version); db.close();
+ });
+ await Promise.all([headResult, oldVersion, tail]);
+
+ const failedHeads = {};
+ for (const failure of ['abort', 'close']) {
+ const name = 'connection-failed-head-' + failure;
+ (await openDatabase(name, 1)).close();
+ const events = [];
+ const failing = indexedDB.open(name, 2);
+ failing.onupgradeneeded = () => {
+ failing.result.createObjectStore('upgrade');
+ if (failure === 'abort') failing.transaction.abort();
+ else failing.result.close();
+ };
+ const failed = requestResult(failing).then(
+ db => { db.close(); events.push('unexpected-success'); },
+ error => events.push(error.name),
+ );
+ const recovered = openDatabase(name).then(db => {
+ events.push('reopened:' + db.version);
+ events.push('stores:' + Array.from(db.objectStoreNames).join(','));
+ db.close();
+ });
+ await Promise.all([failed, recovered]);
+ failedHeads[failure] = events;
+ }
+
+ // The upstream open-request-queue WPT's open/delete interleaving, with a
+ // no-version open at the end to witness version resolution after deletion.
+ const mixedOrder = [];
+ const mixedOpen = (version, label) => {
+ const name = 'connection-mixed-order';
+ const request = version === undefined ? indexedDB.open(name) : indexedDB.open(name, version);
+ request.onupgradeneeded = event => mixedOrder.push(label + ':upgrade:' + event.oldVersion + ':' + event.newVersion);
+ return requestResult(request).then(db => {
+ mixedOrder.push(label + ':success:' + db.version);
+ db.onversionchange = () => { mixedOrder.push(label + ':close'); db.close(); };
+ return db;
+ });
+ };
+ const mixedDelete = label => requestResult(indexedDB.deleteDatabase('connection-mixed-order'))
+ .then(() => mixedOrder.push(label));
+ const mixed = await Promise.all([
+ mixedOpen(2, 'first'), mixedDelete('delete1'),
+ mixedOpen(3, 'second'), mixedDelete('delete2'), mixedOpen(undefined, 'last'),
+ ]);
+ mixed[4].close();
+
+ return {terminal, deletionOrder, initialUpgrades, joinedValue, upgrades,
+ implicitVersion, obsoleteResult, notifications, blockedOrder, failedHeads, mixedOrder};
+})()
diff --git a/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.json b/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.json
new file mode 100644
index 000000000..44099b272
--- /dev/null
+++ b/moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.json
@@ -0,0 +1,35 @@
+{
+ "terminal": {
+ "complete": {
+ "transactionAttachedDuringEvent": true,
+ "create": "InvalidStateError",
+ "remove": "InvalidStateError",
+ "trace": ["complete", "complete-microtask", "open-success"],
+ "transactionClearedAfterResult": true
+ },
+ "abort": {
+ "transactionAttachedDuringEvent": true,
+ "create": "InvalidStateError",
+ "remove": "InvalidStateError",
+ "trace": ["abort", "abort-microtask", "open-error:AbortError"],
+ "transactionClearedAfterResult": true
+ }
+ },
+ "deletionOrder": ["delete1", "delete2"],
+ "initialUpgrades": 1,
+ "joinedValue": "retained",
+ "upgrades": [[1, 2]],
+ "implicitVersion": 2,
+ "obsoleteResult": "VersionError",
+ "notifications": ["versionchange:1:2", "close-microtask", "upgrade", "success"],
+ "blockedOrder": ["blocked", "independent", "head:2", "old:VersionError", "tail:2"],
+ "failedHeads": {
+ "abort": ["AbortError", "reopened:1", "stores:"],
+ "close": ["AbortError", "reopened:2", "stores:upgrade"]
+ },
+ "mixedOrder": [
+ "first:upgrade:0:2", "first:success:2", "first:close", "delete1",
+ "second:upgrade:0:3", "second:success:3", "second:close", "delete2",
+ "last:upgrade:0:1", "last:success:1"
+ ]
+}
diff --git a/moli-renderer-v8/tests/fixtures/indexeddb-upgrade.js b/moli-renderer-v8/tests/fixtures/indexeddb-upgrade.js
new file mode 100644
index 000000000..19cdc0553
--- /dev/null
+++ b/moli-renderer-v8/tests/fixtures/indexeddb-upgrade.js
@@ -0,0 +1,79 @@
+(async () => {
+ const requestResult = request => new Promise((resolve, reject) => {
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ const trace = [];
+ const open = indexedDB.open('upgrade-request-drain', 1);
+ open.onupgradeneeded = () => {
+ trace.push(`upgrade:${open.readyState}`);
+ const db = open.result;
+ const tx = open.transaction;
+ tx.oncomplete = () => {
+ trace.push('complete');
+ queueMicrotask(() => trace.push('complete-microtask'));
+ };
+ const store = db.createObjectStore('source', {keyPath:'id'});
+ store.put({id:1, text:'original'}).onsuccess = () => trace.push('seed');
+ store.getAll().onsuccess = event => {
+ trace.push(`read:${event.target.result[0].text}`);
+ const destination = db.createObjectStore('destination', {keyPath:'id'});
+ destination.createIndex('id', 'id', {unique:true});
+ destination.put(event.target.result[0]).onsuccess = () => trace.push('migration-write');
+ db.deleteObjectStore('source');
+ Promise.resolve().then(() => Promise.resolve()).then(() => {
+ trace.push('request-microtask');
+ destination.put({id:2, text:'microtask write'}).onsuccess = () => trace.push('microtask-write');
+ });
+ };
+ };
+ const db = await requestResult(open);
+ trace.push('open-success');
+ const transactionCleared = open.transaction === null;
+ const records = await requestResult(db.transaction('destination').objectStore('destination').getAll());
+ db.close();
+
+ // No initial request: microtasks from upgradeneeded can still add schema
+ // and requests before end-of-checkpoint deactivation starts auto-commit.
+ const micro = indexedDB.open('upgrade-microtask-only', 1);
+ micro.onupgradeneeded = () => {
+ Promise.resolve().then(() => Promise.resolve()).then(() => {
+ micro.result.createObjectStore('later').put('value', 'key');
+ });
+ };
+ const microDb = await requestResult(micro);
+ const microtaskValue = await requestResult(microDb.transaction('later').objectStore('later').get('key'));
+ microDb.close();
+
+ // Aborting from a request callback must prevent the open-success event,
+ // close the provisional connection, and roll back its version and schema.
+ const abortTrace = [];
+ const aborted = indexedDB.open('upgrade-request-abort', 2);
+ aborted.onupgradeneeded = () => {
+ const tx = aborted.transaction;
+ tx.onabort = () => abortTrace.push('abort');
+ tx.oncomplete = () => abortTrace.push('unexpected-complete');
+ aborted.result.createObjectStore('transient').put('discarded', 1).onsuccess = () => {
+ abortTrace.push('request-success');
+ tx.abort();
+ };
+ };
+ try { (await requestResult(aborted)).close(); abortTrace.push('unexpected-open-success'); }
+ catch (error) { abortTrace.push(`open-error:${error.name}`); }
+ const reopened = indexedDB.open('upgrade-request-abort', 1);
+ let rollback;
+ reopened.onupgradeneeded = event => {
+ rollback = {oldVersion:event.oldVersion, stores:Array.from(reopened.result.objectStoreNames)};
+ reopened.result.createObjectStore('retained');
+ };
+ (await requestResult(reopened)).close();
+ const closed = indexedDB.open('upgrade-close-before-success', 1);
+ closed.onupgradeneeded = () => {
+ const connection = closed.result;
+ closed.transaction.oncomplete = () => queueMicrotask(() => connection.close());
+ };
+ let closedResult;
+ try { (await requestResult(closed)).close(); closedResult = 'unexpected-success'; }
+ catch (error) { closedResult = error.name; }
+ return {trace, transactionCleared, records, microtaskValue, abortTrace, rollback, closedResult};
+})()