#pragma once #include #include #include #include #include #include #include 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 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& 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 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& 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::map> 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 spill_; int spill_tip_height_ = 0; std::string spill_tip_hash_; }; } // namespace ui } // namespace dragonx