feat(lite): M2b-3 — background refresh worker + App::update hook

- LiteWalletController owns a background std::thread worker that, once a wallet is ready,
  refreshes every ~4s and publishes a copyable LiteWalletAppRefreshModel under a mutex.
  Worker auto-starts on lifecycle-ready and is stopped+joined in the destructor. status_
  is written only on the main thread; walletOpen_/syncStarted_ are atomic.
- App::update() calls takeRefreshedModel() and applies it into state_ on the main thread
  (WalletState is non-copyable, so the model crosses the thread boundary, not the state),
  so the existing Balance/Receive/Transactions tabs populate from lite data.
- refreshWalletState() refactored onto refreshModel() (pure, worker-safe).
- testLiteWalletControllerWorkerProducesModel verifies the worker publishes a populated
  model (stable across repeated runs). Builds clean in all configs.

Real-backend smoke (lite_smoke --refresh now runs real output through the parsers) found
two integration bugs, documented in the plan for follow-up:
- syncstatus parser requires synced_blocks/total_blocks but the real idle response is
  {"syncing":"false"} (string), so it fails to parse when not actively syncing.
- the first data query (balance/list) blocks on a full chain sync, which would hang the
  worker's shutdown join — needs a cancel/timeout path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 22:38:34 -05:00
parent 012341b1a4
commit a8b5d2f6a3
7 changed files with 190 additions and 15 deletions

View File

@@ -6,6 +6,7 @@
#include "../data/wallet_state.h"
#include <chrono>
#include <utility>
#include <sodium.h>
@@ -105,6 +106,11 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
status_ = lifecycle_.status();
}
LiteWalletController::~LiteWalletController()
{
stopWorker();
}
std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings)
@@ -122,7 +128,8 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re
if (result.walletReady) {
walletOpen_ = true;
if (persist_) persist_();
startSync(); // begin background sync now that a wallet is ready
startSync(); // begin background sync on the backend
startWorker(); // begin periodic refresh -> WalletState (via takeRefreshedModel)
}
}
@@ -133,9 +140,9 @@ LiteSyncStartResult LiteWalletController::startSync()
return result;
}
bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
{
if (!walletOpen_) return false;
if (!walletOpen_.load()) return std::nullopt;
// Poll sync status first so the refresh bundle (and the mapped sync model) carries it.
LiteWalletRefreshRequest request;
@@ -147,21 +154,60 @@ bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
const auto refreshResult = gateway_.refresh(request);
if (refreshResult.bundle.successfulCommandCount == 0 && !request.haveSyncStatus) {
status_ = refreshResult.status;
return false;
return std::nullopt;
}
const auto mapped = mapLiteWalletRefreshResult(refreshResult);
if (!mapped.ok) {
status_ = refreshResult.status;
return false;
}
if (!mapped.ok) return std::nullopt;
return mapped.model;
}
applyLiteRefreshModelToWalletState(mapped.model, state);
status_ = refreshResult.status;
bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
{
auto model = refreshModel();
if (!model) return false;
applyLiteRefreshModelToWalletState(*model, state);
return true;
}
bool LiteWalletController::takeRefreshedModel(LiteWalletAppRefreshModel& out)
{
std::lock_guard<std::mutex> lock(modelMutex_);
if (!pendingModel_) return false;
out = std::move(*pendingModel_);
pendingModel_.reset();
return true;
}
void LiteWalletController::startWorker()
{
if (running_.exchange(true)) return; // already running
worker_ = std::thread([this] { workerLoop(); });
}
void LiteWalletController::stopWorker()
{
if (!running_.exchange(false)) return; // not running
wakeCv_.notify_all();
if (worker_.joinable()) worker_.join();
}
void LiteWalletController::workerLoop()
{
while (running_.load()) {
if (walletOpen_.load()) {
auto model = refreshModel();
if (model) {
std::lock_guard<std::mutex> lock(modelMutex_);
pendingModel_ = std::move(model);
}
}
std::unique_lock<std::mutex> lock(wakeMutex_);
wakeCv_.wait_for(lock, std::chrono::milliseconds(kRefreshIntervalMs),
[this] { return !running_.load(); });
}
}
LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateRequest request)
{
auto result = lifecycle_.createWallet(request);