Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e2e7aaa
Add vitest browser-mode benchmarking infrastructure
roryabraham Feb 9, 2026
c34ced1
Add production-scale data generators for benchmarks
roryabraham Feb 9, 2026
6193811
Add benchmark suites and branch comparison script
roryabraham Feb 9, 2026
811b153
Add benchmark documentation to README
roryabraham Feb 9, 2026
5a098ad
Extract shared SQLiteQueries and move native provider to SQLiteProvider/
roryabraham Feb 9, 2026
731425a
Add DirtyMap write coalescing to the storage layer
roryabraham Feb 9, 2026
dbfeb57
Add SQLite WASM web provider with worker and BroadcastChannel sync
roryabraham Feb 9, 2026
09b4727
Evolve DirtyMap into a patch-staging layer with SET/MERGE entry types
roryabraham Feb 10, 2026
53efc01
Add automated benchmark report generation
roryabraham Feb 10, 2026
1eb3f68
Add automatic benchmark stabilization for noisy results
roryabraham Feb 10, 2026
ed0decd
Remove benchmark stabilization layer
roryabraham Feb 10, 2026
c3f1dba
Add benchmark results comparing Baseline vs DM+IDB vs DM+SQLite
roryabraham Feb 10, 2026
1eef8e1
Unified provider-agnostic storage web worker
roryabraham Feb 10, 2026
c6bfbd2
Update PROPOSAL_DRAFT.md to reflect unified worker architecture
roryabraham Feb 10, 2026
e304605
Value-bearing cross-tab sync and DirtyMap -> WriteBuffer rename
roryabraham Feb 10, 2026
08b7d9d
Save updated context about multithreading on web vs iOS/Android
roryabraham Feb 10, 2026
86b3779
Revert to official SQLite packages with multi-threaded worker archite…
roryabraham Feb 11, 2026
08a4b00
Fix init/clear benchmark hangs with serial worker message queue
roryabraham Feb 11, 2026
f5e2435
Write finalized proposal
roryabraham Feb 11, 2026
2ed85ad
Fix architecture graph
roryabraham Feb 11, 2026
72fe918
Get build passing
roryabraham Feb 12, 2026
a40c6dd
Remove testing/bench code and scripts
fabioh8010 Apr 13, 2026
7666d05
Merge tag '3.0.60' into feature/sqlite-web
fabioh8010 Apr 17, 2026
30feda1
Lint and Prettier fixes
fabioh8010 Apr 17, 2026
e9e89f0
Implement getAll
fabioh8010 Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ dist/
yalc.lock

# Perf tests
.reassure
.reassure
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,4 @@ you to edit the Onyx source directly in the Onyx repo, and have those changes ho

Now you can make changes directly to the `react-native-onyx` source code and your React Native project should-hot reload with those changes in realtime.

