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:
hailin 2026-01-01 09:09:52 -08:00
parent cc56b8fadf
commit eb3f71fa2e
1 changed files with 44 additions and 7 deletions

View File

@ -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