From faf8c14b59ceb1dcbd7af4fb5df2d08f4d024d80 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 7 Sep 2026 16:54:25 +0800 Subject: [PATCH] fix(indexeddb): finish upgrade opens after transaction completion Retain the exact open request until normal transaction request draining and checkpoint deactivation complete. Publish open success or error in a separate task, handle aborted/closed provisional connections, and preserve FIFO among waiting open/delete requests. Remove eager upgrade-completion and duplicate blocked-request dispatch paths. Update selected-task boundary regressions and add Chromium-calibrated CDP coverage for migration, microtasks, rollback, deletion order and SVG-gated application startup. --- moli-cdp-smoke/README.md | 15 + .../groups/svg_indexeddb_startup.py | 203 +++++++++ moli-cdp-smoke/moli_cdp_smoke/runner.py | 7 + moli-cdp-smoke/tests/test_group_selection.py | 1 + moli-renderer-v8/src/context_bootstrap.rs | 8 +- .../indexed_db/connection_queue.rs | 395 ++++++++++++++++++ .../indexed_db/connection_queue/order.rs | 153 +++++++ .../context_bootstrap/indexed_db/core/env.rs | 4 +- .../indexed_db/core/env/manager.rs | 10 + .../core/objects/creation/database.rs | 2 +- .../indexed_db/core/registry.rs | 5 +- .../indexed_db/core/registry/connections.rs | 47 +-- .../context_bootstrap/indexed_db/database.rs | 29 +- .../database/factory/delete_database.rs | 36 +- .../indexed_db/database/factory/open.rs | 51 +-- .../database/transactions/database.rs | 6 +- .../src/context_bootstrap/indexed_db/mod.rs | 8 +- .../context_bootstrap/indexed_db/runtime.rs | 14 - .../src/context_bootstrap/indexed_db/tasks.rs | 66 ++- .../indexed_db/tasks/dispatch/open.rs | 127 +++++- .../indexed_db/tasks/dispatch/open/abort.rs | 42 -- .../indexed_db/tasks/dispatch/open/commit.rs | 83 ---- .../indexed_db/tasks/dispatch/open/success.rs | 25 -- .../indexed_db/tasks/dispatch/request.rs | 1 - .../tasks/dispatch/request/finish.rs | 3 + .../tasks/dispatch/request/success.rs | 3 + .../indexed_db/tasks/dispatch/router.rs | 14 +- .../indexed_db/tasks/dispatch/transaction.rs | 14 + .../tasks/dispatch/transaction/abort.rs | 3 +- .../tasks/dispatch/transaction/commit.rs | 6 +- .../indexed_db/tasks/open_delete.rs | 4 - .../indexed_db/tasks/open_delete/blocked.rs | 17 - .../tasks/open_delete/blocked/delete_task.rs | 42 -- .../tasks/open_delete/blocked/drain.rs | 87 ---- .../tasks/open_delete/blocked/event.rs | 21 - .../tasks/open_delete/blocked/open_task.rs | 57 --- .../indexed_db/tasks/open_delete/open.rs | 6 + .../indexed_db/tasks/queue.rs | 2 - .../indexed_db/tasks/queue/blocked.rs | 44 -- .../indexed_db/typed_state.rs | 253 +++++------ .../context_host/indexed_db_tasks.rs | 151 ++----- .../src/page_task_queue/indexed_db_task.rs | 2 +- .../src/runtime/page_vm/tests/indexed_db.rs | 177 +++++++- .../src/script_vm/indexed_db_task_body.rs | 13 +- .../src/script_vm/tests/indexed_db.rs | 75 +++- .../indexeddb-connection-lifecycle.js | 170 ++++++++ .../indexeddb-connection-lifecycle.json | 35 ++ .../tests/fixtures/indexeddb-upgrade.js | 79 ++++ 48 files changed, 1668 insertions(+), 948 deletions(-) create mode 100644 moli-cdp-smoke/moli_cdp_smoke/groups/svg_indexeddb_startup.py create mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/connection_queue/order.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/abort.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/commit.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/open/success.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/delete_task.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/drain.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/event.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/open_delete/blocked/open_task.rs delete mode 100644 moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/blocked.rs create mode 100644 moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.js create mode 100644 moli-renderer-v8/tests/fixtures/indexeddb-connection-lifecycle.json create mode 100644 moli-renderer-v8/tests/fixtures/indexeddb-upgrade.js 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}; +})()