fix(indexeddb): preserve UTF-16 database name identity

Carry IndexedDbName through factories, backend state, request queues and
connection notifications so unpaired surrogates stay distinct from U+FFFD.
Preserve the legacy JSON map for Unicode names and persist other names as
UTF-16 code units alongside it.

Cover records, versions, rollback, deletion, cross-realm calls and worker
notifications with a shared browser fixture, plus persistence and queue tests.

Validation: cargo fmt --all; strict workspace clippy; cargo nextest run
--no-fail-fast (19209 passed, 13 skipped).
This commit is contained in:
ldm0
2026-10-11 18:06:39 +08:00
parent c3a0006c10
commit 6a294ae325
26 changed files with 614 additions and 108 deletions
@@ -0,0 +1,182 @@
globalThis.databaseNameChecks = [];
async function databaseNamesProbe(prefix = 'database-names') {
const checks = databaseNameChecks;
const check = (label, pass, actual = '') => checks.push({label, pass, actual: String(actual)});
const units = name => Array.from({length: name.length}, (_, i) => name.charCodeAt(i)).join(',');
const result = request => new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const complete = tx => new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onabort = () => reject(tx.error || new Error('unexpected abort'));
});
const exercise = async (label, Realm, MethodRealm) => {
const here = (name, pass, actual) => check(label + ': ' + name, pass, actual);
const factory = Realm.indexedDB;
const open = (...args) => MethodRealm.IDBFactory.prototype.open.apply(factory, args);
const remove = (...args) => MethodRealm.IDBFactory.prototype.deleteDatabase.apply(factory, args);
const list = () => MethodRealm.IDBFactory.prototype.databases.call(factory);
const getName = Object.getOwnPropertyDescriptor(MethodRealm.IDBDatabase.prototype, 'name').get;
const stem = prefix + '-' + label + '-';
const names = ['', '\0', '\ud800', '\udfff', '\ufffd', '\ud801',
'\udfff\ud800', '\ufffd\ufffd', '💾', 'd800', '\ud800\0'].map(s => stem + s);
const retained = [];
for (let i = 0; i < names.length; ++i) {
const conversion = [];
const name = {[Symbol.toPrimitive](hint) { conversion.push(hint); return names[i]; }};
const version = {valueOf() { conversion.push('version'); return 1; }};
const request = open(name, version);
here('conversion order ' + i, conversion.join() === 'string,version', conversion);
let oldVersion, upgradeName;
request.onupgradeneeded = event => {
oldVersion = event.oldVersion;
upgradeName = getName.call(request.result);
request.result.createObjectStore('records');
};
const db = await result(request);
retained.push(db);
here('upgrade name ' + i, upgradeName === names[i], upgradeName === undefined ? '' : units(upgradeName));
here('independent creation ' + i, oldVersion === 0, oldVersion);
here('open name ' + i, db.name === names[i], units(db.name));
const tx = db.transaction('records', 'readwrite');
const done = complete(tx);
tx.objectStore('records').put(i, 'same-key');
await done;
db.close();
here('closed name ' + i, getName.call(db) === names[i], units(getName.call(db)));
}
let listed = (await list()).filter(db => db.name.startsWith(stem));
here('listing keeps every database', listed.length === names.length, listed.length);
for (let i = 0; i < names.length; ++i) {
here('listing units ' + i, listed.some(db => db.name === names[i] && db.version === 1));
const db = await result(open(new Realm.String(names[i])));
const tx = db.transaction('records');
const done = complete(tx);
const read = tx.objectStore('records').get('same-key');
await done;
here('independent records ' + i, read.result === i, read.result);
db.close();
}
const order = [];
const marker = new Error('name conversion');
let caught;
try {
open({toString() { order.push('name'); throw marker; }}, {valueOf() { order.push('version'); return 1; }});
} catch (error) { caught = error; }
here('name exception preserves identity and prevents version conversion', caught === marker && order.join() === 'name', order);
for (const [method, callback] of [['open', open], ['deleteDatabase', remove]]) {
let error;
try { callback(Symbol('name')); } catch (caught) { error = caught; }
here(method + ' rejects Symbol in callee realm', error !== undefined && Object.getPrototypeOf(error) === MethodRealm.TypeError.prototype, error?.name);
}
const upgrading = open(names[2], 2);
let upgradeOld;
upgrading.onupgradeneeded = event => {
upgradeOld = event.oldVersion;
upgrading.transaction.objectStore('records').put('upgraded', 'same-key');
};
const upgraded = await result(upgrading);
here('upgrade uses exact database', upgradeOld === 1 && upgraded.name === names[2], upgradeOld);
upgraded.close();
const aborted = open(names[3], 3);
aborted.onupgradeneeded = () => {
aborted.transaction.objectStore('records').put('aborted', 'same-key');
aborted.transaction.abort();
};
let abortError;
try { (await result(aborted)).close(); } catch (error) { abortError = error; }
here('aborted upgrade reports AbortError', abortError?.name === 'AbortError', abortError?.name);
listed = (await list()).filter(db => db.name.startsWith(stem));
for (let i = 0; i < names.length; ++i) {
here('versions remain independent ' + i, listed.some(db => db.name === names[i] && db.version === (i === 2 ? 2 : 1)));
}
const rollback = await result(open(names[3]));
const readTx = rollback.transaction('records');
const readDone = complete(readTx);
const rolledBack = readTx.objectStore('records').get('same-key');
await readDone;
here('aborted data remains independent', rolledBack.result === 3, rolledBack.result);
rollback.close();
// Keep a different surrogate name open. A mistaken identity match still
// completes via onblocked so every engine reports the same assertions.
const held = await result(open(names[3]));
let notifications = 0;
held.onversionchange = () => { ++notifications; };
const deleting = remove({toString() { return names[2]; }});
let blocked = false;
deleting.onblocked = () => { blocked = true; held.close(); };
await result(deleting);
held.close();
here('delete does not notify another database', notifications === 0, notifications);
here('delete is not blocked by another database', !blocked, blocked);
listed = (await list()).filter(db => db.name.startsWith(stem));
here('delete removes only the exact database', !listed.some(db => db.name === names[2])
&& listed.length === names.length - 1, listed.length);
here('retained connection name survives deletion', getName.call(retained[2]) === names[2], units(getName.call(retained[2])));
for (const name of names) await result(remove(name));
here('cleanup removes all test databases', !(await list()).some(db => db.name.startsWith(stem)));
};
await exercise('own', globalThis, globalThis);
if (typeof document !== 'undefined') {
const frame = document.createElement('iframe');
const loaded = new Promise(resolve => { frame.onload = resolve; });
frame.src = 'about:blank';
document.body.appendChild(frame);
await loaded;
try {
await exercise('foreign-method', globalThis, frame.contentWindow);
await exercise('foreign-factory', frame.contentWindow, globalThis);
} finally { frame.remove(); }
const firstName = prefix + '-shared-\ud800';
const secondName = prefix + '-shared-\udfff';
for (const name of [firstName, secondName]) {
const request = indexedDB.open(name, 1);
request.onupgradeneeded = () => request.result.createObjectStore('records');
(await result(request)).close();
}
const held = await result(indexedDB.open(firstName));
let notices = 0;
held.onversionchange = () => { ++notices; held.close(); };
const source = `onmessage = async event => {
const request = indexedDB.open(event.data, 2);
let oldVersion;
request.onupgradeneeded = event => { oldVersion = event.oldVersion; };
request.onerror = () => postMessage({error: request.error.name});
request.onsuccess = () => {
const db = request.result;
const name = db.name;
db.close();
postMessage({name, oldVersion});
};
};`;
const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'}));
const worker = new Worker(url);
try {
const reply = new Promise((resolve, reject) => {
worker.onmessage = event => resolve(event.data);
worker.onerror = event => reject(new Error(event.message));
});
worker.postMessage(secondName);
const observed = await reply;
check('worker upgrade preserves database name', observed.name === secondName && observed.oldVersion === 1, JSON.stringify(observed));
check('worker upgrade does not notify a different Window connection', notices === 0, notices);
held.close();
const listed = await indexedDB.databases();
check('Window sees independent versions after worker upgrade', listed.some(db => db.name === firstName && db.version === 1)
&& listed.some(db => db.name === secondName && db.version === 2));
} finally {
held.close();
worker.terminate();
URL.revokeObjectURL(url);
await result(indexedDB.deleteDatabase(firstName));
await result(indexedDB.deleteDatabase(secondName));
}
}
return {state: checks.every(check => check.pass) ? 'pass' : 'fail', checks};
}
@@ -21,6 +21,28 @@ async fn indexed_db_key_ranges_protect_bounds_and_validate_receivers() -> Result
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_database_names_preserve_utf16_identity_across_realms_and_queues() -> Result<()>
{
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let fixture = include_str!("fixtures/indexeddb-database-names.js");
for target in ["window", "child", "worker"] {
let source = format!(
"{fixture}\ndatabaseNamesProbe('database-names-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: databaseNameChecks}}));"
);
let result = run_probe(&browser, &server, target, &source).await?;
assert_eq!(result["state"], "pass", "{target}: {result}");
assert_eq!(
result["checks"].as_array().unwrap().len(),
if target == "worker" { 100 } else { 303 },
"{target}: {result}"
);
}
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn indexed_db_database_attributes_preserve_connection_state_across_upgrade_and_close()
-> Result<()> {
+5 -4
View File
@@ -1,6 +1,7 @@
//! FIFO coordination for IDBFactory open/delete algorithms. Queue ownership
//! spans event loops; callbacks only wake the accepting loop and carry no JS values.
use crate::IndexedDbName;
use parking_lot::Mutex;
use std::{
collections::{BTreeMap, VecDeque},
@@ -17,7 +18,7 @@ pub struct ConnectionRequestQueues {
#[derive(Default)]
struct QueueState {
next_id: u64,
queues: BTreeMap<(String, String), VecDeque<QueuedRequest>>,
queues: BTreeMap<(String, IndexedDbName), VecDeque<QueuedRequest>>,
}
struct QueuedRequest {
@@ -30,7 +31,7 @@ struct QueuedRequest {
#[derive(Clone)]
pub struct ConnectionRequestHandle {
queues: Weak<ConnectionRequestQueues>,
key: (String, String),
key: (String, IndexedDbName),
id: u64,
}
@@ -52,10 +53,10 @@ impl ConnectionRequestQueues {
pub fn enqueue(
self: &Arc<Self>,
storage_key: &str,
name: &str,
name: impl Into<IndexedDbName>,
wake: ConnectionRequestWake,
) -> ConnectionRequestLease {
let key = (storage_key.to_owned(), name.to_owned());
let key = (storage_key.to_owned(), name.into());
let mut state = self.state.lock();
state.next_id = state
.next_id
+14 -8
View File
@@ -135,12 +135,17 @@ impl IndexedDbManager {
})
}
pub fn delete_database(&mut self, origin: &str, name: &str) -> Result<(), IndexedDbError> {
pub fn delete_database(
&mut self,
origin: &str,
name: impl Into<IndexedDbName>,
) -> Result<(), IndexedDbError> {
let name = &name.into();
self.ensure_origin_loaded(origin)?;
if self
.databases
.values()
.any(|db| db.origin == origin && db.name == name)
.any(|db| db.origin == origin && &db.name == name)
{
return Err(IndexedDbError::InvalidState(format!(
"database `{name}` for origin `{origin}` is still open"
@@ -269,7 +274,7 @@ impl IndexedDbManager {
pub(crate) fn committed_origin_usage_except_database(
&self,
origin: &str,
db_name: &str,
db_name: &IndexedDbName,
) -> u64 {
self.origins
.get(origin)
@@ -278,7 +283,7 @@ impl IndexedDbManager {
state
.databases
.iter()
.filter(|(name, _)| name.as_str() != db_name)
.filter(|(name, _)| *name != db_name)
.map(|(name, database)| database_usage_bytes(name, database)),
)
})
@@ -330,8 +335,9 @@ impl IndexedDbManager {
pub fn database_version(
&mut self,
origin: &str,
name: &str,
name: impl Into<IndexedDbName>,
) -> Result<Option<u64>, IndexedDbError> {
let name = &name.into();
self.ensure_origin_loaded(origin)?;
Ok(self
.origins
@@ -1126,7 +1132,7 @@ impl IndexedDbManager {
Ok(())
}
fn allocate_database_handle(&mut self, origin: String, name: String) -> DatabaseHandle {
fn allocate_database_handle(&mut self, origin: String, name: IndexedDbName) -> DatabaseHandle {
let handle =
DatabaseHandle::from_raw(self.next_database_handle.fetch_add(1, Ordering::Relaxed));
self.databases.insert(
@@ -1144,7 +1150,7 @@ impl IndexedDbManager {
&mut self,
database: DatabaseHandle,
origin: &str,
db_name: &str,
db_name: &IndexedDbName,
working_copy: DatabaseData,
) -> TransactionHandle {
let stores = working_copy.stores.keys().cloned().collect::<BTreeSet<_>>();
@@ -1181,7 +1187,7 @@ impl IndexedDbManager {
pub(crate) fn database_data(
&self,
origin: &str,
name: &str,
name: &IndexedDbName,
) -> Result<&DatabaseData, IndexedDbError> {
self.origins
.get(origin)
+38 -9
View File
@@ -24,6 +24,17 @@ struct PersistentOrigin {
#[serde(default)]
origin: Option<String>,
databases: BTreeMap<String, PersistentDatabase>,
// Keep ordinary Unicode names in the legacy JSON map. JSON strings
// cannot represent names containing unpaired UTF-16 surrogates.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
databases_utf16: Vec<PersistentNamedDatabase>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistentNamedDatabase {
name: Vec<u16>,
#[serde(flatten)]
database: PersistentDatabase,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -350,7 +361,17 @@ pub(crate) fn origin_path(storage_root: &Path, origin: &str) -> PathBuf {
fn origin_state_from_persistent(persistent: PersistentOrigin) -> OriginState {
let mut databases = BTreeMap::new();
for (name, database) in persistent.databases {
for (name, database) in persistent
.databases
.into_iter()
.map(|(name, database)| (IndexedDbName::from(name), database))
.chain(
persistent
.databases_utf16
.into_iter()
.map(|named| (IndexedDbName::from_utf16(named.name), named.database)),
)
{
let mut stores = BTreeMap::new();
for (store_name, store) in database
.stores
@@ -417,6 +438,7 @@ fn origin_state_from_persistent(persistent: PersistentOrigin) -> OriginState {
fn persistent_origin_from_state(origin: &str, state: &OriginState) -> PersistentOrigin {
let mut databases = BTreeMap::new();
let mut databases_utf16 = Vec::new();
for (name, database) in &state.databases {
let mut stores = BTreeMap::new();
let mut stores_utf16 = Vec::new();
@@ -465,17 +487,24 @@ fn persistent_origin_from_state(origin: &str, state: &OriginState) -> Persistent
}),
}
}
databases.insert(
name.clone(),
PersistentDatabase {
version: database.version,
stores,
stores_utf16,
},
);
let database = PersistentDatabase {
version: database.version,
stores,
stores_utf16,
};
match String::from_utf16(name.as_utf16()) {
Ok(name) => {
databases.insert(name, database);
}
Err(_) => databases_utf16.push(PersistentNamedDatabase {
name: name.as_utf16().to_vec(),
database,
}),
}
}
PersistentOrigin {
origin: Some(origin.to_owned()),
databases,
databases_utf16,
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ use crate::{
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct OriginState {
pub(crate) databases: BTreeMap<String, DatabaseData>,
pub(crate) databases: BTreeMap<IndexedDbName, DatabaseData>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -38,7 +38,7 @@ pub(crate) struct IndexData {
#[derive(Debug, Clone)]
pub(crate) struct DatabaseHandleState {
pub(crate) origin: String,
pub(crate) name: String,
pub(crate) name: IndexedDbName,
pub(crate) closed: bool,
}
@@ -46,7 +46,7 @@ pub(crate) struct DatabaseHandleState {
pub(crate) struct TransactionState {
pub(crate) database: DatabaseHandle,
pub(crate) origin: String,
pub(crate) db_name: String,
pub(crate) db_name: IndexedDbName,
pub(crate) mode: TransactionMode,
pub(crate) stores: BTreeSet<IndexedDbName>,
pub(crate) state: TransactionLifecycle,
+45 -44
View File
@@ -18,6 +18,7 @@ struct TestDir {
path: PathBuf,
}
mod database_names;
mod durability;
mod index_rename;
mod record_revision;
@@ -271,7 +272,7 @@ fn seed_database_record(manager: &mut IndexedDbManager, origin: &str, name: &str
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: name.to_owned(),
name: name.into(),
version: None,
})
.expect("open should succeed");
@@ -309,7 +310,7 @@ fn open_new_database_creates_upgrade_transaction() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -331,7 +332,7 @@ fn in_memory_manager_does_not_persist_across_reopen() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(1),
})
.expect("open should succeed");
@@ -358,7 +359,7 @@ fn in_memory_manager_does_not_persist_across_reopen() {
let reopened = IndexedDbManager::new_in_memory()
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("reopen should succeed");
@@ -406,7 +407,7 @@ fn upgrade_transaction_can_create_store_and_persist() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(3),
})
.expect("open should succeed");
@@ -435,7 +436,7 @@ fn upgrade_transaction_can_create_store_and_persist() {
.expect("manager should be recreated")
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("reopen should succeed");
@@ -452,7 +453,7 @@ fn upgrade_transaction_can_persist_index_metadata() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(1),
})
.expect("open should succeed");
@@ -507,7 +508,7 @@ fn upgrade_transaction_can_persist_index_metadata() {
let reopened = reopened_manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("reopen should succeed");
@@ -526,7 +527,7 @@ fn index_key_path_may_be_empty_string() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(1),
})
.expect("open should succeed");
@@ -560,7 +561,7 @@ fn readwrite_transaction_can_store_and_reload_records() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -595,7 +596,7 @@ fn readwrite_transaction_can_store_and_reload_records() {
let reopened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("reopen should succeed");
@@ -640,7 +641,7 @@ fn external_blob_file_and_file_system_handle_objects_persist_with_their_record()
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -679,7 +680,7 @@ fn external_blob_file_and_file_system_handle_objects_persist_with_their_record()
let reopened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("database should reopen");
@@ -705,7 +706,7 @@ fn write_quota_rejection_rolls_back_working_copy() {
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -778,7 +779,7 @@ fn external_blob_bytes_participate_in_quota_and_rollback() {
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -837,7 +838,7 @@ fn transaction_commit_rechecks_aggregate_quota_without_publishing_working_copy()
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -911,7 +912,7 @@ fn origin_usage_tracks_committed_metadata_and_record_bytes() {
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1157,7 +1158,7 @@ fn readwrite_transaction_can_list_keys() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1202,7 +1203,7 @@ fn mixed_keys_sort_in_indexeddb_order() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1250,7 +1251,7 @@ fn aborted_transaction_does_not_persist_changes() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1297,7 +1298,7 @@ fn delete_database_removes_persisted_state() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1321,7 +1322,7 @@ fn delete_database_removes_persisted_state() {
.expect("manager should reopen")
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1343,7 +1344,7 @@ fn databases_lists_committed_name_version_snapshot() {
let beta = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "beta".to_owned(),
name: "beta".into(),
version: Some(2),
})
.expect("beta open should succeed");
@@ -1353,7 +1354,7 @@ fn databases_lists_committed_name_version_snapshot() {
let alpha = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "alpha".to_owned(),
name: "alpha".into(),
version: None,
})
.expect("alpha open should succeed");
@@ -1368,11 +1369,11 @@ fn databases_lists_committed_name_version_snapshot() {
infos,
vec![
DatabaseNameAndVersion {
name: "alpha".to_owned(),
name: "alpha".into(),
version: 1,
},
DatabaseNameAndVersion {
name: "beta".to_owned(),
name: "beta".into(),
version: 2,
},
]
@@ -1399,7 +1400,7 @@ fn clear_origin_removes_persisted_state_and_keeps_other_origins() {
let first = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("first open should succeed");
@@ -1416,7 +1417,7 @@ fn clear_origin_removes_persisted_state_and_keeps_other_origins() {
let second = manager
.open(OpenOptions {
origin: "https://other.example".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("second open should succeed");
@@ -1438,7 +1439,7 @@ fn clear_origin_removes_persisted_state_and_keeps_other_origins() {
let cleared = reopened
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("cleared origin open should succeed");
@@ -1453,7 +1454,7 @@ fn clear_origin_removes_persisted_state_and_keeps_other_origins() {
let kept = reopened
.open(OpenOptions {
origin: "https://other.example".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("other origin open should succeed");
@@ -1468,7 +1469,7 @@ fn readonly_transaction_rejects_writes() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1504,7 +1505,7 @@ fn transaction_scope_is_enforced_per_object_store() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1543,7 +1544,7 @@ fn delete_database_rejects_open_connections() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1571,7 +1572,7 @@ fn aborted_upgrade_transaction_does_not_publish_schema() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(2),
})
.expect("open should succeed");
@@ -1592,7 +1593,7 @@ fn aborted_upgrade_transaction_does_not_publish_schema() {
.expect("manager should reopen")
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("reopen should succeed");
@@ -1614,7 +1615,7 @@ fn upgrade_can_delete_object_store_before_commit() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -1653,7 +1654,7 @@ fn distinct_origins_persist_to_distinct_storage_files() {
let first = manager
.open(OpenOptions {
origin: "https://a:b".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("first open should succeed");
@@ -1673,7 +1674,7 @@ fn distinct_origins_persist_to_distinct_storage_files() {
let second = manager
.open(OpenOptions {
origin: "https://a/b".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("second open should succeed");
@@ -1694,14 +1695,14 @@ fn distinct_origins_persist_to_distinct_storage_files() {
let first = reopened
.open(OpenOptions {
origin: "https://a:b".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("first reopen should succeed");
let second = reopened
.open(OpenOptions {
origin: "https://a/b".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("second reopen should succeed");
@@ -1730,7 +1731,7 @@ fn failed_open_does_not_block_later_delete_database() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(2),
})
.expect("open should succeed");
@@ -1750,7 +1751,7 @@ fn failed_open_does_not_block_later_delete_database() {
let error = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: Some(1),
})
.expect_err("lower version open should fail");
@@ -1768,7 +1769,7 @@ fn generated_key_preview_and_failed_quota_write_leave_generator_unchanged() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "key-preview".to_owned(),
name: "key-preview".into(),
version: None,
})
.unwrap();
@@ -1841,7 +1842,7 @@ fn auto_increment_rejects_exhausted_generator_range() {
let opened = manager
.open(OpenOptions {
origin: "https://example.com".to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
+232
View File
@@ -0,0 +1,232 @@
use super::*;
use std::sync::Arc;
const ORIGIN: &str = "https://database-names.test";
fn open(manager: &mut IndexedDbManager, name: &IndexedDbName, version: u64) -> OpenResult {
manager
.open(OpenOptions {
origin: ORIGIN.into(),
name: name.clone(),
version: Some(version),
})
.unwrap()
}
#[test]
fn database_names_preserve_utf16_records_versions_and_legacy_persistence() {
let dir = TestDir::new();
let legacy = IndexedDbName::from("legacy-\0-数据库-💾");
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
let db = open(&mut manager, &legacy, 1);
manager
.commit_transaction(db.upgrade_transaction.unwrap())
.unwrap();
manager.close_database(db.database).unwrap();
drop(manager);
let path = origin_path(&dir.path, ORIGIN);
let persisted: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
assert!(persisted.get("databases_utf16").is_none());
assert_eq!(persisted["databases"]["legacy-\0-数据库-💾"]["version"], 1);
let names = [
vec![],
vec![0],
vec![0xd800],
vec![0xd801],
vec![0xdfff],
vec![0xfffd],
vec![0xdfff, 0xd800],
vec![0xd83d, 0xdcbe],
vec![0xe000],
]
.map(IndexedDbName::from_utf16);
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
let db = open(&mut manager, &legacy, 1);
assert_eq!(db.disposition, OpenDisposition::Existing);
manager.close_database(db.database).unwrap();
for (i, name) in names.iter().enumerate() {
let db = open(&mut manager, name, 1);
assert_eq!(
db.disposition,
OpenDisposition::UpgradeNeeded {
old_version: 0,
new_version: 1
}
);
let tx = db.upgrade_transaction.unwrap();
manager
.create_object_store(tx, "s", ObjectStoreOptions::default())
.unwrap();
manager
.put(tx, "s", Some(Key::from(1)), vec![i as u8])
.unwrap();
manager.commit_transaction(tx).unwrap();
manager.close_database(db.database).unwrap();
}
drop(manager);
let persisted: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
assert_eq!(persisted["databases"].as_object().unwrap().len(), 6);
assert_eq!(persisted["databases_utf16"].as_array().unwrap().len(), 4);
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
let listed = manager
.databases(ORIGIN)
.unwrap()
.into_iter()
.map(|db| db.name)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(listed, names.iter().cloned().chain([legacy]).collect());
for (i, name) in names.iter().enumerate() {
let db = open(&mut manager, name, 1);
assert_eq!(db.disposition, OpenDisposition::Existing);
assert_eq!(manager.database_info(db.database).unwrap().name, *name);
let tx = manager
.begin_transaction(db.database, &["s".into()], TransactionMode::ReadOnly)
.unwrap();
assert_eq!(
manager.get(tx, "s", &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![i as u8].into()))
);
manager.commit_transaction(tx).unwrap();
manager.close_database(db.database).unwrap();
}
let db = open(&mut manager, &names[2], 2);
let tx = db.upgrade_transaction.unwrap();
manager.put(tx, "s", Some(Key::from(1)), vec![99]).unwrap();
manager.abort_transaction(tx).unwrap();
let tx = manager
.begin_transaction(db.database, &["s".into()], TransactionMode::ReadOnly)
.unwrap();
assert_eq!(
manager.get(tx, "s", &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![2].into()))
);
manager.commit_transaction(tx).unwrap();
manager.close_database(db.database).unwrap();
assert_eq!(
manager.database_version(ORIGIN, &names[2]).unwrap(),
Some(1)
);
let db = open(&mut manager, &names[3], 2);
manager
.commit_transaction(db.upgrade_transaction.unwrap())
.unwrap();
manager.close_database(db.database).unwrap();
// A connection to another surrogate name must not block deleting this one.
let held = open(&mut manager, &names[4], 1);
manager.delete_database(ORIGIN, &names[2]).unwrap();
assert!(matches!(
manager.delete_database(ORIGIN, &names[4]),
Err(IndexedDbError::InvalidState(_))
));
manager.close_database(held.database).unwrap();
drop(manager);
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
for (i, name) in names.iter().enumerate() {
assert_eq!(
manager.database_version(ORIGIN, name).unwrap(),
match i {
2 => None,
3 => Some(2),
_ => Some(1),
}
);
}
}
#[test]
fn connection_and_transaction_queues_use_exact_database_code_units() {
let mut manager = IndexedDbManager::new_in_memory();
let names = [0xd800, 0xdfff, 0xfffd].map(|unit| IndexedDbName::from_utf16(vec![unit]));
let queues = manager.connection_request_queues();
let leases = names
.each_ref()
.map(|name| queues.enqueue(ORIGIN, name, Arc::new(|| {})));
let same = queues.enqueue(ORIGIN, &names[0], Arc::new(|| {}));
assert!(leases.iter().all(|lease| lease.handle().is_head()));
assert!(!same.handle().is_head());
let databases = names.each_ref().map(|name| {
let db = open(&mut manager, name, 1);
let tx = db.upgrade_transaction.unwrap();
manager
.create_object_store(tx, "s", ObjectStoreOptions::default())
.unwrap();
manager.commit_transaction(tx).unwrap();
db.database
});
let transactions = databases.map(|db| {
manager
.queue_transaction_start(
db,
&["s".into()],
TransactionMode::ReadWrite,
Arc::new(|| {}),
)
.unwrap()
});
let same_tx = manager
.queue_transaction_start(
databases[0],
&["s".into()],
TransactionMode::ReadWrite,
Arc::new(|| {}),
)
.unwrap();
assert!(transactions.iter().all(|lease| lease.handle().is_ready()));
assert!(!same_tx.handle().is_ready());
for (i, lease) in transactions.into_iter().enumerate() {
let tx = manager.start_queued_transaction(lease.handle()).unwrap();
manager
.put(tx, "s", Some(Key::from(1)), vec![i as u8])
.unwrap();
manager.commit_transaction(tx).unwrap();
}
assert!(same_tx.handle().is_ready());
let tx = manager.start_queued_transaction(same_tx.handle()).unwrap();
assert_eq!(
manager.get(tx, "s", &Key::from(1)).unwrap(),
RequestOutcome::Value(Some(vec![0].into()))
);
manager.abort_transaction(tx).unwrap();
drop(leases);
assert!(same.handle().is_head());
}
#[test]
fn database_name_quota_accounting_preserves_other_databases_on_rollback() {
let mut manager = IndexedDbManager::new_in_memory();
let names = [0xd800, 0xdfff, 0xfffd].map(|unit| IndexedDbName::from_utf16(vec![unit]));
for name in &names {
let db = open(&mut manager, name, 1);
manager
.commit_transaction(db.upgrade_transaction.unwrap())
.unwrap();
manager.close_database(db.database).unwrap();
}
assert_eq!(manager.origin_usage_bytes(ORIGIN).unwrap(), 33);
let db = open(&mut manager, &names[0], 2);
let tx = db.upgrade_transaction.unwrap();
manager
.create_object_store(tx, "s", ObjectStoreOptions::default())
.unwrap();
assert!(matches!(
manager.commit_transaction_with_quota(
tx,
IndexedDbQuotaCheck {
quota: 33,
non_indexed_db_usage: 0
}
),
Err(IndexedDbError::QuotaExceeded { .. })
));
assert_eq!(manager.origin_usage_bytes(ORIGIN).unwrap(), 33);
assert!(
manager
.databases(ORIGIN)
.unwrap()
.iter()
.all(|db| db.version == 1)
);
}
+3 -3
View File
@@ -20,7 +20,7 @@ pub(crate) struct TransactionRequestQueues {
#[derive(Default)]
struct QueueState {
next_id: u64,
queues: BTreeMap<(String, String), VecDeque<QueuedRequest>>,
queues: BTreeMap<(String, IndexedDbName), VecDeque<QueuedRequest>>,
}
struct TransactionScope {
@@ -46,7 +46,7 @@ struct QueuedRequest {
#[derive(Clone)]
pub struct TransactionRequestHandle {
queues: Weak<TransactionRequestQueues>,
key: (String, String),
key: (String, IndexedDbName),
id: u64,
database: DatabaseHandle,
scope: Arc<TransactionScope>,
@@ -71,7 +71,7 @@ impl Drop for TransactionRequestLease {
impl TransactionRequestQueues {
pub(crate) fn enqueue(
self: &Arc<Self>,
key: (String, String),
key: (String, IndexedDbName),
database: DatabaseHandle,
stores: BTreeSet<IndexedDbName>,
mode: TransactionMode,
+3 -3
View File
@@ -54,7 +54,7 @@ pub enum OpenDisposition {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenOptions {
pub origin: String,
pub name: String,
pub name: IndexedDbName,
pub version: Option<u64>,
}
@@ -67,14 +67,14 @@ pub struct OpenResult {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseInfo {
pub name: String,
pub name: IndexedDbName,
pub version: u64,
pub object_store_names: Vec<IndexedDbName>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseNameAndVersion {
pub name: String,
pub name: IndexedDbName,
pub version: u64,
}
+3 -3
View File
@@ -16,15 +16,15 @@ pub(crate) fn origin_usage_bytes(state: &OriginState) -> u64 {
)
}
pub(crate) fn database_usage_bytes(name: &str, database: &DatabaseData) -> u64 {
pub(crate) fn database_usage_bytes(name: &IndexedDbName, database: &DatabaseData) -> u64 {
database_usage_bytes_for_stores(name, database.stores.iter())
}
pub(crate) fn database_usage_bytes_for_stores<'a>(
name: &str,
name: &IndexedDbName,
stores: impl Iterator<Item = (&'a IndexedDbName, &'a ObjectStoreData)>,
) -> u64 {
string_usage_bytes(name)
name.usage_bytes()
.saturating_add(U64_STORAGE_BYTES)
.saturating_add(sum_usage(
stores.map(|(name, store)| object_store_usage_bytes(name, store)),
+1 -1
View File
@@ -151,7 +151,7 @@ fn seed_indexed_db_usage(manager: &moli_core::storage::SharedIndexedDbManager, o
let opened = manager
.open(moli_core::storage::IndexedDbOpenOptions {
origin: origin.to_owned(),
name: "site-data".to_owned(),
name: "site-data".into(),
version: Some(1),
})
.expect("IndexedDB seed open should succeed");
@@ -7,12 +7,12 @@ use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::IDBDatabase, require_prototype)]
struct IdbDatabaseObjectDeclaration {
struct IdbDatabaseObjectDeclaration<'scope> {
#[webapi(slot = INDEXED_DB_EVENT_LISTENERS_SLOT, init = "null_object")]
event_listeners: (),
#[webapi(slot = DATABASE_NAME_SLOT)]
name: String,
name: v8::Local<'scope, v8::String>,
#[webapi(slot = DATABASE_VERSION_SLOT)]
version: f64,
}
@@ -26,7 +26,8 @@ pub(in crate::context_bootstrap::indexed_db) fn create_database_object<'s>(
) -> Option<v8::Local<'s, v8::Object>> {
let storage_key = storage_scope.storage_key().to_owned();
let database_key = database_registry_key(&storage_key, &info.name);
let database = IdbDatabaseObjectDeclaration::new(info.name.clone(), info.version as f64)
let name = idb_name_to_v8(scope, &info.name);
let database = IdbDatabaseObjectDeclaration::new(name, info.version as f64)
.bind(scope)
.ok()?;
register_indexed_db_wrapper_with_owner(
@@ -9,9 +9,17 @@ use crate::context_bootstrap::indexed_db::{
pub(in crate::context_bootstrap::indexed_db) fn database_registry_key(
origin: &str,
name: &str,
name: &IndexedDbName,
) -> String {
format!("{origin}\u{0}{name}")
// Connection notifications carry opaque strings. Encode code units rather
// than formatting a DOMString lossily; four hex digits per unit also keep
// names containing NUL distinct from the storage-key separator.
use std::fmt::Write;
let mut key = format!("{origin}\u{0}");
for unit in name.as_utf16() {
write!(key, "{unit:04x}").expect("database registry key");
}
key
}
pub(in crate::context_bootstrap::indexed_db) fn has_open_database_connections_for_key(
@@ -9,7 +9,7 @@ use super::{
dom_exception_value, enqueue_blocked_delete_task, enqueue_blocked_open_task,
enqueue_drain_blocked_open_requests_task, enqueue_indexed_db_task,
enqueue_ready_transaction_starts, enqueue_transaction_abort_task,
enqueue_transaction_commit_task, indexed_db_databases_settle_task_payload,
enqueue_transaction_commit_task, idb_name_to_v8, 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,
@@ -19,8 +19,7 @@ use super::{
storage_scope_for_window_execution_context, store_request_error,
sync_transaction_object_store_names_from_database, throw_type_error,
transaction_handle_from_value, unregister_open_database_connection,
unregister_regular_transaction, v8_string, validate_storage_bucket_scope,
with_indexed_db_manager,
unregister_regular_transaction, validate_storage_bucket_scope, with_indexed_db_manager,
};
use moli_indexeddb::IndexedDbName;
@@ -70,9 +70,8 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_databases_callback<'
let array = v8::Array::new(scope, infos.len() as i32);
for (index, info) in infos.into_iter().enumerate() {
let object = ObjectLiteralDeclaration::bind(scope);
if let Some(name) = v8_string(scope, &info.name) {
object.set_string_property(scope, "name", name.into());
}
let name = idb_name_to_v8(scope, &info.name);
object.set_string_property(scope, "name", name.into());
let version = v8::Number::new(scope, info.version as f64);
object.set_string_property(scope, "version", version.into());
let _ = array.set_index(scope, index as u32, object.into_value());
@@ -5,8 +5,8 @@ use crate::webidl;
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "IDBFactory.deleteDatabase")]
struct IdbFactoryDeleteDatabaseArgs {
#[webidl(required)]
name: String,
#[webidl(required, with = parse_database_name_arg)]
name: IndexedDbName,
}
pub(in crate::context_bootstrap::indexed_db) fn idb_factory_delete_database_callback<'s>(
@@ -60,3 +60,16 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_delete_database_call
enqueue_blocked_delete_task(scope, request, &origin, &name);
rv.set(request.into());
}
fn parse_database_name_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<IndexedDbName, webidl::WebIdlError> {
webidl::convert::<webidl::DomString16>(
scope,
args.get(index),
webidl::Context::argument("IDBFactory.deleteDatabase", (index + 1) as usize),
)
.map(|name| IndexedDbName::from_utf16(name.0))
}
@@ -5,8 +5,8 @@ use crate::webidl;
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "IDBFactory.open")]
struct IdbFactoryOpenArgs {
#[webidl(required)]
name: String,
#[webidl(required, with = parse_database_name_arg)]
name: IndexedDbName,
#[webidl(converter = "enforce_range_unsigned_long_long")]
version: Option<u64>,
}
@@ -71,3 +71,16 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_factory_open_callback<'s>(
enqueue_blocked_open_task(scope, request, &origin, &name, version);
rv.set(request.into());
}
fn parse_database_name_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<IndexedDbName, webidl::WebIdlError> {
webidl::convert::<webidl::DomString16>(
scope,
args.get(index),
webidl::Context::argument("IDBFactory.open", (index + 1) as usize),
)
.map(|name| IndexedDbName::from_utf16(name.0))
}
@@ -130,7 +130,7 @@ mod tests {
let opened = manager
.open(OpenOptions {
origin: origin.to_owned(),
name: "app".to_owned(),
name: "app".into(),
version: None,
})
.expect("open should succeed");
@@ -5,7 +5,7 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_delete_database_request<
scope: &mut v8::PinScope<'s, '_>,
request: v8::Local<'s, v8::Object>,
storage_scope: IndexedDbStorageScope,
name: String,
name: IndexedDbName,
) {
if let Err(error) = validate_storage_bucket_scope(scope, &storage_scope) {
let error = request_error_object(scope, &error);
@@ -7,7 +7,7 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_open_request<'s>(
scope: &mut v8::PinScope<'s, '_>,
request: v8::Local<'s, v8::Object>,
storage_scope: IndexedDbStorageScope,
name: String,
name: IndexedDbName,
version: Option<u64>,
) {
let database_name = name.clone();
@@ -2,7 +2,7 @@ use super::*;
pub(super) fn opened_database_info(
scope: &mut v8::PinScope<'_, '_>,
database_name: &str,
database_name: &IndexedDbName,
opened: &moli_indexeddb::OpenResult,
) -> Result<DatabaseInfo, moli_indexeddb::IndexedDbError> {
match with_indexed_db_manager(scope, |manager| manager.database_info(opened.database)) {
@@ -17,7 +17,7 @@ pub(super) fn opened_database_info(
old_version: 0,
new_version,
} => Ok(DatabaseInfo {
name: database_name.to_string(),
name: database_name.clone(),
version: *new_version,
object_store_names: Vec::new(),
}),
@@ -34,7 +34,7 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_open_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
request: v8::Local<'s, v8::Object>,
origin: &str,
name: &str,
name: &IndexedDbName,
version: Option<u64>,
) {
let context = request
@@ -59,7 +59,7 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_blocked_delete_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
request: v8::Local<'s, v8::Object>,
origin: &str,
name: &str,
name: &IndexedDbName,
) {
let context = request
.get_creation_context(scope)
@@ -278,7 +278,7 @@ impl IndexedDbOpenTaskPayload {
struct IndexedDbBlockedTaskPayload {
request: RealmValueHandle,
origin: String,
name: String,
name: IndexedDbName,
version: Option<u64>,
old_version: u64,
new_version: Option<u64>,
@@ -294,7 +294,7 @@ impl IndexedDbBlockedTaskPayload {
scope: &mut v8::PinScope<'_, '_>,
request: v8::Local<'_, v8::Object>,
origin: impl Into<String>,
name: impl Into<String>,
name: impl Into<IndexedDbName>,
version: Option<u64>,
old_version: u64,
new_version: Option<u64>,
@@ -319,7 +319,7 @@ impl IndexedDbBlockedTaskPayload {
pub(super) struct IndexedDbBlockedTaskPayloadLocals<'s> {
pub(super) request: v8::Local<'s, v8::Object>,
pub(super) origin: String,
pub(super) name: String,
pub(super) name: IndexedDbName,
pub(super) version: Option<u64>,
pub(super) old_version: u64,
pub(super) new_version: Option<u64>,
@@ -1515,7 +1515,7 @@ fn register_indexed_db_blocked_task<'s>(
kind: IndexedDbTaskKind,
request: v8::Local<'s, v8::Object>,
origin: &str,
name: &str,
name: &IndexedDbName,
version: Option<u64>,
old_version: u64,
new_version: Option<u64>,
@@ -1546,7 +1546,7 @@ pub(super) fn register_indexed_db_blocked_open_task<'s>(
task: v8::Local<'s, v8::Object>,
request: v8::Local<'s, v8::Object>,
origin: &str,
name: &str,
name: &IndexedDbName,
version: Option<u64>,
old_version: u64,
new_version: u64,
@@ -1569,7 +1569,7 @@ pub(super) fn register_indexed_db_blocked_delete_task<'s>(
task: v8::Local<'s, v8::Object>,
request: v8::Local<'s, v8::Object>,
origin: &str,
name: &str,
name: &IndexedDbName,
old_version: u64,
) {
register_indexed_db_blocked_task(
@@ -387,7 +387,7 @@ mod tests {
.lock()
.open(moli_indexeddb::OpenOptions {
origin: "origin".to_owned(),
name: "db".to_owned(),
name: "db".into(),
version: None,
})
.unwrap();
+1 -1
View File
@@ -2765,7 +2765,7 @@ mod tests {
let mut manager = manager.lock();
let opened = manager.open(OpenOptions {
origin: origin.to_owned(),
name: "bucket-db".to_owned(),
name: "bucket-db".into(),
version: None,
})?;
let upgrade = opened