mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 16:01:05 +00:00
fix(indexeddb): snapshot writes before evaluating keys
This commit is contained in:
@@ -11,6 +11,8 @@ FileAPI/idlharness.html
|
||||
FileAPI/url/url-lifetime.html
|
||||
IndexedDB/abort-in-initial-upgradeneeded.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/abort-in-initial-upgradeneeded.any.js?moli-wpt-any=window
|
||||
IndexedDB/clone-before-keypath-eval.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/clone-before-keypath-eval.any.js?moli-wpt-any=window
|
||||
IndexedDB/close-in-upgradeneeded.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/close-in-upgradeneeded.any.js?moli-wpt-any=window
|
||||
IndexedDB/database-names-by-origin.html
|
||||
@@ -25,6 +27,16 @@ IndexedDB/fire-upgradeneeded-event-exception.any.js?moli-wpt-any=window
|
||||
IndexedDB/idb-explicit-commit.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idb-explicit-commit.any.js?moli-wpt-any=window
|
||||
IndexedDB/idb_webworkers.htm
|
||||
IndexedDB/idbcursor-delete-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbcursor-delete-exception-order.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbcursor-update-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbcursor-update-exception-order.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbcursor_delete_index.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbcursor_delete_index.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbcursor_delete_objectstore.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbcursor_delete_objectstore.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbcursor_update_objectstore.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbcursor_update_objectstore.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbdatabase-createObjectStore-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbdatabase-createObjectStore-exception-order.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbdatabase-transaction-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
@@ -47,9 +59,15 @@ IndexedDB/idbfactory_deleteDatabase.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbfactory_open.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbfactory_open.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbindex-cross-realm-methods.html
|
||||
IndexedDB/idbobjectstore-add-put-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbobjectstore-add-put-exception-order.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbobjectstore-cross-realm-methods.html
|
||||
IndexedDB/idbobjectstore-deleteIndex-exception-order.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbobjectstore-deleteIndex-exception-order.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbobjectstore_add.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbobjectstore_add.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbobjectstore_put.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbobjectstore_put.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbrequest_error.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbrequest_error.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbrequest_result.any.js?moli-wpt-any=dedicatedworker
|
||||
@@ -58,11 +76,17 @@ IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbtransaction_abort.any.js?moli-wpt-any=window
|
||||
IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=window
|
||||
IndexedDB/keypath-exceptions.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/keypath-exceptions.any.js?moli-wpt-any=window
|
||||
IndexedDB/keypath-special-identifiers.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/keypath-special-identifiers.any.js?moli-wpt-any=window
|
||||
IndexedDB/keypath_invalid.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/keypath_invalid.any.js?moli-wpt-any=window
|
||||
IndexedDB/open-request-queue.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/open-request-queue.any.js?moli-wpt-any=window
|
||||
IndexedDB/ready-state-destroyed-execution-context.html
|
||||
IndexedDB/structured-clone-transaction-state.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/structured-clone-transaction-state.any.js?moli-wpt-any=window
|
||||
IndexedDB/transaction-abort-index-metadata-revert.any.js?moli-wpt-any=dedicatedworker
|
||||
IndexedDB/transaction-abort-index-metadata-revert.any.js?moli-wpt-any=window
|
||||
IndexedDB/transaction-abort-multiple-metadata-revert.any.js?moli-wpt-any=dedicatedworker
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
'use strict';
|
||||
globalThis.writeChecks = [];
|
||||
function writeCheck(name, pass, detail = '') {
|
||||
writeChecks.push({name, pass: !!pass, detail: String(detail)});
|
||||
}
|
||||
function writeThrows(name, callback, expected) {
|
||||
try { callback(); writeCheck(name, false, 'no exception'); }
|
||||
catch (error) { writeCheck(name, typeof expected === 'string' ? error.name === expected : error === expected, error); }
|
||||
}
|
||||
function writeRequest(request) {
|
||||
return new Promise(resolve => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = event => { event.preventDefault(); resolve({requestError: request.error.name}); };
|
||||
});
|
||||
}
|
||||
function writeTransaction(transaction) {
|
||||
return new Promise(resolve => {
|
||||
transaction.oncomplete = () => resolve('complete');
|
||||
transaction.onabort = () => resolve('abort');
|
||||
transaction.onerror = event => event.preventDefault();
|
||||
});
|
||||
}
|
||||
async function writeDatabase(name) {
|
||||
const request = indexedDB.open(name, 1);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
const store = db.createObjectStore('inline', {keyPath: 'id', autoIncrement: true});
|
||||
store.createIndex('byTag', 'tag', {unique: true});
|
||||
db.createObjectStore('nested', {keyPath: 'a.b.id', autoIncrement: true});
|
||||
db.createObjectStore('out', {autoIncrement: true});
|
||||
db.createObjectStore('length', {keyPath: 'text.length'});
|
||||
db.createObjectStore('special', {keyPath: 'blob.type'});
|
||||
};
|
||||
return writeRequest(request);
|
||||
}
|
||||
async function writeCloneProbe(prefix = 'write-clone-probe') {
|
||||
for (const method of ['add', 'put']) {
|
||||
for (const queued of [false, true]) {
|
||||
const label = `${method}-${queued ? 'queued' : 'started'}`;
|
||||
const db = await writeDatabase(`${prefix}-${label}`);
|
||||
const stores = Array.from(db.objectStoreNames);
|
||||
const blocker = queued ? db.transaction(stores, 'readwrite') : null;
|
||||
const blockerDone = blocker && writeTransaction(blocker);
|
||||
if (blocker) blocker.objectStore('out').get(0);
|
||||
const tx = db.transaction(stores, 'readwrite');
|
||||
const done = writeTransaction(tx);
|
||||
const store = tx.objectStore('inline');
|
||||
const out = tx.objectStore('out');
|
||||
const nested = tx.objectStore('nested');
|
||||
let idReads = 0, tagReads = 0;
|
||||
const record = {
|
||||
get id() { idReads++; return 10; },
|
||||
get tag() {
|
||||
tagReads++;
|
||||
writeThrows(`${label}: inactive during clone`, () => out.get(0), 'TransactionInactiveError');
|
||||
writeThrows(`${label}: commit during clone`, () => tx.commit(), 'InvalidStateError');
|
||||
return 'original';
|
||||
},
|
||||
data: {value: 'before'}
|
||||
};
|
||||
const request = store[method](record);
|
||||
const saved = writeRequest(request);
|
||||
writeCheck(`${label}: getters synchronous and once`, idReads === 1 && tagReads === 1, `${idReads}/${tagReads}`);
|
||||
writeCheck(`${label}: accepted request pending`, request.readyState === 'pending');
|
||||
record.data.value = 'after';
|
||||
const sentinel = {sentinel: label};
|
||||
writeThrows(`${label}: exact getter exception`, () => out[method]({get value() { throw sentinel; }}, 'throws'), sentinel);
|
||||
writeThrows(`${label}: uncloneable value`, () => out[method](() => {}, 'function'), 'DataCloneError');
|
||||
writeThrows(`${label}: invalid inline key`, () => store[method]({id: null, tag: 'invalid'}), 'DataError');
|
||||
writeThrows(`${label}: invalid explicit key`, () => out[method]({}, null), 'DataError');
|
||||
writeThrows(`${label}: inline explicit key before clone`, () => store[method]({get id() { throw sentinel; }}, 1), 'DataError');
|
||||
writeThrows(`${label}: injection through undefined`, () => nested[method]({a: undefined}), 'DataError');
|
||||
writeThrows(`${label}: injection through primitive`, () => nested[method]({a: {b: 'text'}}), 'DataError');
|
||||
const throwingKey = [];
|
||||
Object.defineProperty(throwingKey, '0', {get() { throw sentinel; }, configurable: true});
|
||||
writeThrows(`${label}: exact explicit array getter`, () => out[method]({}, throwingKey), sentinel);
|
||||
let prototypeReads = 0;
|
||||
Object.defineProperty(Object.prototype, 'a', {configurable: true, get() { prototypeReads++; throw sentinel; }});
|
||||
let injected;
|
||||
try { injected = writeRequest(nested[method]({})); }
|
||||
finally { delete Object.prototype.a; }
|
||||
writeCheck(`${label}: injection ignores prototype`, prototypeReads === 0, prototypeReads);
|
||||
const inherited = Object.create({id: 999});
|
||||
inherited.tag = 'generated';
|
||||
const generated = writeRequest(store[method](inherited));
|
||||
const conflict = writeRequest(store[method]({tag: 'original'}));
|
||||
const afterConflict = writeRequest(store[method]({tag: 'after-conflict'}));
|
||||
const sparse = Array(1);
|
||||
Object.setPrototypeOf(sparse, Object.create(Array.prototype, {0: {get() { throw sentinel; }}}));
|
||||
writeThrows(`${label}: array holes ignore inherited getters`, () => out[method]({}, sparse), 'DataError');
|
||||
for (const thrown of [null, undefined, 17, 'sentinel']) {
|
||||
try { out[method]({get value() { throw thrown; }}, 'primitive-throw'); writeCheck(`${label}: primitive exception ${String(thrown)}`, false); }
|
||||
catch (error) { writeCheck(`${label}: primitive exception ${String(thrown)}`, error === thrown, String(error)); }
|
||||
}
|
||||
const arrayKey = ['before', [2]];
|
||||
const arrayWrite = writeRequest(out[method]({data: 'array'}, arrayKey));
|
||||
arrayKey[0] = 'after'; arrayKey[1][0] = 9;
|
||||
const utf16 = writeRequest(tx.objectStore('length')[method]({text: 'A\u{1F600}'}));
|
||||
let blobReads = 0;
|
||||
const oldType = Object.getOwnPropertyDescriptor(Blob.prototype, 'type');
|
||||
Object.defineProperty(Blob.prototype, 'type', {configurable: true, get() { blobReads++; throw sentinel; }});
|
||||
let special;
|
||||
try { special = writeRequest(tx.objectStore('special')[method]({blob: new Blob(['x'], {type: 'text/plain'})})); }
|
||||
finally { Object.defineProperty(Blob.prototype, 'type', oldType); }
|
||||
writeCheck(`${label}: Blob key uses native data`, blobReads === 0, blobReads);
|
||||
Object.defineProperties(store, {
|
||||
keyPath: {configurable: true, get() { throw sentinel; }},
|
||||
autoIncrement: {configurable: true, get() { throw sentinel; }}
|
||||
});
|
||||
let native;
|
||||
try { native = writeRequest(store[method]({id: 20, tag: 'metadata'})); }
|
||||
catch (error) { writeCheck(`${label}: native metadata write`, false, error); }
|
||||
finally { delete store.keyPath; delete store.autoIncrement; }
|
||||
const status = await done;
|
||||
if (blockerDone) await blockerDone;
|
||||
writeCheck(`${label}: transaction completes after caught exceptions`, status === 'complete', status);
|
||||
writeCheck(`${label}: stored primary key`, await saved === 10);
|
||||
writeCheck(`${label}: no delayed getters`, idReads === 1 && tagReads === 1, `${idReads}/${tagReads}`);
|
||||
writeCheck(`${label}: failed injection preserves generator`, await injected === 1);
|
||||
writeCheck(`${label}: missing inherited key generates`, await generated === 11);
|
||||
writeCheck(`${label}: unique constraint rejects generated write`, (await conflict)?.requestError === 'ConstraintError');
|
||||
writeCheck(`${label}: constraint failure preserves generator`, await afterConflict === 12);
|
||||
writeCheck(`${label}: array key snapshot`, JSON.stringify(await arrayWrite) === '["before",[2]]');
|
||||
writeCheck(`${label}: String length counts UTF-16`, await utf16 === 3);
|
||||
writeCheck(`${label}: Blob type extracted`, await special === 'text/plain');
|
||||
if (native) writeCheck(`${label}: native metadata write`, await native === 20);
|
||||
const read = db.transaction(stores);
|
||||
const readDone = writeTransaction(read);
|
||||
const readValue = writeRequest(read.objectStore('inline').get(10));
|
||||
const readNested = writeRequest(read.objectStore('nested').get(1));
|
||||
const readIndex = writeRequest(read.objectStore('inline').index('byTag').getKey('original'));
|
||||
writeCheck(`${label}: value snapshot`, (await readValue)?.data?.value === 'before');
|
||||
writeCheck(`${label}: injected nested key`, (await readNested)?.a?.b?.id === 1);
|
||||
writeCheck(`${label}: index uses same snapshot`, await readIndex === 10);
|
||||
await readDone;
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
const db = await writeDatabase(`${prefix}-cursor`);
|
||||
let tx = db.transaction('inline', 'readwrite');
|
||||
let done = writeTransaction(tx);
|
||||
tx.objectStore('inline').put({id: 1, tag: 'one'});
|
||||
tx.objectStore('inline').put({id: 2, tag: 'two'});
|
||||
await done;
|
||||
tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx);
|
||||
let store = tx.objectStore('inline');
|
||||
const cursor = await writeRequest(store.openCursor(1));
|
||||
const oldValue = cursor.value;
|
||||
let reads = 0;
|
||||
const newValue = {get id() { reads++; return 1; }, tag: 'updated'};
|
||||
const update = writeRequest(cursor.update(newValue));
|
||||
writeCheck('cursor: clone getter once', reads === 1, reads);
|
||||
writeCheck('cursor: cached value unchanged', cursor.value === oldValue && oldValue.tag === 'one');
|
||||
newValue.tag = 'mutated';
|
||||
writeThrows('cursor: reject changed inline key', () => cursor.update({id: 3}), 'DataError');
|
||||
const exact = {cursor: true};
|
||||
writeThrows('cursor: exact clone exception', () => cursor.update({get id() { throw exact; }}), exact);
|
||||
const constrained = writeRequest(cursor.update({id: 1, tag: 'two'}));
|
||||
const fresh = writeRequest(store.get(1));
|
||||
writeCheck('cursor: update result', await update === 1);
|
||||
writeCheck('cursor: unique index constraint', (await constrained)?.requestError === 'ConstraintError');
|
||||
writeCheck('cursor: stored snapshot after failed unique write', (await fresh)?.tag === 'updated');
|
||||
await done;
|
||||
tx = db.transaction('inline'); done = writeTransaction(tx); store = tx.objectStore('inline');
|
||||
let cloneCalls = 0;
|
||||
writeThrows('readonly: reject before cloning', () => store.put({get id() { cloneCalls++; return 3; }}), 'ReadOnlyError');
|
||||
writeCheck('readonly: no getters', cloneCalls === 0, cloneCalls);
|
||||
const keyCursor = await writeRequest(store.openKeyCursor());
|
||||
writeThrows('cursor: readonly before key-only', () => keyCursor.update({}), 'ReadOnlyError');
|
||||
writeThrows('cursor: readonly delete', () => keyCursor.delete(), 'ReadOnlyError');
|
||||
await done;
|
||||
tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx);
|
||||
const writeKeyCursor = await writeRequest(tx.objectStore('inline').openKeyCursor(1));
|
||||
writeThrows('cursor: readwrite key-only delete', () => writeKeyCursor.delete(), 'InvalidStateError');
|
||||
await done;
|
||||
for (const method of ['add', 'put']) {
|
||||
for (const queued of [false, true]) {
|
||||
const label = `abort-${method}-${queued ? 'queued' : 'started'}`;
|
||||
const blocker = queued ? db.transaction('out', 'readwrite') : null;
|
||||
const blockerDone = blocker && writeTransaction(blocker);
|
||||
if (blocker) blocker.objectStore('out').get(99);
|
||||
const aborted = db.transaction('out', 'readwrite');
|
||||
const abortedDone = writeTransaction(aborted);
|
||||
const target = aborted.objectStore('out');
|
||||
let request;
|
||||
try {
|
||||
request = target[method]({get value() { aborted.abort(); return 'cancelled'; }}, 99);
|
||||
writeCheck(`${label}: returns request`, request instanceof IDBRequest && request.readyState === 'pending');
|
||||
} catch (error) { writeCheck(`${label}: returns request`, false, error); }
|
||||
writeCheck(`${label}: transaction aborts`, await abortedDone === 'abort');
|
||||
writeThrows(`${label}: remains inactive`, () => target.get(99), 'TransactionInactiveError');
|
||||
if (blockerDone) await blockerDone;
|
||||
const verification = db.transaction('out');
|
||||
const verificationDone = writeTransaction(verification);
|
||||
writeCheck(`${label}: cancelled value absent`, await writeRequest(verification.objectStore('out').get(99)) === undefined);
|
||||
await verificationDone;
|
||||
}
|
||||
}
|
||||
tx = db.transaction('inline', 'readwrite'); done = writeTransaction(tx);
|
||||
store = tx.objectStore('inline');
|
||||
const abortingCursor = await writeRequest(store.openCursor(1));
|
||||
try {
|
||||
const request = abortingCursor.update({get id() { tx.abort(); return 1; }, tag: 'cancelled'});
|
||||
writeCheck('abort-cursor: returns request', request instanceof IDBRequest && request.readyState === 'pending');
|
||||
} catch (error) { writeCheck('abort-cursor: returns request', false, error); }
|
||||
writeCheck('abort-cursor: transaction aborts', await done === 'abort');
|
||||
writeThrows('abort-cursor: remains inactive', () => store.get(1), 'TransactionInactiveError');
|
||||
tx = db.transaction(['inline', 'out'], 'readwrite'); done = writeTransaction(tx);
|
||||
const preserved = writeRequest(tx.objectStore('inline').get(1));
|
||||
const generated = writeRequest(tx.objectStore('out').put({afterAbort: true}));
|
||||
writeCheck('abort-cursor: stored value preserved', (await preserved)?.tag === 'updated');
|
||||
writeCheck('abort: generator not consumed', await generated === 1);
|
||||
await done;
|
||||
db.close();
|
||||
return {state: writeChecks.every(check => check.pass) ? 'pass' : 'fail', checks: writeChecks};
|
||||
}
|
||||
@@ -1,6 +1,27 @@
|
||||
use super::event_dispatch::run_probe;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn indexed_db_writes_clone_before_key_paths_and_snapshot_queued_values() -> Result<()> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
let fixture = include_str!("fixtures/indexeddb-write-clone.js");
|
||||
for target in ["window", "child", "worker"] {
|
||||
let source = format!(
|
||||
"{fixture}\nwriteCloneProbe('write-clone-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: writeChecks}}));"
|
||||
);
|
||||
let result = run_probe(&browser, &server, target, &source).await?;
|
||||
assert_eq!(result["state"], "pass", "{target}: {result}");
|
||||
assert_eq!(
|
||||
result["checks"].as_array().unwrap().len(),
|
||||
165,
|
||||
"{target}: {result}"
|
||||
);
|
||||
}
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn indexed_db_schema_validation_preserves_key_paths_conversion_and_exception_order()
|
||||
-> Result<()> {
|
||||
|
||||
@@ -14,7 +14,9 @@ use crate::{
|
||||
DatabaseData, DatabaseHandleState, IndexData, IndexedDbManager, ObjectStoreData,
|
||||
TransactionLifecycle, TransactionState,
|
||||
},
|
||||
transaction::{ensure_writeable, resolve_key, transaction_store, transaction_store_mut},
|
||||
transaction::{
|
||||
ensure_writeable, next_generated_key, resolve_key, transaction_store, transaction_store_mut,
|
||||
},
|
||||
usage::{database_usage_bytes, origin_usage_bytes, sum_usage},
|
||||
validate_index_options, validate_object_store_options,
|
||||
};
|
||||
@@ -680,6 +682,18 @@ impl IndexedDbManager {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Inspect the next generated key without consuming it. The binding uses
|
||||
/// this to inject the key and validate indexes before an atomic write.
|
||||
pub fn next_generated_key(
|
||||
&mut self,
|
||||
transaction: TransactionHandle,
|
||||
store_name: &str,
|
||||
) -> Result<Key, IndexedDbError> {
|
||||
let tx = self.active_transaction_mut(transaction)?;
|
||||
ensure_writeable(tx)?;
|
||||
next_generated_key(transaction_store(tx, store_name)?)
|
||||
}
|
||||
|
||||
pub fn generate_key(
|
||||
&mut self,
|
||||
transaction: TransactionHandle,
|
||||
@@ -763,8 +777,10 @@ impl IndexedDbManager {
|
||||
let origin = tx.origin.clone();
|
||||
let store = transaction_store_mut(tx, store_name)?;
|
||||
let previous_store = quota.map(|_| store.clone());
|
||||
let previous_counter = store.auto_increment_counter;
|
||||
let resolved_key = resolve_key(store, key)?;
|
||||
if add_only && store.records.contains_key(&resolved_key) {
|
||||
store.auto_increment_counter = previous_counter;
|
||||
return Err(IndexedDbError::Constraint(
|
||||
"record already exists for key".to_owned(),
|
||||
));
|
||||
|
||||
@@ -1753,6 +1753,78 @@ fn failed_open_does_not_block_later_delete_database() {
|
||||
.expect("delete should still succeed after failed open");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_key_preview_and_failed_quota_write_leave_generator_unchanged() {
|
||||
let dir = TestDir::new();
|
||||
let mut manager = IndexedDbManager::new(&dir.path).unwrap();
|
||||
let opened = manager
|
||||
.open(OpenOptions {
|
||||
origin: "https://example.com".to_owned(),
|
||||
name: "key-preview".to_owned(),
|
||||
version: None,
|
||||
})
|
||||
.unwrap();
|
||||
let tx = opened.upgrade_transaction.unwrap();
|
||||
manager
|
||||
.create_object_store(
|
||||
tx,
|
||||
"items",
|
||||
ObjectStoreOptions {
|
||||
key_path: None,
|
||||
auto_increment: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
manager.next_generated_key(tx, "items").unwrap(),
|
||||
Key::Integer(1)
|
||||
);
|
||||
assert_eq!(
|
||||
manager.next_generated_key(tx, "items").unwrap(),
|
||||
Key::Integer(1)
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.put(tx, "items", Some(Key::Integer(1)), vec![1])
|
||||
.unwrap(),
|
||||
Key::Integer(1)
|
||||
);
|
||||
let error = manager
|
||||
.put_with_quota(
|
||||
tx,
|
||||
"items",
|
||||
Some(Key::Integer(20)),
|
||||
vec![2],
|
||||
IndexedDbQuotaCheck {
|
||||
quota: 0,
|
||||
non_indexed_db_usage: 0,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, IndexedDbError::QuotaExceeded { .. }));
|
||||
assert_eq!(
|
||||
manager.next_generated_key(tx, "items").unwrap(),
|
||||
Key::Integer(2)
|
||||
);
|
||||
assert_eq!(
|
||||
manager.put(tx, "items", None, vec![3]).unwrap(),
|
||||
Key::Integer(2)
|
||||
);
|
||||
manager.commit_transaction(tx).unwrap();
|
||||
let tx = manager
|
||||
.begin_transaction(
|
||||
opened.database,
|
||||
&["items".to_owned()],
|
||||
TransactionMode::ReadWrite,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
manager.next_generated_key(tx, "items").unwrap(),
|
||||
Key::Integer(3)
|
||||
);
|
||||
manager.abort_transaction(tx).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_increment_rejects_exhausted_safe_integer_range() {
|
||||
let dir = TestDir::new();
|
||||
|
||||
@@ -64,15 +64,25 @@ pub(crate) fn resolve_key(
|
||||
return Ok(key);
|
||||
}
|
||||
if store.auto_increment {
|
||||
if store.auto_increment_counter >= MAX_AUTO_INCREMENT_KEY {
|
||||
return Err(IndexedDbError::Constraint(
|
||||
"auto_increment key generator exceeded the maximum safe integer value".to_owned(),
|
||||
));
|
||||
}
|
||||
let key = next_generated_key(store)?;
|
||||
store.auto_increment_counter += 1;
|
||||
return Ok(Key::Integer(store.auto_increment_counter as i64));
|
||||
return Ok(key);
|
||||
}
|
||||
Err(IndexedDbError::InvalidState(
|
||||
"a key is required when auto_increment is disabled".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn next_generated_key(store: &ObjectStoreData) -> Result<Key, IndexedDbError> {
|
||||
if !store.auto_increment {
|
||||
return Err(IndexedDbError::InvalidState(
|
||||
"a key is required when auto_increment is disabled".to_owned(),
|
||||
));
|
||||
}
|
||||
if store.auto_increment_counter >= MAX_AUTO_INCREMENT_KEY {
|
||||
return Err(IndexedDbError::Constraint(
|
||||
"auto_increment key generator exceeded the maximum safe integer value".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(Key::Integer((store.auto_increment_counter + 1) as i64))
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ use super::{
|
||||
INDEXED_DB_DATABASE_HANDLE_SLOT, INDEXED_DB_DATABASE_KEY_SLOT, INDEXED_DB_EVENT_LISTENERS_SLOT,
|
||||
INDEXED_DB_KEY_RANGE_MARKER_SLOT, INDEXED_DB_REQUEST_ERROR_SLOT,
|
||||
INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_RESULT_SLOT,
|
||||
INDEXED_DB_TRANSACTION_DB_KEY_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT,
|
||||
INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, IndexEntry, IndexInfo, IndexedDbError,
|
||||
IndexedDbExecutionOwner, IndexedDbExternalObject, IndexedDbManager,
|
||||
IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbValue,
|
||||
IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite,
|
||||
PreparedObjectStoreWriteError, TransactionHandle, TransactionMode,
|
||||
context_host_ptr_from_global_bridge, global_constructor_prototype,
|
||||
indexed_db_database_store_metadata, indexed_db_object_store_metadata, indexed_db_runtime_array,
|
||||
INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
|
||||
INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_DB_KEY_SLOT,
|
||||
INDEXED_DB_TRANSACTION_FINISHED_SLOT, INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery,
|
||||
IndexEntry, IndexInfo, IndexedDbError, IndexedDbExecutionOwner, IndexedDbExternalObject,
|
||||
IndexedDbManager, IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope,
|
||||
IndexedDbValue, IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite,
|
||||
TransactionHandle, TransactionMode, context_host_ptr_from_global_bridge,
|
||||
global_constructor_prototype, indexed_db_database_store_metadata,
|
||||
indexed_db_object_store_metadata, indexed_db_runtime_array, indexed_db_transaction_mode,
|
||||
indexed_db_typed_execution_owner, indexed_db_typed_storage_scope, new_null_prototype_object,
|
||||
object_bool_property, object_hidden_value, object_number_property, object_property_as_object,
|
||||
object_string_property, push_unique_object_to_indexed_db_runtime_array,
|
||||
|
||||
@@ -517,12 +517,11 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
value: v8::Local<'_, v8::Value>,
|
||||
) -> Option<IndexedDbValue> {
|
||||
let mut should_throw_data_clone_error = false;
|
||||
let external_objects = Rc::new(RefCell::new(Vec::new()));
|
||||
let external_object_sources = Rc::new(RefCell::new(Vec::new()));
|
||||
let serialized = {
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let scope = try_catch.init();
|
||||
let mut scope = try_catch.init();
|
||||
let context = scope.get_current_context();
|
||||
let serializer = v8::ValueSerializer::new(
|
||||
&scope,
|
||||
@@ -535,8 +534,11 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value(
|
||||
match serializer.write_value(context, value) {
|
||||
Some(true) => Some(serializer.release()),
|
||||
_ => {
|
||||
should_throw_data_clone_error =
|
||||
scope.has_caught() && scope.can_continue() && !scope.has_terminated();
|
||||
// The delegate creates DataCloneError for unsupported values.
|
||||
// Exceptions from author getters must retain their identity.
|
||||
if scope.has_caught() {
|
||||
scope.rethrow();
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -545,14 +547,6 @@ pub(in crate::context_bootstrap::indexed_db) fn serialize_js_value(
|
||||
let external_objects = external_objects.borrow().clone();
|
||||
return Some(IndexedDbValue::new(wire_bytes, external_objects));
|
||||
}
|
||||
if should_throw_data_clone_error {
|
||||
let exception = dom_exception_value(
|
||||
scope,
|
||||
"The value could not be cloned for IndexedDB storage.",
|
||||
"DataCloneError",
|
||||
);
|
||||
scope.throw_exception(exception);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -43,23 +43,3 @@ pub(in crate::context_bootstrap::indexed_db) fn key_path_to_js_value<'s>(
|
||||
KeyPath::Sequence(values) => values.as_slice().to_v8_value(scope),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn key_path_from_js_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<KeyPath> {
|
||||
if value.is_null_or_undefined() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(array) = v8::Local::<v8::Array>::try_from(value) {
|
||||
let mut key_path = Vec::with_capacity(array.length() as usize);
|
||||
for index in 0..array.length() {
|
||||
let value = array.get_index(scope, index)?;
|
||||
key_path.push(value.to_string(scope)?.to_rust_string_lossy(scope));
|
||||
}
|
||||
return Some(KeyPath::Sequence(key_path));
|
||||
}
|
||||
value
|
||||
.to_string(scope)
|
||||
.map(|value| KeyPath::String(value.to_rust_string_lossy(scope)))
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ fn parse_idb_key_with_depth(
|
||||
if let Ok(array) = v8::Local::<v8::Array>::try_from(value) {
|
||||
let mut keys = Vec::with_capacity(array.length() as usize);
|
||||
for index in 0..array.length() {
|
||||
let property =
|
||||
v8_string(scope, &index.to_string()).ok_or("IndexedDB key allocation failed.")?;
|
||||
if array.has_own_property(scope, property.into()) != Some(true) {
|
||||
return Err("IndexedDB array keys must not contain missing entries.");
|
||||
}
|
||||
let Some(entry) = array.get_index(scope, index) else {
|
||||
return Err("IndexedDB array keys must not contain missing entries.");
|
||||
};
|
||||
|
||||
@@ -5,10 +5,12 @@ mod extraction;
|
||||
mod injection;
|
||||
mod prepare;
|
||||
|
||||
use self::clone::clone_js_value;
|
||||
use self::injection::inject_key_path_into_value;
|
||||
use self::injection::can_inject_key;
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) use self::clone::clone_value_for_transaction;
|
||||
pub(in crate::context_bootstrap::indexed_db) use self::injection::inject_key_path_into_value;
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) use self::extraction::{
|
||||
derive_object_store_key_from_value, extract_index_keys_from_value,
|
||||
ExtractedKey, extract_index_keys_from_value, extract_key_from_value,
|
||||
};
|
||||
pub(in crate::context_bootstrap::indexed_db) use self::prepare::prepare_object_store_write;
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn clone_js_value<'s>(
|
||||
pub(in crate::context_bootstrap::indexed_db) fn clone_value_for_transaction<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
transaction: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let bytes = serialize_js_value(scope, value)?;
|
||||
deserialize_js_value(scope, &bytes)
|
||||
) -> Option<(v8::Local<'s, v8::Value>, IndexedDbValue)> {
|
||||
set_indexed_db_slot_value(
|
||||
scope,
|
||||
transaction,
|
||||
INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
|
||||
v8::Boolean::new(scope, false).into(),
|
||||
);
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let mut scope = try_catch.init();
|
||||
// Restore activity even after a clone exception, but never resurrect a
|
||||
// transaction that an author getter aborted during serialization.
|
||||
let result = serialize_js_value(&mut scope, value)
|
||||
.and_then(|bytes| deserialize_js_value(&mut scope, &bytes).map(|clone| (clone, bytes)));
|
||||
if !object_bool_property(
|
||||
&mut scope,
|
||||
transaction,
|
||||
INDEXED_DB_TRANSACTION_FINISHED_SLOT,
|
||||
)
|
||||
.unwrap_or(false)
|
||||
&& !object_bool_property(&mut scope, transaction, INDEXED_DB_TRANSACTION_ABORTED_SLOT)
|
||||
.unwrap_or(false)
|
||||
&& !object_bool_property(
|
||||
&mut scope,
|
||||
transaction,
|
||||
INDEXED_DB_TRANSACTION_COMMITTING_SLOT,
|
||||
)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let active = v8::Boolean::new(&scope, true);
|
||||
set_indexed_db_slot_value(
|
||||
&mut scope,
|
||||
transaction,
|
||||
INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
|
||||
active.into(),
|
||||
);
|
||||
}
|
||||
// Keep the exception caught until state restoration is complete: further
|
||||
// V8 property accesses must not consume a pending rethrow.
|
||||
if scope.has_caught() {
|
||||
scope.rethrow();
|
||||
return None;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,40 +1,64 @@
|
||||
use super::*;
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) enum ExtractedKey {
|
||||
Key(Key),
|
||||
Missing,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn extract_key_from_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &KeyPath,
|
||||
) -> ExtractedKey {
|
||||
match key_path {
|
||||
KeyPath::Sequence(paths) => {
|
||||
let mut keys = Vec::with_capacity(paths.len());
|
||||
for path in paths {
|
||||
match extract_string_key(scope, value, path) {
|
||||
ExtractedKey::Key(key) => keys.push(key),
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
ExtractedKey::Key(Key::Array(keys))
|
||||
}
|
||||
KeyPath::String(path) => extract_string_key(scope, value, path),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_string_key<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
path: &str,
|
||||
) -> ExtractedKey {
|
||||
let Some(value) = value_at_string_key_path(scope, value, path) else {
|
||||
return ExtractedKey::Missing;
|
||||
};
|
||||
match parse_idb_key(scope, value) {
|
||||
Ok(Some(key)) => ExtractedKey::Key(key),
|
||||
_ => ExtractedKey::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn extract_index_keys_from_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &KeyPath,
|
||||
multi_entry: bool,
|
||||
) -> Vec<Key> {
|
||||
if let KeyPath::Sequence(paths) = key_path {
|
||||
let mut keys = Vec::with_capacity(paths.len());
|
||||
for path in paths {
|
||||
let Some(key) = extract_key_from_string_key_path(scope, value, path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
keys.push(key);
|
||||
}
|
||||
return vec![Key::Array(keys)];
|
||||
}
|
||||
let KeyPath::String(key_path) = key_path else {
|
||||
return Vec::new();
|
||||
};
|
||||
extract_keys_from_string_key_path(scope, value, key_path, multi_entry)
|
||||
}
|
||||
|
||||
fn extract_keys_from_string_key_path<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &str,
|
||||
multi_entry: bool,
|
||||
) -> Vec<Key> {
|
||||
let Some(current) = value_at_string_key_path(scope, value, key_path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if multi_entry && let Ok(array) = v8::Local::<v8::Array>::try_from(current) {
|
||||
if multi_entry
|
||||
&& let KeyPath::String(path) = key_path
|
||||
&& let Some(value) = value_at_string_key_path(scope, value, path)
|
||||
&& let Ok(array) = v8::Local::<v8::Array>::try_from(value)
|
||||
{
|
||||
let mut keys = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for index in 0..array.length() {
|
||||
let property =
|
||||
v8::String::new(scope, &index.to_string()).expect("array index allocation");
|
||||
if array.has_own_property(scope, property.into()) != Some(true) {
|
||||
continue;
|
||||
}
|
||||
let Some(entry) = array.get_index(scope, index) else {
|
||||
continue;
|
||||
};
|
||||
@@ -46,21 +70,10 @@ fn extract_keys_from_string_key_path<'s>(
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
parse_idb_key(scope, current)
|
||||
.ok()
|
||||
.flatten()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_key_from_string_key_path<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &str,
|
||||
) -> Option<Key> {
|
||||
value_at_string_key_path(scope, value, key_path)
|
||||
.and_then(|value| parse_idb_key(scope, value).ok().flatten())
|
||||
match extract_key_from_value(scope, value, key_path) {
|
||||
ExtractedKey::Key(key) => vec![key],
|
||||
ExtractedKey::Missing | ExtractedKey::Invalid => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn value_at_string_key_path<'s>(
|
||||
@@ -73,29 +86,52 @@ fn value_at_string_key_path<'s>(
|
||||
}
|
||||
let mut current = value;
|
||||
for segment in key_path.split('.') {
|
||||
let Ok(object) = v8::Local::<v8::Object>::try_from(current) else {
|
||||
if segment == "length"
|
||||
&& let Ok(string) = v8::Local::<v8::String>::try_from(current)
|
||||
{
|
||||
current = v8::Number::new(scope, string.length() as f64).into();
|
||||
continue;
|
||||
}
|
||||
let object = v8::Local::<v8::Object>::try_from(current).ok()?;
|
||||
if let Some(special) = special_key_path_property(scope, object, segment) {
|
||||
current = special;
|
||||
continue;
|
||||
}
|
||||
let property = v8_string(scope, segment)?;
|
||||
if object.has_own_property(scope, property.into()) != Some(true) {
|
||||
return None;
|
||||
};
|
||||
let next = object.get(
|
||||
scope,
|
||||
v8_string(scope, segment)
|
||||
.unwrap_or_else(|| v8::String::empty(scope))
|
||||
.into(),
|
||||
)?;
|
||||
current = next;
|
||||
}
|
||||
current = object.get(scope, property.into())?;
|
||||
if current.is_undefined() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(current)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn derive_object_store_key_from_value<'s>(
|
||||
fn special_key_path_property<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
store: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<Key> {
|
||||
let key_path = store
|
||||
.get(scope, v8str(scope, "keyPath").into())
|
||||
.and_then(|value| key_path_from_js_value(scope, value))?;
|
||||
extract_index_keys_from_value(scope, value, &key_path, false)
|
||||
.into_iter()
|
||||
.next()
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
segment: &str,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
// Read the platform data, not mutable Blob/File prototype accessors.
|
||||
if matches!(segment, "size" | "type" | "name" | "lastModified")
|
||||
&& let Some(file) = crate::context_bootstrap::selected_file_from_object(scope, object)
|
||||
{
|
||||
return Some(match segment {
|
||||
"size" => v8::Number::new(scope, file.bytes.len() as f64).into(),
|
||||
"type" => v8_string(scope, &file.mime_type)?.into(),
|
||||
"name" => v8_string(scope, &file.name)?.into(),
|
||||
"lastModified" => v8::Number::new(scope, file.last_modified).into(),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
match segment {
|
||||
"size" => crate::blob::blob_bytes_from_object(scope, object)
|
||||
.map(|bytes| v8::Number::new(scope, bytes.len() as f64).into()),
|
||||
"type" => crate::blob::blob_mime_type_from_object(scope, object)
|
||||
.and_then(|mime| v8_string(scope, &mime))
|
||||
.map(Into::into),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,62 @@
|
||||
use super::*;
|
||||
use moli_webapi_declare::ObjectLiteralDeclaration;
|
||||
|
||||
pub(super) fn inject_key_path_into_value<'s>(
|
||||
pub(super) fn can_inject_key<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &str,
|
||||
) -> bool {
|
||||
if key_path.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut current = value;
|
||||
let mut segments = key_path.split('.').peekable();
|
||||
while let Some(segment) = segments.next() {
|
||||
let Ok(object) = v8::Local::<v8::Object>::try_from(current) else {
|
||||
return false;
|
||||
};
|
||||
if segments.peek().is_none() {
|
||||
return true;
|
||||
}
|
||||
let Some(property) = v8_string(scope, segment) else {
|
||||
return false;
|
||||
};
|
||||
if object.has_own_property(scope, property.into()) != Some(true) {
|
||||
return true;
|
||||
}
|
||||
let Some(next) = object.get(scope, property.into()) else {
|
||||
return false;
|
||||
};
|
||||
current = next;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn inject_key_path_into_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_path: &str,
|
||||
key: &Key,
|
||||
) -> Result<v8::Local<'s, v8::Value>, PreparedObjectStoreWriteError> {
|
||||
let cloned =
|
||||
clone_js_value(scope, value).ok_or(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to clone the value before assigning the generated key.",
|
||||
name: "DataCloneError",
|
||||
})?;
|
||||
let mut current = v8::Local::<v8::Object>::try_from(cloned).map_err(|_| {
|
||||
PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: the value cannot accept an inline key.",
|
||||
name: "DataError",
|
||||
}
|
||||
})?;
|
||||
) -> Option<()> {
|
||||
// Admission checked injectability on the clone. CreateDataProperty avoids
|
||||
// inherited setters, including Object.prototype.__proto__.
|
||||
let mut current = v8::Local::<v8::Object>::try_from(value).ok()?;
|
||||
let mut segments = key_path.split('.').peekable();
|
||||
while let Some(segment) = segments.next() {
|
||||
let property = v8_string(scope, segment).unwrap_or_else(|| v8::String::empty(scope));
|
||||
let property = v8_string(scope, segment)?;
|
||||
if segments.peek().is_none() {
|
||||
let key_value = key_to_js_value(scope, key);
|
||||
let _ = current.set(scope, property.into(), key_value);
|
||||
return Ok(cloned);
|
||||
let key = key_to_js_value(scope, key);
|
||||
return (current.create_data_property(scope, property.into(), key) == Some(true))
|
||||
.then_some(());
|
||||
}
|
||||
let next = current.get(scope, property.into());
|
||||
let next_object = match next {
|
||||
Some(next) if next.is_undefined() => {
|
||||
let nested = generated_key_path_suffix_object(scope, &mut segments, key);
|
||||
let _ = current.set(scope, property.into(), nested.into());
|
||||
return Ok(cloned);
|
||||
}
|
||||
Some(next) => v8::Local::<v8::Object>::try_from(next).map_err(|_| {
|
||||
PreparedObjectStoreWriteError::DomException {
|
||||
message:
|
||||
"Failed to execute the operation: the value cannot accept an inline key.",
|
||||
name: "DataError",
|
||||
}
|
||||
})?,
|
||||
None => {
|
||||
return Err(PreparedObjectStoreWriteError::DomException {
|
||||
message:
|
||||
"Failed to execute the operation: the value cannot accept an inline key.",
|
||||
name: "DataError",
|
||||
});
|
||||
current = if current.has_own_property(scope, property.into())? {
|
||||
v8::Local::<v8::Object>::try_from(current.get(scope, property.into())?).ok()?
|
||||
} else {
|
||||
let nested = v8::Object::new(scope);
|
||||
if current.create_data_property(scope, property.into(), nested.into()) != Some(true) {
|
||||
return None;
|
||||
}
|
||||
nested
|
||||
};
|
||||
current = next_object;
|
||||
}
|
||||
Err(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: keyPath is empty.",
|
||||
name: "DataError",
|
||||
})
|
||||
}
|
||||
|
||||
fn generated_key_path_suffix_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
segments: &mut std::iter::Peekable<std::str::Split<'_, char>>,
|
||||
key: &Key,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let segment = segments
|
||||
.next()
|
||||
.expect("generated key path suffix should include a segment");
|
||||
let property = v8_string(scope, segment).unwrap_or_else(|| v8::String::empty(scope));
|
||||
let value = if segments.peek().is_none() {
|
||||
key_to_js_value(scope, key)
|
||||
} else {
|
||||
generated_key_path_suffix_object(scope, segments, key).into()
|
||||
};
|
||||
let object = ObjectLiteralDeclaration::bind(scope);
|
||||
object.set_value_property(scope, property.into(), value);
|
||||
object.into_object()
|
||||
None
|
||||
}
|
||||
|
||||
@@ -3,70 +3,72 @@ use super::*;
|
||||
pub(in crate::context_bootstrap::indexed_db) fn prepare_object_store_write<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
store: v8::Local<'s, v8::Object>,
|
||||
handle: TransactionHandle,
|
||||
store_name: &str,
|
||||
transaction: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
explicit_key: Option<Key>,
|
||||
) -> Result<PreparedObjectStoreWrite<'s>, PreparedObjectStoreWriteError> {
|
||||
let key_path = store
|
||||
.get(scope, v8str(scope, "keyPath").into())
|
||||
.and_then(|value| key_path_from_js_value(scope, value));
|
||||
let auto_increment = object_bool_property(scope, store, "autoIncrement").unwrap_or(false);
|
||||
if key_path.is_some() && explicit_key.is_some() {
|
||||
return Err(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: the store uses in-line keys and does not accept a key argument.",
|
||||
name: "DataError",
|
||||
});
|
||||
key_value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<PreparedObjectStoreWrite> {
|
||||
if indexed_db_transaction_mode(scope, transaction) == Some(TransactionMode::ReadOnly) {
|
||||
let error = dom_exception_value(scope, "The transaction is readonly.", "ReadOnlyError");
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
}
|
||||
match key_path {
|
||||
Some(key_path) => {
|
||||
if let Some(key) = derive_object_store_key_from_value(scope, store, value) {
|
||||
return Ok(PreparedObjectStoreWrite {
|
||||
key: Some(key),
|
||||
value,
|
||||
});
|
||||
let metadata = indexed_db_object_store_metadata(scope, store)?;
|
||||
let info = metadata.info();
|
||||
let has_key = !key_value.is_undefined();
|
||||
if (info.key_path.is_some() && has_key)
|
||||
|| (info.key_path.is_none() && !info.auto_increment && !has_key)
|
||||
{
|
||||
return invalid_write_key(scope);
|
||||
}
|
||||
let mut key = if has_key {
|
||||
// Explicit array key getters can throw before the record is cloned.
|
||||
let parsed = {
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let mut scope = try_catch.init();
|
||||
let parsed = parse_idb_key(&mut scope, key_value);
|
||||
if scope.has_caught() {
|
||||
scope.rethrow();
|
||||
return None;
|
||||
}
|
||||
if !auto_increment {
|
||||
return Err(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: the value does not contain the object store keyPath.",
|
||||
name: "DataError",
|
||||
});
|
||||
}
|
||||
let generated_key =
|
||||
with_indexed_db_manager(scope, |manager| manager.generate_key(handle, store_name))
|
||||
.map_err(PreparedObjectStoreWriteError::Backend)?;
|
||||
let KeyPath::String(key_path) = key_path else {
|
||||
return Err(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: compound keyPath stores cannot accept generated inline keys.",
|
||||
name: "InvalidAccessError",
|
||||
});
|
||||
};
|
||||
let value = inject_key_path_into_value(scope, value, &key_path, &generated_key)?;
|
||||
Ok(PreparedObjectStoreWrite {
|
||||
key: Some(generated_key),
|
||||
value,
|
||||
})
|
||||
parsed
|
||||
};
|
||||
match parsed {
|
||||
Ok(Some(key)) => Some(key),
|
||||
_ => return invalid_write_key(scope),
|
||||
}
|
||||
None => {
|
||||
if explicit_key.is_none() && !auto_increment {
|
||||
return Err(PreparedObjectStoreWriteError::DomException {
|
||||
message: "Failed to execute the operation: a key is required for stores without autoIncrement.",
|
||||
name: "DataError",
|
||||
});
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (clone, bytes) = clone_value_for_transaction(scope, transaction, value)?;
|
||||
let mut injection_path = None;
|
||||
if let Some(key_path) = &info.key_path {
|
||||
match extract_key_from_value(scope, clone, key_path) {
|
||||
ExtractedKey::Key(extracted) => key = Some(extracted),
|
||||
ExtractedKey::Invalid => return invalid_write_key(scope),
|
||||
ExtractedKey::Missing => {
|
||||
let KeyPath::String(path) = key_path else {
|
||||
return invalid_write_key(scope);
|
||||
};
|
||||
if !info.auto_increment || !can_inject_key(scope, clone, path) {
|
||||
return invalid_write_key(scope);
|
||||
}
|
||||
injection_path = Some(path.clone());
|
||||
}
|
||||
let key = if let Some(key) = explicit_key {
|
||||
Some(key)
|
||||
} else if auto_increment {
|
||||
Some(
|
||||
with_indexed_db_manager(scope, |manager| {
|
||||
manager.generate_key(handle, store_name)
|
||||
})
|
||||
.map_err(PreparedObjectStoreWriteError::Backend)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(PreparedObjectStoreWrite { key, value })
|
||||
}
|
||||
}
|
||||
Some(PreparedObjectStoreWrite {
|
||||
key,
|
||||
value: bytes,
|
||||
injection_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn invalid_write_key<T>(scope: &mut v8::PinScope<'_, '_>) -> Option<T> {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The value or key does not satisfy the object store's key requirements.",
|
||||
"DataError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use super::{
|
||||
CursorDirection, INDEXED_DB_CURSOR_ENTRIES_SLOT, INDEXED_DB_CURSOR_KEY_ONLY_SLOT,
|
||||
CursorDirection, ExtractedKey, INDEXED_DB_CURSOR_ENTRIES_SLOT, INDEXED_DB_CURSOR_KEY_ONLY_SLOT,
|
||||
INDEXED_DB_CURSOR_POSITION_SLOT, INDEXED_DB_CURSOR_REQUEST_SLOT, INDEXED_DB_INDEX_MARKER_SLOT,
|
||||
INDEXED_DB_OBJECT_STORE_NAME_SLOT, INDEXED_DB_PENDING_CURSOR_POSITION_SLOT,
|
||||
INDEXED_DB_PENDING_CURSOR_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, Key, TransactionHandle,
|
||||
create_request_object, cursor_direction_from_cursor, cursor_entry_object,
|
||||
define_public_non_enumerable_value_property, dom_exception_value,
|
||||
extract_index_keys_from_value, indexed_db_request_transaction_object, key_path_from_js_value,
|
||||
key_to_js_value, object_bool_property, object_hidden_value, object_number_property,
|
||||
INDEXED_DB_PENDING_CURSOR_POSITION_SLOT, INDEXED_DB_PENDING_CURSOR_SLOT,
|
||||
INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_SOURCE_SLOT,
|
||||
INDEXED_DB_TRANSACTION_ACTIVE_SLOT, Key, PreparedObjectStoreWrite, TransactionMode,
|
||||
clone_value_for_transaction, create_request_object, cursor_direction_from_cursor,
|
||||
cursor_entry_object, dom_exception_value, execute_object_store_write_request,
|
||||
extract_key_from_value, indexed_db_index_is_deleted, indexed_db_index_object_store,
|
||||
indexed_db_object_store_is_deleted, indexed_db_object_store_metadata,
|
||||
indexed_db_object_store_name, indexed_db_request_transaction_object,
|
||||
indexed_db_transaction_mode, object_bool_property, object_hidden_value, object_number_property,
|
||||
object_property_as_object, object_string_property, parse_idb_key, prepare_cursor_request,
|
||||
queue_transaction_request, request_error_object, serialize_js_value, set_indexed_db_slot_value,
|
||||
storage_bucket_quota_check_for_object_store, store_request_error, store_request_success,
|
||||
throw_type_error, transaction_handle_from_value, v8str, with_indexed_db_manager,
|
||||
queue_transaction_request, request_error_object, set_indexed_db_slot_value,
|
||||
store_request_error, store_request_success, throw_type_error, transaction_handle_from_value,
|
||||
v8str, with_indexed_db_manager,
|
||||
};
|
||||
use crate::webidl;
|
||||
|
||||
|
||||
@@ -6,23 +6,16 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_delete_callback<'s>(
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let cursor = args.this();
|
||||
let Some(position) = cursor_current_position(scope, cursor) else {
|
||||
let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError");
|
||||
scope.throw_exception(error);
|
||||
let Some((store, transaction, primary_key)) = cursor_mutation_state(scope, cursor) else {
|
||||
return;
|
||||
};
|
||||
let Some((request, handle, store_name)) = create_cursor_request(scope, cursor) else {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The transaction is not active.",
|
||||
"TransactionInactiveError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else {
|
||||
return;
|
||||
};
|
||||
let Some(primary_key) = cursor_primary_key_at(scope, cursor, position) else {
|
||||
let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError");
|
||||
scope.throw_exception(error);
|
||||
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
|
||||
return;
|
||||
};
|
||||
let Some(request) = create_request_object(scope, cursor.into(), transaction) else {
|
||||
return;
|
||||
};
|
||||
match with_indexed_db_manager(scope, |manager| {
|
||||
|
||||
@@ -7,44 +7,6 @@ struct IdbCursorUpdateArgs<'s> {
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
}
|
||||
|
||||
fn cursor_entry_update_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
cursor: v8::Local<'s, v8::Object>,
|
||||
position: usize,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<()> {
|
||||
let entry = cursor_entry_object(scope, cursor, position)?;
|
||||
define_public_non_enumerable_value_property(scope, entry, "value", value);
|
||||
if !object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false) {
|
||||
let _ = cursor.set(scope, v8str(scope, "value").into(), value);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn cursor_update_preserves_primary_key<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
cursor: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
primary_key: &Key,
|
||||
) -> bool {
|
||||
let Some(store) = cursor_store_object(scope, cursor) else {
|
||||
return true;
|
||||
};
|
||||
let Some(key_path) = store
|
||||
.get(scope, v8str(scope, "keyPath").into())
|
||||
.and_then(|value| key_path_from_js_value(scope, value))
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
let Some(derived_key) = extract_index_keys_from_value(scope, value, &key_path, false)
|
||||
.into_iter()
|
||||
.next()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
&derived_key == primary_key
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_update_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -54,80 +16,55 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_cursor_update_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let cursor = args.this();
|
||||
if object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false) {
|
||||
let Some((store, transaction, primary_key)) = cursor_mutation_state(scope, cursor) else {
|
||||
return;
|
||||
};
|
||||
let Some((clone, bytes)) = clone_value_for_transaction(scope, transaction, parsed.value) else {
|
||||
return;
|
||||
};
|
||||
let Some(metadata) = indexed_db_object_store_metadata(scope, store) else {
|
||||
return;
|
||||
};
|
||||
if let Some(path) = &metadata.info().key_path
|
||||
&& !matches!(extract_key_from_value(scope, clone, path), ExtractedKey::Key(key) if key == primary_key)
|
||||
{
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The cursor does not expose values.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
}
|
||||
let Some(position) = cursor_current_position(scope, cursor) else {
|
||||
let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError");
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
};
|
||||
let Some((request, handle, store_name)) = create_cursor_request(scope, cursor) else {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The transaction is not active.",
|
||||
"TransactionInactiveError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
};
|
||||
let Some(value_bytes) = serialize_js_value(scope, parsed.value) else {
|
||||
return;
|
||||
};
|
||||
let Some(primary_key) = cursor_primary_key_at(scope, cursor, position) else {
|
||||
let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError");
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
};
|
||||
if !cursor_update_preserves_primary_key(scope, cursor, parsed.value, &primary_key) {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"Failed to execute 'update': the value changes the effective key.",
|
||||
"The value changes the cursor's effective key.",
|
||||
"DataError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
}
|
||||
let quota_check = match cursor_store_object(scope, cursor)
|
||||
.and_then(|store| storage_bucket_quota_check_for_object_store(scope, store))
|
||||
{
|
||||
Some(Ok(quota)) => Some(quota),
|
||||
Some(Err(error)) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
rv.set(request.into());
|
||||
return;
|
||||
}
|
||||
None => None,
|
||||
let Some(request) = create_request_object(scope, cursor.into(), transaction) else {
|
||||
return;
|
||||
};
|
||||
match with_indexed_db_manager(scope, |manager| {
|
||||
if let Some(quota) = quota_check {
|
||||
manager.put_with_quota(
|
||||
handle,
|
||||
&store_name,
|
||||
Some(primary_key.clone()),
|
||||
value_bytes,
|
||||
quota.quota_check,
|
||||
)
|
||||
} else {
|
||||
manager.put(handle, &store_name, Some(primary_key.clone()), value_bytes)
|
||||
}
|
||||
}) {
|
||||
Ok(key) => {
|
||||
let _ = cursor_entry_update_value(scope, cursor, position, parsed.value);
|
||||
let key = key_to_js_value(scope, &key);
|
||||
store_request_success(scope, request, key);
|
||||
}
|
||||
Err(error) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
}
|
||||
}
|
||||
rv.set(request.into());
|
||||
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else {
|
||||
return;
|
||||
};
|
||||
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
|
||||
return;
|
||||
};
|
||||
let prepared = PreparedObjectStoreWrite {
|
||||
key: Some(primary_key),
|
||||
value: bytes,
|
||||
injection_path: None,
|
||||
};
|
||||
// Updating storage does not replace the cursor's cached value. The shared
|
||||
// executor also applies the store's unique-index and quota constraints.
|
||||
execute_object_store_write_request(
|
||||
scope,
|
||||
store,
|
||||
request,
|
||||
handle,
|
||||
&store_name,
|
||||
&prepared,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,25 +45,6 @@ pub(in crate::context_bootstrap::indexed_db) fn cursor_primary_key_at<'s>(
|
||||
parse_idb_key(scope, entry.get(scope, v8str(scope, "primaryKey").into())?).ok()?
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn cursor_store_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
cursor: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let source = object_property_as_object(scope, cursor, "source")?;
|
||||
if cursor_source_is_index(scope, cursor) {
|
||||
return object_property_as_object(scope, source, "objectStore");
|
||||
}
|
||||
Some(source)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn cursor_store_name<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
cursor: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
let store = cursor_store_object(scope, cursor)?;
|
||||
object_string_property(scope, store, INDEXED_DB_OBJECT_STORE_NAME_SLOT)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn cursor_current_position(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
cursor: v8::Local<'_, v8::Object>,
|
||||
@@ -72,20 +53,54 @@ pub(in crate::context_bootstrap::indexed_db) fn cursor_current_position(
|
||||
(position >= 0).then_some(position as usize)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn create_cursor_request<'s>(
|
||||
pub(in crate::context_bootstrap::indexed_db) fn cursor_mutation_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
cursor: v8::Local<'s, v8::Object>,
|
||||
) -> Option<(v8::Local<'s, v8::Object>, TransactionHandle, String)> {
|
||||
let (_, transaction) = cursor_request_and_transaction(scope, cursor)?;
|
||||
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>, Key)> {
|
||||
let (request, transaction) = cursor_request_and_transaction(scope, cursor)?;
|
||||
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The transaction is not active.",
|
||||
"TransactionInactiveError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
}
|
||||
let request = create_request_object(scope, cursor.into(), transaction)?;
|
||||
let handle = transaction_handle_from_value(scope, transaction.into())?;
|
||||
let store_name = cursor_store_name(scope, cursor)?;
|
||||
Some((request, handle, store_name))
|
||||
if indexed_db_transaction_mode(scope, transaction) == Some(TransactionMode::ReadOnly) {
|
||||
let error = dom_exception_value(scope, "The transaction is readonly.", "ReadOnlyError");
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
}
|
||||
let source = object_hidden_value(scope, request, INDEXED_DB_REQUEST_SOURCE_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
|
||||
let store = indexed_db_index_object_store(scope, source).unwrap_or(source);
|
||||
if indexed_db_object_store_is_deleted(scope, store)
|
||||
|| (source != store && indexed_db_index_is_deleted(scope, source))
|
||||
|| object_string_property(scope, request, INDEXED_DB_REQUEST_READY_STATE_SLOT).as_deref()
|
||||
!= Some("done")
|
||||
|| object_bool_property(scope, cursor, INDEXED_DB_CURSOR_KEY_ONLY_SLOT).unwrap_or(false)
|
||||
{
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The cursor cannot modify a record in its current state.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
}
|
||||
let Some(position) = cursor_current_position(scope, cursor) else {
|
||||
let error = dom_exception_value(scope, "The cursor is exhausted.", "InvalidStateError");
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
};
|
||||
Some((
|
||||
store,
|
||||
transaction,
|
||||
cursor_primary_key_at(scope, cursor, position)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn cursor_source_is_index<'s>(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::{
|
||||
array_contains_strict, array_push_value, context_host_ptr_from_global_bridge,
|
||||
define_non_enumerable_value_property as define_public_non_enumerable_value_property,
|
||||
global_constructor_prototype, object_bool_property as public_object_bool_property,
|
||||
object_number_property as public_object_number_property,
|
||||
object_property_as_object as public_object_property_as_object,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{CursorDirection, IdbKeyRangeQuery, IndexInfo};
|
||||
use super::{CursorDirection, IdbKeyRangeQuery, IndexInfo, PreparedObjectStoreWrite};
|
||||
use crate::native_bridge::OwnerDispatchScope;
|
||||
|
||||
pub(super) enum IndexedDbCursorSource {
|
||||
@@ -64,8 +64,7 @@ pub(super) enum IndexedDbTransactionOperationInput<'s> {
|
||||
},
|
||||
OpenCursor(IndexedDbCursorOpenOperation),
|
||||
ObjectStoreWrite {
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key: v8::Local<'s, v8::Value>,
|
||||
prepared: PreparedObjectStoreWrite,
|
||||
add_only: bool,
|
||||
},
|
||||
ObjectStoreDelete {
|
||||
@@ -123,8 +122,7 @@ enum IndexedDbPendingTransactionOperationKind {
|
||||
},
|
||||
OpenCursor(IndexedDbCursorOpenOperation),
|
||||
ObjectStoreWrite {
|
||||
value: v8::Global<v8::Value>,
|
||||
key: v8::Global<v8::Value>,
|
||||
prepared: PreparedObjectStoreWrite,
|
||||
add_only: bool,
|
||||
},
|
||||
ObjectStoreDelete {
|
||||
@@ -181,8 +179,7 @@ pub(super) enum IndexedDbTransactionOperationKindLocals<'s> {
|
||||
},
|
||||
OpenCursor(IndexedDbCursorOpenOperation),
|
||||
ObjectStoreWrite {
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key: v8::Local<'s, v8::Value>,
|
||||
prepared: PreparedObjectStoreWrite,
|
||||
add_only: bool,
|
||||
},
|
||||
ObjectStoreDelete {
|
||||
@@ -251,15 +248,9 @@ impl IndexedDbPendingTransactionOperation {
|
||||
query: v8::Global::new(scope, query),
|
||||
},
|
||||
Input::OpenCursor(operation) => Pending::OpenCursor(operation),
|
||||
Input::ObjectStoreWrite {
|
||||
value,
|
||||
key,
|
||||
add_only,
|
||||
} => Pending::ObjectStoreWrite {
|
||||
value: v8::Global::new(scope, value),
|
||||
key: v8::Global::new(scope, key),
|
||||
add_only,
|
||||
},
|
||||
Input::ObjectStoreWrite { prepared, add_only } => {
|
||||
Pending::ObjectStoreWrite { prepared, add_only }
|
||||
}
|
||||
Input::ObjectStoreDelete { key } => Pending::ObjectStoreDelete {
|
||||
key: v8::Global::new(scope, key),
|
||||
},
|
||||
@@ -348,15 +339,9 @@ impl IndexedDbPendingTransactionOperation {
|
||||
query: v8::Local::new(scope, &query),
|
||||
},
|
||||
Pending::OpenCursor(operation) => Locals::OpenCursor(operation),
|
||||
Pending::ObjectStoreWrite {
|
||||
value,
|
||||
key,
|
||||
add_only,
|
||||
} => Locals::ObjectStoreWrite {
|
||||
value: v8::Local::new(scope, &value),
|
||||
key: v8::Local::new(scope, &key),
|
||||
add_only,
|
||||
},
|
||||
Pending::ObjectStoreWrite { prepared, add_only } => {
|
||||
Locals::ObjectStoreWrite { prepared, add_only }
|
||||
}
|
||||
Pending::ObjectStoreDelete { key } => Locals::ObjectStoreDelete {
|
||||
key: v8::Local::new(scope, &key),
|
||||
},
|
||||
|
||||
@@ -2,21 +2,21 @@ use super::{
|
||||
CursorDirection, CursorSnapshotEntry, INDEXED_DB_TRANSACTION_ACTIVE_SLOT,
|
||||
INDEXED_DB_TRANSACTION_STARTED_SLOT, IdbKeyRangeQuery, IndexInfo, IndexOptions,
|
||||
IndexedDbCursorOpenOperation, IndexedDbError, IndexedDbTransactionOperationInput, KeyPath,
|
||||
PreparedObjectStoreWriteError, TransactionHandle, apply_cursor_direction,
|
||||
apply_index_collection_direction, apply_object_store_collection_direction, compare_idb_keys,
|
||||
create_index_object, create_key_range_object, create_request_object, cursor_direction_to_value,
|
||||
deserialize_js_value, dom_exception_value, enforce_object_store_unique_constraints,
|
||||
enqueue_transaction_operation, execute_object_store_clear_request,
|
||||
execute_object_store_delete_request, index_info_from_store_metadata, indexed_db_index_info,
|
||||
TransactionHandle, apply_cursor_direction, apply_index_collection_direction,
|
||||
apply_object_store_collection_direction, compare_idb_keys, create_index_object,
|
||||
create_key_range_object, create_request_object, cursor_direction_to_value,
|
||||
deserialize_js_value, dom_exception_value, enqueue_transaction_operation,
|
||||
execute_object_store_clear_request, execute_object_store_delete_request,
|
||||
execute_object_store_write_request, index_info_from_store_metadata, indexed_db_index_info,
|
||||
indexed_db_index_object_store, indexed_db_object_store_database, indexed_db_object_store_name,
|
||||
indexed_db_object_store_transaction, key_in_range, key_to_js_value, object_bool_property,
|
||||
optional_count_to_value, parse_cursor_direction, parse_cursor_direction_with_context,
|
||||
parse_idb_key, parse_idb_key_path, parse_key_or_range, parse_key_range_from_value,
|
||||
parse_optional_count, prepare_object_store_write, remove_database_index_metadata,
|
||||
request_error_object, scan_index_entries, scan_object_store_entries, serialize_js_value,
|
||||
set_database_index_metadata, storage_bucket_quota_check_for_object_store, store_request_error,
|
||||
store_request_success, submit_cursor_open_operation, throw_type_error,
|
||||
transaction_handle_from_value, with_indexed_db_manager,
|
||||
request_error_object, scan_index_entries, scan_object_store_entries,
|
||||
set_database_index_metadata, store_request_error, store_request_success,
|
||||
submit_cursor_open_operation, throw_type_error, transaction_handle_from_value,
|
||||
with_indexed_db_manager,
|
||||
};
|
||||
use crate::webidl;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::*;
|
||||
use crate::context_bootstrap::indexed_db::indexed_db_object_store_is_deleted;
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>(
|
||||
pub(in crate::context_bootstrap::indexed_db) fn object_store_active_transaction<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if indexed_db_object_store_is_deleted(scope, source) {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
@@ -26,6 +26,14 @@ pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>(
|
||||
scope.throw_exception(error);
|
||||
return None;
|
||||
}
|
||||
Some(transaction)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap::indexed_db) fn create_store_request<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
|
||||
let transaction = object_store_active_transaction(scope, source)?;
|
||||
let request = create_request_object(scope, source.into(), transaction)?;
|
||||
Some((request, transaction))
|
||||
}
|
||||
|
||||
+29
-31
@@ -1,8 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
mod enqueue;
|
||||
mod execute;
|
||||
|
||||
pub(super) fn object_store_write_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -12,48 +9,49 @@ pub(super) fn object_store_write_callback<'s>(
|
||||
add_only: bool,
|
||||
) {
|
||||
let store = args.this();
|
||||
let Some((request, transaction)) = create_store_request(scope, store) else {
|
||||
let Some(transaction) = object_store_active_transaction(scope, store) else {
|
||||
return;
|
||||
};
|
||||
let Some(prepared) = prepare_object_store_write(scope, store, transaction, value, key) else {
|
||||
return;
|
||||
};
|
||||
let Some(store_name) = indexed_db_object_store_name(scope, store) else {
|
||||
rv.set(request.into());
|
||||
return;
|
||||
};
|
||||
// Only accepted operations allocate a request. Both immediate and queued
|
||||
// writes use the same immutable snapshot and native converted key.
|
||||
let Some(request) = create_request_object(scope, store.into(), transaction) else {
|
||||
return;
|
||||
};
|
||||
rv.set(request.into());
|
||||
// A getter can abort while the value is being serialized. Admission was
|
||||
// already checked; return the request without reviving or writing the
|
||||
// finished transaction.
|
||||
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_ACTIVE_SLOT)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_STARTED_SLOT)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
enqueue::enqueue_deferred_object_store_write(
|
||||
enqueue_transaction_operation(
|
||||
scope,
|
||||
transaction,
|
||||
store,
|
||||
request,
|
||||
&store_name,
|
||||
value,
|
||||
key,
|
||||
IndexedDbTransactionOperationInput::ObjectStoreWrite { prepared, add_only },
|
||||
);
|
||||
} else if let Some(handle) = transaction_handle_from_value(scope, transaction.into()) {
|
||||
execute_object_store_write_request(
|
||||
scope,
|
||||
store,
|
||||
request,
|
||||
handle,
|
||||
&store_name,
|
||||
&prepared,
|
||||
add_only,
|
||||
);
|
||||
rv.set(request.into());
|
||||
return;
|
||||
}
|
||||
let Some(handle) = transaction_handle_from_value(scope, transaction.into()) else {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"The transaction is not active.",
|
||||
"TransactionInactiveError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return;
|
||||
};
|
||||
if execute::execute_started_object_store_write(
|
||||
scope,
|
||||
store,
|
||||
request,
|
||||
handle,
|
||||
&store_name,
|
||||
value,
|
||||
key,
|
||||
add_only,
|
||||
) {
|
||||
rv.set(request.into());
|
||||
}
|
||||
}
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn enqueue_deferred_object_store_write<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
transaction: v8::Local<'s, v8::Object>,
|
||||
store: v8::Local<'s, v8::Object>,
|
||||
request: v8::Local<'s, v8::Object>,
|
||||
store_name: &str,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key: v8::Local<'s, v8::Value>,
|
||||
add_only: bool,
|
||||
) {
|
||||
enqueue_transaction_operation(
|
||||
scope,
|
||||
transaction,
|
||||
store,
|
||||
request,
|
||||
store_name,
|
||||
IndexedDbTransactionOperationInput::ObjectStoreWrite {
|
||||
value,
|
||||
key,
|
||||
add_only,
|
||||
},
|
||||
);
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn execute_started_object_store_write<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
store: v8::Local<'s, v8::Object>,
|
||||
request: v8::Local<'s, v8::Object>,
|
||||
handle: TransactionHandle,
|
||||
store_name: &str,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_value: v8::Local<'s, v8::Value>,
|
||||
add_only: bool,
|
||||
) -> bool {
|
||||
let explicit_key = match parse_idb_key(scope, key_value) {
|
||||
Ok(key) => key,
|
||||
Err(message) => {
|
||||
throw_type_error(scope, message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let prepared =
|
||||
match prepare_object_store_write(scope, store, handle, store_name, value, explicit_key) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(PreparedObjectStoreWriteError::DomException { message, name }) => {
|
||||
let error = dom_exception_value(scope, message, name);
|
||||
scope.throw_exception(error);
|
||||
return false;
|
||||
}
|
||||
Err(PreparedObjectStoreWriteError::Backend(error)) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
let Some(value_bytes) = serialize_js_value(scope, prepared.value) else {
|
||||
return false;
|
||||
};
|
||||
let Some(primary_key) = prepared.key.clone() else {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"Failed to execute the operation: a key is required for stores without autoIncrement.",
|
||||
"DataError",
|
||||
);
|
||||
scope.throw_exception(error);
|
||||
return false;
|
||||
};
|
||||
if let Err(error) = enforce_object_store_unique_constraints(
|
||||
scope,
|
||||
store,
|
||||
handle,
|
||||
store_name,
|
||||
&primary_key,
|
||||
prepared.value,
|
||||
) {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
return true;
|
||||
}
|
||||
let quota_check = match storage_bucket_quota_check_for_object_store(scope, store) {
|
||||
Some(Ok(quota)) => Some(quota),
|
||||
Some(Err(error)) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
return true;
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let result = if add_only {
|
||||
with_indexed_db_manager(scope, |manager| {
|
||||
if let Some(quota) = quota_check {
|
||||
manager.add_with_quota(
|
||||
handle,
|
||||
store_name,
|
||||
prepared.key.clone(),
|
||||
value_bytes,
|
||||
quota.quota_check,
|
||||
)
|
||||
} else {
|
||||
manager.add(handle, store_name, prepared.key.clone(), value_bytes)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
with_indexed_db_manager(scope, |manager| {
|
||||
if let Some(quota) = quota_check {
|
||||
manager.put_with_quota(
|
||||
handle,
|
||||
store_name,
|
||||
prepared.key.clone(),
|
||||
value_bytes,
|
||||
quota.quota_check,
|
||||
)
|
||||
} else {
|
||||
manager.put(handle, store_name, prepared.key.clone(), value_bytes)
|
||||
}
|
||||
})
|
||||
};
|
||||
match result {
|
||||
Ok(key) => {
|
||||
let key_value = key_to_js_value(scope, &key);
|
||||
store_request_success(scope, request, key_value);
|
||||
}
|
||||
Err(error) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use super::{
|
||||
INDEXED_DB_TRANSACTION_STARTED_SLOT, IndexInfo, IndexedDbCursorOpenOperation,
|
||||
IndexedDbCursorSource, IndexedDbError, IndexedDbPendingTransactionOperation,
|
||||
IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbTransactionOperationInput,
|
||||
IndexedDbTransactionOperationKindLocals, OpenOptions, PreparedObjectStoreWriteError,
|
||||
IndexedDbTransactionOperationKindLocals, OpenOptions, PreparedObjectStoreWrite,
|
||||
TransactionHandle, TransactionMode, apply_index_collection_direction,
|
||||
apply_object_store_collection_direction, close_indexed_db_database_connection,
|
||||
create_database_object, create_transaction_object, database_handle_from_value,
|
||||
@@ -27,16 +27,15 @@ use super::{
|
||||
indexed_db_typed_execution_owner, indexed_db_typed_owner_scope,
|
||||
indexed_db_typed_task_execution_context, indexed_db_typed_task_execution_owner,
|
||||
indexed_db_typed_task_id, indexed_db_typed_task_kind, indexed_db_typed_task_owner_scope,
|
||||
indexed_db_typed_task_storage_scope, key_to_js_value,
|
||||
indexed_db_typed_task_storage_scope, inject_key_path_into_value, key_to_js_value,
|
||||
materialize_cursor_result_in_request_realm, object_bool_property, object_hidden_value,
|
||||
object_number_property, object_property_as_object, object_store_cursor_snapshot,
|
||||
object_string_property, parse_idb_key, parse_key_or_range, parse_optional_count,
|
||||
pop_first_indexed_db_task, prepare_object_store_write,
|
||||
push_indexed_db_operation_waiting_for_start, push_object_to_indexed_db_runtime_array,
|
||||
push_unique_object_to_indexed_db_runtime_array, readwrite_transaction_can_start,
|
||||
refresh_cursor_surface, refresh_database_surface, register_blocked_database_context,
|
||||
replace_indexed_db_runtime_array, request_error_object, scan_index_entries,
|
||||
scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value,
|
||||
pop_first_indexed_db_task, push_indexed_db_operation_waiting_for_start,
|
||||
push_object_to_indexed_db_runtime_array, push_unique_object_to_indexed_db_runtime_array,
|
||||
readwrite_transaction_can_start, refresh_cursor_surface, refresh_database_surface,
|
||||
register_blocked_database_context, replace_indexed_db_runtime_array, request_error_object,
|
||||
scan_index_entries, scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value,
|
||||
signal_worker_indexed_db_task_wake, storage_bucket_quota_check_for_object_store,
|
||||
storage_bucket_quota_check_for_transaction, take_indexed_db_operations_waiting_for_start,
|
||||
take_indexed_db_task_by_id, transaction_db_key, transaction_handle_from_value,
|
||||
|
||||
+2
-7
@@ -5,19 +5,14 @@ pub(super) fn try_dispatch_object_store_write_operation<'s>(
|
||||
operation: &operation::QueuedTransactionOperation<'s>,
|
||||
) -> bool {
|
||||
match &operation.kind {
|
||||
IndexedDbTransactionOperationKindLocals::ObjectStoreWrite {
|
||||
value,
|
||||
key,
|
||||
add_only,
|
||||
} => {
|
||||
IndexedDbTransactionOperationKindLocals::ObjectStoreWrite { prepared, add_only } => {
|
||||
execute_object_store_write_request(
|
||||
scope,
|
||||
operation.source,
|
||||
operation.request,
|
||||
operation.handle,
|
||||
&operation.store_name,
|
||||
*value,
|
||||
*key,
|
||||
prepared,
|
||||
*add_only,
|
||||
);
|
||||
}
|
||||
|
||||
+24
-33
@@ -6,43 +6,34 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque
|
||||
request: v8::Local<'s, v8::Object>,
|
||||
handle: TransactionHandle,
|
||||
store_name: &str,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
key_value: v8::Local<'s, v8::Value>,
|
||||
prepared: &PreparedObjectStoreWrite,
|
||||
add_only: bool,
|
||||
) {
|
||||
let explicit_key = match parse_idb_key(scope, key_value) {
|
||||
let primary_key = match prepared.key.clone().map(Ok).unwrap_or_else(|| {
|
||||
with_indexed_db_manager(scope, |manager| {
|
||||
manager.next_generated_key(handle, store_name)
|
||||
})
|
||||
}) {
|
||||
Ok(key) => key,
|
||||
Err(message) => {
|
||||
let error = dom_exception_value(scope, message, "TypeError");
|
||||
Err(error) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let prepared =
|
||||
match prepare_object_store_write(scope, store, handle, store_name, value, explicit_key) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(PreparedObjectStoreWriteError::DomException { message, name }) => {
|
||||
let error = dom_exception_value(scope, message, name);
|
||||
store_request_error(scope, request, error);
|
||||
return;
|
||||
}
|
||||
Err(PreparedObjectStoreWriteError::Backend(error)) => {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(value_bytes) = serialize_js_value(scope, prepared.value) else {
|
||||
let Some(value) = deserialize_js_value(scope, &prepared.value) else {
|
||||
return;
|
||||
};
|
||||
let Some(primary_key) = prepared.key.clone() else {
|
||||
let error = dom_exception_value(
|
||||
scope,
|
||||
"Failed to execute the operation: a key is required for stores without autoIncrement.",
|
||||
"DataError",
|
||||
);
|
||||
store_request_error(scope, request, error);
|
||||
return;
|
||||
let value_bytes = if let Some(path) = &prepared.injection_path {
|
||||
if inject_key_path_into_value(scope, value, path, &primary_key).is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(bytes) = serialize_js_value(scope, value) else {
|
||||
return;
|
||||
};
|
||||
bytes
|
||||
} else {
|
||||
prepared.value.clone()
|
||||
};
|
||||
if let Err(error) = enforce_object_store_unique_constraints(
|
||||
scope,
|
||||
@@ -50,7 +41,7 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque
|
||||
handle,
|
||||
store_name,
|
||||
&primary_key,
|
||||
prepared.value,
|
||||
value,
|
||||
) {
|
||||
let error = request_error_object(scope, &error);
|
||||
store_request_error(scope, request, error);
|
||||
@@ -71,12 +62,12 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque
|
||||
manager.add_with_quota(
|
||||
handle,
|
||||
store_name,
|
||||
prepared.key.clone(),
|
||||
Some(primary_key.clone()),
|
||||
value_bytes,
|
||||
quota.quota_check,
|
||||
)
|
||||
} else {
|
||||
manager.add(handle, store_name, prepared.key.clone(), value_bytes)
|
||||
manager.add(handle, store_name, Some(primary_key.clone()), value_bytes)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -85,12 +76,12 @@ pub(in crate::context_bootstrap::indexed_db) fn execute_object_store_write_reque
|
||||
manager.put_with_quota(
|
||||
handle,
|
||||
store_name,
|
||||
prepared.key.clone(),
|
||||
Some(primary_key.clone()),
|
||||
value_bytes,
|
||||
quota.quota_check,
|
||||
)
|
||||
} else {
|
||||
manager.put(handle, store_name, prepared.key.clone(), value_bytes)
|
||||
manager.put(handle, store_name, Some(primary_key.clone()), value_bytes)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{IndexedDbError, IndexedDbValue, Key};
|
||||
use super::{IndexedDbValue, Key};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct IdbKeyRangeQuery {
|
||||
@@ -22,15 +22,9 @@ pub(super) struct CursorSnapshotEntry {
|
||||
pub(super) value: Option<IndexedDbValue>,
|
||||
}
|
||||
|
||||
pub(super) struct PreparedObjectStoreWrite<'s> {
|
||||
pub(super) struct PreparedObjectStoreWrite {
|
||||
pub(super) key: Option<Key>,
|
||||
pub(super) value: v8::Local<'s, v8::Value>,
|
||||
}
|
||||
|
||||
pub(super) enum PreparedObjectStoreWriteError {
|
||||
Backend(IndexedDbError),
|
||||
DomException {
|
||||
message: &'static str,
|
||||
name: &'static str,
|
||||
},
|
||||
pub(super) value: IndexedDbValue,
|
||||
// Present only when an auto-generated key must be injected at execution.
|
||||
pub(super) injection_path: Option<String>,
|
||||
}
|
||||
|
||||
@@ -3770,38 +3770,37 @@ fn indexed_db_out_of_line_store_missing_key_throws_data_error() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_db_rejects_out_of_range_integer_keys() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-out-of-range-key.test/");
|
||||
fn indexed_db_rejects_invalid_explicit_keys() {
|
||||
let mut vm =
|
||||
new_storage_page_task_executor_test_vm("https://indexeddb-invalid-explicit-key.test/");
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__indexedDbOutOfRangeKeyError = "pending";
|
||||
globalThis.__indexedDbInvalidExplicitKeyError = "pending";
|
||||
const dbName = `app-${Math.random()}`;
|
||||
const open = indexedDB.open(dbName, 1);
|
||||
open.onupgradeneeded = () => open.result.createObjectStore("outline");
|
||||
open.onsuccess = () => {
|
||||
try {
|
||||
open.result
|
||||
.transaction("outline", "readwrite")
|
||||
.objectStore("outline")
|
||||
.put({ value: 1 }, Number.MAX_SAFE_INTEGER + 1);
|
||||
globalThis.__indexedDbOutOfRangeKeyError = "no-error";
|
||||
} catch (error) {
|
||||
globalThis.__indexedDbOutOfRangeKeyError = error.name;
|
||||
const store = open.result.transaction("outline", "readwrite").objectStore("outline");
|
||||
const errors = [];
|
||||
for (const key of [NaN, null, {}, 1n]) {
|
||||
try { store.put({ value: 1 }, key); errors.push("no-error"); }
|
||||
catch (error) { errors.push(error.name); }
|
||||
}
|
||||
globalThis.__indexedDbInvalidExplicitKeyError = errors.join("|");
|
||||
};
|
||||
return "scheduled";
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("indexeddb out-of-range key workflow should schedule");
|
||||
.expect("indexeddb invalid-explicit key workflow should schedule");
|
||||
|
||||
let result = vm
|
||||
.eval_after_selected_page_tasks("String(globalThis.__indexedDbOutOfRangeKeyError)")
|
||||
.expect("indexeddb out-of-range key result should be readable");
|
||||
.eval_after_selected_page_tasks("String(globalThis.__indexedDbInvalidExplicitKeyError)")
|
||||
.expect("indexeddb invalid-explicit key result should be readable");
|
||||
|
||||
assert_eq!(result, "TypeError");
|
||||
assert_eq!(result, "DataError|DataError|DataError|DataError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5316,18 +5315,12 @@ fn indexed_db_cursor_update_and_delete_fail_in_readonly_transactions() {
|
||||
const req = db.transaction("kv").objectStore("kv").openCursor();
|
||||
req.onsuccess = () => {
|
||||
const cursor = req.result;
|
||||
const updateReq = cursor.update({ value: 2 });
|
||||
updateReq.onerror = event => {
|
||||
event.preventDefault();
|
||||
const deleteReq = cursor.delete();
|
||||
deleteReq.onerror = event => {
|
||||
event.preventDefault();
|
||||
globalThis.__indexedDbCursorReadonlyError = [
|
||||
updateReq.error && updateReq.error.name,
|
||||
deleteReq.error && deleteReq.error.name
|
||||
].join("|");
|
||||
};
|
||||
};
|
||||
const errors = [];
|
||||
for (const method of [() => cursor.update({ value: 2 }), () => cursor.delete()]) {
|
||||
try { method(); errors.push("no-error"); }
|
||||
catch (error) { errors.push(error.name); }
|
||||
}
|
||||
globalThis.__indexedDbCursorReadonlyError = errors.join("|");
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user