5.9 KiB
5.9 KiB
PrivyDrop AI Playbook — Reconnect & State Consistency (Deep Dive)
← Back to flow index: docs/ai-playbook/flows.md
(This page is the English edition of content split out from docs/ai-playbook/flows.zh-CN.md, preserving the original section numbering and structure.)
10) Reconnect & State Consistency (Deep Dive)
WebRTC Base-Layer Reconnect Mechanics
Dual disconnect detection:
// webrtc_base.ts
private isSocketDisconnected = false; // Socket.IO connection state
private isPeerDisconnected = false; // P2P connection state
private gracefullyDisconnectedPeers = new Set(); // peers closed gracefully
Reconnect trigger: only start reconnection when both Socket.IO and P2P are disconnected:
// Avoid duplicate reconnects: socket disconnect != P2P disconnect
if (
this.isSocketDisconnected &&
this.isPeerDisconnected &&
!this.reconnectionInProgress
) {
this.attemptReconnection();
}
ICE Candidate Queue Management
Candidate caching strategy:
- Before ready: cache candidates in the
iceCandidatesQueueMap, grouped by peerId - After ready: flush cached candidates and add them to RTCPeerConnection in order
- Invalid handling: re-queue invalid candidates and retry after validating connection state
Implementation detail:
private iceCandidatesQueue = new Map<string, RTCIceCandidate[]>();
// Cache candidates until the connection is ready
if (dataChannel?.readyState !== 'open') {
this.queueIceCandidate(candidate, peerId);
} else {
this.addIceCandidate(candidate, peerId);
}
DataChannel Send-Retry Mechanism
5-attempt retry policy:
async sendToPeer(data: string | ArrayBuffer, peerId: string): Promise<boolean> {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
dataChannel.send(data);
return true;
} catch (error) {
if (this.gracefullyDisconnectedPeers.has(peerId)) {
return false; // skip peers that were closed gracefully
}
if (attempt === 5) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 100)); // 100ms→1000ms
}
}
}
Backoff: 100ms → 200ms → 300ms → 400ms → 500ms, up to 5 attempts
Room-Layer Reconnect Support
Idempotency:
- Long IDs: roomId length ≥ 8 supports room reuse across reconnects
- Short IDs: 4-digit numeric IDs must be re-generated after disconnect to avoid collisions
Cached-ID reconnect optimization:
// useRoomManager.ts
if (roomId.length >= 8) {
// long IDs auto-send initiator-online
this.sendInitiatorOnline();
}
State sync sequence:
- Initiator reconnects: sends
initiator-onlineto signal readiness - Recipient replies:
recipient-readyconfirms readiness - WebRTC negotiation: re-run offer/answer/ICE exchange
- Transfer continues: resume file transfer on the new DataChannel
State Consistency Safeguards
Store as the single source of truth:
// fileTransferStore.ts
export const useFileTransferStore = create<TransferState>((set, get) => ({
sendProgress: new Map(),
receiveProgress: new Map(),
// cleanup APIs to avoid double counting
clearSendProgress: (fileId: string) =>
set((state) => {
const newProgress = new Map(state.sendProgress);
newProgress.delete(fileId);
return { sendProgress: newProgress };
}),
}));
Connection state machine:
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
// react to state transitions
connectionStateChangeHandler(status: ConnectionStatus) {
switch (status) {
case 'connected':
this.gracefullyDisconnectedPeers.clear(peerId);
this.resetReconnectionState();
break;
case 'disconnected':
case 'failed':
this.cleanupExistingConnection(peerId);
break;
}
}
Mobile Optimizations
Wake lock management:
// WakeLockManager
async requestWakeLock(): Promise<void> {
try {
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
this.wakeLock = null;
});
} catch (error) {
console.warn('Wake lock request failed:', error);
}
}
Adapting to network changes:
- Detection: listen to
connectionstatechangeto infer network quality changes - Auto-reconnect:
connectionState: 'disconnected' | 'failed' | 'closed'all route into the same reconnect path (attemptReconnection) - Restore state: after reconnect, restore room status and transfer progress
Mobile background/foreground addendum:
- Auto re-join on socket reconnect: on
socket.on('connect'), if aroomIdexists and (lastJoinedSocketId !== socket.idor!isInRoom), forcejoinRoom(roomId, isInitiator, isInitiator). The initiator auto-broadcastsinitiator-online; the recipient repliesrecipient-ready. - Identity tracking: after a successful
joinRoom, recordlastJoinedSocketId = socket.idto detect “socketId changed after background resume”. - Lowered threshold:
attemptReconnectioncan start as long asroomIdexists and any of the following hold: P2P disconnected / socket disconnected / socketId changed. It no longer requires “socket and P2P disconnected at the same time”.
Reconnect Debugging Notes
Key log points:
- Dual disconnect detection: record timestamps for Socket.IO vs P2P disconnects
- Candidate queue: count cached ICE candidates and flush durations
- Send retries: record retry attempts, delays, and the final result
- State restoration: trace
initiator-online→recipient-readyordering
Common diagnostics:
- Duplicate reconnects: check
reconnectionInProgressand thegracefullyDisconnectedPeersset - Invalid candidates: validate
iceConnectionStateandiceGatheringState - State divergence: confirm store progress cleanup and connection-state synchronization