OXYGENHUB
# 8a657957d5a0 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / flow / FlowViewModels.kt
4.6 KiB · last changed Flow on the phone: a repository's runs, a run's jobs, live logs, rerun and cancel (refs #5) 10 days ago source blame history raw
1
package io.oxygenhub.android.ui.flow
2
3
import androidx.lifecycle.ViewModel
4
import androidx.lifecycle.viewModelScope
5
import io.oxygenhub.android.api.FlowRun
6
import io.oxygenhub.android.api.HubApi
7
import io.oxygenhub.android.api.LogLine
8
import io.oxygenhub.android.api.userMessage
9
import io.oxygenhub.android.ui.common.Load
10
import io.oxygenhub.android.ui.common.load
11
import kotlinx.coroutines.CancellationException
12
import kotlinx.coroutines.Job
13
import kotlinx.coroutines.delay
14
import kotlinx.coroutines.flow.MutableStateFlow
15
import kotlinx.coroutines.flow.StateFlow
16
import kotlinx.coroutines.flow.asStateFlow
17
import kotlinx.coroutines.isActive
18
import kotlinx.coroutines.launch
19
20
/** A repository's runs, newest first (#5). */
21
class RunsViewModel(private val api: HubApi, val owner: String, val name: String) : ViewModel() {
22
    private val _state = MutableStateFlow<Load<List<FlowRun>>>(Load.Loading)
23
    val state: StateFlow<Load<List<FlowRun>>> = _state.asStateFlow()
24
    private var job: Job? = null
25
26
    init {
27
        refresh()
28
    }
29
30
    fun refresh() {
31
        job = load(_state, job) { api.runs(owner, name) }
32
    }
33
}
34
35
/**
36
 * One run and its jobs, fetched again every few seconds while it is queued
37
 * or running, so its jobs' states follow; rerun and cancel it.
38
 */
39
class RunViewModel(
40
    private val api: HubApi,
41
    val owner: String,
42
    val name: String,
43
    val number: Long,
44
    /** How often a live run is fetched again; 0 not at all (tests). */
45
    private val pollMs: Long = 3000,
46
) : ViewModel() {
47
    private val _state = MutableStateFlow<Load<FlowRun>>(Load.Loading)
48
    val state: StateFlow<Load<FlowRun>> = _state.asStateFlow()
49
50
    private val _error = MutableStateFlow<String?>(null)
51
    val error: StateFlow<String?> = _error.asStateFlow()
52
53
    private var job: Job? = null
54
55
    init {
56
        refresh()
57
        if (pollMs > 0) {
58
            viewModelScope.launch {
59
                while (isActive) {
60
                    delay(pollMs)
61
                    if ((_state.value as? Load.Ready)?.value?.live == true) poll()
62
                }
63
            }
64
        }
65
    }
66
67
    fun refresh() {
68
        job = load(_state, job) { api.run(owner, name, number) }
69
    }
70
71
    /** Fetch it again without a spinner. */
72
    suspend fun poll() {
73
        try {
74
            _state.value = Load.Ready(api.run(owner, name, number))
75
        } catch (e: CancellationException) {
76
            throw e
77
        } catch (_: Exception) {
78
            // The next tick tries again; the last state stays shown.
79
        }
80
    }
81
82
    fun rerun() = act { api.rerunRun(owner, name, number) }
83
84
    fun rerunJob(job: String) = act { api.rerunJob(owner, name, number, job) }
85
86
    fun cancel() = act { api.cancelRun(owner, name, number) }
87
88
    private fun act(step: suspend () -> Unit) {
89
        _error.value = null
90
        viewModelScope.launch {
91
            try {
92
                step()
93
                poll()
94
            } catch (e: CancellationException) {
95
                throw e
96
            } catch (e: Exception) {
97
                _error.value = e.userMessage()
98
            }
99
        }
100
    }
101
}
102
103
/**
104
 * A job's log, followed while the job runs: new lines are asked for after
105
 * the last one held, every couple of seconds, until the job has finished.
106
 */
107
class JobLogViewModel(
108
    private val api: HubApi,
109
    val owner: String,
110
    val name: String,
111
    val number: Long,
112
    val job: String,
113
    private val pollMs: Long = 2000,
114
) : ViewModel() {
115
    data class Log(val lines: List<LogLine>, val status: String, val live: Boolean)
116
117
    private val _state = MutableStateFlow<Load<Log>>(Load.Loading)
118
    val state: StateFlow<Load<Log>> = _state.asStateFlow()
119
120
    init {
121
        viewModelScope.launch {
122
            next()
123
            while (pollMs > 0 && isActive && (_state.value as? Load.Ready)?.value?.live != false) {
124
                delay(pollMs)
125
                next()
126
            }
127
        }
128
    }
129
130
    /** The lines after the last one held, and whether the job still runs. */
131
    suspend fun next() {
132
        val held = (_state.value as? Load.Ready)?.value
133
        try {
134
            val after = held?.lines?.lastOrNull()?.seq ?: 0
135
            val more = api.jobLog(owner, name, number, job, after)
136
            val j = api.run(owner, name, number).jobs.firstOrNull { it.name == job }
137
            _state.value = Load.Ready(Log((held?.lines.orEmpty()) + more, j?.status ?: held?.status.orEmpty(), j?.live ?: false))
138
        } catch (e: CancellationException) {
139
            throw e
140
        } catch (e: Exception) {
141
            if (held == null) _state.value = Load.failed(e)
142
        }
143
    }
144
145
    fun retry() {
146
        _state.value = Load.Loading
147
        viewModelScope.launch { next() }
148
    }
149
}