From 9089ced8ff26e438ee71e28d5e4f1dc31ab9f09a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 17 Sep 2026 07:18:36 +0800 Subject: [PATCH] fix(indexeddb): abort invalid unique index builds in request order Validate stored keys when creating a unique index and queue its failure as an owned transaction operation. Preserve earlier request successes, abort pending requests, and roll back upgrade metadata and data without throwing from createIndex. Later mutations cannot erase the captured failure. Deduplicate multiEntry keys within each record in shared key extraction so index creation, writes, queries and cursors agree on uniqueness. Cover Window, iframe and Worker execution, persisted upgrades, explicit commit/abort, rollback, request event ordering and multiEntry duplicates. The 102-case official WPT sample improves from 88 to 92 passes without regressions; createIndex also gains two passing subtests per environment. --- .../wpt-cross-current/passed-cases.txt | 4 + .../fixtures/indexeddb-index-build.js | 238 ++++++++++++++++++ .../tests/web_apis/indexed_db_transaction.rs | 21 ++ .../indexed_db/core/keys/scan.rs | 4 +- .../indexed_db/core/keys/scan/constraints.rs | 34 ++- .../indexed_db/core/keys/write/extraction.rs | 5 +- .../object_store/schema/create_index.rs | 12 +- .../indexed_db/tasks/dispatch/router.rs | 3 + .../indexed_db/tasks/dispatch/transaction.rs | 20 ++ .../indexed_db/tasks/queue/transaction.rs | 15 ++ .../indexed_db/typed_state.rs | 43 +++- 11 files changed, 386 insertions(+), 13 deletions(-) create mode 100644 moli-core/tests/web_apis/fixtures/indexeddb-index-build.js diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index cb641c6f1a..83eec0fd7e 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -33,6 +33,8 @@ IndexedDB/idbdatabase_transaction.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbdatabase_transaction.any.js?moli-wpt-any=window IndexedDB/idbfactory-open-error-properties.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbfactory-open-error-properties.any.js?moli-wpt-any=window +IndexedDB/idbfactory-open-request-error.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbfactory-open-request-error.any.js?moli-wpt-any=window IndexedDB/idbfactory-open-request-success.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbfactory-open-request-success.any.js?moli-wpt-any=window IndexedDB/idbfactory-origin-isolation.html @@ -46,6 +48,8 @@ IndexedDB/idbrequest_error.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbrequest_error.any.js?moli-wpt-any=window IndexedDB/idbrequest_result.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbrequest_result.any.js?moli-wpt-any=window +IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=dedicatedworker +IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=window IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=window IndexedDB/open-request-queue.any.js?moli-wpt-any=dedicatedworker diff --git a/moli-core/tests/web_apis/fixtures/indexeddb-index-build.js b/moli-core/tests/web_apis/fixtures/indexeddb-index-build.js new file mode 100644 index 0000000000..713ac264d9 --- /dev/null +++ b/moli-core/tests/web_apis/fixtures/indexeddb-index-build.js @@ -0,0 +1,238 @@ +globalThis.indexBuildProbe = async function (name = "idb-index-build") { + const checks = globalThis.indexBuildChecks = []; + const check = (label, pass, actual) => checks.push({ label, pass, actual }); + const same = (label, actual, expected) => check(label, + JSON.stringify(actual) === JSON.stringify(expected), actual); + const connections = []; + const open = (suffix, version, upgrade) => new Promise((resolve, reject) => { + const request = indexedDB.open(`${name}-${suffix}`, version); + request.onupgradeneeded = () => { + connections.push(request.result); + try { upgrade(request.result, request.transaction); } + catch (error) { request.transaction.abort(); reject(error); } + }; + request.onerror = () => reject(request.error); + request.onsuccess = () => { + connections.push(request.result); + resolve(request.result); + }; + }); + const result = request => new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const finished = tx => new Promise(resolve => { + tx.oncomplete = () => resolve("complete"); + tx.onabort = () => resolve("abort"); + }); + const throws = (label, expected, callback) => { + try { callback(); check(label, false, "accepted"); } + catch (error) { same(label, error.name, expected); } + }; + const failedBuild = async (label, { persisted = false, keyPath = "group", + multiEntry = false, records = [{ group: "same", rank: 1 }, { group: "same", rank: 1 }], + after = () => {}, explicitAbort = false } = {}) => { + if (persisted) { + const db = await open(label, 1, db => { + const store = db.createObjectStore("s"); + store.createIndex("original", "rank"); + records.forEach((value, i) => store.put(value, i + 1)); + }); + db.close(); + } + const log = []; + const requests = []; + let tx, store, index, db; + const outcome = await new Promise((resolve, reject) => { + const request = indexedDB.open(`${name}-${label}`, persisted ? 2 : 1); + request.onupgradeneeded = () => { + try { + db = request.result; + connections.push(db); + tx = request.transaction; + tx.oncomplete = () => log.push("tx:complete"); + tx.onabort = () => log.push(`tx:abort:${tx.error?.name || "null"}`); + db.onabort = () => log.push(`db:abort:${tx.error?.name || "null"}`); + tx.onerror = event => log.push(`tx:error:${event.target.error.name}`); + db.onerror = event => log.push(`db:error:${event.target.error.name}`); + store = persisted ? tx.objectStore("s") : db.createObjectStore("s"); + const watch = (request, prefix, precedesIndex = false) => { + requests.push({ request, prefix, precedesIndex }); + request.onsuccess = () => log.push(`${prefix}:success`); + request.onerror = event => { + log.push(`${prefix}:error:${request.error.name}`); + // Canceling pending request errors cannot rescue an aborted index build. + event.preventDefault(); + }; + return request; + }; + if (!persisted) records.forEach((value, i) => watch(store.put(value, i + 1), `put${i}`, true)); + index = store.createIndex("unique", keyPath, { unique: true, multiEntry }); + check(`${label}:returns-index`, index instanceof IDBIndex && index.objectStore === store && + index.unique && index.multiEntry === multiEntry, index.name); + check(`${label}:synchronous-metadata`, store.indexNames.contains("unique") && tx.error === null, + Array.from(store.indexNames)); + after({ db, tx, store, index, watch }); + log.push("upgrade:return"); + } catch (error) { reject(error); } + }; + request.onerror = event => { + event.preventDefault(); + log.push(`open:error:${request.error.name}`); + check(`${label}:open-transaction-cleared`, request.transaction === null, request.transaction); + resolve("error"); + }; + request.onsuccess = () => { + connections.push(request.result); + log.push("open:success"); + resolve("success"); + }; + }); + same(`${label}:open-fails`, outcome, "error"); + same(`${label}:transaction-error`, tx.error?.name || null, explicitAbort ? null : "ConstraintError"); + check(`${label}:error-realm`, explicitAbort ? tx.error === null : tx.error instanceof DOMException, + tx.error?.constructor.name); + check(`${label}:abort-before-open-error`, log.at(-3) === `tx:abort:${explicitAbort ? "null" : "ConstraintError"}` && + log.at(-2) === `db:abort:${explicitAbort ? "null" : "ConstraintError"}` && + log.at(-1) === "open:error:AbortError", log); + for (const { request, prefix, precedesIndex } of requests) { + const success = precedesIndex && !explicitAbort; + check(`${label}:${prefix}:result`, request.readyState === "done" && + (success ? request.error === null : request.error?.name === "AbortError"), + request.error?.name || "success"); + } + same(`${label}:version-rollback`, db.version, persisted ? 1 : 0); + throws(`${label}:index-invalid-after-abort`, "InvalidStateError", () => index.get("same")); + if (label === "ordered") { + same("ordered:event-order", log, ["upgrade:return", "put0:success", "put1:success", + "late:error:AbortError", "tx:error:AbortError", "db:error:AbortError", + "tx:abort:ConstraintError", "db:abort:ConstraintError", "open:error:AbortError"]); + } + if (persisted) { + db.close(); + const restored = await open(label, 1, () => check(`${label}:unexpected-upgrade`, false)); + const read = restored.transaction("s"); + const done = finished(read); + const store = read.objectStore("s"); + same(`${label}:metadata-rollback`, Array.from(store.indexNames), ["original"]); + same(`${label}:data-rollback`, await result(store.getAll()), records); + same(`${label}:read-complete`, await done, "complete"); + restored.close(); + } + }; + try { + await failedBuild("ordered", { after: ({ store, watch }) => watch(store.put({ group: "same" }, 3), "late") }); + await failedBuild("clear", { after: ({ store, watch }) => watch(store.clear(), "clear") }); + await failedBuild("delete-index", { after: ({ store, watch }) => { + store.deleteIndex("unique"); + watch(store.put({ group: "different" }, 2), "replace"); + store.createIndex("unique", "rank"); + } }); + await failedBuild("delete-store", { after: ({ db }) => db.deleteObjectStore("s") }); + await failedBuild("multiple-failures", { after: ({ store }) => store.createIndex("other", "group", { unique: true }) }); + await failedBuild("compound", { keyPath: ["group", "rank"] }); + await failedBuild("multientry-conflict", { multiEntry: true, + records: [{ group: ["same", "same"] }, { group: ["same"] }] }); + await failedBuild("explicit-abort", { explicitAbort: true, after: ({ tx }) => tx.abort() }); + await failedBuild("persisted", { persisted: true }); + await failedBuild("commit", { persisted: true, after: ({ tx }) => tx.commit() }); + await failedBuild("persisted-clear", { persisted: true, after: ({ store, watch }) => watch(store.clear(), "clear") }); + + for (const unique of [false, true]) { + for (const before of [false, true]) { + const label = `multi-${unique}-${before}`; + const db = await open(label, 1, db => { + const store = db.createObjectStore("s", { keyPath: "id" }); + const create = () => store.createIndex("tags", "tags", { multiEntry: true, unique }); + if (!before) create(); + store.put({ id: 1, tags: [1, 1, "a", "a", ["a", 2], ["a", 2], null, undefined, {}, NaN] }); + store.put({ id: 2, tags: [3, 3, "b", "b"] }); + store.put({ id: 3, tags: [] }); + store.put({ id: 4 }); + store.put({ id: 5, tags: "scalar" }); + if (before) create(); + }); + const tx = db.transaction("s"); + const done = finished(tx); + const index = tx.objectStore("s").index("tags"); + const values = await Promise.all([ + result(index.count()), result(index.count("a")), result(index.getAllKeys()), + result(index.getAll("a")), result(index.getAllKeys(["a", 2])) + ]); + same(`${label}:count`, values[0], 6); + same(`${label}:one-entry-per-record`, values[1], 1); + same(`${label}:keys`, values[2], [1, 2, 1, 2, 5, 1]); + same(`${label}:get-all-once`, values[3].map(record => record.id), [1]); + same(`${label}:array-key-equality`, values[4], [1]); + same(`${label}:read-complete`, await done, "complete"); + + const cursorTx = db.transaction("s"); + const cursorDone = finished(cursorTx); + const keys = await new Promise((resolve, reject) => { + const entries = []; + const request = cursorTx.objectStore("s").index("tags").openKeyCursor(); + request.onerror = () => reject(request.error); + request.onsuccess = () => { + const cursor = request.result; + if (!cursor) { resolve(entries); return; } + entries.push([cursor.key, cursor.primaryKey]); + cursor.continue(); + }; + }); + same(`${label}:cursor-no-duplicates`, keys, [[1, 1], [3, 2], ["a", 1], ["b", 2], ["scalar", 5], [["a", 2], 1]]); + same(`${label}:cursor-complete`, await cursorDone, "complete"); + + const write = db.transaction("s", "readwrite"); + const written = finished(write); + const store = write.objectStore("s"); + // Replacing the same primary key is allowed even in a unique index. + const saved = await Promise.all([ + result(store.put({ id: 1, tags: ["a", "a", ["a", 2], ["a", 2]] })), + result(store.add({ id: 6, tags: unique ? ["c", "c"] : ["a", "a"] })) + ]); + same(`${label}:write-results`, saved, [1, 6]); + same(`${label}:write-complete`, await written, "complete"); + + if (unique) { + const conflict = db.transaction("s", "readwrite"); + const settled = finished(conflict); + const conflicting = conflict.objectStore("s").add({ id: 7, tags: ["a", "a"] }); + const error = await new Promise(resolve => { + conflicting.onsuccess = () => resolve(null); + conflicting.onerror = event => { event.preventDefault(); resolve(conflicting.error.name); }; + }); + same(`${label}:cross-record-conflict`, error, "ConstraintError"); + same(`${label}:canceled-write-error`, await settled, "complete"); + } + const verify = db.transaction("s"); + const verified = finished(verify); + const current = verify.objectStore("s"); + const verifiedValues = await Promise.all([result(current.index("tags").getAllKeys("a")), result(current.get(7))]); + same(`${label}:updated-keys`, verifiedValues[0], unique ? [1] : [1, 6]); + check(`${label}:failed-write-absent`, verifiedValues[1] === undefined, verifiedValues[1]); + same(`${label}:verify-complete`, await verified, "complete"); + db.close(); + } + } + + for (const unique of [false, true]) { + const db = await open(`missing-${unique}`, 1, db => { + const store = db.createObjectStore("s"); + const records = [{}, {}, { group: null }, { group: null }, { group: {} }, { group: NaN }, + { group: 1 }, { group: "1" }, { group: [] }, { group: [1, 2] }]; + if (!unique) records.push({ group: 1 }); + records.forEach((value, i) => store.put(value, i)); + store.createIndex("index", "group", { unique }); + }); + const tx = db.transaction("s"); + const done = finished(tx); + const keys = await result(tx.objectStore("s").index("index").getAllKeys()); + same(`missing-${unique}:only-valid-keys`, keys, unique ? [6, 7, 8, 9] : [6, 10, 7, 8, 9]); + same(`missing-${unique}:complete`, await done, "complete"); + db.close(); + } + return { state: checks.every(check => check.pass) ? "pass" : "fail", checks }; + } finally { + for (const db of connections) db.close(); + } +}; diff --git a/moli-core/tests/web_apis/indexed_db_transaction.rs b/moli-core/tests/web_apis/indexed_db_transaction.rs index 5f0d085113..29c61e275b 100644 --- a/moli-core/tests/web_apis/indexed_db_transaction.rs +++ b/moli-core/tests/web_apis/indexed_db_transaction.rs @@ -1,6 +1,27 @@ use super::event_dispatch::run_probe; use super::*; +#[tokio::test(flavor = "multi_thread")] +async fn indexed_db_unique_index_builds_abort_in_order_and_deduplicate_multi_entry_keys() +-> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let fixture = include_str!("fixtures/indexeddb-index-build.js"); + for target in ["window", "child", "worker"] { + let source = format!( + "{fixture}\nindexBuildProbe('index-build-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: indexBuildChecks}}));" + ); + let result = run_probe(&browser, &server, target, &source).await?; + assert_eq!(result["state"], "pass", "{target}: {result}"); + assert!( + result["checks"].as_array().unwrap().len() >= 150, + "{target}: {result}" + ); + } + server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn indexed_db_events_propagate_and_abort_only_active_transactions() -> Result<()> { let server = FixtureServer::spawn().await?; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan.rs index 4cd1c4518b..4dbfa5f5bd 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan.rs @@ -4,6 +4,8 @@ mod constraints; mod index; mod object_store; -pub(in crate::context_bootstrap::indexed_db) use self::constraints::enforce_object_store_unique_constraints; +pub(in crate::context_bootstrap::indexed_db) use self::constraints::{ + enforce_object_store_unique_constraints, validate_existing_index_entries, +}; pub(in crate::context_bootstrap::indexed_db) use self::index::scan_index_entries; pub(in crate::context_bootstrap::indexed_db) use self::object_store::scan_object_store_entries; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan/constraints.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan/constraints.rs index f069730340..9a1a35e436 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan/constraints.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/scan/constraints.rs @@ -1,5 +1,29 @@ use super::*; +pub(in crate::context_bootstrap::indexed_db) fn validate_existing_index_entries( + scope: &mut v8::PinScope<'_, '_>, + handle: TransactionHandle, + store_name: &str, + index: &IndexInfo, +) -> Result<(), IndexedDbError> { + if !index.unique { + return Ok(()); + } + let entries = scan_index_entries(scope, handle, store_name, index, None)?; + // The scan sorts by index key and deduplicates each record's multiEntry + // keys. Equal keys in adjacent records therefore violate uniqueness. + if entries + .windows(2) + .any(|pair| pair[0].index_key == pair[1].index_key) + { + return Err(IndexedDbError::Constraint(format!( + "unique index `{}` already contains key", + index.name + ))); + } + Ok(()) +} + pub(in crate::context_bootstrap::indexed_db) fn enforce_object_store_unique_constraints<'s>( scope: &mut v8::PinScope<'s, '_>, store: v8::Local<'s, v8::Object>, @@ -22,15 +46,7 @@ pub(in crate::context_bootstrap::indexed_db) fn enforce_object_store_unique_cons continue; } - let mut seen = BTreeSet::new(); - for key in &candidate_keys { - if !seen.insert(key.clone()) { - return Err(IndexedDbError::Constraint(format!( - "unique index `{}` already contains key", - index.name - ))); - } - } + let seen: BTreeSet<_> = candidate_keys.into_iter().collect(); let existing = scan_index_entries(scope, handle, store_name, &index, None)?; if existing diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs index 3e04ed99a6..4b846c7310 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/keys/write/extraction.rs @@ -33,11 +33,14 @@ fn extract_keys_from_string_key_path<'s>( }; if multi_entry && let Ok(array) = v8::Local::::try_from(current) { let mut keys = Vec::new(); + let mut seen = BTreeSet::new(); for index in 0..array.length() { let Some(entry) = array.get_index(scope, index) else { continue; }; - if let Ok(Some(key)) = parse_idb_key(scope, entry) { + if let Ok(Some(key)) = parse_idb_key(scope, entry) + && seen.insert(key.clone()) + { keys.push(key); } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs index c501d1ff6b..34caac2a8f 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/stores/object_store/schema/create_index.rs @@ -1,4 +1,7 @@ use super::*; +use crate::context_bootstrap::indexed_db::{ + enqueue_transaction_operation_error, validate_existing_index_entries, +}; use crate::webidl; use moli_indexeddb::{IndexOptionsValidationError, validate_index_options}; @@ -44,7 +47,7 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_create_index_ca return; } let store = args.this(); - let Some((_transaction, database, handle, store_name)) = + let Some((transaction, database, handle, store_name)) = object_store_versionchange_common(scope, store) else { let error = dom_exception_value( @@ -69,6 +72,13 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_object_store_create_index_ca }) { Ok(info) => { let _ = set_database_index_metadata(scope, database, &store_name, &info); + // Existing operations execute eagerly, but their results are + // delivered in the database task queue. Capture this creation's + // constraint result now so later writes/deletes cannot change it, + // and deliver its failure in the same operation order. + if let Err(error) = validate_existing_index_entries(scope, handle, &store_name, &info) { + enqueue_transaction_operation_error(scope, transaction, error); + } if let Some(index) = create_index_object(scope, store, &info) { rv.set(index.into()); } else { 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 f4c080bfb2..e0b55c458f 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 @@ -78,6 +78,9 @@ fn flush_indexed_db_task<'s>( IndexedDbTaskKind::TransactionStart => flush_transaction_start_task(scope, task), IndexedDbTaskKind::TransactionCommit => flush_transaction_commit_task(scope, task), IndexedDbTaskKind::TransactionAbort => flush_transaction_abort_task(scope, task), + IndexedDbTaskKind::TransactionOperationError => { + flush_transaction_operation_error_task(scope, task) + } } if !indexed_db_runtime_array_contains_object( scope, 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 7200a5dba8..10f669ae63 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,23 @@ 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; + +pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_operation_error_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, +) { + let Some(transaction) = indexed_db_transaction_task_transaction(scope, task) else { + return; + }; + let Some(error) = + crate::context_bootstrap::indexed_db::indexed_db_transaction_task_error(scope, task) + else { + return; + }; + let error = request_error_object(scope, &error); + crate::context_bootstrap::indexed_db::abort_indexed_db_transaction_with_error( + scope, + transaction, + error, + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs index 12de9f31f5..4b11f621f5 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs @@ -9,6 +9,21 @@ pub(in crate::context_bootstrap::indexed_db) use self::abort::enqueue_transactio pub(in crate::context_bootstrap::indexed_db) use self::commit::enqueue_transaction_commit_task; pub(in crate::context_bootstrap::indexed_db) use self::successor::enqueue_next_readwrite_transaction_start; +pub(in crate::context_bootstrap::indexed_db) fn enqueue_transaction_operation_error<'s>( + scope: &mut v8::PinScope<'s, '_>, + transaction: v8::Local<'s, v8::Object>, + error: IndexedDbError, +) { + let task = v8::Object::new(scope); + crate::context_bootstrap::indexed_db::register_indexed_db_transaction_error_task( + scope, + task, + transaction, + error, + ); + enqueue_indexed_db_task(scope, task); +} + fn create_transaction_task<'s>( scope: &mut v8::PinScope<'s, '_>, kind: &'static str, 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 eb636a15b9..d3193d674d 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 @@ -50,6 +50,7 @@ pub(super) enum IndexedDbTaskKind { TransactionStart, TransactionCommit, TransactionAbort, + TransactionOperationError, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -204,7 +205,8 @@ impl IndexedDbTaskState { | IndexedDbTaskKind::DatabasesSettle | IndexedDbTaskKind::TransactionStart | IndexedDbTaskKind::TransactionCommit - | IndexedDbTaskKind::TransactionAbort => self.owner.dispatch_scope(), + | IndexedDbTaskKind::TransactionAbort + | IndexedDbTaskKind::TransactionOperationError => self.owner.dispatch_scope(), } } } @@ -336,6 +338,7 @@ struct IndexedDbVersionChangeTaskPayload { struct IndexedDbTransactionTaskPayload { transaction: v8::Global, + operation_error: Option, } impl IndexedDbTransactionTaskPayload { @@ -343,6 +346,7 @@ impl IndexedDbTransactionTaskPayload { let transaction: v8::Local<'_, v8::Value> = transaction.into(); Self { transaction: v8::Global::new(scope, transaction), + operation_error: None, } } } @@ -1609,6 +1613,7 @@ pub(super) fn register_indexed_db_transaction_task<'s>( IndexedDbTaskKind::TransactionStart | IndexedDbTaskKind::TransactionCommit | IndexedDbTaskKind::TransactionAbort + | IndexedDbTaskKind::TransactionOperationError )); let owner = indexed_db_typed_execution_owner(scope, transaction) .expect("IDB transaction task should have typed owner state"); @@ -1619,6 +1624,42 @@ pub(super) fn register_indexed_db_transaction_task<'s>( table.borrow_mut().transaction_tasks.insert(id, payload); } +pub(super) fn register_indexed_db_transaction_error_task<'s>( + scope: &mut v8::PinScope<'s, '_>, + task: v8::Local<'s, v8::Object>, + transaction: v8::Local<'s, v8::Object>, + error: IndexedDbError, +) { + register_indexed_db_transaction_task( + scope, + task, + IndexedDbTaskKind::TransactionOperationError, + transaction, + ); + let id = indexed_db_typed_task_id(scope, task).expect("registered transaction error task"); + let table = indexed_db_runtime_state_table_for_object(scope, task); + table + .borrow_mut() + .transaction_tasks + .get_mut(&id) + .expect("registered transaction error payload") + .operation_error = Some(error); +} + +pub(super) fn indexed_db_transaction_task_error<'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); + table + .borrow() + .transaction_tasks + .get(&id)? + .operation_error + .clone() +} + pub(super) fn indexed_db_transaction_task_transaction<'s>( scope: &mut v8::PinScope<'s, '_>, task: v8::Local<'s, v8::Object>,