12 files changed
+1168
-125
No files matched your search
@@ -7,6 +7,7 @@ import com.m57.hermescontrol.data.model.ConnectorCatalogEntry
|
||||
import com.m57.hermescontrol.data.model.ConnectorError
|
||||
import com.m57.hermescontrol.data.model.ConnectorPolicy
|
||||
import com.m57.hermescontrol.data.model.ConnectorTool
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectionRespondParams
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectorOwner
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectorsAccountsParams
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectorsAccountsRemoveParams
|
||||
@@ -61,10 +62,9 @@ interface AccountConnectorRepository {
|
||||
expectedRevision: String,
|
||||
): AccountConnectorResult<ConnectorPolicy>
|
||||
|
||||
suspend fun operationRequest(
|
||||
method: String,
|
||||
params: Map<String, Any>,
|
||||
): Any?
|
||||
suspend fun operationRespond(params: ConnectionRespondParams): Any?
|
||||
|
||||
suspend fun operationWake(params: ConnectorsOperationStatusParams): Any?
|
||||
|
||||
suspend fun operationStatus(
|
||||
opId: String,
|
||||
@@ -74,26 +74,8 @@ interface AccountConnectorRepository {
|
||||
}
|
||||
|
||||
class HermesAccountConnectorRepository(
|
||||
private val rpc: suspend (String, Map<String, Any>) -> Any? = { method, params ->
|
||||
val deferred = HermesWsClient.request(method, params)
|
||||
try {
|
||||
deferred.await()
|
||||
} catch (
|
||||
e: CancellationException,
|
||||
) {
|
||||
deferred.cancel(e)
|
||||
throw e
|
||||
} finally {
|
||||
if (!deferred.isCompleted) deferred.cancel()
|
||||
}
|
||||
},
|
||||
private val caller: TypedRpcCaller = HermesRpcCaller,
|
||||
) : AccountConnectorRepository {
|
||||
private suspend fun request(
|
||||
method: String,
|
||||
params: Map<String, Any> = emptyMap(),
|
||||
) = rpc(method, params)
|
||||
|
||||
private suspend fun <T> guarded(block: suspend () -> T): AccountConnectorResult<T> =
|
||||
try {
|
||||
AccountConnectorResult.Success(block())
|
||||
@@ -111,12 +93,6 @@ class HermesAccountConnectorRepository(
|
||||
AccountConnectorResult.Failure(ConnectorError.NetworkError("Failed to communicate with gateway."))
|
||||
}
|
||||
|
||||
private fun accountParams(params: Map<String, Any> = emptyMap()): Map<String, Any> =
|
||||
buildMap {
|
||||
putAll(params)
|
||||
AuthManager.activeProfileId.value?.let { put("profile", it) }
|
||||
}
|
||||
|
||||
private fun obj(value: Any?): JsonObject = ConnectorParser.toJsonElement(value).jsonObject
|
||||
|
||||
private fun JsonObject.str(k: String) = this[k]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||
@@ -173,10 +149,23 @@ class HermesAccountConnectorRepository(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun operationRequest(
|
||||
method: String,
|
||||
params: Map<String, Any>,
|
||||
): Any? = request(method, accountParams(params + ("owner" to mapOf("type" to "account"))))
|
||||
override suspend fun operationRespond(params: ConnectionRespondParams): Any? =
|
||||
caller.call(
|
||||
RpcMethods.CONNECTION_RESPOND,
|
||||
params.copy(
|
||||
owner = ConnectorOwner.account(),
|
||||
profile = AuthManager.activeProfileId.value,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun operationWake(params: ConnectorsOperationStatusParams): Any? =
|
||||
caller.call(
|
||||
RpcMethods.CONNECTORS_OPERATION_WAKE,
|
||||
params.copy(
|
||||
owner = ConnectorOwner.account(),
|
||||
profile = AuthManager.activeProfileId.value,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun operationStatus(opId: String) =
|
||||
guarded {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.m57.hermescontrol.data.ws.contract
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ConnectionAnswerTarget(
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("status") val status: String,
|
||||
@SerialName("detail") val detail: String? = null,
|
||||
@SerialName("env") val env: Map<String, String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConnectionAnswer(
|
||||
@SerialName("targets") val targets: List<ConnectionAnswerTarget>? = null,
|
||||
@SerialName("settled_by") val settledBy: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConnectionRespondParams(
|
||||
@SerialName("owner") val owner: ConnectorOwner,
|
||||
@SerialName("op_id") val opId: String,
|
||||
@SerialName("result") val result: ConnectionAnswer,
|
||||
@SerialName("profile") val profile: String? = null,
|
||||
)
|
||||
@@ -315,6 +315,22 @@ object RpcMethods {
|
||||
JsonElement.serializer(),
|
||||
)
|
||||
|
||||
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||
val CONNECTION_RESPOND: RpcMethod<ConnectionRespondParams, JsonElement> =
|
||||
RpcMethod(
|
||||
WsMethods.CONNECTION_RESPOND,
|
||||
ConnectionRespondParams.serializer(),
|
||||
JsonElement.serializer(),
|
||||
)
|
||||
|
||||
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||
val CONNECTORS_OPERATION_WAKE: RpcMethod<ConnectorsOperationStatusParams, JsonElement> =
|
||||
RpcMethod(
|
||||
WsMethods.CONNECTORS_OPERATION_WAKE,
|
||||
ConnectorsOperationStatusParams.serializer(),
|
||||
JsonElement.serializer(),
|
||||
)
|
||||
|
||||
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */
|
||||
val all: List<RpcMethod<*, *>> =
|
||||
listOf(
|
||||
@@ -356,5 +372,7 @@ object RpcMethods {
|
||||
CONNECTORS_POLICY_GET,
|
||||
CONNECTORS_POLICY_SET,
|
||||
CONNECTORS_TOOLS,
|
||||
CONNECTION_RESPOND,
|
||||
CONNECTORS_OPERATION_WAKE,
|
||||
)
|
||||
}
|
||||
+52
-36
@@ -3,14 +3,27 @@ package com.m57.hermescontrol.ui.chat
|
||||
import com.m57.hermescontrol.data.model.ConnectionOperationSnapshot
|
||||
import com.m57.hermescontrol.data.model.ConnectorError
|
||||
import com.m57.hermescontrol.data.ws.ConnectorParser
|
||||
import com.m57.hermescontrol.data.ws.ConnectorRepository
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectionAnswer
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectionAnswerTarget
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectionRespondParams
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectorOwner
|
||||
import com.m57.hermescontrol.data.ws.contract.ConnectorsOperationStatusParams
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
sealed interface ConnectionOperationRequest {
|
||||
data class Respond(
|
||||
val params: ConnectionRespondParams,
|
||||
) : ConnectionOperationRequest
|
||||
|
||||
data class Wake(
|
||||
val params: ConnectorsOperationStatusParams,
|
||||
) : ConnectionOperationRequest
|
||||
}
|
||||
|
||||
sealed class ConnectionPendingAction {
|
||||
abstract val opId: String
|
||||
abstract val observedSeq: Long
|
||||
@@ -45,10 +58,7 @@ data class ConnectionOperationUiState(
|
||||
)
|
||||
|
||||
fun interface ConnectionOperationRequester {
|
||||
suspend fun request(
|
||||
method: String,
|
||||
params: Map<String, Any>,
|
||||
): Any?
|
||||
suspend fun request(action: ConnectionOperationRequest): Any?
|
||||
}
|
||||
|
||||
internal class ConnectionResumeCheckpoint(
|
||||
@@ -163,19 +173,20 @@ class ChatConnectionOperationDelegate(
|
||||
observedSeq = current.seq,
|
||||
),
|
||||
) ?: return
|
||||
val answer =
|
||||
buildMap<String, Any> {
|
||||
put("name", target)
|
||||
put("status", if (approved) "approved" else "skipped")
|
||||
if (safeEnv.isNotEmpty()) put("env", safeEnv)
|
||||
}
|
||||
val answerTarget =
|
||||
ConnectionAnswerTarget(
|
||||
name = target,
|
||||
status = if (approved) "approved" else "skipped",
|
||||
env = safeEnv.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
dispatch(
|
||||
method = WsMethods.CONNECTION_RESPOND,
|
||||
params =
|
||||
mapOf(
|
||||
"owner" to ownerParams(),
|
||||
"op_id" to snapshot.opId,
|
||||
"result" to mapOf("targets" to listOf(answer)),
|
||||
action =
|
||||
ConnectionOperationRequest.Respond(
|
||||
ConnectionRespondParams(
|
||||
owner = owner(),
|
||||
opId = snapshot.opId,
|
||||
result = ConnectionAnswer(targets = listOf(answerTarget)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -183,12 +194,13 @@ class ChatConnectionOperationDelegate(
|
||||
suspend fun continueOperation() {
|
||||
val snapshot = begin(ConnectionPendingAction.Continue(currentOp(), currentSeq())) ?: return
|
||||
dispatch(
|
||||
method = WsMethods.CONNECTION_RESPOND,
|
||||
params =
|
||||
mapOf(
|
||||
"owner" to ownerParams(),
|
||||
"op_id" to snapshot.opId,
|
||||
"result" to mapOf("settled_by" to "continue"),
|
||||
action =
|
||||
ConnectionOperationRequest.Respond(
|
||||
ConnectionRespondParams(
|
||||
owner = owner(),
|
||||
opId = snapshot.opId,
|
||||
result = ConnectionAnswer(settledBy = "continue"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -198,36 +210,40 @@ class ChatConnectionOperationDelegate(
|
||||
if (current.opId != expectedOpId) return
|
||||
val snapshot = begin(ConnectionPendingAction.Wake(current.opId, current.seq)) ?: return
|
||||
dispatch(
|
||||
method = WsMethods.CONNECTORS_OPERATION_WAKE,
|
||||
params = mapOf("owner" to ownerParams(), "op_id" to snapshot.opId),
|
||||
action =
|
||||
ConnectionOperationRequest.Wake(
|
||||
ConnectorsOperationStatusParams(
|
||||
owner = owner(),
|
||||
opId = snapshot.opId,
|
||||
),
|
||||
),
|
||||
clearOnSuccess = true,
|
||||
unknownOperationSettles = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun dispatch(
|
||||
method: String,
|
||||
params: Map<String, Any>,
|
||||
action: ConnectionOperationRequest,
|
||||
clearOnSuccess: Boolean = false,
|
||||
unknownOperationSettles: Boolean = false,
|
||||
) {
|
||||
val action = _state.value.pendingAction
|
||||
val pending = _state.value.pendingAction
|
||||
val actionGeneration = generation
|
||||
try {
|
||||
requester.request(method, params)
|
||||
requester.request(action)
|
||||
// Success only acknowledges receipt. Keep the exactly-once lock until
|
||||
// a newer authoritative snapshot advances the operation sequence.
|
||||
if (clearOnSuccess && generation == actionGeneration && _state.value.pendingAction == action) {
|
||||
if (clearOnSuccess && generation == actionGeneration && _state.value.pendingAction == pending) {
|
||||
_state.value = _state.value.copy(pendingAction = null)
|
||||
}
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
if (unknownOperationSettles && error is HermesWsClient.HermesRpcException && error.code == 4004) {
|
||||
if (generation == actionGeneration && _state.value.pendingAction == action) {
|
||||
action?.opId?.let(::markSettled)
|
||||
if (generation == actionGeneration && _state.value.pendingAction == pending) {
|
||||
pending?.opId?.let(::markSettled)
|
||||
}
|
||||
} else if (generation == actionGeneration && _state.value.pendingAction == action) {
|
||||
} else if (generation == actionGeneration && _state.value.pendingAction == pending) {
|
||||
_state.value =
|
||||
_state.value.copy(
|
||||
pendingAction = null,
|
||||
@@ -269,8 +285,8 @@ class ChatConnectionOperationDelegate(
|
||||
snapshot.sessionId == sessionId
|
||||
}
|
||||
|
||||
private fun ownerParams(): Map<String, String> =
|
||||
if (accountOwned) mapOf("type" to "account") else ConnectorRepository.sessionOwner(checkNotNull(sessionId))
|
||||
private fun owner(): ConnectorOwner =
|
||||
if (accountOwned) ConnectorOwner.account() else ConnectorOwner.session(checkNotNull(sessionId))
|
||||
|
||||
private fun currentOp(): String =
|
||||
_state.value.operation
|
||||
|
||||
@@ -461,8 +461,16 @@ class ChatViewModel(
|
||||
constructor(application: Application) : this(application, startCleanup = true)
|
||||
|
||||
private val connectionOperationDelegate =
|
||||
ChatConnectionOperationDelegate { method, params ->
|
||||
HermesWsClient.request(method, params).await()
|
||||
ChatConnectionOperationDelegate { action ->
|
||||
when (action) {
|
||||
is ConnectionOperationRequest.Respond -> {
|
||||
HermesWsClient.call(RpcMethods.CONNECTION_RESPOND, action.params)
|
||||
}
|
||||
|
||||
is ConnectionOperationRequest.Wake -> {
|
||||
HermesWsClient.call(RpcMethods.CONNECTORS_OPERATION_WAKE, action.params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal state ───────────────────────────────────────────────────
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.m57.hermescontrol.data.ws.AccountConnectorRepository
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.ui.chat.ChatConnectionOperationDelegate
|
||||
import com.m57.hermescontrol.ui.chat.ConnectionOperationRequest
|
||||
import com.m57.hermescontrol.ui.chat.ConnectionOperationRequester
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -51,7 +52,13 @@ class AccountConnectorsViewModel(
|
||||
private val scopedJobs = mutableListOf<Job>()
|
||||
private val operation =
|
||||
ChatConnectionOperationDelegate(
|
||||
requester = ConnectionOperationRequester(repository::operationRequest),
|
||||
requester =
|
||||
ConnectionOperationRequester { action ->
|
||||
when (action) {
|
||||
is ConnectionOperationRequest.Respond -> repository.operationRespond(action.params)
|
||||
is ConnectionOperationRequest.Wake -> repository.operationWake(action.params)
|
||||
}
|
||||
},
|
||||
accountOwned = true,
|
||||
)
|
||||
val connectionState = operation.state
|
||||
|
||||
Reference in new issue
Block a user