tryNow method

bool tryNow()

Retry NOW from a live SyncStatus_Stalled (#399 item 4) — the sheet's "Try now" affordance. The sync loop's retry backoff caps at 600s and never resets on connectivity return (no connectivity listener exists — the residual), so after a long outage a user who just fixed their Wi-Fi could stare at "retries automatically" for up to 10 minutes. A stop+start resets the ladder by design (durable scan progress is kept — stopping loses nothing), giving them an immediate attempt instead.

One serialized chain LINK for both commands (never two _commands): the pair must be atomic against a concurrently-arriving lifecycle stop/start, and the intermediate stop must not write its suspended settle state over a surface that never stopped wanting to run. Stop failure is swallowed (best-effort — the START is the point; a wedged stop is bounded by walletFfiWedgeTimeout like the dispose link); a START failure lands on the honest failed (the retry notice renders). Returns whether the restart pair was actually ENQUEUED — a gate rejection returns false so the caller doesn't open a post-tap live window over a restart that never happened (audit H).

Implementation

bool tryNow() {
  final session = ref.read(walletSessionProvider);
  if (session == null) return false;
  // Policy gate (the retry() belt): never a way past a deliberate host off.
  if (!ref.read(walletSyncPolicyProvider)) return false;
  // Only from a LIVE-or-UNCERTAIN loop. The belt this gate exists for is a
  // stale tap racing a background transition (`suspended`): stop+start there
  // would restart sync in the BACKGROUND, past the battery policy, with no
  // later lifecycle stop coming.
  //
  // #409 R2 ADDS `failed`, and this is the whole of that ticket's real harm.
  // A start that merely TIMED OUT lands on `failed` while the Rust loop is
  // most likely running and reporting `Stalled` — and the kick fires exactly
  // on `Stalled`. Gating on `running` alone therefore let a timeout foreclose
  // the one automatic recovery path in the package, permanently, on the very
  // wallet that needed it; desktop never fires `paused`, so nothing cleared
  // it there at all. Admitting `failed` is not a new power: `retry()` already
  // performs this same stop+start from this same state, and a start that
  // succeeds writes `running` and clears `_startFailed` on the way.
  if (state != WalletSyncDrive.running && state != WalletSyncDrive.failed) {
    return false;
  }
  // No optimistic state write (unlike retry()): the gate above just proved
  // `running`, and the drive stays internal here — the tap's visible
  // feedback is the DISPLAY status following the restarted loop (the
  // sheet pairs this call with `followRawNow`).
  final gen = _generation;
  _inFlight = _inFlight
      .then((_) async {
        // The gate above closes only once the pause's stop SETTLES (the
        // pause path writes no optimistic state), so a tap already in the
        // event queue when `paused` lands can pass it (review M1).
        // `_wasPaused` is the pause INTENT flag, set synchronously at the
        // event — re-checked here, inside the serialized link, so a
        // backgrounded restart is foreclosed at execution time too.
        if (_disposed || gen != _generation || _wasPaused) return;
        try {
          await session.stopSync().timeout(
            walletFfiWedgeTimeout,
            onTimeout: () {},
          );
        } catch (_) {
          // Best-effort: an un-stopped loop just keeps its old ladder; the
          // start below is still idempotent-safe.
        }
        if (_disposed || gen != _generation || _wasPaused) return;
        try {
          // Bounded like the stop above (#407 R8): the kick appends to this
          // chain automatically, so a wedged start must not park every later
          // command behind it. A timeout lands on `failed` via the catch —
          // the CONSERVATIVE reading, and #409 R2 keeps it that way; what the
          // bound must not do is DISCARD the eventual answer, which is why the
          // call is also watched to completion by [_watchLateStartVerdict].
          await _watchLateStartVerdict(
            session.startSync(),
            gen,
          ).timeout(walletFfiWedgeTimeout);
          _startFailed = false;
          if (!_disposed && gen == _generation) {
            state = WalletSyncDrive.running;
          }
        } catch (_) {
          _startFailed = true;
          if (_disposed || gen != _generation) return;
          state = WalletSyncDrive.failed;
        }
      })
      .catchError((Object _) {});
  return true;
}