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:
@@ -390,6 +390,14 @@ void App::update()
|
||||
if (fast_worker_) {
|
||||
fast_worker_->drainResults();
|
||||
}
|
||||
|
||||
// Apply any lite-wallet refresh the controller's background worker produced (main thread).
|
||||
if (lite_wallet_) {
|
||||
wallet::LiteWalletAppRefreshModel liteModel;
|
||||
if (lite_wallet_->takeRefreshedModel(liteModel)) {
|
||||
wallet::applyLiteRefreshModelToWalletState(liteModel, state_);
|
||||
}
|
||||
}
|
||||
async_tasks_.reapCompleted();
|
||||
|
||||
// Auto-lock check (only when connected + encrypted + unlocked)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user