Repair connectivity correctly v1
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import type { PeerData } from '../realtime.types';
|
||||
import { PeerConnectionManager } from '../peer-connection-manager/peer-connection.manager';
|
||||
import { IncomingSignalingMessageHandler } from './signaling-message-handler';
|
||||
import { ServerSignalingCoordinator } from './server-signaling-coordinator';
|
||||
|
||||
describe('IncomingSignalingMessageHandler user_left handling', () => {
|
||||
it('preserves an active voice peer on a transient user_left signal', () => {
|
||||
const context = createHandlerContext({ voiceConnected: true });
|
||||
|
||||
context.coordinator.addJoinedServer('ws://signal-a', 'server-1');
|
||||
context.coordinator.trackPeerInServer('peer-a', 'server-1', 'ws://signal-a');
|
||||
context.peerManager.activePeerConnections.set('peer-a', createPeerData('connected', 'open'));
|
||||
|
||||
context.handler.handleMessage({ type: 'user_left', oderId: 'peer-a', serverId: 'server-1' }, 'ws://signal-a');
|
||||
|
||||
expect(context.peerManager.removePeer).not.toHaveBeenCalled();
|
||||
expect(context.coordinator.getPeerSignalUrl('peer-a')).toBe('ws://signal-a');
|
||||
expect(context.coordinator.hasTrackedPeerServers('peer-a')).toBe(true);
|
||||
});
|
||||
|
||||
it('removes a peer on user_left when local voice is not active', () => {
|
||||
const context = createHandlerContext({ voiceConnected: false });
|
||||
|
||||
context.coordinator.trackPeerInServer('peer-a', 'server-1', 'ws://signal-a');
|
||||
context.peerManager.activePeerConnections.set('peer-a', createPeerData('connected', 'open'));
|
||||
|
||||
context.handler.handleMessage({ type: 'user_left', oderId: 'peer-a', serverId: 'server-1' }, 'ws://signal-a');
|
||||
|
||||
expect(context.peerManager.removePeer).toHaveBeenCalledWith('peer-a');
|
||||
expect(context.coordinator.getPeerSignalUrl('peer-a')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes a stale voice peer when no active P2P transport remains', () => {
|
||||
const context = createHandlerContext({ voiceConnected: true });
|
||||
|
||||
context.coordinator.trackPeerInServer('peer-a', 'server-1', 'ws://signal-a');
|
||||
context.peerManager.activePeerConnections.set('peer-a', createPeerData('failed', 'closed'));
|
||||
|
||||
context.handler.handleMessage({ type: 'user_left', oderId: 'peer-a', serverId: 'server-1' }, 'ws://signal-a');
|
||||
|
||||
expect(context.peerManager.removePeer).toHaveBeenCalledWith('peer-a');
|
||||
expect(context.coordinator.getPeerSignalUrl('peer-a')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
interface HandlerContext {
|
||||
coordinator: ServerSignalingCoordinator<unknown>;
|
||||
handler: IncomingSignalingMessageHandler;
|
||||
peerManager: PeerConnectionManager & {
|
||||
activePeerConnections: Map<string, PeerData>;
|
||||
removePeer: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
}
|
||||
|
||||
function createHandlerContext(options: { voiceConnected: boolean }): HandlerContext {
|
||||
const coordinator = new ServerSignalingCoordinator<unknown>({
|
||||
createManager: vi.fn(),
|
||||
handleConnectionStatus: vi.fn(),
|
||||
handleHeartbeatTick: vi.fn(),
|
||||
handleMessage: vi.fn()
|
||||
});
|
||||
const peerManager = {
|
||||
activePeerConnections: new Map<string, PeerData>(),
|
||||
removePeer: vi.fn()
|
||||
} as unknown as HandlerContext['peerManager'];
|
||||
const handler = new IncomingSignalingMessageHandler({
|
||||
getEffectiveServerId: () => 'server-1',
|
||||
getLocalOderId: () => 'local-user',
|
||||
isVoiceConnected: () => options.voiceConnected,
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
logStream: vi.fn(),
|
||||
traffic: vi.fn(),
|
||||
warn: vi.fn()
|
||||
} as unknown as ConstructorParameters<typeof IncomingSignalingMessageHandler>[0]['logger'],
|
||||
peerManager,
|
||||
setServerTime: vi.fn(),
|
||||
signalingCoordinator: coordinator
|
||||
});
|
||||
|
||||
return { coordinator, handler, peerManager };
|
||||
}
|
||||
|
||||
function createPeerData(
|
||||
connectionState: RTCPeerConnectionState,
|
||||
dataChannelState: RTCDataChannelState
|
||||
): PeerData {
|
||||
return {
|
||||
audioSender: undefined,
|
||||
connection: {
|
||||
connectionState
|
||||
} as RTCPeerConnection,
|
||||
createdAt: Date.now(),
|
||||
dataChannel: {
|
||||
readyState: dataChannelState
|
||||
} as RTCDataChannel,
|
||||
isInitiator: true,
|
||||
pendingIceCandidates: [],
|
||||
remoteCameraStreamIds: new Set<string>(),
|
||||
remoteScreenShareStreamIds: new Set<string>(),
|
||||
remoteVoiceStreamIds: new Set<string>(),
|
||||
videoSender: undefined
|
||||
};
|
||||
}
|
||||
@@ -45,6 +45,7 @@ interface IncomingSignalingMessageHandlerDependencies {
|
||||
logger: WebRTCLogger;
|
||||
getLocalOderId(): string | null;
|
||||
getEffectiveServerId(): string | null;
|
||||
isVoiceConnected(): boolean;
|
||||
setServerTime(serverTime: number): void;
|
||||
}
|
||||
|
||||
@@ -294,7 +295,7 @@ export class IncomingSignalingMessageHandler {
|
||||
if (message.oderId) {
|
||||
this.clearUserJoinedFallbackOffer(message.oderId);
|
||||
this.nonInitiatorWaitStart.delete(message.oderId);
|
||||
|
||||
const existing = this.dependencies.peerManager.activePeerConnections.get(message.oderId);
|
||||
const hasRemainingSharedServers = Array.isArray(message.serverIds)
|
||||
? this.dependencies.signalingCoordinator.replacePeerSharedServers(message.oderId, signalUrl, message.serverIds)
|
||||
: message.serverId
|
||||
@@ -302,6 +303,16 @@ export class IncomingSignalingMessageHandler {
|
||||
: false;
|
||||
|
||||
if (!hasRemainingSharedServers) {
|
||||
const serverIdsToPreserve = Array.isArray(message.serverIds)
|
||||
? message.serverIds
|
||||
: message.serverId
|
||||
? [message.serverId]
|
||||
: [];
|
||||
|
||||
if (this.shouldPreserveActiveVoicePeerAfterLeave(message.oderId, existing, signalUrl, serverIdsToPreserve)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dependencies.peerManager.removePeer(message.oderId);
|
||||
this.dependencies.signalingCoordinator.deletePeerTracking(message.oderId);
|
||||
}
|
||||
@@ -533,6 +544,41 @@ export class IncomingSignalingMessageHandler {
|
||||
return connectionState === 'connected' || peer.dataChannel?.readyState === 'open';
|
||||
}
|
||||
|
||||
private shouldPreserveActiveVoicePeerAfterLeave(
|
||||
peerId: string,
|
||||
peer: PeerData | undefined,
|
||||
signalUrl: string,
|
||||
serverIds: readonly string[]
|
||||
): boolean {
|
||||
if (!this.dependencies.isVoiceConnected() || !this.hasActivePeerConnection(peer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let restoredServerScope = false;
|
||||
|
||||
for (const serverId of serverIds) {
|
||||
if (!this.dependencies.signalingCoordinator.hasJoinedServer(serverId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.dependencies.signalingCoordinator.trackPeerInServer(peerId, serverId, signalUrl);
|
||||
restoredServerScope = true;
|
||||
}
|
||||
|
||||
if (!restoredServerScope) {
|
||||
this.dependencies.signalingCoordinator.setPeerSignalUrl(peerId, signalUrl);
|
||||
}
|
||||
|
||||
this.dependencies.logger.warn('Preserving active voice peer after transient user_left', {
|
||||
connectionState: peer?.connection.connectionState ?? 'unknown',
|
||||
dataChannelState: peer?.dataChannel?.readyState ?? 'missing',
|
||||
peerId,
|
||||
signalUrl
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isPeerConnectionNegotiating(peer: PeerData | undefined): boolean {
|
||||
if (!peer || this.hasActivePeerConnection(peer))
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user