run method

Implementation

Future<MissionRunResult> run(
  MissionMessage mission, {
  MissionCancelToken? cancel,
}) async {
  final token = cancel ?? MissionCancelToken();

  // Cancelled before it ever ran (a cancel that overtook its mission, or a
  // run superseded while queued): exactly ONE frame, `cancelled
  // step_index:0` — no `started`, and the exploration log of whatever runs
  // next is left alone (Android MissionRunner prologue).
  if (token.isCancelled) {
    _log('MISSION id=${mission.missionId} cancelled before it ran');
    _emitStatus(MissionStatusMessage(
      missionId: mission.missionId,
      status: MissionStatus.cancelled,
      stepIndex: 0,
      timestamp: _now(),
      error: token.reason ?? 'cancelled',
      stepsCompleted: 0,
      stepsTotal: mission.declaredStepCount,
      stepsSkipped: _range(0, mission.declaredStepCount),
    ));
    return MissionRunResult(
      missionId: mission.missionId,
      terminalStatus: MissionStatus.cancelled,
      stepsCompleted: 0,
      error: token.reason ?? 'cancelled',
    );
  }

  // A frame whose steps could not be parsed: answer it honestly instead of
  // letting the backend wait out its 25s mission timeout.
  if (!mission.isValid) {
    final idx = mission.invalidStepIndex ?? 0;
    _log('MISSION id=${mission.missionId} invalid (step $idx malformed)');
    _emitStatus(MissionStatusMessage(
      missionId: mission.missionId,
      status: MissionStatus.failed,
      stepIndex: idx,
      timestamp: _now(),
      error: 'invalid_mission',
      stepsCompleted: 0,
      stepsTotal: mission.declaredStepCount,
      // Nothing ran: a malformed frame is refused whole.
      stepsSkipped: _range(0, mission.declaredStepCount),
    ));
    return MissionRunResult(
      missionId: mission.missionId,
      terminalStatus: MissionStatus.failed,
      stepsCompleted: 0,
      error: 'invalid_mission',
    );
  }

  final start = _now();
  var currentStep = 0;
  String? firstScreen;
  _succeededSteps = 0;
  try {
    return await _runSteps(mission, token, start, (i) => currentStep = i,
        (s) => firstScreen ??= s);
  } catch (e, st) {
    // Any unexpected throw must STILL surface a terminal status, or the
    // backend — and the border glow — hang on `started` forever.
    debugPrint('[MissionRunner] mission ${mission.missionId} threw at step '
        '$currentStep: $e\n$st');
    return _finishMission(mission, currentStep, MissionStatus.failed,
        stepIndex: currentStep,
        screenAtStart: firstScreen,
        missionStart: start,
        error: 'exception');
  }
}