Skip to content

refactor(qauld-tui): fetch in a background task, off the render loop - #929

Open
dastansam wants to merge 3 commits into
mainfrom
refactor/qauld-tui-async-refresh
Open

refactor(qauld-tui): fetch in a background task, off the render loop#929
dastansam wants to merge 3 commits into
mainfrom
refactor/qauld-tui-async-refresh

Conversation

@dastansam

@dastansam dastansam commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Stacked on #928 — set to merge into fix/qauld-tui-dtn-fetch; GitHub will retarget this to main automatically once #928 merges.

Polling used to run inline in the render loop's select tick — 8 RPCs awaited sequentially, freezing input/redraws while they ran and up to ~40s/cycle if the daemon was unreachable. Move polling to a background task: refresh_once fans the fetches out concurrently (one round-trip's latency, not eight) and posts a Send-able Snapshot to the loop, which applies it synchronously. No network .await anywhere in the loop. r and post-feed-send just fire a trigger.

Crypto events now dedup through one path (the poll floor lives with the task), so poll/push overlaps and the daemon's inclusive since_ms re-sends are dropped. Three new dedup unit tests.

…t before priming

Two fixes found by driving the real TUI through a pty harness against
live daemons:

- data.rs sent user_id: Vec::new() on every request envelope — despite
  its own comment explaining that libqaul rejects envelopes whose
  user_id fails to decode. libqaul's DTN RPC handler decodes user_id
  first and silently drops the request, so fetch_dtn_state and
  fetch_dtn_config burned their full timeout on every refresh cycle:
  with the default 5s CLI timeout and 3s auto-refresh the event loop
  was stalled ~10s out of every ~10s, keystrokes only being processed
  in brief windows between stalls (tab switching, filtering and the
  detail drawer all appeared dead). round_trip now takes the caller id;
  the DTN fetchers thread the default account id through and fail fast
  with a clear error when no account exists yet, mirroring the existing
  send_feed guard.

- run() entered the alternate screen and then awaited the full priming
  refresh before the first draw, so startup showed a blank screen until
  every fetch settled (10s+ with the bug above; still fetch-bound
  without it). The frame skeleton is now painted once before priming.

Tests: fetch_dtn_state/fetch_dtn_config refuse an empty user id before
any socket I/O (red before the fix). Verified with a pyte pty harness
against two live daemons: 22/22 feature checks pass — startup render,
5-tab cycle + wrap both directions, cursor, detail drawer open/close,
filter apply/clear, feed compose+send, cross-node delivery, live event
updates, r refresh, clean q quit, no panics.
The refresh cycle used to run inline in the render loop's tokio::select
tick arm: eight RPCs awaited one after another, each on a fresh socket.
While it ran, no keystrokes or redraws were processed — and if the
daemon was slow or unreachable, the loop froze for up to (8 fetches x
timeout) per cycle (~40s at the 5s default), making the whole UI feel
dead.

Move polling into a dedicated background task:

- data::refresh_once fans the fetches out concurrently (tokio::join!)
  after resolving the default user first (the DTN fetches need its id),
  so a cycle costs one round-trip's latency instead of the sum of
  eight. It returns a Send-able Snapshot (Result<_, String> fields).
- The task loops on an interval (first tick immediate → non-blocking
  prime) plus a manual-trigger channel, and posts each Snapshot to the
  render loop over an mpsc.
- The render loop's select now has a snapshot arm that calls the pure,
  synchronous apply_snapshot — no .await on network anywhere in the
  loop. 'r' and post-feed-send just fire the trigger.

Crypto-event dedup: the poll floor now lives with the task (sole owner
of the poll path), so the daemon's inclusive since_ms filter re-sends
the boundary event each cycle. append_crypto_events now routes every
event through the same dedup as the push path (merge_crypto_event), so
poll/push overlaps and boundary re-sends are dropped without the two
paths sharing a floor.

Tests: three append_crypto_events dedup cases (boundary re-send,
push/poll overlap, distinct-same-timestamp). Verified live against two
daemons — 22/22 feature checks still pass — plus a daemon-death probe:
with every fetch hitting the 5s timeout, tab switches still register in
~50ms (identical to daemon-alive), where the old design would have
frozen for up to 40s.
@dastansam dastansam self-assigned this Jul 31, 2026
@sonarqubecloud

Copy link
Copy Markdown

Base automatically changed from fix/qauld-tui-dtn-fetch to main August 5, 2026 12:24
@MathJud

MathJud commented Aug 5, 2026

Copy link
Copy Markdown
Member

I merged #935, which is this PR without the last commit, which contains the demo script.
The demo script has issues which should be resolved.
I suggest to move the demo script with the explanation to utilities/demo/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants