Repairs dev:3ee9099switched the Explorer call sites (and a test) to ExplorerBlockCache::loadRangeCached() / revision(), but those definitions were sitting uncommitted in the working tree (left over from the explorer block-search feature,ebbeb84) and were not staged with that commit — so dev as pushed referenced undefined members and would not compile on a clean checkout. The infra: loadRangeCached() memoizes loadRange() per (min,max), invalidated by revision_, which is bumped on every mutation that can change what loadRange returns (open/close/store/clear/prune/spill-reset); the memo is capped at 16 ranges. My local builds passed only because the working tree already had these. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
5.2 KiB
C++
126 lines
5.2 KiB
C++
#pragma once
|
|
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
struct sqlite3;
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
struct ExplorerBlockSummary {
|
|
int height = 0;
|
|
std::string hash;
|
|
int tx_count = 0;
|
|
int size = 0;
|
|
std::int64_t time = 0;
|
|
double difficulty = 0.0;
|
|
};
|
|
|
|
class ExplorerBlockCache {
|
|
public:
|
|
struct SavedTipValidation {
|
|
bool needed = false;
|
|
int height = 0;
|
|
std::string expectedHash;
|
|
};
|
|
|
|
ExplorerBlockCache();
|
|
ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath);
|
|
~ExplorerBlockCache();
|
|
|
|
ExplorerBlockCache(const ExplorerBlockCache&) = delete;
|
|
ExplorerBlockCache& operator=(const ExplorerBlockCache&) = delete;
|
|
|
|
static std::string defaultDatabasePath();
|
|
static std::string defaultLegacyJsonPath();
|
|
|
|
bool ensureOpen();
|
|
bool isOpen() const { return db_ != nullptr; }
|
|
const std::string& databasePath() const { return database_path_; }
|
|
|
|
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
|
// loadRange memoized per (min,max) and invalidated by revision() so the per-frame Explorer list only hits SQLite
|
|
// after the cache actually changed (store / clear / prune / spill reset / open / close). The reference is valid
|
|
// until the next call; copy it if you call again before using it.
|
|
const std::map<int, ExplorerBlockSummary>& loadRangeCached(int minHeight, int maxHeight);
|
|
std::uint64_t revision() const { return revision_; }
|
|
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
|
|
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
|
|
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
|
|
// Always returns the DB result; a block the DB refuses (unopenable file, failed insert) is kept in a small
|
|
// in-memory spill so the Explorer still shows it instead of refetching it forever.
|
|
bool storeBlock(const ExplorerBlockSummary& block);
|
|
// storeBlock for a batch inside one SAVEPOINT (one WAL commit instead of one per block). True if every block hit the DB.
|
|
bool storeBlocks(const std::vector<ExplorerBlockSummary>& blocks);
|
|
int cachedBlockCount();
|
|
void clearBlocks();
|
|
// Keep only the `maxRows` highest blocks (data-safe: the table is a re-fetchable RPC cache).
|
|
void pruneTo(int maxRows);
|
|
static constexpr int kMaxCachedBlocks = 20000;
|
|
|
|
// A failed open is not retried until this interval has passed (default 30 s). openAttempts() counts real attempts.
|
|
void setOpenRetryInterval(std::chrono::milliseconds interval) { open_retry_interval_ = interval; }
|
|
int openAttempts() const { return open_attempts_; }
|
|
std::size_t spillCount() const { return spill_.size(); }
|
|
|
|
SavedTipValidation prepareValidation(int currentHeight, const std::string& currentBestHash);
|
|
void applySavedTipValidation(const SavedTipValidation& validation,
|
|
const std::string& actualHash,
|
|
int currentHeight,
|
|
const std::string& currentBestHash);
|
|
void updateTip(int height, const std::string& hash);
|
|
|
|
// Rows changed through this connection since it opened (sqlite3_total_changes); 0 when closed.
|
|
// Lets tests check that a steady tip writes nothing.
|
|
int totalChanges() const;
|
|
|
|
private:
|
|
bool exec(const char* sql);
|
|
std::string getMetaValue(const std::string& key);
|
|
int getMetaInt(const std::string& key, int fallback);
|
|
bool setMetaValue(const std::string& key, const std::string& value);
|
|
bool createSchema();
|
|
void migrateLegacyJsonIfNeeded();
|
|
void close();
|
|
bool storeBlockRow(const ExplorerBlockSummary& block); // DB write only
|
|
void spillBlock(const ExplorerBlockSummary& block);
|
|
bool failOpen(const char* why);
|
|
|
|
sqlite3* db_ = nullptr;
|
|
std::string database_path_;
|
|
std::string legacy_json_path_;
|
|
|
|
// The saved tip as last written or read, so the per-frame check can skip SQLite while the chain
|
|
// tip hasn't moved. The database stays the source of truth; this is only a copy of it.
|
|
bool tip_known_ = false;
|
|
int tip_height_ = 0;
|
|
std::string tip_hash_;
|
|
|
|
// Failure latch for ensureOpen(): after a failed open nothing retries until retry_not_before_.
|
|
std::chrono::steady_clock::time_point retry_not_before_{};
|
|
std::chrono::milliseconds open_retry_interval_{30000};
|
|
int open_attempts_ = 0;
|
|
int stores_since_prune_ = 0;
|
|
|
|
// Bumped by every mutation that can change what loadRange returns; keys the loadRangeCached memo.
|
|
std::uint64_t revision_ = 1;
|
|
std::uint64_t range_cache_revision_ = 0;
|
|
std::map<std::pair<int, int>, std::map<int, ExplorerBlockSummary>> range_cache_;
|
|
|
|
// Blocks the DB refused, bounded and UI-thread only. Evicts the entry farthest from the last spilled height
|
|
// (the Explorer fetches around the visible window). Cleared with clearBlocks() and when the chain tip regresses.
|
|
static constexpr std::size_t kMaxSpillBlocks = 512;
|
|
std::map<int, ExplorerBlockSummary> spill_;
|
|
int spill_tip_height_ = 0;
|
|
std::string spill_tip_hash_;
|
|
};
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|