Files
ladybird/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp
Andreas Kling 9eeb43e265 LibWeb: Use binary search for IndexedDB ObjectStore record insertion
Instead of appending and re-sorting the entire records vector on every
insert (O(n log n)), use binary search to find the correct insertion
position and insert directly (O(log n) comparisons + O(n) shift).
2026-03-21 08:41:13 -05:00

214 lines
6.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2025, stelar7 <dudedbz@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Math.h>
#include <LibWeb/IndexedDB/IDBKeyRange.h>
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
namespace Web::IndexedDB {
GC_DEFINE_ALLOCATOR(ObjectStore);
ObjectStore::~ObjectStore() = default;
GC::Ref<ObjectStore> ObjectStore::create(JS::Realm& realm, GC::Ref<Database> database, String name, bool auto_increment, Optional<KeyPath> const& key_path)
{
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))
, m_key_path(key_path)
{
database->add_object_store(*this);
if (auto_increment)
m_key_generator = KeyGenerator {};
}
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);
}
}
void ObjectStore::remove_records_in_range(GC::Ref<IDBKeyRange> range)
{
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);
});
}
bool ObjectStore::has_record_with_key(GC::Ref<Key> key)
{
auto index = m_records.find_if([&key](auto const& record) {
return Key::equals(key, record.key);
});
return index != m_records.end();
}
void ObjectStore::store_a_record(ObjectStoreRecord const& record)
{
if (m_mutation_log)
m_mutation_log->note_record_stored(record.key);
// NOTE: The record is stored in the object stores list of records such that the list is sorted according to the key of the records in ascending order.
// We use binary search to find the correct insertion position.
size_t lo = 0;
size_t hi = m_records.size();
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (Key::compare_two_keys(m_records[mid].key, record.key) < 0)
lo = mid + 1;
else
hi = mid;
}
m_records.insert(lo, record);
}
u64 ObjectStore::count_records_in_range(GC::Ref<IDBKeyRange> range)
{
u64 count = 0;
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
++count;
}
return count;
}
Optional<ObjectStoreRecord&> ObjectStore::first_in_range(GC::Ref<IDBKeyRange> range)
{
return m_records.first_matching([&](auto const& record) {
return range->is_in_range(record.key);
});
}
void ObjectStore::clear_records()
{
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
ErrorOr<u64> ObjectStore::generate_a_key()
{
// 1. Let generator be store's key generator.
auto& generator = key_generator();
// 2. Let key be generator's current number.
auto key = generator.current_number();
// 3. If key is greater than 2^53 (9007199254740992), then return failure.
if (key > static_cast<u64>(MAX_KEY_GENERATOR_VALUE))
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.
return key;
}
// https://w3c.github.io/IndexedDB/#possibly-update-the-key-generator
void ObjectStore::possibly_update_the_key_generator(GC::Ref<Key> key)
{
// 1. If the type of key is not number, abort these steps.
if (key->type() != Key::KeyType::Number)
return;
// 2. Let value be the value of key.
auto value = key->value_as_double();
// 3. Set value to the minimum of value and 2^53 (9007199254740992).
value = min(value, MAX_KEY_GENERATOR_VALUE);
// 4. Set value to the largest integer not greater than value.
value = AK::floor(value);
// 5. Let generator be store's key generator.
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 (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)
{
GC::ConservativeVector<ObjectStoreRecord> records(range->heap());
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
records.append(record);
if (count.has_value() && records.size() >= *count)
break;
}
return records;
}
GC::ConservativeVector<ObjectStoreRecord> ObjectStore::last_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
{
GC::ConservativeVector<ObjectStoreRecord> records(range->heap());
for (auto const& record : m_records.in_reverse()) {
if (range->is_in_range(record.key))
records.append(record);
if (count.has_value() && records.size() >= *count)
break;
}
return records;
}
}