LibWeb: Implement IndexedDB request/transaction reverts
To allow these to be reverted, we store mutation logs per object store in the scope of a readwrite transaction to track the modifications that were made by it. If a revert is needed, the log is played in reverse to bring us back to the original state.
This commit is contained in:
parent
8b1b2ae568
commit
547d4eb1f5
25 changed files with 1145 additions and 22 deletions
|
|
@ -722,6 +722,7 @@ set(SOURCES
|
|||
IndexedDB/Internal/Database.cpp
|
||||
IndexedDB/Internal/Index.cpp
|
||||
IndexedDB/Internal/Key.cpp
|
||||
IndexedDB/Internal/MutationLog.cpp
|
||||
IndexedDB/Internal/ObjectStore.cpp
|
||||
IndexedDB/Internal/RequestList.cpp
|
||||
Infra/ByteSequences.cpp
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ WebIDL::ExceptionOr<GC::Ref<IDBObjectStore>> IDBDatabase::create_object_store(St
|
|||
// AD-HOC: Add newly created object store to this's object store set.
|
||||
add_to_object_store_set(object_store);
|
||||
|
||||
// AD-HOC: Set up a mutation log for this store and log its creation for potential revert on abort.
|
||||
transaction->set_up_mutation_log_for_new_store(object_store);
|
||||
|
||||
// 10. Return a new object store handle associated with store and transaction.
|
||||
transaction->add_to_scope(object_store);
|
||||
return transaction->get_or_create_object_store_handle(object_store);
|
||||
|
|
@ -201,6 +204,9 @@ WebIDL::ExceptionOr<void> IDBDatabase::delete_object_store(String const& name)
|
|||
for (auto const& [_, index] : store->index_set())
|
||||
index->set_deleted(true);
|
||||
|
||||
// AD-HOC: Log the deletion for potential revert on abort.
|
||||
store->mutation_log()->note_object_store_deleted();
|
||||
|
||||
// 7. Destroy store.
|
||||
database->remove_object_store(*store);
|
||||
|
||||
|
|
@ -341,8 +347,11 @@ void IDBDatabase::block_on_conflicting_transactions(GC::Ref<IDBTransaction> tran
|
|||
blocking.append(other);
|
||||
}
|
||||
|
||||
if (blocking.is_empty())
|
||||
if (blocking.is_empty()) {
|
||||
if (!transaction->is_readonly())
|
||||
transaction->set_up_mutation_logs();
|
||||
return;
|
||||
}
|
||||
|
||||
transaction->request_list().block_execution();
|
||||
wait_for_transactions_to_finish(blocking, GC::create_function(realm().heap(), [transaction] {
|
||||
|
|
@ -362,6 +371,8 @@ void IDBDatabase::block_on_conflicting_transactions(GC::Ref<IDBTransaction> tran
|
|||
commit_a_transaction(transaction->realm(), transaction);
|
||||
return;
|
||||
}
|
||||
if (!transaction->is_readonly())
|
||||
transaction->set_up_mutation_logs();
|
||||
transaction->request_list().unblock_execution();
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ IDBIndex::IDBIndex(JS::Realm& realm, GC::Ref<Index> index, GC::Ref<IDBObjectStor
|
|||
|
||||
GC::Ref<IDBIndex> IDBIndex::create(JS::Realm& realm, GC::Ref<Index> index, GC::Ref<IDBObjectStore> object_store)
|
||||
{
|
||||
return realm.create<IDBIndex>(realm, index, object_store);
|
||||
auto handle = realm.create<IDBIndex>(realm, index, object_store);
|
||||
object_store->transaction()->register_index_handle({}, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
void IDBIndex::initialize(JS::Realm& realm)
|
||||
|
|
@ -78,6 +80,9 @@ WebIDL::ExceptionOr<void> IDBIndex::set_name(String const& value)
|
|||
if (index->object_store()->index_set().contains(name))
|
||||
return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_utf16);
|
||||
|
||||
// AD-HOC: Log the rename for potential revert on abort.
|
||||
m_object_store_handle->store()->mutation_log()->note_index_renamed(index, index->name());
|
||||
|
||||
// 9. Set index’s name to name.
|
||||
index->set_name(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ public:
|
|||
GC::Ref<IDBTransaction> transaction() { return m_object_store_handle->transaction(); }
|
||||
GC::Ref<Index> index() { return m_index; }
|
||||
|
||||
void update_name() { m_name = m_index->name(); }
|
||||
|
||||
protected:
|
||||
explicit IDBIndex(JS::Realm&, GC::Ref<Index>, GC::Ref<IDBObjectStore>);
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ WebIDL::ExceptionOr<void> IDBObjectStore::set_name(String const& value)
|
|||
if (store->database()->object_store_with_name(name))
|
||||
return WebIDL::ConstraintError::create(realm, "Object store with the given name already exists"_utf16);
|
||||
|
||||
// AD-HOC: Log the rename for potential revert on abort.
|
||||
store->mutation_log()->note_object_store_renamed(store->name());
|
||||
|
||||
// 9. Set store’s name to name.
|
||||
store->set_name(name);
|
||||
|
||||
|
|
@ -191,6 +194,9 @@ WebIDL::ExceptionOr<GC::Ref<IDBIndex>> IDBObjectStore::create_index(String const
|
|||
// 12. Add index to this's index set.
|
||||
this->index_set().set(name, index);
|
||||
|
||||
// AD-HOC: Log the creation for potential revert on abort.
|
||||
store->mutation_log()->note_index_created(index);
|
||||
|
||||
// 13. Return a new index handle associated with index and this.
|
||||
return IDBIndex::create(realm, index, *this);
|
||||
}
|
||||
|
|
@ -255,6 +261,9 @@ WebIDL::ExceptionOr<void> IDBObjectStore::delete_index(String const& name)
|
|||
// AD-HOC: Mark the index as deleted so that stale handles throw InvalidStateError.
|
||||
index.value()->set_deleted(true);
|
||||
|
||||
// AD-HOC: Log the deletion for potential revert on abort.
|
||||
store->mutation_log()->note_index_deleted(*index.value());
|
||||
|
||||
// 8. Destroy index.
|
||||
store->index_set().remove(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ public:
|
|||
WebIDL::ExceptionOr<GC::Ref<IDBRequest>> add_or_put(GC::Ref<IDBObjectStore>, JS::Value, Optional<JS::Value> const&, bool);
|
||||
GC::Ref<ObjectStore> store() const { return m_store; }
|
||||
|
||||
void update_name() { m_name = m_store->name(); }
|
||||
void update_index_set() { m_indexes = m_store->index_set(); }
|
||||
|
||||
protected:
|
||||
explicit IDBObjectStore(JS::Realm&, GC::Ref<ObjectStore>, GC::Ref<IDBTransaction>);
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
#include <LibWeb/IndexedDB/IDBIndex.h>
|
||||
#include <LibWeb/IndexedDB/IDBObjectStore.h>
|
||||
#include <LibWeb/IndexedDB/IDBTransaction.h>
|
||||
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
|
||||
|
|
@ -48,8 +49,15 @@ void IDBTransaction::visit_edges(Visitor& visitor)
|
|||
visitor.visit(m_associated_request);
|
||||
visitor.visit(m_scope);
|
||||
visitor.visit(m_cleanup_event_loop);
|
||||
|
||||
for (auto& [store, handle] : m_object_store_handles)
|
||||
visitor.visit(handle);
|
||||
visitor.visit(m_index_handles);
|
||||
|
||||
for (auto& entry : m_store_mutation_logs) {
|
||||
visitor.visit(entry.store);
|
||||
visitor.visit(entry.log);
|
||||
}
|
||||
}
|
||||
|
||||
void IDBTransaction::set_onabort(WebIDL::CallbackType* event_handler)
|
||||
|
|
@ -177,9 +185,49 @@ GC::Ptr<IDBObjectStore> IDBTransaction::object_store_handle_for(GC::Ref<ObjectSt
|
|||
void IDBTransaction::set_state(TransactionState state)
|
||||
{
|
||||
m_state = state;
|
||||
}
|
||||
|
||||
if (m_state == TransactionState::Finished)
|
||||
m_connection->check_pending_transaction_waits();
|
||||
void IDBTransaction::register_index_handle(Badge<IDBIndex>, GC::Ref<IDBIndex> handle)
|
||||
{
|
||||
m_index_handles.append(handle);
|
||||
}
|
||||
|
||||
void IDBTransaction::set_up_mutation_logs()
|
||||
{
|
||||
m_original_version = m_connection->associated_database()->version();
|
||||
|
||||
for (auto& store : m_scope) {
|
||||
auto log = MutationLog::create(realm());
|
||||
store->set_mutation_log(log);
|
||||
m_store_mutation_logs.append({ store, log });
|
||||
}
|
||||
}
|
||||
|
||||
void IDBTransaction::set_up_mutation_log_for_new_store(GC::Ref<ObjectStore> store)
|
||||
{
|
||||
auto log = MutationLog::create(realm());
|
||||
store->set_mutation_log(log);
|
||||
m_store_mutation_logs.append({ store, log });
|
||||
log->note_object_store_created();
|
||||
}
|
||||
|
||||
void IDBTransaction::revert_all_mutations()
|
||||
{
|
||||
auto database = m_connection->associated_database();
|
||||
for (size_t i = m_store_mutation_logs.size(); i > 0; --i) {
|
||||
auto& entry = m_store_mutation_logs[i - 1];
|
||||
entry.log->revert(*entry.store, database, m_connection);
|
||||
}
|
||||
|
||||
database->set_version(m_original_version);
|
||||
m_connection->set_version(m_original_version);
|
||||
}
|
||||
|
||||
void IDBTransaction::discard_mutation_logs()
|
||||
{
|
||||
for (auto& entry : m_store_mutation_logs)
|
||||
entry.store->set_mutation_log(nullptr);
|
||||
m_store_mutation_logs.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGC/Ptr.h>
|
||||
|
|
@ -16,11 +17,13 @@
|
|||
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
||||
#include <LibWeb/IndexedDB/IDBDatabase.h>
|
||||
#include <LibWeb/IndexedDB/IDBRequest.h>
|
||||
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
|
||||
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
|
||||
#include <LibWeb/IndexedDB/Internal/RequestList.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
||||
class IDBIndex;
|
||||
class IDBObjectStore;
|
||||
|
||||
// https://w3c.github.io/IndexedDB/#transaction
|
||||
|
|
@ -69,10 +72,30 @@ public:
|
|||
|
||||
GC::Ptr<ObjectStore> object_store_named(String const& name) const;
|
||||
void add_to_scope(GC::Ref<ObjectStore> object_store) { m_scope.append(object_store); }
|
||||
void remove_from_scope(GC::Ref<ObjectStore> object_store) { m_scope.remove_first_matching([&](auto& other) { return object_store == other; }); }
|
||||
void remove_from_scope(GC::Ref<ObjectStore> object_store)
|
||||
{
|
||||
m_scope.remove_first_matching([&](auto& other) { return object_store == other; });
|
||||
}
|
||||
void set_scope(Vector<GC::Ref<ObjectStore>> scope) { m_scope = move(scope); }
|
||||
|
||||
GC::Ref<IDBObjectStore> get_or_create_object_store_handle(GC::Ref<ObjectStore>);
|
||||
GC::Ptr<IDBObjectStore> object_store_handle_for(GC::Ref<ObjectStore>);
|
||||
void for_each_object_store_handle(CallableAs<IterationDecision, GC::Ref<IDBObjectStore>> auto callback)
|
||||
{
|
||||
for (auto const& [object_store, handle] : m_object_store_handles) {
|
||||
if (callback(handle) == IterationDecision::Break)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void register_index_handle(Badge<IDBIndex>, GC::Ref<IDBIndex>);
|
||||
ReadonlySpan<GC::Ref<IDBIndex>> index_handles() const { return m_index_handles; }
|
||||
|
||||
// Mutation log management. Logs are per-store and track both data and schema mutations.
|
||||
void set_up_mutation_logs();
|
||||
void set_up_mutation_log_for_new_store(GC::Ref<ObjectStore>);
|
||||
void revert_all_mutations();
|
||||
void discard_mutation_logs();
|
||||
|
||||
WebIDL::ExceptionOr<void> abort();
|
||||
WebIDL::ExceptionOr<void> commit();
|
||||
|
|
@ -122,6 +145,15 @@ private:
|
|||
GC::Ptr<HTML::EventLoop> m_cleanup_event_loop;
|
||||
|
||||
HashMap<GC::RawPtr<ObjectStore>, GC::Ref<IDBObjectStore>> m_object_store_handles;
|
||||
Vector<GC::Ref<IDBIndex>> m_index_handles;
|
||||
|
||||
// AD-HOC: Per-store mutation logs for data and schema change reversion.
|
||||
struct StoreMutationLog {
|
||||
GC::Ref<ObjectStore> store;
|
||||
GC::Ref<MutationLog> log;
|
||||
};
|
||||
Vector<StoreMutationLog> m_store_mutation_logs;
|
||||
u64 m_original_version { 0 };
|
||||
|
||||
// NOTE: Used for debug purposes
|
||||
String m_uuid;
|
||||
|
|
|
|||
|
|
@ -422,6 +422,10 @@ void upgrade_a_database(JS::Realm& realm, GC::Ref<IDBDatabase> connection, u64 v
|
|||
// 7. Let old version be db’s version.
|
||||
auto old_version = db->version();
|
||||
|
||||
// AD-HOC: Set up per-store mutation logs. This also records the current database version
|
||||
// so it can be restored if the transaction is aborted.
|
||||
transaction->set_up_mutation_logs();
|
||||
|
||||
// 8. Set db’s version to version. This change is considered part of the transaction, and so if the transaction is aborted, this change is reverted.
|
||||
db->set_version(version);
|
||||
|
||||
|
|
@ -571,6 +575,49 @@ void delete_a_database(JS::Realm& realm, StorageAPI::StorageKey storage_key, Str
|
|||
}));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction
|
||||
static void abort_an_upgrade_transaction(GC::Ref<IDBTransaction> transaction)
|
||||
{
|
||||
// 1. Let connection be transaction’s connection.
|
||||
auto connection = transaction->connection();
|
||||
|
||||
// 2. Let database be connection’s database.
|
||||
auto database = connection->associated_database();
|
||||
|
||||
// 3. Set connection’s version to database’s version if database previously existed, or 0 (zero) if database was
|
||||
// newly created.
|
||||
connection->set_version(database->version());
|
||||
|
||||
// 4. Set connection’s object store set to the set of object stores in database if database previously existed, or
|
||||
// the empty set if database was newly created.
|
||||
// NB: MutationLog reverts all changes to the connection's object store set alongside the deletion/creation of the
|
||||
// underlying ObjectStore instances.
|
||||
// However, the transaction scope will still be outdated here. Upgrade transactions are always scoped to the
|
||||
// entire database, so this is easy.
|
||||
transaction->set_scope(Vector(connection->object_store_set()));
|
||||
|
||||
// 5. For each object store handle handle associated with transaction, including those for object stores that were
|
||||
// created or deleted during transaction:
|
||||
transaction->for_each_object_store_handle([&](auto const& handle) {
|
||||
// 1. If handle’s object store was not newly created during transaction, set handle’s name to its object
|
||||
// store’s name.
|
||||
if (database->object_stores().contains_slow(handle->store()))
|
||||
handle->update_name();
|
||||
|
||||
// 2. Set handle’s index set to the set of indexes that reference its object store.
|
||||
handle->update_index_set();
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
// 6. For each index handle handle associated with transaction, including those for indexes that were created or
|
||||
// deleted during transaction:
|
||||
for (auto const& handle : transaction->index_handles()) {
|
||||
// 1. If handle’s index was not newly created during transaction, set handle’s name to its index’s name.
|
||||
if (handle->index()->object_store()->index_set().contains(handle->index()->name()))
|
||||
handle->update_name();
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/IndexedDB/#abort-a-transaction
|
||||
void abort_a_transaction(GC::Ref<IDBTransaction> transaction, GC::Ptr<WebIDL::DOMException> error)
|
||||
{
|
||||
|
|
@ -582,13 +629,15 @@ void abort_a_transaction(GC::Ref<IDBTransaction> transaction, GC::Ptr<WebIDL::DO
|
|||
if (transaction->is_finished())
|
||||
return;
|
||||
|
||||
// FIXME: 2. All the changes made to the database by the transaction are reverted.
|
||||
// 2. All the changes made to the database by the transaction are reverted.
|
||||
// For upgrade transactions this includes changes to the set of object stores and indexes, as well as the change to the version.
|
||||
// Any object stores and indexes which were created during the transaction are now considered deleted for the purposes of other algorithms.
|
||||
transaction->revert_all_mutations();
|
||||
transaction->discard_mutation_logs();
|
||||
|
||||
// FIXME: 3. If transaction is an upgrade transaction, run the steps to abort an upgrade transaction with transaction.
|
||||
// if (transaction.is_upgrade_transaction())
|
||||
// abort_an_upgrade_transaction(transaction);
|
||||
// 3. If transaction is an upgrade transaction, run the steps to abort an upgrade transaction with transaction.
|
||||
if (transaction->is_upgrade_transaction())
|
||||
abort_an_upgrade_transaction(transaction);
|
||||
|
||||
// 4. Set transaction’s state to finished.
|
||||
transaction->set_state(IDBTransaction::TransactionState::Finished);
|
||||
|
|
@ -832,6 +881,9 @@ void commit_a_transaction(JS::Realm& realm, GC::Ref<IDBTransaction> transaction)
|
|||
if (transaction->is_upgrade_transaction())
|
||||
transaction->connection()->associated_database()->set_upgrade_transaction(nullptr);
|
||||
|
||||
// AD-HOC: Discard mutation logs now that changes are permanent.
|
||||
transaction->discard_mutation_logs();
|
||||
|
||||
// 2. Set transaction’s state to finished.
|
||||
transaction->set_state(IDBTransaction::TransactionState::Finished);
|
||||
|
||||
|
|
@ -1206,6 +1258,22 @@ GC::Ref<IDBRequest> asynchronously_execute_a_request(JS::Realm& realm, IDBReques
|
|||
return cursor->transaction();
|
||||
});
|
||||
|
||||
// AD-HOC: Determine the object store being operated on so that we can get the exact mutation log this
|
||||
// operation will be writing to.
|
||||
auto store = source.visit(
|
||||
[](Empty) -> GC::Ref<ObjectStore> {
|
||||
VERIFY_NOT_REACHED();
|
||||
},
|
||||
[](GC::Ref<IDBObjectStore> object_store) -> GC::Ref<ObjectStore> {
|
||||
return object_store->store();
|
||||
},
|
||||
[](GC::Ref<IDBIndex> index) -> GC::Ref<ObjectStore> {
|
||||
return index->object_store()->store();
|
||||
},
|
||||
[](GC::Ref<IDBCursor> cursor) -> GC::Ref<ObjectStore> {
|
||||
return cursor->effective_object_store();
|
||||
});
|
||||
|
||||
// 2. Assert: transaction’s state is active.
|
||||
VERIFY(transaction->state() == IDBTransaction::TransactionState::Active);
|
||||
|
||||
|
|
@ -1219,7 +1287,7 @@ GC::Ref<IDBRequest> asynchronously_execute_a_request(JS::Realm& realm, IDBReques
|
|||
// 4. Add request to the end of transaction’s request list.
|
||||
// 5. Run these steps in parallel:
|
||||
// 1. Wait until request is the first item in transaction’s request list that is not processed.
|
||||
transaction->request_list().enqueue(request, GC::create_function(realm.heap(), [&realm, transaction, operation, request]() {
|
||||
transaction->request_list().enqueue(request, GC::create_function(realm.heap(), [&realm, transaction, store, operation, request]() {
|
||||
if (request->aborted()) {
|
||||
dbgln_if(IDB_DEBUG, "asynchronously_execute_a_request: executing request {} canceled due to abort", request->uuid());
|
||||
return;
|
||||
|
|
@ -1227,6 +1295,9 @@ GC::Ref<IDBRequest> asynchronously_execute_a_request(JS::Realm& realm, IDBReques
|
|||
|
||||
dbgln_if(IDB_DEBUG, "asynchronously_execute_a_request: step 5.1: performing operation for request {}", request->uuid());
|
||||
|
||||
// AD-HOC: Determine where the mutation logs for this operation begin in case we need to revert.
|
||||
auto log_position = store->mutation_log_position();
|
||||
|
||||
// 2. Let result be the result of performing operation.
|
||||
auto result = operation->function()();
|
||||
|
||||
|
|
@ -1237,7 +1308,9 @@ GC::Ref<IDBRequest> asynchronously_execute_a_request(JS::Realm& realm, IDBReques
|
|||
return;
|
||||
}
|
||||
|
||||
// FIXME: 4. If result is an error, then revert all changes made by operation.
|
||||
// 4. If result is an error, then revert all changes made by operation.
|
||||
if (result.is_error())
|
||||
store->revert_mutations_from(log_position);
|
||||
|
||||
// 5. Set request’s processed flag to true.
|
||||
request->set_processed(true);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <AK/QuickSort.h>
|
||||
#include <LibWeb/IndexedDB/Internal/Index.h>
|
||||
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
|
||||
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
|
@ -74,7 +75,9 @@ HTML::SerializationRecord Index::referenced_value(IndexRecord const& index_recor
|
|||
|
||||
void Index::clear_records()
|
||||
{
|
||||
m_records.clear();
|
||||
auto deleted = move(m_records);
|
||||
if (auto log = m_object_store->mutation_log(); log && !deleted.is_empty())
|
||||
log->note_index_records_deleted(*this, move(deleted));
|
||||
}
|
||||
|
||||
Optional<IndexRecord&> Index::first_in_range(GC::Ref<IDBKeyRange> range)
|
||||
|
|
@ -124,6 +127,9 @@ u64 Index::count_records_in_range(GC::Ref<IDBKeyRange> range)
|
|||
|
||||
void Index::store_a_record(IndexRecord const& record)
|
||||
{
|
||||
if (auto log = m_object_store->mutation_log())
|
||||
log->note_index_record_stored(*this, record);
|
||||
|
||||
m_records.append(record);
|
||||
|
||||
// NOTE: The record is stored in index’s list of records such that the list is sorted primarily on the records keys, and secondarily on the records values, in ascending order.
|
||||
|
|
@ -136,11 +142,29 @@ void Index::store_a_record(IndexRecord const& record)
|
|||
});
|
||||
}
|
||||
|
||||
void Index::remove_records_with_value_in_range(GC::Ref<IDBKeyRange> range)
|
||||
void Index::remove_record(IndexRecord const& record)
|
||||
{
|
||||
m_records.remove_all_matching([&](auto const& record) {
|
||||
return range->is_in_range(record.value);
|
||||
m_records.remove_first_matching([&](auto const& existing) {
|
||||
return Key::equals(existing.key, record.key) && Key::equals(existing.value, record.value);
|
||||
});
|
||||
}
|
||||
|
||||
void Index::remove_records_with_value_in_range(GC::Ref<IDBKeyRange> range)
|
||||
{
|
||||
auto log = m_object_store->mutation_log();
|
||||
Vector<IndexRecord> removed_records;
|
||||
for (size_t i = 0; i < m_records.size();) {
|
||||
auto const& record = m_records[i];
|
||||
if (range->is_in_range(record.value)) {
|
||||
auto removed_record = m_records.take(i);
|
||||
if (log)
|
||||
removed_records.append(removed_record);
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!removed_records.is_empty())
|
||||
log->note_index_records_deleted(*this, move(removed_records));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public:
|
|||
GC::ConservativeVector<IndexRecord> last_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count);
|
||||
u64 count_records_in_range(GC::Ref<IDBKeyRange> range);
|
||||
void store_a_record(IndexRecord const& record);
|
||||
void remove_record(IndexRecord const& record);
|
||||
void remove_records_with_value_in_range(GC::Ref<IDBKeyRange> range);
|
||||
|
||||
HTML::SerializationRecord referenced_value(IndexRecord const& index_record) const;
|
||||
|
|
|
|||
195
Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp
Normal file
195
Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright (c) 2025, zaggy1024
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/IndexedDB/IDBDatabase.h>
|
||||
#include <LibWeb/IndexedDB/Internal/Database.h>
|
||||
#include <LibWeb/IndexedDB/Internal/Index.h>
|
||||
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
|
||||
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(MutationLog);
|
||||
|
||||
MutationLog::MutationLog() = default;
|
||||
|
||||
GC::Ref<MutationLog> MutationLog::create(JS::Realm& realm)
|
||||
{
|
||||
return realm.create<MutationLog>();
|
||||
}
|
||||
|
||||
void MutationLog::visit_edges(Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
for (auto& entry : m_entries) {
|
||||
entry.visit(
|
||||
[&](ObjectStoreCreated&) {
|
||||
},
|
||||
[&](ObjectStoreDeleted&) {
|
||||
},
|
||||
[&](IndexCreated& e) {
|
||||
visitor.visit(e.index);
|
||||
},
|
||||
[&](IndexDeleted& e) {
|
||||
visitor.visit(e.index);
|
||||
},
|
||||
[&](ObjectStoreRenamed&) {
|
||||
},
|
||||
[&](IndexRenamed& e) {
|
||||
visitor.visit(e.index);
|
||||
},
|
||||
[&](KeyGeneratorChanged&) {
|
||||
},
|
||||
[&](RecordsDeleted& e) {
|
||||
for (auto& record : e.records)
|
||||
visitor.visit(record.key);
|
||||
},
|
||||
[&](RecordStored& e) {
|
||||
visitor.visit(e.key);
|
||||
},
|
||||
[&](IndexRecordsDeleted& e) {
|
||||
visitor.visit(e.index);
|
||||
for (auto& record : e.records) {
|
||||
visitor.visit(record.key);
|
||||
visitor.visit(record.value);
|
||||
}
|
||||
},
|
||||
[&](IndexRecordStored& e) {
|
||||
visitor.visit(e.index);
|
||||
visitor.visit(e.record.key);
|
||||
visitor.visit(e.record.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void MutationLog::note_object_store_created()
|
||||
{
|
||||
m_entries.append(ObjectStoreCreated {});
|
||||
}
|
||||
|
||||
void MutationLog::note_object_store_deleted()
|
||||
{
|
||||
m_entries.append(ObjectStoreDeleted {});
|
||||
}
|
||||
|
||||
void MutationLog::note_object_store_renamed(String old_name)
|
||||
{
|
||||
m_entries.append(ObjectStoreRenamed { move(old_name) });
|
||||
}
|
||||
|
||||
void MutationLog::note_index_created(GC::Ref<Index> index)
|
||||
{
|
||||
m_entries.append(IndexCreated { index });
|
||||
}
|
||||
|
||||
void MutationLog::note_index_deleted(GC::Ref<Index> index)
|
||||
{
|
||||
m_entries.append(IndexDeleted { index });
|
||||
}
|
||||
|
||||
void MutationLog::note_index_renamed(GC::Ref<Index> index, String old_name)
|
||||
{
|
||||
m_entries.append(IndexRenamed { index, move(old_name) });
|
||||
}
|
||||
|
||||
void MutationLog::note_key_generator_changed(u64 old_value)
|
||||
{
|
||||
m_entries.append(KeyGeneratorChanged { old_value });
|
||||
}
|
||||
|
||||
void MutationLog::note_records_deleted(Vector<ObjectStoreRecord> records)
|
||||
{
|
||||
m_entries.append(RecordsDeleted { move(records) });
|
||||
}
|
||||
|
||||
void MutationLog::note_record_stored(GC::Ref<Key> key)
|
||||
{
|
||||
m_entries.append(RecordStored { key });
|
||||
}
|
||||
|
||||
void MutationLog::note_index_records_deleted(GC::Ref<Index> index, Vector<IndexRecord> records)
|
||||
{
|
||||
m_entries.append(IndexRecordsDeleted { index, move(records) });
|
||||
}
|
||||
|
||||
void MutationLog::note_index_record_stored(GC::Ref<Index> index, IndexRecord record)
|
||||
{
|
||||
m_entries.append(IndexRecordStored { index, record });
|
||||
}
|
||||
|
||||
void MutationLog::revert(ObjectStore& store, GC::Ref<Database> database, GC::Ref<IDBDatabase> connection)
|
||||
{
|
||||
revert_entries(store, 0, database, connection);
|
||||
}
|
||||
|
||||
void MutationLog::revert_from(ObjectStore& store, size_t position)
|
||||
{
|
||||
revert_entries(store, position, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void MutationLog::revert_entries(ObjectStore& store, size_t from_position, GC::Ptr<Database> database, GC::Ptr<IDBDatabase> connection)
|
||||
{
|
||||
// Temporarily clear the store's mutation log pointer so that the operations below
|
||||
// don't re-log into this log while we're reverting it.
|
||||
auto saved_log = store.mutation_log();
|
||||
store.set_mutation_log(nullptr);
|
||||
|
||||
for (size_t i = m_entries.size(); i-- > from_position;) {
|
||||
m_entries[i].visit(
|
||||
[&](ObjectStoreCreated&) {
|
||||
VERIFY(database);
|
||||
VERIFY(connection);
|
||||
database->remove_object_store(store);
|
||||
connection->remove_from_object_store_set(store);
|
||||
},
|
||||
[&](ObjectStoreDeleted&) {
|
||||
VERIFY(database);
|
||||
VERIFY(connection);
|
||||
store.set_deleted(false);
|
||||
for (auto const& [_, index] : store.index_set())
|
||||
index->set_deleted(false);
|
||||
database->add_object_store(store);
|
||||
connection->add_to_object_store_set(store);
|
||||
},
|
||||
[&](IndexCreated& e) {
|
||||
VERIFY(database);
|
||||
e.index->object_store()->index_set().remove(e.index->name());
|
||||
},
|
||||
[&](IndexDeleted& e) {
|
||||
VERIFY(database);
|
||||
e.index->set_deleted(false);
|
||||
e.index->object_store()->index_set().set(e.index->name(), e.index);
|
||||
},
|
||||
[&](ObjectStoreRenamed& e) {
|
||||
store.set_name(e.old_name);
|
||||
},
|
||||
[&](IndexRenamed& e) {
|
||||
e.index->set_name(move(e.old_name));
|
||||
},
|
||||
[&](KeyGeneratorChanged& e) {
|
||||
store.key_generator().set(e.old_value);
|
||||
},
|
||||
[&](RecordsDeleted& e) {
|
||||
for (auto& record : e.records)
|
||||
store.store_a_record(record);
|
||||
},
|
||||
[&](RecordStored& e) {
|
||||
store.remove_record_with_key(e.key);
|
||||
},
|
||||
[&](IndexRecordsDeleted& e) {
|
||||
for (auto& record : e.records)
|
||||
e.index->store_a_record(record);
|
||||
},
|
||||
[&](IndexRecordStored& e) {
|
||||
e.index->remove_record(e.record);
|
||||
});
|
||||
}
|
||||
m_entries.shrink(from_position);
|
||||
|
||||
store.set_mutation_log(saved_log);
|
||||
}
|
||||
|
||||
}
|
||||
126
Libraries/LibWeb/IndexedDB/Internal/MutationLog.h
Normal file
126
Libraries/LibWeb/IndexedDB/Internal/MutationLog.h
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (c) 2025, zaggy1024
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Variant.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGC/Ptr.h>
|
||||
#include <LibJS/Heap/Cell.h>
|
||||
#include <LibWeb/IndexedDB/IDBRecord.h>
|
||||
#include <LibWeb/IndexedDB/Internal/KeyGenerator.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
||||
class Database;
|
||||
class IDBDatabase;
|
||||
class Index;
|
||||
class ObjectStore;
|
||||
|
||||
// Tracks mutations to an object store and its indexes so they can be reverted on request failure
|
||||
// (asynchronously execute a request step 5.4) or transaction abort (abort a transaction step 2).
|
||||
//
|
||||
// For upgrade transactions, this also tracks schema-level changes (store/index creation and deletion).
|
||||
// Schema entries are always at the boundaries: ObjectStoreCreated is always first (if present),
|
||||
// and ObjectStoreDeleted is always last (if present). Index schema entries appear in order between
|
||||
// data mutations.
|
||||
class MutationLog : public JS::Cell {
|
||||
GC_CELL(MutationLog, JS::Cell);
|
||||
GC_DECLARE_ALLOCATOR(MutationLog);
|
||||
|
||||
public:
|
||||
[[nodiscard]] static GC::Ref<MutationLog> create(JS::Realm&);
|
||||
|
||||
// Schema-level entries (upgrade transactions only).
|
||||
void note_object_store_created();
|
||||
void note_object_store_deleted();
|
||||
void note_object_store_renamed(String old_name);
|
||||
void note_index_created(GC::Ref<Index>);
|
||||
void note_index_deleted(GC::Ref<Index>);
|
||||
void note_index_renamed(GC::Ref<Index>, String old_name);
|
||||
|
||||
// Record that the key generator value was changed, saving the old value for revert.
|
||||
void note_key_generator_changed(u64 old_value);
|
||||
|
||||
// Record that records were deleted from the object store, saving them for re-insertion on revert.
|
||||
void note_records_deleted(Vector<ObjectStoreRecord>);
|
||||
|
||||
// Record that a new record was stored in the object store, saving the key for deletion on revert.
|
||||
void note_record_stored(GC::Ref<Key> key);
|
||||
|
||||
// Record that records were deleted from an index, saving them for re-insertion on revert.
|
||||
void note_index_records_deleted(GC::Ref<Index>, Vector<IndexRecord>);
|
||||
|
||||
// Record that a new record was stored in an index, saving it for deletion on revert.
|
||||
void note_index_record_stored(GC::Ref<Index>, IndexRecord);
|
||||
|
||||
// Undo all logged mutations in reverse order. Database and connection are required for schema entries.
|
||||
void revert(ObjectStore&, GC::Ref<Database>, GC::Ref<IDBDatabase>);
|
||||
|
||||
// Undo logged mutations from the given position forward (used for per-request revert on failure).
|
||||
// Asserts that no schema entries are encountered.
|
||||
void revert_from(ObjectStore&, size_t position);
|
||||
|
||||
// Clear the log without reverting (used after successful transaction commit).
|
||||
void clear() { m_entries.clear(); }
|
||||
|
||||
[[nodiscard]] size_t position() const { return m_entries.size(); }
|
||||
|
||||
protected:
|
||||
explicit MutationLog();
|
||||
virtual void visit_edges(Visitor&) override;
|
||||
|
||||
private:
|
||||
void revert_entries(ObjectStore&, size_t from_position, GC::Ptr<Database>, GC::Ptr<IDBDatabase>);
|
||||
|
||||
struct ObjectStoreCreated { };
|
||||
struct ObjectStoreDeleted { };
|
||||
|
||||
struct IndexCreated {
|
||||
GC::Ref<Index> index;
|
||||
};
|
||||
|
||||
struct IndexDeleted {
|
||||
GC::Ref<Index> index;
|
||||
};
|
||||
|
||||
struct ObjectStoreRenamed {
|
||||
String old_name;
|
||||
};
|
||||
|
||||
struct IndexRenamed {
|
||||
GC::Ref<Index> index;
|
||||
String old_name;
|
||||
};
|
||||
|
||||
struct KeyGeneratorChanged {
|
||||
u64 old_value;
|
||||
};
|
||||
|
||||
struct RecordsDeleted {
|
||||
Vector<ObjectStoreRecord> records;
|
||||
};
|
||||
|
||||
struct RecordStored {
|
||||
GC::Ref<Key> key;
|
||||
};
|
||||
|
||||
struct IndexRecordsDeleted {
|
||||
GC::Ref<Index> index;
|
||||
Vector<IndexRecord> records;
|
||||
};
|
||||
|
||||
struct IndexRecordStored {
|
||||
GC::Ref<Index> index;
|
||||
IndexRecord record;
|
||||
};
|
||||
|
||||
using Entry = Variant<ObjectStoreCreated, ObjectStoreDeleted, ObjectStoreRenamed, IndexCreated, IndexDeleted, IndexRenamed, KeyGeneratorChanged, RecordsDeleted, RecordStored, IndexRecordsDeleted, IndexRecordStored>;
|
||||
|
||||
Vector<Entry> m_entries;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include <AK/Math.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <LibWeb/IndexedDB/IDBKeyRange.h>
|
||||
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
|
||||
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
|
@ -20,6 +21,19 @@ GC::Ref<ObjectStore> ObjectStore::create(JS::Realm& realm, GC::Ref<Database> dat
|
|||
return realm.create<ObjectStore>(database, name, auto_increment, key_path);
|
||||
}
|
||||
|
||||
size_t ObjectStore::mutation_log_position() const
|
||||
{
|
||||
if (!m_mutation_log)
|
||||
return 0;
|
||||
return m_mutation_log->position();
|
||||
}
|
||||
|
||||
void ObjectStore::revert_mutations_from(size_t position)
|
||||
{
|
||||
if (m_mutation_log)
|
||||
m_mutation_log->revert_from(*this, position);
|
||||
}
|
||||
|
||||
ObjectStore::ObjectStore(GC::Ref<Database> database, String name, bool auto_increment, Optional<KeyPath> const& key_path)
|
||||
: m_database(database)
|
||||
, m_name(move(name))
|
||||
|
|
@ -36,6 +50,7 @@ void ObjectStore::visit_edges(Visitor& visitor)
|
|||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_database);
|
||||
visitor.visit(m_indexes);
|
||||
visitor.visit(m_mutation_log);
|
||||
|
||||
for (auto& record : m_records) {
|
||||
visitor.visit(record.key);
|
||||
|
|
@ -44,8 +59,25 @@ void ObjectStore::visit_edges(Visitor& visitor)
|
|||
|
||||
void ObjectStore::remove_records_in_range(GC::Ref<IDBKeyRange> range)
|
||||
{
|
||||
m_records.remove_all_matching([&](auto const& record) {
|
||||
return range->is_in_range(record.key);
|
||||
Vector<ObjectStoreRecord> deleted;
|
||||
for (size_t i = 0; i < m_records.size();) {
|
||||
auto const& record = m_records[i];
|
||||
if (range->is_in_range(record.key)) {
|
||||
auto record = m_records.take(i);
|
||||
if (m_mutation_log)
|
||||
deleted.append(record);
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!deleted.is_empty())
|
||||
m_mutation_log->note_records_deleted(move(deleted));
|
||||
}
|
||||
|
||||
void ObjectStore::remove_record_with_key(GC::Ref<Key> key)
|
||||
{
|
||||
m_records.remove_first_matching([&](auto const& record) {
|
||||
return Key::equals(record.key, key);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +92,9 @@ bool ObjectStore::has_record_with_key(GC::Ref<Key> key)
|
|||
|
||||
void ObjectStore::store_a_record(ObjectStoreRecord const& record)
|
||||
{
|
||||
if (m_mutation_log)
|
||||
m_mutation_log->note_record_stored(record.key);
|
||||
|
||||
m_records.append(record);
|
||||
|
||||
// NOTE: The record is stored in the object store’s list of records such that the list is sorted according to the key of the records in ascending order.
|
||||
|
|
@ -87,7 +122,9 @@ Optional<ObjectStoreRecord&> ObjectStore::first_in_range(GC::Ref<IDBKeyRange> ra
|
|||
|
||||
void ObjectStore::clear_records()
|
||||
{
|
||||
m_records.clear();
|
||||
auto deleted_records = move(m_records);
|
||||
if (m_mutation_log && !deleted_records.is_empty())
|
||||
m_mutation_log->note_records_deleted(deleted_records);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/IndexedDB/#generate-a-key
|
||||
|
|
@ -104,6 +141,8 @@ ErrorOr<u64> ObjectStore::generate_a_key()
|
|||
return Error::from_string_literal("Key is greater than 2^53 while trying to generate a key");
|
||||
|
||||
// 4. Increase generator's current number by 1.
|
||||
if (m_mutation_log)
|
||||
m_mutation_log->note_key_generator_changed(key);
|
||||
generator.increment(1);
|
||||
|
||||
// 5. Return key.
|
||||
|
|
@ -130,8 +169,11 @@ void ObjectStore::possibly_update_the_key_generator(GC::Ref<Key> key)
|
|||
auto& generator = key_generator();
|
||||
|
||||
// 6. If value is greater than or equal to generator's current number, then set generator's current number to value + 1.
|
||||
if (value >= static_cast<double>(generator.current_number()))
|
||||
if (value >= static_cast<double>(generator.current_number())) {
|
||||
if (m_mutation_log)
|
||||
m_mutation_log->note_key_generator_changed(generator.current_number());
|
||||
generator.set(static_cast<u64>(value + 1));
|
||||
}
|
||||
}
|
||||
|
||||
GC::ConservativeVector<ObjectStoreRecord> ObjectStore::first_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <LibWeb/IndexedDB/Internal/Database.h>
|
||||
#include <LibWeb/IndexedDB/Internal/Index.h>
|
||||
#include <LibWeb/IndexedDB/Internal/KeyGenerator.h>
|
||||
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
|
||||
|
||||
namespace Web::IndexedDB {
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ public:
|
|||
void remove_records_in_range(GC::Ref<IDBKeyRange> range);
|
||||
bool has_record_with_key(GC::Ref<Key> key);
|
||||
void store_a_record(ObjectStoreRecord const& record);
|
||||
void remove_record_with_key(GC::Ref<Key> key);
|
||||
u64 count_records_in_range(GC::Ref<IDBKeyRange> range);
|
||||
Optional<ObjectStoreRecord&> first_in_range(GC::Ref<IDBKeyRange> range);
|
||||
void clear_records();
|
||||
|
|
@ -62,6 +64,12 @@ public:
|
|||
// https://w3c.github.io/IndexedDB/#possibly-update-the-key-generator
|
||||
void possibly_update_the_key_generator(GC::Ref<Key>);
|
||||
|
||||
// Mutation log access. The log is set externally by the readwrite transaction that owns it.
|
||||
GC::Ptr<MutationLog> mutation_log() const { return m_mutation_log; }
|
||||
void set_mutation_log(GC::Ptr<MutationLog> log) { m_mutation_log = log; }
|
||||
[[nodiscard]] size_t mutation_log_position() const;
|
||||
void revert_mutations_from(size_t position);
|
||||
|
||||
protected:
|
||||
virtual void visit_edges(Visitor&) override;
|
||||
|
||||
|
|
@ -87,6 +95,9 @@ private:
|
|||
Vector<ObjectStoreRecord> m_records;
|
||||
|
||||
bool m_deleted { false };
|
||||
|
||||
// AD-HOC: Tracks mutations for revert on request failure or transaction abort.
|
||||
GC::Ptr<MutationLog> m_mutation_log;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
before rename: store=original_store index=original_index
|
||||
after rename: store=renamed_store index=renamed_index
|
||||
after abort: store=original_store index=original_index
|
||||
|
|
@ -2,8 +2,7 @@ Harness status: OK
|
|||
|
||||
Found 29 tests
|
||||
|
||||
28 Pass
|
||||
1 Fail
|
||||
29 Pass
|
||||
Pass IDBFactory.open() - request has no source
|
||||
Pass IDBFactory.open() - database 'name' and 'version' are correctly set
|
||||
Pass IDBFactory.open() - no version opens current database
|
||||
|
|
@ -30,6 +29,6 @@ Pass Calling open() with version argument object (third) should throw TypeError.
|
|||
Pass Calling open() with version argument 1.5 should not throw.
|
||||
Pass Calling open() with version argument 9007199254740991 should not throw.
|
||||
Pass Calling open() with version argument undefined should not throw.
|
||||
Fail IDBFactory.open() - error in upgradeneeded resets db
|
||||
Pass IDBFactory.open() - error in upgradeneeded resets db
|
||||
Pass IDBFactory.open() - second open's transaction is available to get objectStores
|
||||
Pass IDBFactory.open() - upgradeneeded gets VersionChangeEvent
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 2 tests
|
||||
|
||||
2 Pass
|
||||
Pass IndexedDB object store rename in aborted transaction
|
||||
Pass IndexedDB object store creation and rename in an aborted transaction
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass small values
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
setTimeout(() => {
|
||||
spoofCurrentURL("https://example.com/indexeddb-abort-upgrade-reverts-handle-names.html");
|
||||
|
||||
// Steps 5-6 of "abort an upgrade transaction" require that object store
|
||||
// and index handles revert their names when an upgrade is aborted, if the
|
||||
// store/index existed before the transaction.
|
||||
|
||||
const dbName = "test-abort-revert-names-" + Date.now() + Math.random();
|
||||
|
||||
// Version 1: create a store with an index.
|
||||
const req1 = indexedDB.open(dbName, 1);
|
||||
req1.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
const store = db.createObjectStore("original_store");
|
||||
store.createIndex("original_index", "key");
|
||||
};
|
||||
req1.onsuccess = (e) => {
|
||||
e.target.result.close();
|
||||
|
||||
// Version 2: rename store and index, then abort.
|
||||
let storeHandle, indexHandle;
|
||||
const req2 = indexedDB.open(dbName, 2);
|
||||
req2.onupgradeneeded = (e) => {
|
||||
const tx = e.target.transaction;
|
||||
storeHandle = tx.objectStore("original_store");
|
||||
indexHandle = storeHandle.index("original_index");
|
||||
|
||||
println("before rename: store=" + storeHandle.name + " index=" + indexHandle.name);
|
||||
|
||||
storeHandle.name = "renamed_store";
|
||||
indexHandle.name = "renamed_index";
|
||||
|
||||
println("after rename: store=" + storeHandle.name + " index=" + indexHandle.name);
|
||||
|
||||
tx.abort();
|
||||
};
|
||||
req2.onerror = (e) => {
|
||||
println("after abort: store=" + storeHandle.name + " index=" + indexHandle.name);
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
done();
|
||||
};
|
||||
req2.onsuccess = () => {
|
||||
println("FAIL: expected error, got success");
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
done();
|
||||
};
|
||||
};
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>IndexedDB: object store renaming support in aborted transactions</title>
|
||||
<script>
|
||||
self.GLOBAL = {
|
||||
isWindow: function() { return true; },
|
||||
isWorker: function() { return false; },
|
||||
isShadowRealm: function() { return false; },
|
||||
};
|
||||
</script>
|
||||
<script src="../resources/testharness.js"></script>
|
||||
<script src="../resources/testharnessreport.js"></script>
|
||||
<script src="resources/support-promises.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../IndexedDB/idbobjectstore-rename-abort.any.js"></script>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// META: global=window,worker
|
||||
// META: title=IndexedDB: object store renaming support in aborted transactions
|
||||
// META: script=resources/support-promises.js
|
||||
|
||||
// Spec: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-name
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(testCase => {
|
||||
const dbName = databaseName(testCase);
|
||||
let bookStore = null;
|
||||
let bookStore2 = null;
|
||||
return createDatabase(
|
||||
testCase,
|
||||
(database, transaction) => {
|
||||
createBooksStore(testCase, database);
|
||||
})
|
||||
.then(database => {
|
||||
database.close();
|
||||
})
|
||||
.then(
|
||||
() => migrateDatabase(
|
||||
testCase, 2,
|
||||
(database, transaction) => {
|
||||
bookStore = transaction.objectStore('books');
|
||||
bookStore.name = 'renamed_books';
|
||||
|
||||
transaction.abort();
|
||||
|
||||
assert_equals(
|
||||
bookStore.name, 'books',
|
||||
'IDBObjectStore.name should not reflect the rename any more ' +
|
||||
'immediately after transaction.abort() returns');
|
||||
assert_array_equals(
|
||||
database.objectStoreNames, ['books'],
|
||||
'IDBDatabase.objectStoreNames should not reflect the rename ' +
|
||||
'any more immediately after transaction.abort() returns');
|
||||
assert_array_equals(
|
||||
transaction.objectStoreNames, ['books'],
|
||||
'IDBTransaction.objectStoreNames should not reflect the ' +
|
||||
'rename any more immediately after transaction.abort() returns');
|
||||
}))
|
||||
.then(event => {
|
||||
assert_equals(
|
||||
bookStore.name, 'books',
|
||||
'IDBObjectStore.name should not reflect the rename any more ' +
|
||||
'after the versionchange transaction is aborted');
|
||||
const request = indexedDB.open(dbName, 1);
|
||||
return promiseForRequest(testCase, request);
|
||||
})
|
||||
.then(database => {
|
||||
assert_array_equals(
|
||||
database.objectStoreNames, ['books'],
|
||||
'IDBDatabase.objectStoreNames should not reflect the rename ' +
|
||||
'after the versionchange transaction is aborted');
|
||||
|
||||
const transaction = database.transaction('books', 'readonly');
|
||||
bookStore2 = transaction.objectStore('books');
|
||||
return checkStoreContents(
|
||||
testCase, bookStore2,
|
||||
'Aborting an object store rename transaction should not change ' +
|
||||
'the store\'s records')
|
||||
.then(() => database.close());
|
||||
})
|
||||
.then(() => {
|
||||
assert_equals(
|
||||
bookStore.name, 'books',
|
||||
'IDBObjectStore used in aborted rename transaction should not ' +
|
||||
'reflect the rename after the transaction is aborted');
|
||||
assert_equals(
|
||||
bookStore2.name, 'books',
|
||||
'IDBObjectStore obtained after an aborted rename transaction ' +
|
||||
'should not reflect the rename');
|
||||
});
|
||||
}, 'IndexedDB object store rename in aborted transaction');
|
||||
|
||||
promise_test(testCase => {
|
||||
const dbName = databaseName(testCase);
|
||||
let notBookStore = null;
|
||||
return createDatabase(testCase, (database, transaction) => {})
|
||||
.then(database => {
|
||||
database.close();
|
||||
})
|
||||
.then(
|
||||
() => migrateDatabase(
|
||||
testCase, 2,
|
||||
(database, transaction) => {
|
||||
notBookStore = createNotBooksStore(testCase, database);
|
||||
notBookStore.name = 'not_books_renamed';
|
||||
notBookStore.name = 'not_books_renamed_again';
|
||||
|
||||
transaction.abort();
|
||||
|
||||
assert_equals(
|
||||
notBookStore.name, 'not_books_renamed_again',
|
||||
'IDBObjectStore.name should reflect the last rename ' +
|
||||
'immediately after transaction.abort() returns');
|
||||
assert_array_equals(
|
||||
database.objectStoreNames, [],
|
||||
'IDBDatabase.objectStoreNames should not reflect the creation ' +
|
||||
'or the rename any more immediately after transaction.abort() ' +
|
||||
'returns');
|
||||
assert_array_equals(
|
||||
transaction.objectStoreNames, [],
|
||||
'IDBTransaction.objectStoreNames should not reflect the ' +
|
||||
'creation or the rename any more immediately after ' +
|
||||
'transaction.abort() returns');
|
||||
assert_array_equals(
|
||||
notBookStore.indexNames, [],
|
||||
'IDBObjectStore.indexNames for the newly created store ' +
|
||||
'should be empty immediately after transaction.abort() ' +
|
||||
'returns');
|
||||
}))
|
||||
.then(event => {
|
||||
assert_equals(
|
||||
notBookStore.name, 'not_books_renamed_again',
|
||||
'IDBObjectStore.name should reflect the last rename after the ' +
|
||||
'versionchange transaction is aborted');
|
||||
assert_array_equals(
|
||||
notBookStore.indexNames, [],
|
||||
'IDBObjectStore.indexNames for the newly created store ' +
|
||||
'should be empty after the versionchange transaction is aborted ' +
|
||||
'returns');
|
||||
const request = indexedDB.open(dbName, 1);
|
||||
return promiseForRequest(testCase, request);
|
||||
})
|
||||
.then(database => {
|
||||
assert_array_equals(
|
||||
database.objectStoreNames, [],
|
||||
'IDBDatabase.objectStoreNames should not reflect the creation or ' +
|
||||
'the rename after the versionchange transaction is aborted');
|
||||
|
||||
database.close();
|
||||
});
|
||||
}, 'IndexedDB object store creation and rename in an aborted transaction');
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>IndexedDB: request result events are delivered in order</title>
|
||||
<meta name="timeout" content="long">
|
||||
<script>
|
||||
self.GLOBAL = {
|
||||
isWindow: function() { return true; },
|
||||
isWorker: function() { return false; },
|
||||
isShadowRealm: function() { return false; },
|
||||
};
|
||||
</script>
|
||||
<script src="../resources/testharness.js"></script>
|
||||
<script src="../resources/testharnessreport.js"></script>
|
||||
<script src="resources/support-promises.js"></script>
|
||||
<script src="resources/support.js"></script>
|
||||
<script src="resources/request-event-ordering-common.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../IndexedDB/request-event-ordering-small-values.any.js"></script>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// META: title=IndexedDB: request result events are delivered in order
|
||||
// META: global=window,worker
|
||||
// META: script=resources/support-promises.js
|
||||
// META: script=resources/support.js
|
||||
// META: script=resources/request-event-ordering-common.js
|
||||
// META: timeout=long
|
||||
|
||||
// Spec: https://w3c.github.io/IndexedDB/#abort-transaction
|
||||
|
||||
'use strict';
|
||||
|
||||
eventsTest('small values', [
|
||||
['get', 2], ['count', 4], ['continue-empty', null],
|
||||
['get-empty', 5], ['add', 5], ['open', 2],
|
||||
['continue', 2], ['get', 4], ['get-empty', 6],
|
||||
['count', 5], ['put-with-id', 5], ['put', 6],
|
||||
['error', 3], ['continue', 4], ['count', 6],
|
||||
['get-empty', 7], ['open', 4], ['open-empty', 7],
|
||||
['add', 7],
|
||||
]);
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
'use strict';
|
||||
|
||||
// Should be large enough to trigger large value handling in the IndexedDB
|
||||
// engines that have special code paths for large values.
|
||||
const wrapThreshold = 128 * 1024;
|
||||
|
||||
function populateStore(store) {
|
||||
store.put({id: 1, key: 'k1', value: largeValue(wrapThreshold, 1)});
|
||||
store.put({id: 2, key: 'k2', value: ['small-2']});
|
||||
store.put({id: 3, key: 'k3', value: largeValue(wrapThreshold, 3)});
|
||||
store.put({id: 4, key: 'k4', value: ['small-4']});
|
||||
}
|
||||
|
||||
// Assigns cursor indexes for operations that require open cursors.
|
||||
//
|
||||
// Returns the number of open cursors required to perform all operations.
|
||||
function assignCursors(operations) {
|
||||
return cursorCount;
|
||||
}
|
||||
|
||||
// Opens index cursors for operations that require open cursors.
|
||||
//
|
||||
// onsuccess is called if all cursors are opened successfully. Otherwise,
|
||||
// onerror will be called at least once.
|
||||
function openCursors(testCase, index, operations, onerror, onsuccess) {
|
||||
let pendingCursors = 0;
|
||||
|
||||
for (let operation of operations) {
|
||||
const opcode = operation[0];
|
||||
const primaryKey = operation[1];
|
||||
let request;
|
||||
switch (opcode) {
|
||||
case 'continue':
|
||||
request =
|
||||
index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey - 1}`));
|
||||
break;
|
||||
case 'continue-empty':
|
||||
// k4 is the last key in the data set, so calling continue() will get
|
||||
// the cursor past the end of the store.
|
||||
request = index.openCursor(IDBKeyRange.lowerBound('k4'));
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
operation[2] = request;
|
||||
++pendingCursors;
|
||||
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
--pendingCursors;
|
||||
if (!pendingCursors)
|
||||
onsuccess();
|
||||
});
|
||||
request.onerror = testCase.step_func(onerror);
|
||||
}
|
||||
|
||||
if (!pendingCursors)
|
||||
onsuccess();
|
||||
}
|
||||
|
||||
function doOperation(testCase, store, index, operation, requestId, results) {
|
||||
const opcode = operation[0];
|
||||
const primaryKey = operation[1];
|
||||
const cursor = operation[2];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request;
|
||||
switch (opcode) {
|
||||
case 'add': // Tests returning a primary key.
|
||||
request =
|
||||
store.add({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
||||
break;
|
||||
case 'put': // Tests returning a primary key.
|
||||
request =
|
||||
store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
||||
break;
|
||||
case 'put-with-id': // Tests returning success or a primary key.
|
||||
request = store.put({
|
||||
key: `k${primaryKey}`,
|
||||
value: [`small-${primaryKey}`],
|
||||
id: primaryKey
|
||||
});
|
||||
break;
|
||||
case 'get': // Tests returning a value.
|
||||
case 'get-empty': // Tests returning undefined.
|
||||
request = store.get(primaryKey);
|
||||
break;
|
||||
case 'getall': // Tests returning an array of values.
|
||||
request = store.getAll();
|
||||
break;
|
||||
case 'error': // Tests returning an error.
|
||||
request =
|
||||
store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
||||
request.onerror = testCase.step_func(event => {
|
||||
event.preventDefault();
|
||||
results.push([requestId, request.error]);
|
||||
resolve();
|
||||
});
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
reject(new Error('put with duplicate primary key succeded'));
|
||||
});
|
||||
break;
|
||||
case 'continue': // Tests returning a key, primary key, and value.
|
||||
request = cursor;
|
||||
cursor.result.continue();
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
const result = request.result;
|
||||
results.push(
|
||||
[requestId, result.key, result.primaryKey, result.value]);
|
||||
resolve();
|
||||
});
|
||||
request.onerror = null;
|
||||
break;
|
||||
case 'open': // Tests returning a cursor, key, primary key, and value.
|
||||
request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
const result = request.result;
|
||||
results.push(
|
||||
[requestId, result.key, result.primaryKey, result.value]);
|
||||
resolve();
|
||||
});
|
||||
break;
|
||||
case 'continue-empty': // Tests returning a null result.
|
||||
request = cursor;
|
||||
cursor.result.continue();
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
results.push([requestId, request.result]);
|
||||
resolve();
|
||||
});
|
||||
request.onerror = null;
|
||||
break;
|
||||
case 'open-empty': // Tests returning a null cursor.
|
||||
request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
const result = request.result;
|
||||
results.push([requestId, request.result]);
|
||||
resolve();
|
||||
});
|
||||
break;
|
||||
case 'count': // Tests returning a numeric result.
|
||||
request = index.count();
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
results.push([requestId, request.result]);
|
||||
resolve();
|
||||
});
|
||||
break;
|
||||
};
|
||||
|
||||
if (!request.onsuccess) {
|
||||
request.onsuccess = testCase.step_func(() => {
|
||||
results.push([requestId, request.result]);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
if (!request.onerror)
|
||||
request.onerror = testCase.step_func(event => {
|
||||
event.preventDefault();
|
||||
reject(request.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkOperationResult(operation, result, requestId) {
|
||||
const opcode = operation[0];
|
||||
const primaryKey = operation[1];
|
||||
|
||||
const expectedValue = (primaryKey == 1 || primaryKey == 3) ?
|
||||
largeValue(wrapThreshold, primaryKey) :
|
||||
[`small-${primaryKey}`];
|
||||
|
||||
const requestIndex = result[0];
|
||||
assert_equals(
|
||||
requestIndex, requestId, 'result event order should match request order');
|
||||
switch (opcode) {
|
||||
case 'put':
|
||||
case 'put-with-id':
|
||||
case 'add':
|
||||
assert_equals(
|
||||
result[1], primaryKey,
|
||||
`${opcode} result should be the new object's primary key`);
|
||||
break;
|
||||
case 'get':
|
||||
assert_equals(
|
||||
result[1].id, primaryKey,
|
||||
'get result should match put value (primary key)');
|
||||
assert_equals(
|
||||
result[1].key, `k${primaryKey}`,
|
||||
'get result should match put value (key)');
|
||||
assert_equals(
|
||||
result[1].value.join(','), expectedValue.join(','),
|
||||
'get result should match put value (nested value)');
|
||||
break;
|
||||
case 'getall':
|
||||
assert_equals(
|
||||
result[1].length, primaryKey,
|
||||
'getAll should return all the objects in the store');
|
||||
for (let i = 0; i < primaryKey; ++i) {
|
||||
const object = result[1][i];
|
||||
assert_equals(
|
||||
object.id, i + 1,
|
||||
`getAll result ${i + 1} should match put value (primary key)`);
|
||||
assert_equals(
|
||||
object.key, `k${i + 1}`,
|
||||
`get result ${i + 1} should match put value (key)`);
|
||||
|
||||
const expectedValue = (i == 0 || i == 2) ?
|
||||
largeValue(wrapThreshold, i + 1) :
|
||||
[`small-${i + 1}`];
|
||||
assert_equals(
|
||||
object.value.join(','), object.value.join(','),
|
||||
`get result ${i + 1} should match put value (nested value)`);
|
||||
}
|
||||
break;
|
||||
case 'get-empty':
|
||||
assert_equals(
|
||||
result[1], undefined, 'get-empty result should be undefined');
|
||||
break;
|
||||
case 'error':
|
||||
assert_equals(
|
||||
result[1].name, 'ConstraintError',
|
||||
'incorrect error from put with duplicate primary key');
|
||||
break;
|
||||
case 'continue':
|
||||
case 'open':
|
||||
assert_equals(
|
||||
result[1], `k${primaryKey}`,
|
||||
`${opcode} key should match the key in the put value`);
|
||||
assert_equals(
|
||||
result[2], primaryKey,
|
||||
`${opcode} primary key should match the put value's primary key`);
|
||||
assert_equals(
|
||||
result[3].id, primaryKey,
|
||||
`${opcode} value should match put value (primary key)`);
|
||||
assert_equals(
|
||||
result[3].key, `k${primaryKey}`,
|
||||
`${opcode} value should match put value (key)`);
|
||||
assert_equals(
|
||||
result[3].value.join(','), expectedValue.join(','),
|
||||
`${opcode} value should match put value (nested value)`);
|
||||
break;
|
||||
case 'continue-empty':
|
||||
case 'open-empty':
|
||||
assert_equals(result[1], null, `${opcode} result should be null`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function eventsTest(label, operations) {
|
||||
promise_test(testCase => {
|
||||
return createDatabase(
|
||||
testCase,
|
||||
(database, transaction) => {
|
||||
const store = database.createObjectStore(
|
||||
'test-store', {autoIncrement: true, keyPath: 'id'});
|
||||
store.createIndex('test-index', 'key', {unique: true});
|
||||
populateStore(store);
|
||||
})
|
||||
.then(database => {
|
||||
const transaction = database.transaction(['test-store'], 'readwrite');
|
||||
const store = transaction.objectStore('test-store');
|
||||
const index = store.index('test-index');
|
||||
return new Promise((resolve, reject) => {
|
||||
openCursors(testCase, index, operations, reject, () => {
|
||||
const results = [];
|
||||
const promises = [];
|
||||
for (let i = 0; i < operations.length; ++i) {
|
||||
const promise = doOperation(
|
||||
testCase, store, index, operations[i], i, results);
|
||||
promises.push(promise);
|
||||
};
|
||||
resolve(Promise.all(promises).then(() => results));
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
assert_equals(
|
||||
results.length, operations.length,
|
||||
'Promise.all should resolve after all sub-promises resolve');
|
||||
for (let i = 0; i < operations.length; ++i)
|
||||
checkOperationResult(operations[i], results[i], i);
|
||||
});
|
||||
}, label);
|
||||
}
|
||||
Loading…
Reference in a new issue