perf(litebe): change-gate the idle lite refresh worker

The background 2 s refresh worker re-queried and re-copied the whole
wallet every cycle (a fresh TLS refresh plus a multi-megabyte JSON
build, copies and DOM parse), even when nothing had changed, so CPU and
the network never went idle.

The worker now skips the full refresh and the re-publish on a cycle that
is provably unchanged, while never letting the UI show stale confirmed
balance or transactions:

- The lite bridge keeps a monotonic mutation epoch, bumped after any
  command not on a read-only allow-list, so a local change made on any
  thread (send, shield, import, new address, encrypt/unlock/lock,
  console) is detected at one choke point.
- Each cycle the worker runs one cheap sync probe (one connection, the
  mandatory block ingest) and a local height read, and refreshes only
  when the scanned height advanced, the mutation epoch moved, the
  encryption state changed, a sync is in progress, or a ~10 s heartbeat
  elapsed; otherwise it skips. Direct/synchronous callers are not gated.
- A failed probe backs off instead of running a second sync, so a dead
  server is not retried back-to-back.

Confirmed funds can never be stale beyond one cycle (every confirmed
change advances the height or bumps the epoch); only unconfirmed mempool
rows can lag up to the heartbeat, and the spend path re-queries the
backend directly rather than reading the gated model. Average idle cost
drops from three data connections per cycle to about one.

Tests: a bridge mutation-epoch test and a worker change-gate test
(skip-when-unchanged; refresh on height advance, local mutation and
encryption change) against the fake backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 12:00:26 -05:00
co-authored by Claude Opus 4.8
parent 39292b1b1f
commit ef14518089
6 changed files with 241 additions and 14 deletions
+11
View File
@@ -5,6 +5,8 @@
#pragma once
#include <string>
#include <atomic>
#include <cstdint>
namespace dragonx {
namespace wallet {
@@ -69,15 +71,24 @@ public:
LiteBridgeStringResult execute(const std::string& command, const std::string& args);
void shutdown();
// Monotonic counter bumped AFTER every execute() of a command that could change wallet-visible
// data (anything not on the read-only allow-list). The refresh worker samples this to detect
// local mutations made on ANY thread without hooking each controller method. Compare-only (never
// cleared): the worker stores a baseline and refreshes whenever it differs, so a mutation can
// never be "consumed"/lost in a race.
std::uint64_t mutationEpoch() const { return mutationEpoch_.load(std::memory_order_acquire); }
private:
LiteClientBridge(LiteClientBridgeApi api, std::string unavailableReason);
LiteBridgeStringResult unavailableResult() const;
LiteBridgeStringResult takeOwnedString(char* rawValue) const;
static bool commandIsReadOnly(const std::string& command);
LiteClientBridgeApi api_;
std::string unavailableReason_;
bool shutdownCalled_ = false;
std::atomic<std::uint64_t> mutationEpoch_{0};
};
} // namespace wallet