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

@@ -25,9 +25,14 @@
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
namespace dragonx {
struct WalletState; // data/wallet_state.h
@@ -55,6 +60,11 @@ public:
LiteClientBridge bridge,
LiteWalletControllerOptions options = LiteWalletControllerOptions{});
~LiteWalletController(); // stops + joins the background refresh worker
LiteWalletController(const LiteWalletController&) = delete;
LiteWalletController& operator=(const LiteWalletController&) = delete;
// Production factory: links the SDXL backend compiled into this build.
static std::unique_ptr<LiteWalletController> createLinked(
WalletCapabilities capabilities,
@@ -82,19 +92,41 @@ public:
// Poll sync status + fetch balance/addresses/transactions, and apply the result into the
// app's WalletState. Returns true if state was updated. Safe no-op when no wallet is open.
// Synchronous (blocks on the backend); used by tests and as the worker's unit of work.
bool refreshWalletState(dragonx::WalletState& state);
// Synchronous refresh that returns the mapped model (or nullopt). Pure w.r.t. shared
// state; safe to call from the background worker. nullopt when no wallet is open or the
// refresh produced nothing usable.
std::optional<LiteWalletAppRefreshModel> refreshModel();
// Main-thread handoff: if the background worker has produced a fresh model since the last
// call, move it into `out` and return true. Apply it with applyLiteRefreshModelToWalletState.
bool takeRefreshedModel(LiteWalletAppRefreshModel& out);
private:
void onLifecycleResult(const LiteWalletLifecycleResult& result);
void startWorker();
void stopWorker();
void workerLoop();
LiteClientBridge bridge_; // the single owned bridge; services below borrow &bridge_
LiteWalletLifecycleService lifecycle_;
LiteWalletGateway gateway_;
LiteSyncService sync_;
std::function<void()> persist_;
bool walletOpen_ = false;
bool syncStarted_ = false;
WalletBackendStatus status_;
std::atomic<bool> walletOpen_{false};
std::atomic<bool> syncStarted_{false};
WalletBackendStatus status_; // written only on the main thread (lifecycle ops)
// Background refresh worker.
std::thread worker_;
std::atomic<bool> running_{false};
std::mutex wakeMutex_;
std::condition_variable wakeCv_;
std::mutex modelMutex_;
std::optional<LiteWalletAppRefreshModel> pendingModel_; // guarded by modelMutex_
static constexpr int kRefreshIntervalMs = 4000;
};
} // namespace wallet