fix(indexeddb): schedule transactions across scopes and agents

Coordinate regular transactions across connections and event loops using
native admission leases. Include pending predecessors in scope conflicts,
wake the accepting realm after commit or abort, and retire admissions when
connections or workers close. Deferred starts retain native mode and scope.

Readonly transactions no longer publish snapshots. Merge ordinary writes
only within their store scope and check quota against the merged database
so disjoint commits cannot overwrite each other or bypass quota.

Cover FIFO admission, persistent stores, quota and teardown in native tests,
and exercise transaction ordering and worker termination in Window, child
Window and Worker integration tests.
This commit is contained in:
ldm0
2026-09-23 09:16:32 +08:00
parent abbb218f15
commit 0f272ee00a
30 changed files with 1229 additions and 307 deletions
@@ -130,8 +130,14 @@ IndexedDB/transaction-abort-request-error.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-abort-request-error.any.js?moli-wpt-any=window
IndexedDB/transaction-create_in_versionchange.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-create_in_versionchange.any.js?moli-wpt-any=window
IndexedDB/transaction-lifetime-empty.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-lifetime-empty.any.js?moli-wpt-any=window
IndexedDB/transaction-lifetime.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-lifetime.any.js?moli-wpt-any=window
IndexedDB/transaction-scheduling-mixed-scopes.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-scheduling-mixed-scopes.any.js?moli-wpt-any=window
IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js?moli-wpt-any=window
IndexedDB/transaction_bubble-and-capture.any.js?moli-wpt-any=dedicatedworker
IndexedDB/transaction_bubble-and-capture.any.js?moli-wpt-any=window
IndexedDB/upgrade-transaction-lifecycle-backend-aborted.any.js?moli-wpt-any=dedicatedworker
@@ -0,0 +1,272 @@
globalThis.schedulingChecks = [];
async function schedulingProbe(prefix = 'transaction-scheduling-' + Math.random()) {
const checks = globalThis.schedulingChecks;
const check = (name, pass, detail) => checks.push({name, pass: !!pass, detail});
const request = req => new Promise(resolve => {
req.onsuccess = () => resolve({value: req.result});
req.onerror = event => { event.preventDefault(); resolve({error: req.error.name}); };
});
const done = (tx, log, label) => new Promise(resolve => {
tx.addEventListener('complete', () => { if (log) log.push(label); resolve(true); });
tx.addEventListener('abort', () => { if (log) log.push(label + ' abort'); resolve(false); });
});
const open = name => {
const req = indexedDB.open(name, 1);
req.onupgradeneeded = () => {
for (const name of ['a', 'b', 'c']) req.result.createObjectStore(name).put('old', 'key');
};
return request(req).then(result => {
if (result.error) throw new Error(result.error);
return result.value;
});
};
const run = async (label, body) => {
const name = prefix + '-' + label, connections = [];
try {
const db = await open(name);
connections.push(db);
const other = await open(name);
connections.push(other);
await body(db, other, name);
} catch (error) {
check(label + ' unexpected exception', false, String(error));
} finally {
for (const db of connections) db.close();
}
};
const read = async (db, stores) => {
const tx = db.transaction(stores), completion = done(tx);
const values = await Promise.all(stores.map(name => request(tx.objectStore(name).get('key'))));
await completion;
return values.map(result => result.value);
};
for (const crossConnection of [false, true]) {
for (const firstMode of ['readonly', 'readwrite']) {
for (const secondMode of ['readonly', 'readwrite']) {
for (const [firstScope, secondScope] of [
[['a'], ['a']], [['a', 'b'], ['b', 'c']], [['a'], ['b']]
]) {
const label = [crossConnection, firstMode, secondMode, firstScope.join(''), secondScope.join('')].join('-');
await run(label, async (db, other) => {
const log = [];
const first = db.transaction(firstScope, firstMode), firstDone = done(first, log, 'first');
const second = (crossConnection ? other : db).transaction(secondScope, secondMode);
const secondDone = done(second, log, 'second');
if (firstMode === 'readwrite') {
for (const name of firstScope) first.objectStore(name).put('first', 'key');
}
// Keep the older transaction active through several request events.
// An overlapping younger writer must not execute during these events.
const firstValues = [];
const pump = left => {
const req = first.objectStore(firstScope[0]).get('key');
req.onsuccess = () => {
firstValues.push(req.result);
if (left) pump(left - 1);
};
};
pump(5);
const observations = secondScope.map(name => {
const req = second.objectStore(name).get('key');
return request(req).then(result => { log.push('read ' + name); return result; });
});
if (secondMode === 'readwrite') {
for (const name of secondScope) second.objectStore(name).put('second', 'key');
}
const results = await Promise.all(observations);
check(label + ' completes', await firstDone && await secondDone);
const overlap = firstScope.some(name => secondScope.includes(name));
if (overlap && (firstMode === 'readwrite' || secondMode === 'readwrite')) {
check(label + ' waits for predecessor', log.indexOf('first') < log.indexOf('read ' + secondScope[0]), log);
}
check(label + ' stable older snapshot', firstValues.length === 6 &&
firstValues.every(value => value === (firstMode === 'readwrite' ? 'first' : 'old')), firstValues);
for (const [index, name] of secondScope.entries()) {
const expected = firstMode === 'readwrite' && firstScope.includes(name) ? 'first' : 'old';
check(label + ' observes ' + name, !results[index].error && results[index].value === expected, results[index]);
}
const final = await read(db, ['a', 'b', 'c']);
const expected = ['a', 'b', 'c'].map(name =>
secondMode === 'readwrite' && secondScope.includes(name) ? 'second' :
firstMode === 'readwrite' && firstScope.includes(name) ? 'first' : 'old');
check(label + ' preserves every committed store', JSON.stringify(final) === JSON.stringify(expected), final);
});
}
}
}
}
await run('pending-writer-fairness', async db => {
const log = [];
const first = db.transaction('a'), firstDone = done(first, log, 'first');
const writer = db.transaction(['a', 'b'], 'readwrite'), writerDone = done(writer, log, 'writer');
const last = db.transaction('b'), lastDone = done(last, log, 'last');
const pump = left => {
first.objectStore('a').get('key').onsuccess = () => { if (left) pump(left - 1); };
};
pump(8);
writer.objectStore('b').put('writer', 'key');
const result = await request(last.objectStore('b').get('key'));
check('pending writer blocks a later reader of its other store', result.value === 'writer', result);
check('pending fairness completes', await firstDone && await writerDone && await lastDone);
check('pending fairness order', log.join(',') === 'first,writer,last', log);
});
for (const pending of [false, true]) {
await run('abort-' + pending, async db => {
const blocker = pending ? db.transaction('a') : null;
const blockerDone = blocker ? done(blocker) : Promise.resolve(true);
const writer = db.transaction(['a', 'b'], 'readwrite'), writerDone = done(writer);
const write = request(writer.objectStore('b').put('aborted', 'key'));
const later = db.transaction('b'), laterDone = done(later);
const value = request(later.objectStore('b').get('key'));
// Pending work has no backend handle; both paths must release admission.
writer.abort();
check('abort ' + pending + ' rolls back and unblocks reader', (await value).value === 'old');
check('abort ' + pending + ' dispatches request error', (await write).error === 'AbortError');
check('abort ' + pending + ' finishes', !(await writerDone) && await blockerDone && await laterDone);
});
}
await run('immutable-start-scope', async db => {
const writer = db.transaction('a', 'readwrite'), writerDone = done(writer);
const reader = db.transaction('a'), readerDone = done(reader);
const value = request(reader.objectStore('a').get('key'));
writer.objectStore('a').put('new', 'key');
for (const name of ['db', 'mode', 'objectStoreNames']) {
Object.defineProperty(reader, name, {get() { throw new Error('scheduler read ' + name); }});
}
check('deferred start uses immutable native scope', (await value).value === 'new');
check('deferred readonly completes', await writerDone && await readerDone);
});
await run('close-with-queued-reader', async (db, other) => {
const writer = db.transaction('a', 'readwrite'), writerDone = done(writer);
const reader = other.transaction('a'), readerDone = done(reader);
const value = request(reader.objectStore('a').get('key'));
writer.objectStore('a').put('new', 'key');
other.close();
let error;
try { other.transaction('a'); } catch (caught) { error = caught.name; }
check('close rejects new transactions', error === 'InvalidStateError', error);
check('close lets accepted reader observe earlier write', (await value).value === 'new');
check('close drains accepted transactions', await writerDone && await readerDone);
});
await run('empty-readers', async db => {
const log = [], empty = [];
const writer = db.transaction('a', 'readwrite'), writerDone = done(writer, log, 'writer');
const store = writer.objectStore('a');
store.put('one', 'key').onsuccess = () => {
log.push('one');
empty.push(done(db.transaction('a'), log, 'reader1'), done(db.transaction('a'), log, 'reader2'));
store.put('two', 'key').onsuccess = () => log.push('two');
};
await writerDone;
check('empty readers complete', (await Promise.all(empty)).every(Boolean));
check('empty readers wait for writer', log.join(',') === 'one,two,writer,reader1,reader2', log);
});
for (const [mode, laterMode, action] of [
['readwrite', 'readonly', 'commit'],
['readwrite', 'readwrite', 'abort'],
['readonly', 'readwrite', 'commit'],
['readwrite', 'readonly', 'terminate']
]) {
const label = [mode, laterMode, action].join('-');
await run('worker-' + label, async (db, other, name) => {
const source = '(' + schedulingWorker.toString() + ')()';
const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'}));
const worker = new Worker(url), messages = [], waiters = [];
worker.onmessage = event => {
if (waiters.length) waiters.shift()(event.data); else messages.push(event.data);
};
const next = () => messages.length ? Promise.resolve(messages.shift()) : new Promise(resolve => waiters.push(resolve));
try {
worker.postMessage({name, mode});
check(label + ' worker holds transaction', (await next()).state === 'holding');
const tx = db.transaction('a', laterMode), completion = done(tx);
const req = tx.objectStore('a').get('key'), value = request(req);
if (laterMode === 'readwrite') tx.objectStore('a').put('parent', 'key');
worker.postMessage({checkpoint: true});
check(label + ' worker checkpoint', (await next()).state === 'checkpoint');
check(label + ' waits across agents', req.readyState === 'pending', req.readyState);
if (action === 'terminate') worker.terminate();
else worker.postMessage({release: action});
const expected = mode === 'readwrite' && action === 'commit' ? 'worker' : 'old';
check(label + ' observes committed snapshot', (await value).value === expected);
check(label + ' parent completes', await completion);
if (action !== 'terminate') {
check(label + ' worker finishes', (await next()).state === (action === 'abort' ? 'aborted' : 'complete'));
}
check(label + ' final value', (await read(other, ['a']))[0] === (laterMode === 'readwrite' ? 'parent' : expected));
} finally {
worker.terminate();
URL.revokeObjectURL(url);
}
});
}
await run('terminate-pending-worker', async (db, other, name) => {
const url = URL.createObjectURL(new Blob(['(' + schedulingWorker.toString() + ')()'], {type: 'text/javascript'}));
const worker = new Worker(url);
let keepAlive = true;
const writer = db.transaction('a', 'readwrite'), writerDone = done(writer);
writer.objectStore('a').put('parent', 'key');
const pump = () => {
writer.objectStore('a').get('key').onsuccess = () => { if (keepAlive) pump(); };
};
pump();
try {
const queued = new Promise(resolve => { worker.onmessage = event => resolve(event.data); });
worker.postMessage({name, mode: 'readwrite', notifyQueued: true});
check('pending worker accepts transaction', (await queued).state === 'queued');
const reader = other.transaction('a'), readerDone = done(reader);
const value = request(reader.objectStore('a').get('key'));
worker.terminate();
keepAlive = false;
check('terminating pending worker unblocks its successor', (await value).value === 'parent');
check('pending worker teardown preserves live transactions', await writerDone && await readerDone);
} finally {
keepAlive = false;
worker.terminate();
URL.revokeObjectURL(url);
}
});
return {state: checks.every(entry => entry.pass) ? 'pass' : 'fail', checks};
}
// Executed in its own agent. Request chaining keeps the transaction live until
// an explicit handshake releases it; no timeout is used to infer completion.
function schedulingWorker() {
let release, checkpoint = false;
onmessage = event => {
const message = event.data;
if (message.release) { release = message.release; return; }
if (message.checkpoint) { checkpoint = true; return; }
const req = indexedDB.open(message.name, 1);
req.onerror = () => postMessage({state: 'error', error: req.error.name});
req.onsuccess = () => {
const db = req.result;
let tx;
try { tx = db.transaction('a', message.mode); }
catch (error) { db.close(); postMessage({state: 'error', error: error.name}); return; }
const store = tx.objectStore('a');
if (message.notifyQueued) postMessage({state: 'queued'});
tx.oncomplete = () => { db.close(); postMessage({state: 'complete'}); };
tx.onabort = () => { db.close(); postMessage({state: 'aborted'}); };
const first = message.mode === 'readwrite' ? store.put('worker', 'key') : store.get('key');
const pump = () => {
store.get('key').onsuccess = () => {
if (release === 'abort') tx.abort();
else if (!release) {
pump();
if (checkpoint) { checkpoint = false; postMessage({state: 'checkpoint'}); }
}
};
};
first.onsuccess = () => { pump(); postMessage({state: 'holding'}); };
};
};
}
@@ -1,6 +1,52 @@
use super::event_dispatch::run_probe;
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_schedules_overlapping_transactions_across_connections_and_agents() -> Result<()>
{
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let fixture = include_str!("fixtures/indexeddb-transaction-scheduling.js");
for target in ["window", "child", "worker"] {
let source = format!(
"{fixture}\nschedulingProbe('scheduling-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: schedulingChecks}}));"
);
let result = run_probe(&browser, &server, target, &source).await?;
assert_eq!(result["state"], "pass", "{target}: {result}");
assert_eq!(
result["checks"].as_array().unwrap().len(),
162,
"{target}: {result}"
);
}
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_queries_snapshot_arguments_and_delete_key_ranges() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let fixture = include_str!("fixtures/indexeddb-query-snapshots.js");
for target in ["window", "child", "worker"] {
let source = format!(
"{fixture}\nquerySnapshotProbe('query-snapshots-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: queryChecks}}));"
);
let result = tokio::time::timeout(
Duration::from_secs(30),
run_probe(&browser, &server, target, &source),
).await??;
assert_eq!(result["state"], "pass", "{target}: {result}");
assert_eq!(
result["checks"].as_array().unwrap().len(),
548,
"{target}: {result}"
);
}
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_keys_preserve_types_order_and_binary_snapshots() -> Result<()> {
let server = FixtureServer::spawn().await?;
@@ -106,31 +152,6 @@ async fn indexed_db_events_propagate_and_abort_only_active_transactions() -> Res
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_queries_snapshot_arguments_and_delete_key_ranges() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let fixture = include_str!("fixtures/indexeddb-query-snapshots.js");
for target in ["window", "child", "worker"] {
let source = format!(
"{fixture}\nquerySnapshotProbe('query-snapshots-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: queryChecks}}));"
);
let result = tokio::time::timeout(
Duration::from_secs(30),
run_probe(&browser, &server, target, &source),
)
.await??;
assert_eq!(result["state"], "pass", "{target}: {result}");
assert_eq!(
result["checks"].as_array().unwrap().len(),
548,
"{target}: {result}"
);
}
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_transaction_validates_arguments_and_upgrade_boundaries() -> Result<()> {
let server = FixtureServer::spawn().await?;
+2
View File
@@ -16,6 +16,7 @@ mod state;
#[cfg(test)]
mod tests;
mod transaction;
mod transaction_queue;
mod types;
mod usage;
@@ -37,6 +38,7 @@ pub use options::{
validate_index_options, validate_object_store_options,
};
pub use state::IndexedDbManager;
pub use transaction_queue::{TransactionRequestHandle, TransactionRequestLease};
pub use types::{
DatabaseHandle, DatabaseInfo, DatabaseNameAndVersion, IndexInfo, IndexOptions,
IndexedDbExternalObject, IndexedDbFileSystemHandleBucket, IndexedDbFileSystemHandleKind,
+132 -49
View File
@@ -17,7 +17,7 @@ use crate::{
transaction::{
ensure_writeable, next_generated_key, resolve_key, transaction_store, transaction_store_mut,
},
usage::{database_usage_bytes, origin_usage_bytes, sum_usage},
usage::{database_usage_bytes, database_usage_bytes_for_stores, origin_usage_bytes, sum_usage},
validate_index_options, validate_object_store_options,
};
@@ -29,6 +29,7 @@ impl IndexedDbManager {
pub fn new_in_memory() -> Self {
Self {
transaction_requests: Default::default(),
connection_requests: Default::default(),
connection_notifications: Default::default(),
backend: IndexedDbPersistenceBackend::InMemory,
@@ -45,6 +46,7 @@ impl IndexedDbManager {
fs::create_dir_all(&storage_root)
.map_err(|err| IndexedDbError::Io(format!("failed to create storage root: {err}")))?;
Ok(Self {
transaction_requests: Default::default(),
connection_requests: Default::default(),
connection_notifications: Default::default(),
backend: IndexedDbPersistenceBackend::JsonFiles { storage_root },
@@ -222,8 +224,14 @@ impl IndexedDbManager {
}
pub fn clear_origins_with_prefix(&mut self, origin_prefix: &str) -> Result<(), IndexedDbError> {
self.databases
.retain(|_, db| !db.origin.starts_with(origin_prefix));
let handles: Vec<_> = self
.databases
.iter()
.filter_map(|(handle, db)| db.origin.starts_with(origin_prefix).then_some(*handle))
.collect();
for handle in handles {
self.force_close_database(handle)?;
}
self.transactions
.retain(|_, tx| !tx.origin.starts_with(origin_prefix));
self.origins
@@ -295,6 +303,7 @@ impl IndexedDbManager {
self.databases.remove(&handle).ok_or_else(|| {
IndexedDbError::InvalidState(format!("database handle {:?} is invalid", handle))
})?;
self.transaction_requests.cancel_connection(handle);
self.connection_notifications.unregister(handle);
Ok(())
}
@@ -393,6 +402,42 @@ impl IndexedDbManager {
})
}
pub fn queue_transaction_start(
&self,
database: DatabaseHandle,
store_names: &[String],
mode: TransactionMode,
wake: crate::ConnectionRequestWake,
) -> Result<crate::TransactionRequestLease, IndexedDbError> {
let db = self.database_state(database)?;
if db.closed || mode == TransactionMode::VersionChange {
return Err(IndexedDbError::InvalidState(
"regular transaction requires an open connection".to_owned(),
));
}
let stores = store_names.iter().cloned().collect();
self.ensure_store_names_exist(self.database_data(&db.origin, &db.name)?, &stores)?;
Ok(self.transaction_requests.enqueue(
(db.origin.clone(), db.name.clone()),
database,
stores,
mode,
wake,
))
}
pub fn start_queued_transaction(
&mut self,
request: &crate::TransactionRequestHandle,
) -> Result<TransactionHandle, IndexedDbError> {
if !request.belongs_to(&self.transaction_requests) || !request.is_ready() {
return Err(IndexedDbError::InvalidState(
"transaction is not ready to start".to_owned(),
));
}
self.begin_transaction(request.database(), &request.store_names(), request.mode())
}
pub fn begin_transaction(
&mut self,
database: DatabaseHandle,
@@ -415,16 +460,15 @@ impl IndexedDbManager {
let store_set = store_names.iter().cloned().collect::<BTreeSet<_>>();
self.ensure_store_names_exist(&current, &store_set)?;
if mode == TransactionMode::ReadWrite
&& self.transactions.values().any(|tx| {
tx.state == TransactionLifecycle::Active
&& tx.mode == TransactionMode::ReadWrite
&& tx.origin == db.origin
&& tx.db_name == db.name
})
{
if self.transactions.values().any(|tx| {
tx.state == TransactionLifecycle::Active
&& tx.origin == db.origin
&& tx.db_name == db.name
&& (mode != TransactionMode::ReadOnly || tx.mode != TransactionMode::ReadOnly)
&& !tx.stores.is_disjoint(&store_set)
}) {
return Err(IndexedDbError::InvalidState(
"concurrent readwrite transactions are not supported in the MVP backend".to_owned(),
"an overlapping transaction has not finished".to_owned(),
));
}
@@ -770,11 +814,9 @@ impl IndexedDbManager {
add_only: bool,
quota: Option<IndexedDbQuotaCheck>,
) -> Result<Key, IndexedDbError> {
let (origin, db_name, working_copy_usage, resolved_key, previous_store) = {
let (resolved_key, previous_store) = {
let tx = self.active_transaction_mut(transaction)?;
ensure_writeable(tx)?;
let db_name = tx.db_name.clone();
let origin = tx.origin.clone();
let store = transaction_store_mut(tx, store_name)?;
let previous_store = quota.map(|_| store.clone());
let previous_counter = store.auto_increment_counter;
@@ -786,21 +828,17 @@ impl IndexedDbManager {
));
}
store.records.insert(resolved_key.clone(), value);
let working_copy_usage = database_usage_bytes(&db_name, &tx.working_copy);
(
origin,
db_name,
working_copy_usage,
resolved_key,
previous_store,
)
(resolved_key, previous_store)
};
if let Some(quota) = quota {
let tx = self.active_transaction(transaction)?;
let requested = quota
.non_indexed_db_usage
.saturating_add(self.committed_origin_usage_except_database(&origin, &db_name))
.saturating_add(working_copy_usage);
.saturating_add(
self.committed_origin_usage_except_database(&tx.origin, &tx.db_name),
)
.saturating_add(self.transaction_commit_usage(tx)?);
if requested > quota.quota {
if let Some(previous_store) = previous_store {
let tx = self.active_transaction_mut(transaction)?;
@@ -847,22 +885,39 @@ impl IndexedDbManager {
&mut self,
transaction: TransactionHandle,
) -> Result<(), IndexedDbError> {
let (database, origin, db_name, working_copy) = {
let (database, origin, db_name, mode) = {
let tx = self.active_transaction_mut(transaction)?;
tx.state = TransactionLifecycle::Committed;
(
tx.database,
tx.origin.clone(),
tx.db_name.clone(),
tx.working_copy.clone(),
)
(tx.database, tx.origin.clone(), tx.db_name.clone(), tx.mode)
};
let tx = self
.transactions
.remove(&transaction)
.expect("active transaction");
let result = if mode == TransactionMode::ReadOnly {
// Readers never publish their snapshot, even when another store
// in the same database has changed since the reader started.
Ok(())
} else {
let origin_state = self.origins.get_mut(&origin).ok_or_else(|| {
IndexedDbError::NotFound(format!("origin `{origin}` was not found during commit"))
})?;
if mode == TransactionMode::VersionChange {
origin_state.databases.insert(db_name, tx.working_copy);
} else {
let current = origin_state.databases.get_mut(&db_name).ok_or_else(|| {
IndexedDbError::NotFound(format!(
"database `{db_name}` was not found during commit"
))
})?;
for (name, store) in tx.working_copy.stores {
if tx.stores.contains(&name) {
current.stores.insert(name, store);
}
}
}
self.persist_origin(&origin)
};
let origin_state = self.origins.get_mut(&origin).ok_or_else(|| {
IndexedDbError::NotFound(format!("origin `{origin}` was not found during commit"))
})?;
origin_state.databases.insert(db_name, working_copy);
self.transactions.remove(&transaction);
let result = self.persist_origin(&origin);
self.finish_pending_database_close(database);
result
}
@@ -872,19 +927,15 @@ impl IndexedDbManager {
transaction: TransactionHandle,
quota: IndexedDbQuotaCheck,
) -> Result<(), IndexedDbError> {
let (database, origin, db_name, working_copy_usage) = {
let tx = self.active_transaction_mut(transaction)?;
(
tx.database,
tx.origin.clone(),
tx.db_name.clone(),
database_usage_bytes(&tx.db_name, &tx.working_copy),
)
};
let tx = self.active_transaction(transaction)?;
if tx.mode == TransactionMode::ReadOnly {
return self.commit_transaction(transaction);
}
let database = tx.database;
let requested = quota
.non_indexed_db_usage
.saturating_add(self.committed_origin_usage_except_database(&origin, &db_name))
.saturating_add(working_copy_usage);
.saturating_add(self.committed_origin_usage_except_database(&tx.origin, &tx.db_name))
.saturating_add(self.transaction_commit_usage(tx)?);
if requested > quota.quota {
self.transactions.remove(&transaction);
self.finish_pending_database_close(database);
@@ -896,6 +947,24 @@ impl IndexedDbManager {
self.commit_transaction(transaction)
}
fn transaction_commit_usage(&self, tx: &TransactionState) -> Result<u64, IndexedDbError> {
if tx.mode == TransactionMode::VersionChange {
return Ok(database_usage_bytes(&tx.db_name, &tx.working_copy));
}
let current = self.database_data(&tx.origin, &tx.db_name)?;
Ok(database_usage_bytes_for_stores(
&tx.db_name,
current.stores.iter().map(|(name, store)| {
let store = if tx.stores.contains(name) {
&tx.working_copy.stores[name]
} else {
store
};
(name, store)
}),
))
}
pub fn abort_transaction(
&mut self,
transaction: TransactionHandle,
@@ -974,6 +1043,20 @@ impl IndexedDbManager {
})
}
fn active_transaction(
&self,
handle: TransactionHandle,
) -> Result<&TransactionState, IndexedDbError> {
self.transactions
.get(&handle)
.filter(|tx| tx.state == TransactionLifecycle::Active)
.ok_or_else(|| {
IndexedDbError::TransactionInactive(format!(
"transaction handle {handle:?} is not active"
))
})
}
pub(crate) fn active_transaction_mut(
&mut self,
handle: TransactionHandle,
+2
View File
@@ -63,6 +63,8 @@ pub(crate) enum TransactionLifecycle {
pub struct IndexedDbManager {
pub(crate) connection_notifications: std::sync::Arc<crate::ConnectionNotifications>,
pub(crate) connection_requests: std::sync::Arc<crate::ConnectionRequestQueues>,
pub(crate) transaction_requests:
std::sync::Arc<crate::transaction_queue::TransactionRequestQueues>,
pub(crate) backend: IndexedDbPersistenceBackend,
pub(crate) origins: BTreeMap<String, OriginState>,
pub(crate) databases: BTreeMap<DatabaseHandle, DatabaseHandleState>,
+2
View File
@@ -16,6 +16,8 @@ struct TestDir {
path: PathBuf,
}
mod transaction_scheduling;
#[test]
fn schema_validation_preserves_exception_order_and_does_not_create_invalid_metadata() {
let mut manager = IndexedDbManager::new_in_memory();
@@ -0,0 +1,237 @@
use super::*;
use std::sync::Arc;
fn open_database(manager: &mut IndexedDbManager) -> DatabaseHandle {
let open = manager
.open(OpenOptions {
origin: "origin".into(),
name: "db".into(),
version: None,
})
.unwrap();
if let Some(upgrade) = open.upgrade_transaction {
for store in ["a", "b", "c"] {
manager
.create_object_store(
upgrade,
store,
ObjectStoreOptions {
auto_increment: true,
..Default::default()
},
)
.unwrap();
}
manager.commit_transaction(upgrade).unwrap();
}
open.database
}
fn queue(
manager: &IndexedDbManager,
database: DatabaseHandle,
stores: &[&str],
mode: TransactionMode,
) -> TransactionRequestLease {
manager
.queue_transaction_start(
database,
&stores.iter().map(|s| (*s).into()).collect::<Vec<_>>(),
mode,
Arc::new(|| {}),
)
.unwrap()
}
#[test]
fn queued_transactions_take_their_snapshot_after_overlapping_predecessors_finish() {
let mut manager = IndexedDbManager::new_in_memory();
let first_db = open_database(&mut manager);
let second_db = open_database(&mut manager);
let first = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite);
let first_tx = manager.start_queued_transaction(first.handle()).unwrap();
let reader = queue(&manager, second_db, &["a"], TransactionMode::ReadOnly);
let last = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite);
assert!(!reader.handle().is_ready());
assert!(manager.start_queued_transaction(reader.handle()).is_err());
assert!(!last.handle().is_ready());
manager.put(first_tx, "a", None, vec![1]).unwrap();
manager.commit_transaction(first_tx).unwrap();
drop(first);
let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap();
assert_eq!(
manager.get(reader_tx, "a", &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![1].into()))
);
assert!(!last.handle().is_ready());
manager.abort_transaction(reader_tx).unwrap();
drop(reader);
let last_tx = manager.start_queued_transaction(last.handle()).unwrap();
assert_eq!(
manager.put(last_tx, "a", None, vec![2]).unwrap(),
Key::from(2)
);
manager.commit_transaction(last_tx).unwrap();
drop(last);
}
#[test]
fn disjoint_writers_and_readers_preserve_other_commits_in_memory_and_on_disk() {
let dir = TestDir::new();
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
let database = open_database(&mut manager);
let first = queue(&manager, database, &["a"], TransactionMode::ReadWrite);
let second = queue(&manager, database, &["b"], TransactionMode::ReadWrite);
let reader = queue(&manager, database, &["c"], TransactionMode::ReadOnly);
let first_tx = manager.start_queued_transaction(first.handle()).unwrap();
let second_tx = manager.start_queued_transaction(second.handle()).unwrap();
let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap();
manager.put(first_tx, "a", None, vec![1]).unwrap();
manager.put(second_tx, "b", None, vec![2]).unwrap();
manager.commit_transaction(first_tx).unwrap();
manager.commit_transaction(second_tx).unwrap();
// A readonly transaction must neither revert the writes nor fail quota.
manager
.commit_transaction_with_quota(
reader_tx,
IndexedDbQuotaCheck {
quota: 0,
non_indexed_db_usage: 0,
},
)
.unwrap();
drop((first, second, reader));
for reload in [false, true] {
if reload {
manager = IndexedDbManager::new(&dir.path).unwrap();
}
let db = open_database(&mut manager);
let tx = manager
.begin_transaction(db, &["a".into(), "b".into()], TransactionMode::ReadWrite)
.unwrap();
for (store, value) in [("a", 1), ("b", 2)] {
assert_eq!(
manager.get(tx, store, &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![value].into()))
);
assert_eq!(manager.put(tx, store, None, vec![3]).unwrap(), Key::from(2));
}
manager.abort_transaction(tx).unwrap();
manager.close_database(db).unwrap();
}
}
#[test]
fn quota_checks_include_new_commits_to_other_stores() {
for at_commit in [false, true] {
let mut manager = IndexedDbManager::new_in_memory();
let database = open_database(&mut manager);
let initial_usage = manager.origin_usage_bytes("origin").unwrap();
let first = manager
.begin_transaction(database, &["a".into()], TransactionMode::ReadWrite)
.unwrap();
let second = manager
.begin_transaction(database, &["b".into()], TransactionMode::ReadWrite)
.unwrap();
let quota = IndexedDbQuotaCheck {
quota: initial_usage + 180,
non_indexed_db_usage: 0,
};
if at_commit {
manager
.put_with_quota(second, "b", None, vec![2; 100], quota)
.unwrap();
}
manager.put(first, "a", None, vec![1; 100]).unwrap();
manager.commit_transaction_with_quota(first, quota).unwrap();
let published_usage = manager.origin_usage_bytes("origin").unwrap();
let result = if at_commit {
manager
.commit_transaction_with_quota(second, quota)
.map(|_| Key::from(0))
} else {
manager.put_with_quota(second, "b", None, vec![2; 100], quota)
};
assert!(matches!(result, Err(IndexedDbError::QuotaExceeded { .. })));
if !at_commit {
assert_eq!(
manager.next_generated_key(second, "b").unwrap(),
Key::from(1)
);
manager.commit_transaction(second).unwrap();
}
assert_eq!(
manager.origin_usage_bytes("origin").unwrap(),
published_usage
);
let read = manager
.begin_transaction(
database,
&["a".into(), "b".into()],
TransactionMode::ReadOnly,
)
.unwrap();
assert_eq!(
manager.get(read, "a", &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![1; 100].into()))
);
assert_eq!(
manager.get(read, "b", &Key::from(1)).unwrap(),
RequestOutcome::Value(None)
);
manager.commit_transaction(read).unwrap();
}
}
#[test]
fn force_close_aborts_before_releasing_active_and_pending_admissions() {
let mut manager = IndexedDbManager::new_in_memory();
let first_db = open_database(&mut manager);
let second_db = open_database(&mut manager);
let first = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite);
let first_tx = manager.start_queued_transaction(first.handle()).unwrap();
manager.put(first_tx, "a", None, vec![1]).unwrap();
let pending = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite);
let reader = queue(&manager, second_db, &["a"], TransactionMode::ReadOnly);
assert!(!reader.handle().is_ready());
manager.force_close_database(first_db).unwrap();
assert!(manager.commit_transaction(first_tx).is_err());
assert!(!first.handle().is_ready());
assert!(!pending.handle().is_ready());
assert!(reader.handle().is_ready());
let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap();
assert_eq!(
manager.get(reader_tx, "a", &Key::from(1)).unwrap(),
RequestOutcome::Value(None)
);
manager.commit_transaction(reader_tx).unwrap();
drop((first, pending, reader));
}
#[test]
fn clearing_storage_retires_admissions_before_reopening_the_same_database() {
for prefix in [false, true] {
let mut manager = IndexedDbManager::new_in_memory();
let db = open_database(&mut manager);
let first = queue(&manager, db, &["a"], TransactionMode::ReadWrite);
let tx = manager.start_queued_transaction(first.handle()).unwrap();
manager.put(tx, "a", None, vec![1]).unwrap();
let pending = queue(&manager, db, &["a"], TransactionMode::ReadOnly);
if prefix {
manager.clear_origins_with_prefix("orig").unwrap();
} else {
manager.clear_origin("origin").unwrap();
}
assert!(!first.handle().is_ready());
assert!(!pending.handle().is_ready());
let db = open_database(&mut manager);
let read = queue(&manager, db, &["a"], TransactionMode::ReadOnly);
let tx = manager.start_queued_transaction(read.handle()).unwrap();
assert_eq!(
manager.get(tx, "a", &Key::from(1)).unwrap(),
RequestOutcome::Value(None)
);
manager.commit_transaction(tx).unwrap();
drop((first, pending, read));
}
}
+317
View File
@@ -0,0 +1,317 @@
//! Admission of regular transactions across connections and event loops.
//!
//! A transaction waits for every older, unfinished transaction whose scope
//! overlaps and whose mode conflicts, including transactions still waiting to
//! start. The accepting realm owns a lease until backend commit or rollback.
use parking_lot::Mutex;
use std::{
collections::{BTreeMap, BTreeSet, VecDeque},
sync::{Arc, Weak},
};
use crate::{ConnectionRequestWake, DatabaseHandle, TransactionMode};
#[derive(Default)]
pub(crate) struct TransactionRequestQueues {
state: Mutex<QueueState>,
}
#[derive(Default)]
struct QueueState {
next_id: u64,
queues: BTreeMap<(String, String), VecDeque<QueuedRequest>>,
}
struct TransactionScope {
stores: BTreeSet<String>,
mode: TransactionMode,
}
impl TransactionScope {
fn conflicts_with(&self, other: &Self) -> bool {
(self.mode != TransactionMode::ReadOnly || other.mode != TransactionMode::ReadOnly)
&& !self.stores.is_disjoint(&other.stores)
}
}
struct QueuedRequest {
id: u64,
database: DatabaseHandle,
scope: Arc<TransactionScope>,
ready: bool,
wake: ConnectionRequestWake,
}
#[derive(Clone)]
pub struct TransactionRequestHandle {
queues: Weak<TransactionRequestQueues>,
key: (String, String),
id: u64,
database: DatabaseHandle,
scope: Arc<TransactionScope>,
}
/// Drop only after committing or aborting the backend transaction. The lease
/// never retains the storage partition or any JS object.
pub struct TransactionRequestLease(TransactionRequestHandle);
impl TransactionRequestLease {
pub fn handle(&self) -> &TransactionRequestHandle {
&self.0
}
}
impl Drop for TransactionRequestLease {
fn drop(&mut self) {
self.0.finish();
}
}
impl TransactionRequestQueues {
pub(crate) fn enqueue(
self: &Arc<Self>,
key: (String, String),
database: DatabaseHandle,
stores: BTreeSet<String>,
mode: TransactionMode,
wake: ConnectionRequestWake,
) -> TransactionRequestLease {
let scope = Arc::new(TransactionScope { stores, mode });
let mut state = self.state.lock();
state.next_id = state
.next_id
.checked_add(1)
.expect("IDB transaction id overflow");
let id = state.next_id;
let queue = state.queues.entry(key.clone()).or_default();
let ready = !queue.iter().any(|older| scope.conflicts_with(&older.scope));
queue.push_back(QueuedRequest {
id,
database,
scope: scope.clone(),
ready,
wake,
});
// Initial readiness is returned to the caller. A wake here could run
// before the accepting realm has attached its transaction wrapper.
TransactionRequestLease(TransactionRequestHandle {
queues: Arc::downgrade(self),
key,
id,
database,
scope,
})
}
/// Called after rolling back the connection's backend transactions. Wakes
/// may inspect this coordinator, but must not reenter the storage manager.
pub(crate) fn cancel_connection(&self, database: DatabaseHandle) {
let wakes = {
let mut state = self.state.lock();
let mut wakes = Vec::new();
state.queues.retain(|_, queue| {
let before = queue.len();
queue.retain(|entry| entry.database != database);
if before != queue.len() {
wake_newly_ready(queue, &mut wakes);
}
!queue.is_empty()
});
wakes
};
for wake in wakes {
wake();
}
}
}
fn wake_newly_ready(queue: &mut VecDeque<QueuedRequest>, wakes: &mut Vec<ConnectionRequestWake>) {
for index in 0..queue.len() {
if !queue[index].ready
&& !queue
.iter()
.take(index)
.any(|older| queue[index].scope.conflicts_with(&older.scope))
{
queue[index].ready = true;
wakes.push(queue[index].wake.clone());
}
}
}
impl TransactionRequestHandle {
pub fn is_ready(&self) -> bool {
self.queues.upgrade().is_some_and(|queues| {
queues
.state
.lock()
.queues
.get(&self.key)
.is_some_and(|queue| queue.iter().any(|entry| entry.id == self.id && entry.ready))
})
}
pub(crate) fn belongs_to(&self, queues: &Arc<TransactionRequestQueues>) -> bool {
self.queues.ptr_eq(&Arc::downgrade(queues))
}
pub(crate) fn database(&self) -> DatabaseHandle {
self.database
}
pub(crate) fn store_names(&self) -> Vec<String> {
self.scope.stores.iter().cloned().collect()
}
pub(crate) fn mode(&self) -> TransactionMode {
self.scope.mode
}
fn finish(&self) {
let Some(queues) = self.queues.upgrade() else {
return;
};
let wakes = {
let mut state = queues.state.lock();
let Some(queue) = state.queues.get_mut(&self.key) else {
return;
};
let Some(index) = queue.iter().position(|entry| entry.id == self.id) else {
return;
};
queue.remove(index);
let mut wakes = Vec::new();
wake_newly_ready(queue, &mut wakes);
if queue.is_empty() {
state.queues.remove(&self.key);
}
wakes
};
for wake in wakes {
wake();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn request(
queues: &Arc<TransactionRequestQueues>,
connection: u64,
stores: &[&str],
mode: TransactionMode,
wake: ConnectionRequestWake,
) -> TransactionRequestLease {
queues.enqueue(
("origin".into(), "db".into()),
DatabaseHandle::from_raw(connection),
stores.iter().map(|store| (*store).into()).collect(),
mode,
wake,
)
}
#[test]
fn pending_writers_block_later_readers_across_connections_and_cancel_on_drop() {
let queues = Arc::new(TransactionRequestQueues::default());
let wakes = Arc::new(AtomicUsize::new(0));
let wake: ConnectionRequestWake = {
let queues = Arc::downgrade(&queues);
let wakes = wakes.clone();
Arc::new(move || {
// Wakes must run without holding the coordinator lock.
assert!(queues.upgrade().unwrap().state.try_lock().is_some());
wakes.fetch_add(1, Ordering::SeqCst);
})
};
let reader = request(&queues, 1, &["a"], TransactionMode::ReadOnly, wake.clone());
let parallel_reader = request(&queues, 2, &["a"], TransactionMode::ReadOnly, wake.clone());
let writer = request(
&queues,
2,
&["a", "b"],
TransactionMode::ReadWrite,
wake.clone(),
);
let last_reader = request(&queues, 3, &["b"], TransactionMode::ReadOnly, wake.clone());
let independent = request(&queues, 1, &["c"], TransactionMode::ReadWrite, wake);
assert!(reader.0.is_ready());
assert!(parallel_reader.0.is_ready());
assert!(!writer.0.is_ready());
assert!(!last_reader.0.is_ready());
assert!(independent.0.is_ready());
assert_eq!(wakes.load(Ordering::SeqCst), 0);
drop(reader);
assert!(!writer.0.is_ready());
std::thread::spawn(move || drop(parallel_reader))
.join()
.unwrap();
assert!(writer.0.is_ready());
assert!(!last_reader.0.is_ready());
assert_eq!(wakes.load(Ordering::SeqCst), 1);
drop(writer);
assert!(last_reader.0.is_ready());
assert_eq!(wakes.load(Ordering::SeqCst), 2);
drop((last_reader, independent));
assert!(queues.state.lock().queues.is_empty());
}
#[test]
fn canceling_middle_writer_unblocks_readers_and_connection_cancellation_is_idempotent() {
let queues = Arc::new(TransactionRequestQueues::default());
let wake: ConnectionRequestWake = Arc::new(|| {});
let first = request(&queues, 1, &["a"], TransactionMode::ReadOnly, wake.clone());
let writer = request(
&queues,
2,
&["a", "b"],
TransactionMode::ReadWrite,
wake.clone(),
);
let reader = request(
&queues,
3,
&["a", "b"],
TransactionMode::ReadOnly,
wake.clone(),
);
let independent_writer =
request(&queues, 2, &["c"], TransactionMode::ReadWrite, wake.clone());
let independent_reader = request(&queues, 3, &["c"], TransactionMode::ReadOnly, wake);
assert!(!reader.0.is_ready());
assert!(!independent_reader.0.is_ready());
queues.cancel_connection(DatabaseHandle::from_raw(2));
assert!(first.0.is_ready());
assert!(reader.0.is_ready());
assert!(independent_reader.0.is_ready());
assert!(!writer.0.is_ready());
assert!(!independent_writer.0.is_ready());
drop((writer, independent_writer));
assert!(reader.0.is_ready());
drop((first, reader, independent_reader));
assert!(queues.state.lock().queues.is_empty());
}
#[test]
fn database_names_and_storage_keys_are_independent_and_leases_do_not_retain_partition() {
let queues = Arc::new(TransactionRequestQueues::default());
let leases =
[("origin", "db"), ("origin", "other"), ("bucket", "db")].map(|(origin, name)| {
queues.enqueue(
(origin.into(), name.into()),
DatabaseHandle::from_raw(1),
BTreeSet::from(["store".into()]),
TransactionMode::ReadWrite,
Arc::new(|| {}),
)
});
assert!(leases.iter().all(|lease| lease.0.is_ready()));
drop(queues);
assert!(leases.iter().all(|lease| !lease.0.is_ready()));
drop(leases);
}
}
+8 -4
View File
@@ -17,13 +17,17 @@ pub(crate) fn origin_usage_bytes(state: &OriginState) -> u64 {
}
pub(crate) fn database_usage_bytes(name: &str, database: &DatabaseData) -> u64 {
database_usage_bytes_for_stores(name, database.stores.iter())
}
pub(crate) fn database_usage_bytes_for_stores<'a>(
name: &str,
stores: impl Iterator<Item = (&'a String, &'a ObjectStoreData)>,
) -> u64 {
string_usage_bytes(name)
.saturating_add(U64_STORAGE_BYTES)
.saturating_add(sum_usage(
database
.stores
.iter()
.map(|(name, store)| object_store_usage_bytes(name, store)),
stores.map(|(name, store)| object_store_usage_bytes(name, store)),
))
}
+4 -3
View File
@@ -281,9 +281,10 @@ pub(crate) use self::image_data::{
pub(crate) use self::indexed_db::{
ConnectionRequestHandle, IndexedDbTaskSourceEntry, discard_indexed_db_task_by_id,
flush_blocked_indexed_db_requests, flush_indexed_db_connection_notification,
flush_indexed_db_task_by_id, flush_next_indexed_db_task, indexed_db_has_pending_tasks,
install_worker_indexed_db_runtime_state, retire_indexed_db_context,
set_indexed_db_manager_for_context, set_worker_indexed_db_task_wake_for_context,
flush_indexed_db_task_by_id, flush_indexed_db_transaction_starts, flush_next_indexed_db_task,
indexed_db_has_pending_tasks, install_worker_indexed_db_runtime_state,
retire_indexed_db_context, set_indexed_db_manager_for_context,
set_worker_indexed_db_task_wake_for_context,
};
#[cfg(test)]
pub(crate) use self::indexed_db::{
@@ -6,11 +6,11 @@ use super::{
INDEXED_DB_KEY_RANGE_MARKER_SLOT, INDEXED_DB_REQUEST_ERROR_SLOT,
INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_RESULT_SLOT,
INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_DB_KEY_SLOT,
INDEXED_DB_TRANSACTION_FINISHED_SLOT, INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery,
IndexEntry, IndexInfo, IndexedDbError, IndexedDbExecutionOwner, IndexedDbExternalObject,
IndexedDbManager, IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope,
IndexedDbValue, IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite,
INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT,
INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, IndexEntry, IndexInfo, IndexedDbError,
IndexedDbExecutionOwner, IndexedDbExternalObject, IndexedDbManager,
IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbValue,
IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite,
TransactionHandle, TransactionMode, context_host_ptr_from_global_bridge,
global_constructor_prototype, indexed_db_database_store_metadata,
indexed_db_object_store_metadata, indexed_db_runtime_array, indexed_db_transaction_mode,
@@ -1,7 +1,7 @@
use super::*;
mod connections;
mod readwrite;
mod transactions;
pub(in crate::context_bootstrap::indexed_db) use self::connections::{
database_connection_for_handle, database_registry_key,
@@ -9,7 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) use self::connections::{
register_blocked_database_context, register_open_database_connection,
unregister_blocked_database_context, unregister_open_database_connection,
};
pub(in crate::context_bootstrap::indexed_db) use self::readwrite::{
has_unfinished_readwrite_transaction_for_db, readwrite_transaction_can_start,
register_readwrite_transaction, transaction_db_key, unregister_readwrite_transaction,
pub(in crate::context_bootstrap::indexed_db) use self::transactions::{
register_regular_transaction, unregister_regular_transaction,
};
@@ -1,96 +0,0 @@
use super::*;
pub(in crate::context_bootstrap::indexed_db) fn transaction_db_key(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) -> Option<String> {
object_string_property(scope, transaction, INDEXED_DB_TRANSACTION_DB_KEY_SLOT)
}
pub(in crate::context_bootstrap::indexed_db) fn register_readwrite_transaction(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) {
push_unique_object_to_indexed_db_runtime_array(
scope,
IndexedDbRuntimeArray::ReadwriteTransactions,
transaction,
);
}
pub(in crate::context_bootstrap::indexed_db) fn unregister_readwrite_transaction(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) {
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions)
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;
};
if value.strict_equals(transaction.into()) {
continue;
}
let _ = next.set_index(scope, next.length(), value);
}
replace_indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions, next);
}
pub(in crate::context_bootstrap::indexed_db) fn readwrite_transaction_can_start(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) -> bool {
let Some(db_key) = transaction_db_key(scope, transaction) else {
return true;
};
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions)
else {
return true;
};
for index in 0..queue.length() {
let Some(value) = queue.get_index(scope, index) else {
continue;
};
let Ok(candidate) = v8::Local::<v8::Object>::try_from(value) else {
continue;
};
if candidate.strict_equals(transaction.into()) {
return true;
}
if transaction_db_key(scope, candidate).as_deref() == Some(&db_key)
&& !object_bool_property(scope, candidate, INDEXED_DB_TRANSACTION_FINISHED_SLOT)
.unwrap_or(false)
{
return false;
}
}
true
}
pub(in crate::context_bootstrap::indexed_db) fn has_unfinished_readwrite_transaction_for_db(
scope: &mut v8::PinScope<'_, '_>,
db_key: &str,
) -> bool {
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions)
else {
return false;
};
for index in 0..queue.length() {
let Some(value) = queue.get_index(scope, index) else {
continue;
};
let Ok(transaction) = v8::Local::<v8::Object>::try_from(value) else {
continue;
};
if transaction_db_key(scope, transaction).as_deref() == Some(db_key)
&& !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT)
.unwrap_or(false)
{
return true;
}
}
false
}
@@ -0,0 +1,35 @@
use super::*;
pub(in crate::context_bootstrap::indexed_db) fn register_regular_transaction(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) {
push_unique_object_to_indexed_db_runtime_array(
scope,
IndexedDbRuntimeArray::Transactions,
transaction,
);
}
pub(in crate::context_bootstrap::indexed_db) fn unregister_regular_transaction(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) {
crate::context_bootstrap::indexed_db::finish_indexed_db_transaction_start_request(
scope,
transaction,
);
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions) 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;
};
if !value.strict_equals(transaction.into()) {
let _ = next.set_index(scope, next.length(), value);
}
}
replace_indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions, next);
}
@@ -1,27 +1,25 @@
use super::{
INDEXED_DB_DATABASE_CLOSED_SLOT, INDEXED_DB_DATABASE_KEY_SLOT,
INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT,
INDEXED_DB_TRANSACTION_ACTIVE_SLOT, INDEXED_DB_TRANSACTION_COMMITTING_SLOT,
INDEXED_DB_TRANSACTION_FINISHED_SLOT, IndexedDbExecutionOwner, IndexedDbStorageScope,
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, dom_exception_value,
enqueue_blocked_delete_task, enqueue_blocked_open_task,
INDEXED_DB_DATABASE_CLOSED_SLOT, INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT,
INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT,
IndexedDbExecutionOwner, IndexedDbStorageScope, 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,
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, 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_optional_idb_key_path_member,
register_indexed_db_databases_settle_task, register_readwrite_transaction,
remove_database_store_metadata, request_error_object, require_idb_key,
set_database_store_metadata, set_indexed_db_slot_value,
enqueue_ready_transaction_starts, enqueue_transaction_abort_task,
enqueue_transaction_commit_task, 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, origin_allows_indexed_db,
parse_optional_idb_key_path_member, register_indexed_db_databases_settle_task,
register_regular_transaction, remove_database_store_metadata, request_error_object,
require_idb_key, 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,
sync_transaction_object_store_names_from_database, throw_type_error,
transaction_handle_from_value, unregister_open_database_connection,
unregister_readwrite_transaction, v8_string, v8str, validate_storage_bucket_scope,
unregister_regular_transaction, v8_string, v8str, validate_storage_bucket_scope,
with_indexed_db_manager,
};
@@ -1,5 +1,9 @@
use super::*;
use crate::context_bootstrap::indexed_db::schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint;
use crate::context_bootstrap::indexed_db::{
indexed_db_transaction_start_wake,
schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint,
set_indexed_db_transaction_start_request,
};
use crate::webidl;
use moli_indexeddb::{DatabaseHandle, IndexedDbError};
@@ -63,7 +67,7 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_database_transaction_callbac
let mut store_names = parsed.store_names.0;
store_names.sort_unstable();
store_names.dedup();
// Validate before queuing a readwrite transaction as well: its backend
// Validate before queuing a transaction as well: its backend
// handle may only be allocated after an earlier transaction finishes.
if store_names
.iter()
@@ -123,33 +127,28 @@ fn create_regular_transaction<'s>(
// Objects and task queues belong to the connection's realm even when the
// operation was borrowed from another Window. Conversion and exceptions
// remain in the callback's realm.
if mode == TransactionMode::ReadWrite {
let db_key = object_string_property(scope, database, INDEXED_DB_DATABASE_KEY_SLOT)
.unwrap_or_default();
let transaction_handle = if has_unfinished_readwrite_transaction_for_db(scope, &db_key) {
None
} else {
Some(with_indexed_db_manager(scope, |manager| {
manager.begin_transaction(handle, store_names, mode)
})?)
};
let Some(transaction) =
create_transaction_object(scope, database, transaction_handle, mode, store_names)
else {
return Ok(None);
};
register_readwrite_transaction(scope, transaction);
schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint(scope, transaction);
return Ok(Some(transaction));
}
let transaction_handle = with_indexed_db_manager(scope, |manager| {
manager.begin_transaction(handle, store_names, mode)
let owner = indexed_db_typed_execution_owner(scope, database).expect("IDB database owner");
let wake = indexed_db_transaction_start_wake(scope, owner);
let request = with_indexed_db_manager(scope, |manager| {
manager.queue_transaction_start(handle, store_names, mode, wake)
})?;
let transaction_handle = if request.handle().is_ready() {
Some(with_indexed_db_manager(scope, |manager| {
manager.start_queued_transaction(request.handle())
})?)
} else {
None
};
let Some(transaction) =
create_transaction_object(scope, database, Some(transaction_handle), mode, store_names)
create_transaction_object(scope, database, transaction_handle, mode, store_names)
else {
if let Some(handle) = transaction_handle {
let _ = with_indexed_db_manager(scope, |manager| manager.abort_transaction(handle));
}
return Ok(None);
};
set_indexed_db_transaction_start_request(scope, transaction, request);
register_regular_transaction(scope, transaction);
schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint(scope, transaction);
Ok(Some(transaction))
}
@@ -28,11 +28,8 @@ pub(in crate::context_bootstrap::indexed_db) fn finish_transaction_abort<'s>(
v8::Boolean::new(scope, true).into(),
);
abort_queued_transaction_requests(scope, transaction);
let db_key = transaction_db_key(scope, transaction);
unregister_readwrite_transaction(scope, transaction);
if let Some(db_key) = db_key {
enqueue_next_readwrite_transaction_start(scope, &db_key);
}
unregister_regular_transaction(scope, transaction);
enqueue_ready_transaction_starts(scope);
enqueue_transaction_abort_task(scope, transaction);
}
@@ -63,6 +63,10 @@ pub(crate) fn flush_blocked_indexed_db_requests(scope: &mut v8::PinScope<'_, '_>
flush_drain_blocked_open_requests_task(scope);
}
pub(crate) fn flush_indexed_db_transaction_starts(scope: &mut v8::PinScope<'_, '_>) {
enqueue_ready_transaction_starts(scope);
}
pub(crate) fn flush_indexed_db_connection_notification(
scope: &mut v8::PinScope<'_, '_>,
handle: moli_indexeddb::DatabaseHandle,
@@ -10,8 +10,7 @@ const INDEXED_DB_FACTORY_INITIALIZED_FIELD: &str = "moli.IndexedDb.runtime.facto
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";
const INDEXED_DB_TRANSACTION_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.transactionQueue";
#[derive(Default, WebApiObject)]
#[webapi(interface = web_api_interfaces::IDBFactory, require_prototype)]
@@ -33,6 +32,7 @@ pub(crate) enum IndexedDbTaskSourceEntry {
RuntimeQueue(IndexedDbTaskId),
DrainBlockedOpenRequests,
VersionChange(DatabaseHandle),
TransactionsReady,
}
#[derive(Clone, Copy)]
@@ -40,7 +40,7 @@ pub(in crate::context_bootstrap::indexed_db) enum IndexedDbRuntimeArray {
TaskQueue,
OpenDatabases,
BlockedOpenQueue,
ReadwriteTransactions,
Transactions,
}
impl IndexedDbRuntimeArray {
@@ -49,7 +49,7 @@ impl IndexedDbRuntimeArray {
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,
Self::Transactions => INDEXED_DB_TRANSACTION_QUEUE_FIELD,
}
}
}
@@ -159,6 +159,16 @@ pub(super) fn indexed_db_connection_notification_wake(
)
}
pub(super) fn indexed_db_transaction_start_wake(
scope: &mut v8::PinScope<'_, '_>,
owner: IndexedDbExecutionOwner,
) -> state::ConnectionRequestWake {
// Connection retirement aborts native transactions and cancels their
// admissions. This callback only wakes the owner; it must not reenter the
// storage manager when cancellation is running under the manager lock.
indexed_db_task_wake(scope, owner, IndexedDbTaskSourceEntry::TransactionsReady)
}
pub(super) fn indexed_db_has_external_task_source(scope: &mut v8::PinScope<'_, '_>) -> bool {
context_host_ptr_from_global_bridge(scope).is_some()
|| scope
@@ -323,7 +333,7 @@ fn ensure_runtime_state_fields<'s>(
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)?;
ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::Transactions)?;
Some(())
}
@@ -14,31 +14,30 @@ use super::{
IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbTransactionOperation, OpenOptions,
PreparedObjectStoreWrite, 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, dom_exception_value,
dom_string_list_values, enforce_object_store_unique_constraints,
create_database_object, create_transaction_object, database_registry_key,
define_non_enumerable_value_property, deserialize_js_value, dispatch_idb_named_event,
dispatch_version_change_event, dom_exception_value, enforce_object_store_unique_constraints,
enqueue_version_change_to_open_connections, flush_databases_settle_task,
has_open_database_connections_for_key, index_cursor_snapshot, indexed_db_blocked_task_payload,
indexed_db_index_info, indexed_db_open_task_payload, indexed_db_request_dispatch_task_request,
indexed_db_request_transaction_object, indexed_db_runtime_array,
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_typed_task_id, indexed_db_typed_task_kind, indexed_db_typed_task_owner_scope,
indexed_db_typed_task_storage_scope, inject_key_path_into_value, key_in_range, key_to_js_value,
indexed_db_runtime_array_contains_object, indexed_db_transaction_start_request,
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_typed_task_id, indexed_db_typed_task_kind,
indexed_db_typed_task_owner_scope, indexed_db_typed_task_storage_scope,
inject_key_path_into_value, key_in_range, 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, pop_first_indexed_db_task, push_indexed_db_operation_waiting_for_start,
object_number_property, object_store_cursor_snapshot, object_string_property,
pop_first_indexed_db_task, 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_slot_value,
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_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, unregister_readwrite_transaction, v8str,
validate_storage_bucket_scope, with_indexed_db_manager,
take_indexed_db_task_by_id, transaction_handle_from_value, unregister_blocked_database_context,
unregister_regular_transaction, v8str, validate_storage_bucket_scope, with_indexed_db_manager,
};
use super::{
IndexedDbTaskSourceEntry, context_host_ptr_from_global_bridge,
@@ -23,6 +23,7 @@ pub(crate) fn flush_next_indexed_db_task(scope: &mut v8::PinScope<'_, '_>) -> bo
scope, handle,
);
}
IndexedDbTaskSourceEntry::TransactionsReady => enqueue_ready_transaction_starts(scope),
}
return true;
}
@@ -126,9 +126,6 @@ fn finish_transaction<'s>(
INDEXED_DB_TRANSACTION_FINISHED_SLOT,
v8::Boolean::new(scope, true).into(),
);
let db_key = transaction_db_key(scope, transaction);
unregister_readwrite_transaction(scope, transaction);
if let Some(db_key) = db_key {
enqueue_next_readwrite_transaction_start(scope, &db_key);
}
unregister_regular_transaction(scope, transaction);
enqueue_ready_transaction_starts(scope);
}
@@ -21,27 +21,21 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_start_task<'s>
.unwrap_or(false)
|| object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT)
.unwrap_or(false)
|| !readwrite_transaction_can_start(scope, transaction)
{
return;
}
let Some(database) = object_property_as_object(scope, transaction, "db") else {
let Some(request) = indexed_db_transaction_start_request(scope, transaction) else {
return;
};
let Some(handle) = database_handle_from_value(scope, database.into()) else {
if !request.is_ready() {
return;
};
let store_names = object_property_as_object(scope, transaction, "objectStoreNames")
.map(|names| dom_string_list_values(scope, names))
.unwrap_or_default();
match with_indexed_db_manager(scope, |manager| {
manager.begin_transaction(handle, &store_names, TransactionMode::ReadWrite)
}) {
}
match with_indexed_db_manager(scope, |manager| manager.start_queued_transaction(&request)) {
Ok(transaction_handle) => {
success::start_readwrite_transaction(scope, transaction, transaction_handle);
success::start_regular_transaction(scope, transaction, transaction_handle);
}
Err(error) => {
fail::fail_readwrite_transaction_start(scope, transaction, &error);
fail::fail_regular_transaction_start(scope, transaction, &error);
}
}
}
@@ -1,30 +1,11 @@
use super::*;
use moli_indexeddb::IndexedDbError;
pub(super) fn fail_readwrite_transaction_start<'s>(
pub(super) fn fail_regular_transaction_start<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
error: &IndexedDbError,
) {
let error = request_error_object(scope, error);
let _ = transaction.set(scope, v8str(scope, "error").into(), error);
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(),
);
unregister_readwrite_transaction(scope, transaction);
if let Some(database) =
crate::context_bootstrap::indexed_db::indexed_db_transaction_database(scope, transaction)
{
crate::context_bootstrap::indexed_db::finish_indexed_db_database_close(scope, database);
}
let _ = dispatch_idb_named_event(scope, transaction, "error", |_, _| {});
crate::context_bootstrap::indexed_db::finish_transaction_abort(scope, transaction, error);
}
@@ -1,6 +1,6 @@
use super::*;
pub(super) fn start_readwrite_transaction<'s>(
pub(super) fn start_regular_transaction<'s>(
scope: &mut v8::PinScope<'s, '_>,
transaction: v8::Local<'s, v8::Object>,
transaction_handle: TransactionHandle,
@@ -7,7 +7,7 @@ mod successor;
pub(in crate::context_bootstrap::indexed_db) use self::abort::enqueue_transaction_abort_task;
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) use self::successor::enqueue_ready_transaction_starts;
pub(in crate::context_bootstrap::indexed_db) fn enqueue_transaction_operation_error<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -1,12 +1,10 @@
use super::start::enqueue_transaction_start_task;
use super::*;
pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transaction_start(
pub(in crate::context_bootstrap::indexed_db) fn enqueue_ready_transaction_starts(
scope: &mut v8::PinScope<'_, '_>,
db_key: &str,
) {
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions)
else {
let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions) else {
return;
};
for index in 0..queue.length() {
@@ -16,9 +14,6 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transacti
let Ok(transaction) = v8::Local::<v8::Object>::try_from(value) else {
continue;
};
if transaction_db_key(scope, transaction).as_deref() != Some(db_key) {
continue;
}
if object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT)
.unwrap_or(false)
|| object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_STARTED_SLOT)
@@ -26,7 +21,10 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transacti
{
continue;
}
enqueue_transaction_start_task(scope, transaction);
break;
if indexed_db_transaction_start_request(scope, transaction)
.is_some_and(|request| request.is_ready())
{
enqueue_transaction_start_task(scope, transaction);
}
}
}
@@ -398,6 +398,7 @@ struct IndexedDbTransactionLifecycleState {
database: Option<v8::Global<v8::Object>>,
upgrade_open_request: Option<v8::Global<v8::Object>>,
handle: Option<TransactionHandle>,
start_request: Option<moli_indexeddb::TransactionRequestLease>,
mode: TransactionMode,
active: bool,
committing: bool,
@@ -425,6 +426,7 @@ impl IndexedDbTransactionLifecycleState {
database: Some(database),
upgrade_open_request: None,
handle,
start_request: None,
mode,
active: true,
committing: false,
@@ -683,6 +685,9 @@ impl IndexedDbRuntimeStateTable {
for request in self.requests.values_mut() {
drop(request.connection_request.take());
}
for transaction in self.transactions.values_mut() {
drop(transaction.start_request.take());
}
}
}
@@ -880,6 +885,55 @@ pub(super) fn indexed_db_transaction_mode(
table.borrow().transactions.get(&id).map(|state| state.mode)
}
pub(super) fn set_indexed_db_transaction_start_request(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
request: moli_indexeddb::TransactionRequestLease,
) {
let id = indexed_db_typed_state_id(scope, transaction).expect("transaction id");
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("transaction state");
assert!(state.start_request.is_none(), "transaction scheduled twice");
state.start_request = Some(request);
}
pub(super) fn indexed_db_transaction_start_request(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) -> Option<moli_indexeddb::TransactionRequestHandle> {
let id = indexed_db_typed_state_id(scope, transaction)?;
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
Some(
table
.borrow()
.transactions
.get(&id)?
.start_request
.as_ref()?
.handle()
.clone(),
)
}
pub(super) fn finish_indexed_db_transaction_start_request(
scope: &mut v8::PinScope<'_, '_>,
transaction: v8::Local<'_, v8::Object>,
) {
let Some(id) = indexed_db_typed_state_id(scope, transaction) else {
return;
};
let table = indexed_db_runtime_state_table_for_object(scope, transaction);
let request = table
.borrow_mut()
.transactions
.get_mut(&id)
.and_then(|state| state.start_request.take());
// Releasing admission wakes other event loops after native commit/rollback
// and after releasing the local state borrow.
drop(request);
}
pub(in crate::context_bootstrap::indexed_db) fn schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint<
's,
>(
@@ -84,6 +84,10 @@ impl ScriptVm {
scope, handle,
)
}
RendererPageIndexedDbTaskKind::TransactionsReady => {
crate::context_bootstrap::flush_indexed_db_transaction_starts(scope);
true
}
};
execution_context
.dispatch_scope()
@@ -118,7 +122,8 @@ impl ScriptVm {
unsafe { &*host_ptr }.finish_indexed_db_blocked_drain(execution_context);
true
}
RendererPageIndexedDbTaskKind::VersionChange(_) => false,
RendererPageIndexedDbTaskKind::VersionChange(_)
| RendererPageIndexedDbTaskKind::TransactionsReady => false,
})
})?;
Ok(if removed {