mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 00:06:19 +00:00
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:
@@ -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<()> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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());
|
||||
|
||||
+15
-2
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user