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>,