feat: stream scene execution progress
This commit is contained in:
+17
-5
@@ -8,6 +8,9 @@ import com.flagship.abox.manager.domain.Scene
|
|||||||
import com.flagship.abox.manager.domain.SceneExecutionResult
|
import com.flagship.abox.manager.domain.SceneExecutionResult
|
||||||
import com.flagship.abox.manager.domain.SceneExecutor
|
import com.flagship.abox.manager.domain.SceneExecutor
|
||||||
import com.flagship.abox.manager.domain.SceneId
|
import com.flagship.abox.manager.domain.SceneId
|
||||||
|
import com.flagship.abox.manager.domain.SceneExecutionId
|
||||||
|
import com.flagship.abox.manager.domain.SceneExecutionSource
|
||||||
|
import com.flagship.abox.manager.domain.SceneExecutionUpdate
|
||||||
import com.flagship.abox.manager.domain.SceneRepository
|
import com.flagship.abox.manager.domain.SceneRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -15,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
|
||||||
sealed interface SceneExecutionUiState {
|
sealed interface SceneExecutionUiState {
|
||||||
data object Loading : SceneExecutionUiState
|
data object Loading : SceneExecutionUiState
|
||||||
@@ -71,11 +75,19 @@ class SceneExecutionViewModel @Inject constructor(
|
|||||||
_uiState.value = SceneExecutionUiState.Failed("Active profile mismatch")
|
_uiState.value = SceneExecutionUiState.Failed("Active profile mismatch")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
when (val result = sceneExecutor.execute(scene)) {
|
sceneExecutor.execute(
|
||||||
is DomainResult.Success -> _uiState.value = SceneExecutionUiState.Completed(result.value)
|
scene = scene,
|
||||||
is DomainResult.Failure -> _uiState.value = SceneExecutionUiState.Failed(
|
executionId = SceneExecutionId("manual-${scene.id.value}"),
|
||||||
result.error.detail ?: "Execution failed",
|
source = SceneExecutionSource.USER,
|
||||||
)
|
).collect { update ->
|
||||||
|
when (update) {
|
||||||
|
is SceneExecutionUpdate.Completed -> _uiState.value = SceneExecutionUiState.Completed(update.result)
|
||||||
|
is SceneExecutionUpdate.Rejected -> _uiState.value = SceneExecutionUiState.Failed(
|
||||||
|
update.error.detail ?: "Execution failed",
|
||||||
|
)
|
||||||
|
is SceneExecutionUpdate.Started -> _uiState.value = SceneExecutionUiState.Running(update.scene)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Production implementation of [SceneExecutor].
|
* Production implementation of [SceneExecutor].
|
||||||
@@ -24,6 +26,50 @@ class DefaultSceneExecutor(
|
|||||||
) : SceneExecutor {
|
) : SceneExecutor {
|
||||||
private val runningScenes = ConcurrentHashMap<SceneId, Boolean>()
|
private val runningScenes = ConcurrentHashMap<SceneId, Boolean>()
|
||||||
|
|
||||||
|
override fun execute(
|
||||||
|
scene: Scene,
|
||||||
|
executionId: SceneExecutionId,
|
||||||
|
source: SceneExecutionSource,
|
||||||
|
): Flow<SceneExecutionUpdate> = flow {
|
||||||
|
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
|
||||||
|
if (alreadyRunning) {
|
||||||
|
emit(SceneExecutionUpdate.Rejected(DomainError.OperationFailed(detail = "Scene is already running")))
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
try {
|
||||||
|
emit(SceneExecutionUpdate.Started(executionId, scene))
|
||||||
|
val results = mutableListOf<SceneActionExecutionResult>()
|
||||||
|
var aborted = false
|
||||||
|
for (action in scene.actions) {
|
||||||
|
if (aborted) {
|
||||||
|
val skipped = SceneActionExecutionResult(action, SceneActionOutcome.SKIPPED, null)
|
||||||
|
results += skipped
|
||||||
|
emit(SceneExecutionUpdate.StepFinished(skipped))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
emit(SceneExecutionUpdate.StepPending(action))
|
||||||
|
emit(SceneExecutionUpdate.StepRunning(action))
|
||||||
|
val started = System.currentTimeMillis()
|
||||||
|
val result = executeAction(scene.profileId, action).copy(
|
||||||
|
executionDurationMillis = System.currentTimeMillis() - started,
|
||||||
|
)
|
||||||
|
results += result
|
||||||
|
emit(SceneExecutionUpdate.StepFinished(result))
|
||||||
|
if (result.outcome == SceneActionOutcome.FAILURE && scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) {
|
||||||
|
aborted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
SceneExecutionUpdate.Completed(
|
||||||
|
SceneExecutionResult(scene, results, System.currentTimeMillis() - startTime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
runningScenes.remove(scene.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
|
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
|
||||||
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
|
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
|
||||||
if (alreadyRunning) {
|
if (alreadyRunning) {
|
||||||
|
|||||||
Reference in New Issue
Block a user