switchSyncServer method

Future<SwitchServerResult> switchSyncServer(
  1. SyncServerChoice choice
)

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);
  }
}