switchSyncServer method
Switch the ACTIVE wallet onto choice (the picker, P3-13) — the
rescanActiveWallet shape: the provisioner swaps the session in place
and a FRESH session identity goes into the gate, so the live-sync graph
rebuilds and sync restarts on the new server. SAME identityEpoch (the
same wallet's life, over the same DB) and kind carried, as for a rescan.
Three fault arms, honestly distinct (see SwitchServerResult): a pre-swap REFUSAL leaves everything as it was (the SDK handed the handle back — no re-open, no state write); a post-stop fault is recovered by re-opening exactly like a rescan fault; a failed recovery routes to the failed surface. Single-flighted with every other mutation (the operation generation): a delete while the switch is in flight is refused, not raced.
BOUNDED by switchServerTimeout: a provisioner that never answers ends the switch's generation there — SwitchServerFailedRecovered, the session in the gate unchanged, a delete accepted again — and its late answer, success or fault, lands nowhere.
Implementation
Future<SwitchServerResult> switchSyncServer(SyncServerChoice choice) async {
if (state is! OnboardingActive || _mutationInFlight) {
return const SwitchServerNotActive();
}
final provisioner = _provisioner;
if (provisioner == null) {
_set(const OnboardingUnavailable());
return const SwitchServerFailedClosed();
}
final generation = _beginMutation()!;
try {
try {
final session = await provisioner
.switchSyncServer(choice)
.timeout(
switchServerTimeout,
onTimeout: () => throw const _SwitchTimedOut(),
);
if (_stale(generation)) return const SwitchServerNotActive();
_set(
OnboardingActive(
session,
identityEpoch: _currentIdentityEpoch(),
isWatchOnly: _currentIsWatchOnly(),
),
);
return const SwitchServerSuccess();
} on _SwitchTimedOut {
// The switch never answered. Its generation ends in the finally below,
// so its late answer is stale; nothing is re-opened over a handle the
// Rust side may still hold, and the session in the gate stays as it
// was. The picker reads the server now in use, as for a recovery.
return const SwitchServerFailedRecovered();
} catch (switchError) {
if (_stale(generation)) return const SwitchServerNotActive();
if (switchError is WalletApiError && _isPreSwapRefusal(switchError)) {
// The handle is still open and untouched: the session in the gate
// is the live one. Report, change nothing.
return SwitchServerRefused(switchError);
}
// Past the stop-join the handle is closed (the rescan contract):
// recover by re-opening — the choice is honoured if its write landed.
try {
final reopened = await provisioner.open();
if (_stale(generation)) return const SwitchServerNotActive();
_set(
OnboardingActive(
reopened,
identityEpoch: _currentIdentityEpoch(),
isWatchOnly: _currentIsWatchOnly(),
),
);
return const SwitchServerFailedRecovered();
} catch (openError) {
if (_stale(generation)) return const SwitchServerNotActive();
_set(OnboardingFailed(classifyOnboardingFailure(openError)));
return const SwitchServerFailedClosed();
}
}
} finally {
_endMutation(generation);
}
}