fix(android): fix session_started event race condition with pendingSessionId
Problem: - Android initiator/joiner could miss session_started events due to race condition - Events arriving between joinSession() and _currentSession.value assignment were ignored - This caused keygen timeout because parties never started the TSS protocol Solution: - Add pendingSessionId field set BEFORE joinSession() call - Modify startSessionEventSubscription() to match events against both activeSession and pendingSessionId - Clear pendingSessionId on session completion, failure, or cancellation This ensures session_started events are correctly processed even if they arrive before _currentSession is fully initialized. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc56b8fadf
commit
eb3f71fa2e
|
|
@ -46,6 +46,10 @@ class TssRepository @Inject constructor(
|
|||
private var messageCollectionJob: Job? = null
|
||||
private var sessionEventJob: Job? = null
|
||||
|
||||
// Pre-registered session ID for event matching (set before joinSession to avoid race condition)
|
||||
// This allows session_started events to be matched even if _currentSession is not yet set
|
||||
private var pendingSessionId: String? = null
|
||||
|
||||
/**
|
||||
* Get the current party ID
|
||||
*/
|
||||
|
|
@ -161,12 +165,18 @@ class TssRepository @Inject constructor(
|
|||
android.util.Log.d("TssRepository", " sessionId: ${event.sessionId}")
|
||||
android.util.Log.d("TssRepository", " selectedParties: ${event.selectedParties}")
|
||||
|
||||
// Check if this event is for our active session
|
||||
// Check if this event is for our active session OR pending session
|
||||
// (pendingSessionId handles race condition where session_started arrives before _currentSession is set)
|
||||
val activeSession = _currentSession.value
|
||||
val pendingId = pendingSessionId
|
||||
android.util.Log.d("TssRepository", " activeSession: ${activeSession?.sessionId ?: "null"}")
|
||||
android.util.Log.d("TssRepository", " pendingSessionId: ${pendingId ?: "null"}")
|
||||
|
||||
if (activeSession != null && event.sessionId == activeSession.sessionId) {
|
||||
android.util.Log.d("TssRepository", " → Event matches active session!")
|
||||
val matchesActiveSession = activeSession != null && event.sessionId == activeSession.sessionId
|
||||
val matchesPendingSession = pendingId != null && event.sessionId == pendingId
|
||||
|
||||
if (matchesActiveSession || matchesPendingSession) {
|
||||
android.util.Log.d("TssRepository", " → Event matches session! (active=$matchesActiveSession, pending=$matchesPendingSession)")
|
||||
when (event.eventType) {
|
||||
"session_started" -> {
|
||||
android.util.Log.d("TssRepository", " → Processing session_started event")
|
||||
|
|
@ -186,10 +196,10 @@ class TssRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d("TssRepository", " → Event does NOT match active session (ignored)")
|
||||
if (activeSession == null) {
|
||||
android.util.Log.d("TssRepository", " Reason: activeSession is null")
|
||||
} else {
|
||||
android.util.Log.d("TssRepository", " → Event does NOT match any session (ignored)")
|
||||
if (activeSession == null && pendingId == null) {
|
||||
android.util.Log.d("TssRepository", " Reason: both activeSession and pendingSessionId are null")
|
||||
} else if (activeSession != null) {
|
||||
android.util.Log.d("TssRepository", " Reason: sessionId mismatch (event: ${event.sessionId}, active: ${activeSession.sessionId})")
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +300,12 @@ class TssRepository @Inject constructor(
|
|||
|
||||
android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}")
|
||||
|
||||
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||
// This ensures session_started events can be matched even if they arrive
|
||||
// before _currentSession is set
|
||||
pendingSessionId = sessionId
|
||||
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching")
|
||||
|
||||
// Auto-join session via gRPC (matching Electron behavior)
|
||||
var partyIndex = 0
|
||||
var sessionAlreadyInProgress = false
|
||||
|
|
@ -585,6 +601,12 @@ class TssRepository @Inject constructor(
|
|||
try {
|
||||
android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}")
|
||||
|
||||
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||
// This ensures session_started events can be matched even if they arrive
|
||||
// before _currentSession is set
|
||||
pendingSessionId = sessionId
|
||||
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (joiner)")
|
||||
|
||||
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||
if (joinResult.isFailure) {
|
||||
|
|
@ -738,6 +760,7 @@ class TssRepository @Inject constructor(
|
|||
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
pendingSessionId = null // Clear pending session ID on completion
|
||||
|
||||
android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address, partyIndex=$actualPartyIndex")
|
||||
|
||||
|
|
@ -746,6 +769,7 @@ class TssRepository @Inject constructor(
|
|||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
|
||||
_sessionStatus.value = SessionStatus.FAILED
|
||||
pendingSessionId = null // Clear pending session ID on failure
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -783,6 +807,12 @@ class TssRepository @Inject constructor(
|
|||
|
||||
// Note: Password is verified during actual sign execution, same as Electron
|
||||
|
||||
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||
// This ensures session_started events can be matched even if they arrive
|
||||
// before _currentSession is set
|
||||
pendingSessionId = sessionId
|
||||
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (sign joiner)")
|
||||
|
||||
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||
if (joinResult.isFailure) {
|
||||
|
|
@ -906,6 +936,7 @@ class TssRepository @Inject constructor(
|
|||
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
|
||||
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
pendingSessionId = null // Clear pending session ID on completion
|
||||
messageCollectionJob?.cancel()
|
||||
|
||||
android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")
|
||||
|
|
@ -915,6 +946,7 @@ class TssRepository @Inject constructor(
|
|||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
|
||||
_sessionStatus.value = SessionStatus.FAILED
|
||||
pendingSessionId = null // Clear pending session ID on failure
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1437,6 +1469,7 @@ class TssRepository @Inject constructor(
|
|||
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
pendingSessionId = null // Clear pending session ID on completion
|
||||
sessionEventJob?.cancel()
|
||||
|
||||
Result.success(shareEntity.copy(id = id).toShareRecord())
|
||||
|
|
@ -1444,6 +1477,7 @@ class TssRepository @Inject constructor(
|
|||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
|
||||
_sessionStatus.value = SessionStatus.FAILED
|
||||
pendingSessionId = null // Clear pending session ID on failure
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1458,6 +1492,9 @@ class TssRepository @Inject constructor(
|
|||
_currentSession.value = null
|
||||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
// Clear pending session ID (race condition prevention)
|
||||
pendingSessionId = null
|
||||
|
||||
// Clear reconnection recovery params
|
||||
currentMessageRoutingSessionId = null
|
||||
currentMessageRoutingPartyIndex = null
|
||||
|
|
|
|||
Loading…
Reference in New Issue