tryNow method
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;
}