Skip to content

Latest commit

 

History

History
93 lines (78 loc) · 4.29 KB

File metadata and controls

93 lines (78 loc) · 4.29 KB

Session Expiration Race Condition Fix

Problem

When a user creates a pending session and exits the app, if the other user accepts the room just before expiration, the first user would see the session marked as "expired" when reopening the app, even though it was actually accepted.

Root Cause

The issue occurred because:

  1. Client creates pending session with 5-minute expiry
  2. Client exits app
  3. Other user accepts the session at T=4:50 (just before expiry)
  4. Server deletes pending record and creates room record
  5. Original client reopens app at T=5:10 (after local expiry)
  6. pollPendingAndValidateRooms() calls /api/rooms/check
  7. Server returns 404 (pending not found - because it was converted to room)
  8. App incorrectly interprets 404 as "expired" instead of "accepted"

Why 404 is Ambiguous

The server returns 404 when calling /api/rooms/check in two cases:

  • Pending truly expired and was deleted by server
  • Pending was accepted and converted to a room (pending record deleted)

Solution

Modified pollPendingAndValidateRooms() in ChatManager.swift to require both local and server confirmation before marking a session as expired:

New Logic

When server returns 404 (.expired):

  1. Check if local expiresAt time has passed
  2. If YES: Mark as expired (confirmed by both local timer and server)
  3. If NO: Keep polling as pending (likely accepted, just waiting for next poll to resolve)

Code Changes

File: /Users/benceszilagyi/dev/trackit/Inviso/Inviso/Chat/ChatManager.swift

Function: pollPendingAndValidateRooms()

case .expired:
    // Server returned 404 (pending not found)
    // This could mean: (a) truly expired, OR (b) was accepted and pending deleted
    // 
    // To avoid false expiration marking due to race conditions:
    // - If we have an expiresAt date AND we're past it locally: mark expired
    // - If we don't have expiresAt OR not yet expired locally: keep checking
    if let expiresAt = s.expiresAt, Date() > expiresAt {
        // Local timer confirms expiration - safe to mark as expired
        withAnimation(.spring(response: 0.5, dampingFraction: 0.75)) {
            sessions[i].status = .expired
        }
        persistSessions()
    } else {
        // Server says 404 but local timer hasn't expired yet
        // This is likely because the pending was accepted (race condition)
        // Keep it pending and continue polling
    }

Benefits

  1. Eliminates false expiration: Sessions accepted near expiry time no longer get marked as expired
  2. No backend changes required: Solution works with existing server API
  3. Keeps local countdown: UI countdown timer continues to work as expected
  4. Server remains source of truth: Still respects server status when it says "still pending"

User Experience

  • Before: Accepted sessions near expiration showed as "Expired" ❌
  • After: Accepted sessions correctly show as "Accepted" ✅
  • Local countdown shows "Expired" when time hits 0:00, but status only changes to expired when confirmed by server AND local time

Edge Cases Handled

  1. Session accepted after local expiry: Keeps polling until accepted status is retrieved
  2. Session truly expired: Marks as expired when both local and server confirm
  3. Network issues: Keeps current state on errors, doesn't prematurely expire
  4. App backgrounded: Polling pauses when offline, resumes when reconnected

Testing Recommendations

  1. Create pending session with 1-minute expiry
  2. Exit app after 30 seconds
  3. Have peer accept at 55 seconds
  4. Reopen app at 70 seconds (after local expiry)
  5. Verify session shows as "Accepted" not "Expired"

Related Files

  • /Users/benceszilagyi/dev/trackit/Inviso/Inviso/Chat/ChatManager.swift - Core fix
  • /Users/benceszilagyi/dev/trackit/Inviso/Inviso/Views/Sessions/CountdownTimerView.swift - Display only
  • /Users/benceszilagyi/dev/trackit/Inviso/Inviso/Views/Sessions/SessionsView.swift - UI (no changes)

Backend Context

The signaling server (/Users/benceszilagyi/dev/trackit/chat-server/index.js):

  • Creates pending records via POST /api/rooms
  • Converts pending to room via POST /api/rooms/accept
  • Checks pending status via POST /api/rooms/check
  • Returns 404 when pending not found (ambiguous: could be expired OR accepted)

No backend changes were made per requirements.