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.
The issue occurred because:
- Client creates pending session with 5-minute expiry
- Client exits app
- Other user accepts the session at T=4:50 (just before expiry)
- Server deletes pending record and creates room record
- Original client reopens app at T=5:10 (after local expiry)
pollPendingAndValidateRooms()calls/api/rooms/check- Server returns 404 (pending not found - because it was converted to room)
- App incorrectly interprets 404 as "expired" instead of "accepted"
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)
Modified pollPendingAndValidateRooms() in ChatManager.swift to require both local and server confirmation before marking a session as expired:
When server returns 404 (.expired):
- Check if local
expiresAttime has passed - If YES: Mark as expired (confirmed by both local timer and server)
- If NO: Keep polling as pending (likely accepted, just waiting for next poll to resolve)
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
}- Eliminates false expiration: Sessions accepted near expiry time no longer get marked as expired
- No backend changes required: Solution works with existing server API
- Keeps local countdown: UI countdown timer continues to work as expected
- Server remains source of truth: Still respects server status when it says "still pending"
- 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
- Session accepted after local expiry: Keeps polling until accepted status is retrieved
- Session truly expired: Marks as expired when both local and server confirm
- Network issues: Keeps current state on errors, doesn't prematurely expire
- App backgrounded: Polling pauses when offline, resumes when reconnected
- Create pending session with 1-minute expiry
- Exit app after 30 seconds
- Have peer accept at 55 seconds
- Reopen app at 70 seconds (after local expiry)
- Verify session shows as "Accepted" not "Expired"
/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)
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.