Repair connectivity correctly v1

This commit is contained in:
2026-05-17 15:15:14 +02:00
parent e769a6ee4a
commit 9d0a4478b2
18 changed files with 1125 additions and 25 deletions

View File

@@ -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
};
}

View File

@@ -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;