fix: Clean up stale participant sessions from map
- Add cleanupStaleParticipants() to remove participants not seen in last hour - Use persistent participant ID per browser/room to prevent ghost duplicates - Fixes issue where old versions of yourself appeared on map reload 🤖 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
760e27564c
commit
eeab19ceac
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
id: task-12
|
||||
title: Fix stale participant sessions showing on map
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2025-12-19 18:00'
|
||||
updated_date: '2025-12-19 18:20'
|
||||
labels:
|
||||
- bug
|
||||
- ux
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
When opening rMaps, old participant sessions from previous visits are still showing on the map as "ghost" locations. This happens because:
|
||||
1. Each session generates a new participantId via nanoid()
|
||||
2. Old participants are persisted in localStorage
|
||||
3. No cleanup of stale sessions occurs on reload
|
||||
|
||||
Fix: Clean up stale participants on room load and use a persistent participant ID per browser.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Old participant sessions are cleaned up on room load
|
||||
- [x] #2 Same user keeps consistent participant ID across sessions
|
||||
- [x] #3 No ghost duplicates appear on the map
|
||||
- [x] #4 Build compiles successfully
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
## Implementation
|
||||
|
||||
1. **Added stale participant cleanup** (`src/lib/sync.ts:133-160`)
|
||||
- `cleanupStaleParticipants()` removes participants not seen in last hour
|
||||
- Runs automatically when loading room state from localStorage
|
||||
- Logs how many stale participants were removed
|
||||
|
||||
2. **Persistent participant ID** (`src/hooks/useRoom.ts:30-54`)
|
||||
- `getOrCreateParticipantId()` stores a consistent ID per room in localStorage
|
||||
- Same user returning to a room gets the same participant ID
|
||||
- Prevents creating duplicate ghost entries on each page reload
|
||||
- Falls back to temporary ID if localStorage unavailable
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
|
@ -27,6 +27,32 @@ const COLORS = [
|
|||
'#06b6d4', // cyan
|
||||
];
|
||||
|
||||
/**
|
||||
* Get or create a persistent participant ID for this browser/room combination.
|
||||
* This prevents creating duplicate "ghost" participants on page reload.
|
||||
*/
|
||||
function getOrCreateParticipantId(slug: string): string {
|
||||
const storageKey = `rmaps_participant_id_${slug}`;
|
||||
|
||||
try {
|
||||
let participantId = localStorage.getItem(storageKey);
|
||||
|
||||
if (!participantId) {
|
||||
participantId = nanoid();
|
||||
localStorage.setItem(storageKey, participantId);
|
||||
console.log('Created new participant ID:', participantId);
|
||||
} else {
|
||||
console.log('Using existing participant ID:', participantId);
|
||||
}
|
||||
|
||||
return participantId;
|
||||
} catch {
|
||||
// localStorage not available (SSR or private browsing)
|
||||
console.warn('localStorage not available, generating temporary participant ID');
|
||||
return nanoid();
|
||||
}
|
||||
}
|
||||
|
||||
interface UseRoomOptions {
|
||||
slug: string;
|
||||
userName: string;
|
||||
|
|
@ -59,7 +85,8 @@ export function useRoom({ slug, userName, userEmoji }: UseRoomOptions): UseRoomR
|
|||
const [roomName, setRoomName] = useState(slug);
|
||||
|
||||
const syncRef = useRef<RoomSync | null>(null);
|
||||
const participantIdRef = useRef<string>(nanoid());
|
||||
// Initialize with empty string - will be set properly on client side
|
||||
const participantIdRef = useRef<string>('');
|
||||
|
||||
// Handle state updates from sync
|
||||
const handleStateChange = useCallback((state: RoomState) => {
|
||||
|
|
@ -84,7 +111,9 @@ export function useRoom({ slug, userName, userEmoji }: UseRoomOptions): UseRoomR
|
|||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const participantId = participantIdRef.current;
|
||||
// Use persistent participant ID to avoid creating duplicate "ghost" participants
|
||||
const participantId = getOrCreateParticipantId(slug);
|
||||
participantIdRef.current = participantId;
|
||||
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||
|
||||
// Create sync instance
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ export class RoomSync {
|
|||
try {
|
||||
const stored = localStorage.getItem(`rmaps_room_${this.slug}`);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
const state = JSON.parse(stored) as RoomState;
|
||||
// Clean up stale participants (not seen in last hour)
|
||||
return this.cleanupStaleParticipants(state);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load room state:', e);
|
||||
|
|
@ -128,6 +130,35 @@ export class RoomSync {
|
|||
return null;
|
||||
}
|
||||
|
||||
private cleanupStaleParticipants(state: RoomState): RoomState {
|
||||
const STALE_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
|
||||
const now = Date.now();
|
||||
const cleanedParticipants: Record<string, ParticipantState> = {};
|
||||
let removedCount = 0;
|
||||
|
||||
for (const [id, participant] of Object.entries(state.participants)) {
|
||||
const lastSeen = new Date(participant.lastSeen).getTime();
|
||||
const isStale = now - lastSeen > STALE_THRESHOLD_MS;
|
||||
const isCurrentUser = id === this.participantId;
|
||||
|
||||
// Keep current user (they'll be updated) and non-stale participants
|
||||
if (isCurrentUser || !isStale) {
|
||||
cleanedParticipants[id] = participant;
|
||||
} else {
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(`Cleaned up ${removedCount} stale participant(s) from room state`);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
participants: cleanedParticipants,
|
||||
};
|
||||
}
|
||||
|
||||
private saveState(): void {
|
||||
try {
|
||||
localStorage.setItem(`rmaps_room_${this.slug}`, JSON.stringify(this.state));
|
||||
|
|
|
|||
Loading…
Reference in New Issue