Everywhere: Implement persistence of localStorage using sqlite

This change follows the pattern of our cookies persistence
implementation: the "browser" process is responsible for interacting
with the sqlite database, and WebContent communicates all storage
operations via IPC.

The new database table uses (storage_endpoint, storage_key, bottle_key)
as the primary key. This design follows concepts from the
https://storage.spec.whatwg.org/ and is intended to support reuse of the
persistence layer for other APIs (e.g., CacheStorage, IndexedDB). For
now, `storage_endpoint` is always "localStorage", `storage_key` is the
website's origin, and `bottle_key` is the name of the localStorage key.
This commit is contained in:
Aliaksandr Kalenik 2025-06-08 23:35:46 +02:00 committed by Alexander Kalenik
commit 84b9224121
Notes: github-actions[bot] 2025-06-12 15:05:54 +00:00
24 changed files with 694 additions and 118 deletions

View file

@ -46,9 +46,6 @@ Storage::Storage(JS::Realm& realm, Type type, GC::Ref<StorageAPI::StorageBottle>
.named_property_deleter_has_identifier = true,
};
for (auto const& item : map())
m_stored_bytes += item.key.byte_count() + item.value.byte_count();
all_storages().set(*this);
}
@ -75,74 +72,46 @@ void Storage::visit_edges(GC::Cell::Visitor& visitor)
size_t Storage::length() const
{
// The length getter steps are to return this's map's size.
return map().size();
return m_storage_bottle->size();
}
// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key
Optional<String> Storage::key(size_t index)
{
// 1. If index is greater than or equal to this's map's size, then return null.
if (index >= map().size())
if (index >= m_storage_bottle->size())
return {};
// 2. Let keys be the result of running get the keys on this's map.
auto keys = map().keys();
auto keys = m_storage_bottle->keys();
// 3. Return keys[index].
return keys[index];
}
// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-getitem
Optional<String> Storage::get_item(StringView key) const
Optional<String> Storage::get_item(String const& key) const
{
// 1. If this's map[key] does not exist, then return null.
auto it = map().find(key);
if (it == map().end())
return {};
// 2. Return this's map[key].
return it->value;
return m_storage_bottle->get(key);
}
// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-setitem
WebIDL::ExceptionOr<void> Storage::set_item(String const& key, String const& value)
{
auto& realm = this->realm();
// 1. Let oldValue be null.
Optional<String> old_value;
// 2. Let reorder be true.
bool reorder = true;
// 3. If this's map[key] exists:
auto new_size = m_stored_bytes;
if (auto it = map().find(key); it != map().end()) {
// 1. Set oldValue to this's map[key].
old_value = it->value;
// 2. If oldValue is value, then return.
if (old_value == value)
return {};
// 3. Set reorder to false.
reorder = false;
} else {
new_size += key.bytes().size();
}
// 4. If value cannot be stored, then throw a "QuotaExceededError" DOMException exception.
new_size += value.bytes().size() - old_value.value_or(String {}).bytes().size();
if (m_storage_bottle->quota.has_value() && new_size > *m_storage_bottle->quota)
return WebIDL::QuotaExceededError::create(realm, MUST(String::formatted("Unable to store more than {} bytes in storage", *m_storage_bottle->quota)));
// 5. Set this's map[key] to value.
map().set(key, value);
m_stored_bytes = new_size;
// 6. If reorder is true, then reorder this.
if (reorder)
this->reorder();
auto error = m_storage_bottle->set(key, value);
if (error == WebView::StorageOperationError::QuotaExceededError) {
return WebIDL::QuotaExceededError::create(realm(), MUST(String::formatted("Unable to store more than {} bytes in storage", *m_storage_bottle->quota())));
}
// 7. Broadcast this with key, oldValue, and value.
broadcast(key, old_value, value);
@ -154,16 +123,13 @@ WebIDL::ExceptionOr<void> Storage::set_item(String const& key, String const& val
void Storage::remove_item(String const& key)
{
// 1. If this's map[key] does not exist, then return.
auto it = map().find(key);
if (it == map().end())
// 2. Set oldValue to this's map[key].
auto old_value = m_storage_bottle->get(key);
if (!old_value.has_value())
return;
// 2. Set oldValue to this's map[key].
auto old_value = it->value;
// 3. Remove this's map[key].
map().remove(it);
m_stored_bytes = m_stored_bytes - key.bytes().size() - old_value.bytes().size();
m_storage_bottle->remove(key);
// 4. Reorder this.
reorder();
@ -176,7 +142,7 @@ void Storage::remove_item(String const& key)
void Storage::clear()
{
// 1. Clear this's map.
map().clear();
m_storage_bottle->clear();
// 2. Broadcast this with null, null, and null.
broadcast({}, {}, {});
@ -253,8 +219,9 @@ Vector<FlyString> Storage::supported_property_names() const
{
// The supported property names on a Storage object storage are the result of running get the keys on storage's map.
Vector<FlyString> names;
names.ensure_capacity(map().size());
for (auto const& key : map().keys())
auto keys = m_storage_bottle->keys();
names.ensure_capacity(keys.size());
for (auto const& key : keys)
names.unchecked_append(key);
return names;
}
@ -271,7 +238,7 @@ Optional<JS::Value> Storage::item_value(size_t index) const
JS::Value Storage::named_item_value(FlyString const& name) const
{
auto value = get_item(name);
auto value = get_item(String(name));
if (!value.has_value())
// AD-HOC: Spec leaves open to a description at: https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
// However correct behavior expected here: https://github.com/whatwg/html/issues/8684
@ -302,10 +269,12 @@ WebIDL::ExceptionOr<void> Storage::set_value_of_named_property(String const& key
void Storage::dump() const
{
dbgln("Storage ({} key(s))", map().size());
auto keys = m_storage_bottle->keys();
dbgln("Storage ({} key(s))", keys.size());
size_t i = 0;
for (auto const& it : map()) {
dbgln("[{}] \"{}\": \"{}\"", i, it.key, it.value);
for (auto const& key : keys) {
auto value = m_storage_bottle->get(key);
dbgln("[{}] \"{}\": \"{}\"", i, key, value.value());
++i;
}
}

View file

@ -8,7 +8,6 @@
#pragma once
#include <AK/HashMap.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
@ -33,12 +32,10 @@ public:
size_t length() const;
Optional<String> key(size_t index);
Optional<String> get_item(StringView key) const;
Optional<String> get_item(String const& key) const;
WebIDL::ExceptionOr<void> set_item(String const& key, String const& value);
void remove_item(String const& key);
void clear();
auto const& map() const { return m_storage_bottle->map; }
auto& map() { return m_storage_bottle->map; }
Type type() const { return m_type; }
void dump() const;
@ -63,7 +60,6 @@ private:
Type m_type {};
GC::Ref<StorageAPI::StorageBottle> m_storage_bottle;
u64 m_stored_bytes { 0 };
};
}

View file

@ -66,6 +66,7 @@
#include <LibWeb/RequestIdleCallback/IdleDeadline.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::HTML {
@ -464,7 +465,11 @@ WebIDL::ExceptionOr<GC::Ref<Storage>> Window::local_storage()
return GC::Ref { *storage };
// 2. Let map be the result of running obtain a local storage bottle map with this's relevant settings object and "localStorage".
auto map = StorageAPI::obtain_a_local_storage_bottle_map(relevant_settings_object(*this), "localStorage"sv);
GC::Ptr<StorageAPI::LocalStorageBottle> map;
auto storage_key = StorageAPI::obtain_a_storage_key(relevant_settings_object(*this));
if (storage_key.has_value()) {
map = StorageAPI::LocalStorageBottle::create(heap(), page(), storage_key.value(), StorageAPI::StorageEndpoint::LOCAL_STORAGE_QUOTA);
}
// 3. If map is failure, then throw a "SecurityError" DOMException.
if (!map)
@ -491,7 +496,7 @@ WebIDL::ExceptionOr<GC::Ref<Storage>> Window::session_storage()
return GC::Ref { *storage };
// 2. Let map be the result of running obtain a session storage bottle map with this's relevant settings object and "sessionStorage".
auto map = StorageAPI::obtain_a_session_storage_bottle_map(relevant_settings_object(*this), "sessionStorage"sv);
auto map = StorageAPI::obtain_a_session_storage_bottle_map(relevant_settings_object(*this), StorageAPI::StorageEndpointType::SessionStorage);
// 3. If map is failure, then throw a "SecurityError" DOMException.
if (!map)