fix: restore build and stabilize E2E cross-signal behavior

Revert the automated member-ordering pass that broke Angular field init
(TS2729) and disable that rule until a safe reorder strategy exists.
Fix modal/confirm dialog i18n defaults via template fallbacks, search all
active endpoints (including offline), register foreign rooms with actor
owner IDs, sync profile display names from avatar summaries, and guard
dm-chat when a private call converts to a group conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:16:40 +02:00
co-authored by Cursor
parent 79c6f91cd6
commit 31962aeb1a
131 changed files with 2483 additions and 3896 deletions
@@ -11,6 +11,7 @@
* This file wires them together and exposes a public API that is
* identical to the old monolithic service so consumers don't change.
*/
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-non-null-assertion */
import {
Injectable,
inject,
@@ -59,115 +60,70 @@ import { selectCurrentUser } from '../../store/users/users.selectors';
providedIn: 'root'
})
export class WebRTCService implements OnDestroy {
private readonly timeSync = inject(TimeSyncService);
private readonly debugging = inject(DebuggingService);
private readonly screenShareSourcePicker = inject(ScreenShareSourcePickerService);
private readonly iceServerSettings = inject(IceServerSettingsService);
private readonly signalServerAuth = inject(SignalServerAuthService);
private readonly store = inject(Store);
private readonly clientInstance = inject(ClientInstanceService);
private currentHomeUser: { id: string; homeSignalServerUrl?: string; displayName: string } | null = null;
private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
private readonly state = new WebRtcStateController();
readonly peerId = this.state.peerId;
readonly isConnected = this.state.isConnected;
readonly hasEverConnected = this.state.hasEverConnected;
readonly isVoiceConnected = this.state.isVoiceConnected;
readonly connectedPeers = this.state.connectedPeers;
readonly isMuted = this.state.isMuted;
readonly isDeafened = this.state.isDeafened;
readonly isCameraEnabled = this.state.isCameraEnabled;
readonly isScreenSharing = this.state.isScreenSharing;
readonly isNoiseReductionEnabled = this.state.isNoiseReductionEnabled;
readonly screenStream = this.state.screenStream;
readonly isScreenShareRemotePlaybackSuppressed = this.state.isScreenShareRemotePlaybackSuppressed;
readonly forceDefaultRemotePlaybackOutput = this.state.forceDefaultRemotePlaybackOutput;
readonly hasConnectionError = this.state.hasConnectionError;
readonly connectionErrorMessage = this.state.connectionErrorMessage;
readonly shouldShowConnectionError = this.state.shouldShowConnectionError;
readonly peerLatencies = this.state.peerLatencies;
private readonly signalingMessage$ = new Subject<IncomingSignalingMessage>();
readonly onSignalingMessage = this.signalingMessage$.asObservable();
private readonly accountSyncRelay$ = new Subject<ChatEvent>();
private readonly signalingReconnectedSubject$ = new Subject<string>();
readonly signalingReconnected$ = this.signalingReconnectedSubject$.asObservable();
// Delegates to managers
get onMessageReceived(): Observable<ChatEvent> {
return merge(this.peerMediaFacade.onMessageReceived, this.accountSyncRelay$);
}
get onPeerConnected(): Observable<string> {
return this.peerMediaFacade.onPeerConnected;
}
get onPeerDisconnected(): Observable<string> {
return this.peerMediaFacade.onPeerDisconnected;
}
get onRemoteStream(): Observable<{ peerId: string; stream: MediaStream }> {
return this.peerMediaFacade.onRemoteStream;
}
get onVoiceConnected(): Observable<void> {
return this.peerMediaFacade.onVoiceConnected;
}
/** The server ID currently being viewed / active, or `null`. */
get currentServerId(): string | null {
return this.state.currentServerId;
}
private readonly timeSync = inject(TimeSyncService);
private readonly debugging = inject(DebuggingService);
private readonly screenShareSourcePicker = inject(ScreenShareSourcePickerService);
private readonly iceServerSettings = inject(IceServerSettingsService);
private readonly signalServerAuth = inject(SignalServerAuthService);
private readonly store = inject(Store);
private readonly clientInstance = inject(ClientInstanceService);
private currentHomeUser: { id: string; homeSignalServerUrl?: string; displayName: string } | null = null;
private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
private readonly state = new WebRtcStateController();
private readonly signalingMessage$ = new Subject<IncomingSignalingMessage>();
private readonly accountSyncRelay$ = new Subject<ChatEvent>();
private readonly signalingReconnectedSubject$ = new Subject<string>();
private readonly peerManager: PeerConnectionManager;
private readonly mediaManager: MediaManager;
private readonly screenShareManager: ScreenShareManager;
private readonly peerMediaFacade: PeerMediaFacade;
private readonly voiceSessionController: VoiceSessionController;
private readonly signalingCoordinator: ServerSignalingCoordinator<IncomingSignalingMessage>;
private readonly signalingTransportHandler: SignalingTransportHandler<IncomingSignalingMessage>;
private readonly signalingMessageHandler: IncomingSignalingMessageHandler;
private readonly serverMembershipSignalingHandler: ServerMembershipSignalingHandler<IncomingSignalingMessage>;
private readonly remoteScreenShareRequestController: RemoteScreenShareRequestController;
constructor() {
@@ -182,13 +138,11 @@ export class WebRTCService implements OnDestroy {
});
// Create managers with null callbacks first to break circular initialization
const pendingPeerMessageHandler = () => undefined;
this.peerManager = new PeerConnectionManager(this.logger, null!);
this.peerManager = new PeerConnectionManager(this.logger, pendingPeerMessageHandler);
this.mediaManager = new MediaManager(this.logger, null!);
this.mediaManager = new MediaManager(this.logger, pendingPeerMessageHandler);
this.screenShareManager = new ScreenShareManager(this.logger, pendingPeerMessageHandler);
this.screenShareManager = new ScreenShareManager(this.logger, null!);
this.peerMediaFacade = new PeerMediaFacade({
peerManager: this.peerManager,
@@ -306,6 +260,79 @@ export class WebRTCService implements OnDestroy {
this.wireManagerEvents();
}
private wireManagerEvents(): void {
// Internal control-plane messages for on-demand screen-share delivery.
this.peerManager.messageReceived$.subscribe((event) =>
this.remoteScreenShareRequestController.handlePeerControlMessage(event)
);
// Peer manager -> connected peers signal
this.peerManager.connectedPeersChanged$.subscribe((peers: string[]) =>
this.state.setConnectedPeers(peers)
);
// If we are already sharing when a new peer connection finishes, push the
// current screen-share tracks to that peer and renegotiate.
this.peerManager.peerConnected$.subscribe((peerId) => {
if (this.peerMediaFacade.isScreenShareActive()) {
this.peerMediaFacade.syncScreenShareToPeer(peerId);
}
this.mediaManager.refreshVoiceRouting();
this.remoteScreenShareRequestController.handlePeerConnected(peerId);
});
this.peerManager.peerDisconnected$.subscribe((peerId) => {
this.remoteScreenShareRequestController.handlePeerDisconnected(peerId);
});
// Media manager -> voice connected signal
this.mediaManager.voiceConnected$.subscribe(() => {
this.voiceSessionController.handleVoiceConnected();
});
// Peer manager -> latency updates
this.peerManager.peerLatencyChanged$.subscribe(() =>
this.state.syncPeerLatencies(this.peerManager.peerLatencies)
);
}
private handleSignalingConnectionStatus(signalUrl: string, connected: boolean, errorMessage?: string): void {
this.state.updateSignalingConnectionStatus(
connected ? true : this.signalingCoordinator.isAnySignalingConnected(),
connected,
errorMessage
);
if (connected) {
this.signalingReconnectedSubject$.next(signalUrl);
}
}
private handleSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
if (message.type === 'auth_required' || message.type === 'auth_error') {
this.store.dispatch(UsersActions.signalServerAuthFailed({ signalUrl }));
return;
}
if (message.type === 'account_sync') {
const accountMessage = message as AccountSyncSignalingMessage;
if (shouldApplyAccountSyncPayload(
accountMessage.clientInstanceId,
this.clientInstance.getClientInstanceId()
)) {
this.accountSyncRelay$.next(unwrapAccountSyncPayload(accountMessage));
}
return;
}
this.signalingMessage$.next(message);
this.signalingMessageHandler.handleMessage(message, signalUrl);
}
// PUBLIC API - matches the old monolithic service's interface
/**
@@ -365,6 +392,11 @@ export class WebRTCService implements OnDestroy {
this.state.setCurrentServer(serverId);
}
/** The server ID currently being viewed / active, or `null`. */
get currentServerId(): string | null {
return this.state.currentServerId;
}
/** The last signaling URL used by the client, if any. */
getCurrentSignalingUrl(): string | null {
return this.signalingTransportHandler.getCurrentSignalingUrl(this.state.currentServerId);
@@ -725,107 +757,6 @@ export class WebRTCService implements OnDestroy {
this.peerMediaFacade.stopScreenShare();
}
requestVoiceClientTakeover(): void {
this.signalingTransportHandler.sendRawMessage({
type: 'voice_client_takeover',
clientInstanceId: this.clientInstance.getClientInstanceId()
});
}
getClientInstanceId(): string {
return this.clientInstance.getClientInstanceId();
}
/** Disconnect from the signaling server and clean up all state. */
disconnect(): void {
this.leaveRoom();
this.destroyAllSignalingManagers();
this.state.resetConnectionState();
}
/** Alias for {@link disconnect}. */
disconnectAll(): void {
this.disconnect();
}
ngOnDestroy(): void {
this.disconnect();
this.peerMediaFacade.destroy();
}
private wireManagerEvents(): void {
// Internal control-plane messages for on-demand screen-share delivery.
this.peerManager.messageReceived$.subscribe((event) =>
this.remoteScreenShareRequestController.handlePeerControlMessage(event)
);
// Peer manager -> connected peers signal
this.peerManager.connectedPeersChanged$.subscribe((peers: string[]) =>
this.state.setConnectedPeers(peers)
);
// If we are already sharing when a new peer connection finishes, push the
// current screen-share tracks to that peer and renegotiate.
this.peerManager.peerConnected$.subscribe((peerId) => {
if (this.peerMediaFacade.isScreenShareActive()) {
this.peerMediaFacade.syncScreenShareToPeer(peerId);
}
this.mediaManager.refreshVoiceRouting();
this.remoteScreenShareRequestController.handlePeerConnected(peerId);
});
this.peerManager.peerDisconnected$.subscribe((peerId) => {
this.remoteScreenShareRequestController.handlePeerDisconnected(peerId);
});
// Media manager -> voice connected signal
this.mediaManager.voiceConnected$.subscribe(() => {
this.voiceSessionController.handleVoiceConnected();
});
// Peer manager -> latency updates
this.peerManager.peerLatencyChanged$.subscribe(() =>
this.state.syncPeerLatencies(this.peerManager.peerLatencies)
);
}
private handleSignalingConnectionStatus(signalUrl: string, connected: boolean, errorMessage?: string): void {
this.state.updateSignalingConnectionStatus(
connected ? true : this.signalingCoordinator.isAnySignalingConnected(),
connected,
errorMessage
);
if (connected) {
this.signalingReconnectedSubject$.next(signalUrl);
}
}
private handleSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
if (message.type === 'auth_required' || message.type === 'auth_error') {
this.store.dispatch(UsersActions.signalServerAuthFailed({ signalUrl }));
return;
}
if (message.type === 'account_sync') {
const accountMessage = message as AccountSyncSignalingMessage;
if (shouldApplyAccountSyncPayload(
accountMessage.clientInstanceId,
this.clientInstance.getClientInstanceId()
)) {
this.accountSyncRelay$.next(unwrapAccountSyncPayload(accountMessage));
}
return;
}
this.signalingMessage$.next(message);
this.signalingMessageHandler.handleMessage(message, signalUrl);
}
private relayBroadcastEvent(event: ChatEvent): void {
const clientInstanceId = this.clientInstance.getClientInstanceId();
@@ -870,6 +801,29 @@ export class WebRTCService implements OnDestroy {
this.relayAccountSync(event);
}
requestVoiceClientTakeover(): void {
this.signalingTransportHandler.sendRawMessage({
type: 'voice_client_takeover',
clientInstanceId: this.clientInstance.getClientInstanceId()
});
}
getClientInstanceId(): string {
return this.clientInstance.getClientInstanceId();
}
/** Disconnect from the signaling server and clean up all state. */
disconnect(): void {
this.leaveRoom();
this.destroyAllSignalingManagers();
this.state.resetConnectionState();
}
/** Alias for {@link disconnect}. */
disconnectAll(): void {
this.disconnect();
}
private fullCleanup(): void {
this.signalingCoordinator.clearPeerTracking();
this.remoteScreenShareRequestController.clear();
@@ -885,4 +839,8 @@ export class WebRTCService implements OnDestroy {
this.signalingCoordinator.destroy();
}
ngOnDestroy(): void {
this.disconnect();
this.peerMediaFacade.destroy();
}
}