_Note:_ If you want to unlink `react-native-onyx`, simply run `npm install` from your React Native project directory again. That will reinstall `react-native-onyx` from npm.
_Note:_ If you want to unlink `react-native-onyx`, simply run `npm install` from your React Native project directory again. That will reinstall `react-native-onyx` from npm.
21 changes: 21 additions & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.16)
project(OnyxNativeBufferStore LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ---------------------------------------------------------------------------
# NativeBufferStore (C++ -- thread-safe buffer HybridObject)
# ---------------------------------------------------------------------------
# Pure buffer with std::shared_mutex. No SQLite, no background thread.
# The Worklet Worker Runtime handles persistence via react-native-nitro-sqlite.
add_library(onyx_native_buffer_store STATIC NativeBufferStore.cpp)
target_include_directories(onyx_native_buffer_store PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# ---------------------------------------------------------------------------
# Test harness
# ---------------------------------------------------------------------------
if(BUILD_TESTING)
add_executable(native_buffer_store_test test_native_buffer_store.cpp)
target_link_libraries(native_buffer_store_test PRIVATE onyx_native_buffer_store)
endif()
74 changes: 74 additions & 0 deletions cpp/NativeBufferStore.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* NativeBufferStore implementation.
*
* Pure thread-safe buffer -- no background thread, no SQLite.
* Uses std::shared_mutex for reader-writer locking:
* - shared_lock for reads (get, has, size, entries)
* - unique_lock for writes (set, erase, clear, drain)
*/

#include "NativeBufferStore.h"

#include <utility>

namespace onyx {

const NativeBufferEntry* NativeBufferStore::get(const std::string& key) {
std::shared_lock lock(mutex_);
auto it = buffer_.find(key);
if (it == buffer_.end()) {
return nullptr;
}
return &it->second;
}

void NativeBufferStore::set(const std::string& key, NativeBufferEntry entry) {
std::unique_lock lock(mutex_);
buffer_[key] = std::move(entry);
}

bool NativeBufferStore::erase(const std::string& key) {
std::unique_lock lock(mutex_);
return buffer_.erase(key) > 0;
}

bool NativeBufferStore::has(const std::string& key) {
std::shared_lock lock(mutex_);
return buffer_.find(key) != buffer_.end();
}

size_t NativeBufferStore::size() {
std::shared_lock lock(mutex_);
return buffer_.size();
}

void NativeBufferStore::clear() {
std::unique_lock lock(mutex_);
buffer_.clear();
}

std::vector<std::pair<std::string, NativeBufferEntry>> NativeBufferStore::entries() {
std::shared_lock lock(mutex_);
std::vector<std::pair<std::string, NativeBufferEntry>> result;
result.reserve(buffer_.size());
for (const auto& [key, entry] : buffer_) {
result.emplace_back(key, entry);
}
return result;
}

std::vector<std::pair<std::string, NativeBufferEntry>> NativeBufferStore::drain() {
std::unique_lock lock(mutex_);

// Move all entries out of the buffer atomically
std::vector<std::pair<std::string, NativeBufferEntry>> result;
result.reserve(buffer_.size());
for (auto& [key, entry] : buffer_) {
result.emplace_back(std::move(key), std::move(entry));
}
buffer_.clear();

return result;
}

} // namespace onyx
142 changes: 142 additions & 0 deletions cpp/NativeBufferStore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* NativeBufferStore -- Thread-safe buffer backed by a shared_mutex.
*
* This is the C++ backing store for the native (iOS/Android) BufferStore
* HybridObject. It provides a simple key-value buffer where:
*
* - The main JS thread populates entries via set()
* - The Worklet Worker Runtime drains all entries atomically via drain()
*
* Thread safety:
* - Read methods (get, has, size) use shared_lock (concurrent readers OK)
* - Write methods (set, erase, clear) use unique_lock (exclusive access)
* - drain() uses unique_lock and atomically swaps out the entire buffer
*
* This class stores values as AnyValue-equivalent C++ types (std::string
* for JSON, since the actual AnyMap-to-JSON serialization happens on the
* Worklet Worker Runtime side after draining). The Worklet Worker Runtime
* handles JSON.stringify and calls react-native-nitro-sqlite for persistence.
*
* Future optimization: store AnyMap values directly (no JSON strings) and
* serialize to JSON using Glaze on a pure C++ worker thread, eliminating
* the JS round-trip entirely.
*/

#pragma once

#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>

namespace onyx {

/**
* Entry types matching the TypeScript BufferEntry.entryType.
*/
enum class NativeEntryType {
Set,
Merge,
};

/**
* A single buffer entry, storing the key, JSON value, and type.
*
* The value is stored as a JSON string. In the current architecture,
* Nitro's JSIConverter converts JS objects to AnyValue (deep C++ copy)
* on the main thread. The Worklet Worker Runtime then calls drain(),
* converts AnyValue back to JS objects (on the worker thread), and
* JSON.stringifies them for SQLite persistence.
*
* For the C++ buffer, we store pre-serialized JSON strings since the
* BufferStore interface on the TS side handles the AnyMap -> JS
* conversion. This keeps the C++ side simple and focused on
* thread-safe buffering.
*/
struct NativeBufferEntry {
std::string key;
std::string valueJSON;
NativeEntryType entryType;
// replaceNullPatches stored as serialized JSON array string
std::string replaceNullPatchesJSON;
};

class NativeBufferStore {
public:
NativeBufferStore() = default;
~NativeBufferStore() = default;

// Non-copyable, non-movable
NativeBufferStore(const NativeBufferStore&) = delete;
NativeBufferStore& operator=(const NativeBufferStore&) = delete;
NativeBufferStore(NativeBufferStore&&) = delete;
NativeBufferStore& operator=(NativeBufferStore&&) = delete;

// -----------------------------------------------------------------------
// BufferStore interface (called from JS main thread via JSI)
// -----------------------------------------------------------------------

/**
* Get a buffer entry by key, or nullptr if not found.
* Thread-safe: acquires shared_lock (allows concurrent readers).
*/
const NativeBufferEntry* get(const std::string& key);

/**
* Insert or replace a buffer entry.
* Thread-safe: acquires unique_lock (exclusive access).
*/
void set(const std::string& key, NativeBufferEntry entry);

/**
* Delete a key from the buffer.
* Thread-safe: acquires unique_lock.
*/
bool erase(const std::string& key);

/**
* Check if a key exists in the buffer.
* Thread-safe: acquires shared_lock.
*/
bool has(const std::string& key);

/**
* Get the number of pending entries.
* Thread-safe: acquires shared_lock.
*/
size_t size();

/**
* Clear all pending entries.
* Thread-safe: acquires unique_lock.
*/
void clear();

/**
* Get a snapshot of all entries (for the TS side to iterate).
* Thread-safe: acquires shared_lock.
*/
std::vector<std::pair<std::string, NativeBufferEntry>> entries();

// -----------------------------------------------------------------------
// Drain (called from Worklet Worker Runtime)
// -----------------------------------------------------------------------

/**
* Atomically drain all pending entries from the buffer.
*
* Returns all entries and clears the buffer in a single atomic operation.
* The lock is held only for the duration of a swap -- microseconds.
* The caller (Worklet Worker Runtime) gets sole ownership of the
* returned data and can take its time serializing and persisting.
*
* Thread-safe: acquires unique_lock.
*/
std::vector<std::pair<std::string, NativeBufferEntry>> drain();

private:
mutable std::shared_mutex mutex_;
std::unordered_map<std::string, NativeBufferEntry> buffer_;
};

} // namespace onyx
Loading
Loading