Skip to content

Commit 2e5e6b5

Browse files
committed
Unify LiveKit and Matrix connection states
1 parent f05d4b1 commit 2e5e6b5

File tree

10 files changed

+242
-235
lines changed

10 files changed

+242
-235
lines changed

src/room/GroupCallView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export const GroupCallView: FC<Props> = ({
160160
}, [rtcSession]);
161161

162162
// TODO move this into the callViewModel LocalMembership.ts
163+
// We might actually not need this at all. Since we get into fatalError on those errors already?
163164
useTypedEventEmitter(
164165
rtcSession,
165166
MatrixRTCSessionEvent.MembershipManagerError,

src/state/CallViewModel/CallViewModel.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -452,18 +452,14 @@ export function createCallViewModel$(
452452

453453
const localMembership = createLocalMembership$({
454454
scope: scope,
455-
homeserverConnected$: createHomeserverConnected$(
455+
homeserverConnected: createHomeserverConnected$(
456456
scope,
457457
client,
458458
matrixRTCSession,
459459
),
460460
muteStates: muteStates,
461-
joinMatrixRTC: async (transport: LivekitTransport) => {
462-
return enterRTCSession(
463-
matrixRTCSession,
464-
transport,
465-
connectOptions$.value,
466-
);
461+
joinMatrixRTC: (transport: LivekitTransport) => {
462+
enterRTCSession(matrixRTCSession, transport, connectOptions$.value);
467463
},
468464
createPublisherFactory: (connection: Connection) => {
469465
return new Publisher(
@@ -573,17 +569,6 @@ export function createCallViewModel$(
573569
),
574570
);
575571

576-
/**
577-
* Whether various media/event sources should pretend to be disconnected from
578-
* all network input, even if their connection still technically works.
579-
*/
580-
// We do this when the app is in the 'reconnecting' state, because it might be
581-
// that the LiveKit connection is still functional while the homeserver is
582-
// down, for example, and we want to avoid making people worry that the app is
583-
// in a split-brained state.
584-
// DISCUSSION own membership manager ALSO this probably can be simplifis
585-
const reconnecting$ = localMembership.reconnecting$;
586-
587572
const audioParticipants$ = scope.behavior(
588573
matrixLivekitMembers$.pipe(
589574
switchMap((membersWithEpoch) => {
@@ -631,7 +616,7 @@ export function createCallViewModel$(
631616
);
632617

633618
const handsRaised$ = scope.behavior(
634-
handsRaisedSubject$.pipe(pauseWhen(reconnecting$)),
619+
handsRaisedSubject$.pipe(pauseWhen(localMembership.reconnecting$)),
635620
);
636621

637622
const reactions$ = scope.behavior(
@@ -644,7 +629,7 @@ export function createCallViewModel$(
644629
]),
645630
),
646631
),
647-
pauseWhen(reconnecting$),
632+
pauseWhen(localMembership.reconnecting$),
648633
),
649634
);
650635

@@ -735,7 +720,7 @@ export function createCallViewModel$(
735720
livekitRoom$,
736721
focusUrl$,
737722
mediaDevices,
738-
reconnecting$,
723+
localMembership.reconnecting$,
739724
displayName$,
740725
matrixMemberMetadataStore.createAvatarUrlBehavior$(userId),
741726
handsRaised$.pipe(map((v) => v[participantId]?.time ?? null)),
@@ -827,11 +812,17 @@ export function createCallViewModel$(
827812
}),
828813
);
829814

830-
const leave$: Observable<"user" | "timeout" | "decline" | "allOthersLeft"> =
831-
merge(
832-
autoLeave$,
833-
merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)),
834-
).pipe(scope.share);
815+
const shouldLeave$: Observable<
816+
"user" | "timeout" | "decline" | "allOthersLeft"
817+
> = merge(
818+
autoLeave$,
819+
merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)),
820+
).pipe(scope.share);
821+
822+
shouldLeave$.pipe(scope.bind()).subscribe((reason) => {
823+
logger.info(`Call left due to ${reason}`);
824+
localMembership.requestDisconnect();
825+
});
835826

836827
const spotlightSpeaker$ = scope.behavior<UserMediaViewModel | null>(
837828
userMedia$.pipe(
@@ -1453,7 +1444,7 @@ export function createCallViewModel$(
14531444
autoLeave$: autoLeave$,
14541445
callPickupState$: callPickupState$,
14551446
ringOverlay$: ringOverlay$,
1456-
leave$: leave$,
1447+
leave$: shouldLeave$,
14571448
hangup: (): void => userHangup$.next(),
14581449
join: localMembership.requestConnect,
14591450
toggleScreenSharing: toggleScreenSharing,
@@ -1500,7 +1491,7 @@ export function createCallViewModel$(
15001491
showFooter$: showFooter$,
15011492
earpieceMode$: earpieceMode$,
15021493
audioOutputSwitcher$: audioOutputSwitcher$,
1503-
reconnecting$: reconnecting$,
1494+
reconnecting$: localMembership.reconnecting$,
15041495
};
15051496
}
15061497

src/state/CallViewModel/localMember/HomeserverConnected.test.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -97,106 +97,106 @@ describe("createHomeserverConnected$", () => {
9797
// LLM generated test cases. They are a bit overkill but I improved the mocking so it is
9898
// easy enough to read them so I think they can stay.
9999
it("is false when sync state is not Syncing", () => {
100-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
101-
expect(hsConnected$.value).toBe(false);
100+
const hsConnected = createHomeserverConnected$(scope, client, session);
101+
expect(hsConnected.combined$.value).toBe(false);
102102
});
103103

104104
it("remains false while membership status is not Connected even if sync is Syncing", () => {
105-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
105+
const hsConnected = createHomeserverConnected$(scope, client, session);
106106
client.setSyncState(SyncState.Syncing);
107-
expect(hsConnected$.value).toBe(false); // membership still disconnected
107+
expect(hsConnected.combined$.value).toBe(false); // membership still disconnected
108108
});
109109

110110
it("is false when membership status transitions to Connected but ProbablyLeft is true", () => {
111-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
111+
const hsConnected = createHomeserverConnected$(scope, client, session);
112112
// Make sync loop OK
113113
client.setSyncState(SyncState.Syncing);
114114
// Indicate probable leave before connection
115115
session.setProbablyLeft(true);
116116
session.setMembershipStatus(Status.Connected);
117-
expect(hsConnected$.value).toBe(false);
117+
expect(hsConnected.combined$.value).toBe(false);
118118
});
119119

120120
it("becomes true only when all three conditions are satisfied", () => {
121-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
121+
const hsConnected = createHomeserverConnected$(scope, client, session);
122122
// 1. Sync loop connected
123123
client.setSyncState(SyncState.Syncing);
124-
expect(hsConnected$.value).toBe(false); // not yet membership connected
124+
expect(hsConnected.combined$.value).toBe(false); // not yet membership connected
125125
// 2. Membership connected
126126
session.setMembershipStatus(Status.Connected);
127-
expect(hsConnected$.value).toBe(true); // probablyLeft is false
127+
expect(hsConnected.combined$.value).toBe(true); // probablyLeft is false
128128
});
129129

130130
it("drops back to false when sync loop leaves Syncing", () => {
131-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
131+
const hsConnected = createHomeserverConnected$(scope, client, session);
132132
// Reach connected state
133133
client.setSyncState(SyncState.Syncing);
134134
session.setMembershipStatus(Status.Connected);
135-
expect(hsConnected$.value).toBe(true);
135+
expect(hsConnected.combined$.value).toBe(true);
136136

137137
// Sync loop error => should flip false
138138
client.setSyncState(SyncState.Error);
139-
expect(hsConnected$.value).toBe(false);
139+
expect(hsConnected.combined$.value).toBe(false);
140140
});
141141

142142
it("drops back to false when membership status becomes disconnected", () => {
143-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
143+
const hsConnected = createHomeserverConnected$(scope, client, session);
144144
client.setSyncState(SyncState.Syncing);
145145
session.setMembershipStatus(Status.Connected);
146-
expect(hsConnected$.value).toBe(true);
146+
expect(hsConnected.combined$.value).toBe(true);
147147

148148
session.setMembershipStatus(Status.Disconnected);
149-
expect(hsConnected$.value).toBe(false);
149+
expect(hsConnected.combined$.value).toBe(false);
150150
});
151151

152152
it("drops to false when ProbablyLeft is emitted after being true", () => {
153-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
153+
const hsConnected = createHomeserverConnected$(scope, client, session);
154154
client.setSyncState(SyncState.Syncing);
155155
session.setMembershipStatus(Status.Connected);
156-
expect(hsConnected$.value).toBe(true);
156+
expect(hsConnected.combined$.value).toBe(true);
157157

158158
session.setProbablyLeft(true);
159-
expect(hsConnected$.value).toBe(false);
159+
expect(hsConnected.combined$.value).toBe(false);
160160
});
161161

162162
it("recovers to true if ProbablyLeft becomes false again while other conditions remain true", () => {
163-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
163+
const hsConnected = createHomeserverConnected$(scope, client, session);
164164
client.setSyncState(SyncState.Syncing);
165165
session.setMembershipStatus(Status.Connected);
166-
expect(hsConnected$.value).toBe(true);
166+
expect(hsConnected.combined$.value).toBe(true);
167167

168168
session.setProbablyLeft(true);
169-
expect(hsConnected$.value).toBe(false);
169+
expect(hsConnected.combined$.value).toBe(false);
170170

171171
// Simulate clearing the flag (in realistic scenario membership manager would update)
172172
session.setProbablyLeft(false);
173-
expect(hsConnected$.value).toBe(true);
173+
expect(hsConnected.combined$.value).toBe(true);
174174
});
175175

176176
it("composite sequence reflects each individual failure reason", () => {
177-
const hsConnected$ = createHomeserverConnected$(scope, client, session);
177+
const hsConnected = createHomeserverConnected$(scope, client, session);
178178

179179
// Initially false (sync error + disconnected + not probably left)
180-
expect(hsConnected$.value).toBe(false);
180+
expect(hsConnected.combined$.value).toBe(false);
181181

182182
// Fix sync only
183183
client.setSyncState(SyncState.Syncing);
184-
expect(hsConnected$.value).toBe(false);
184+
expect(hsConnected.combined$.value).toBe(false);
185185

186186
// Fix membership
187187
session.setMembershipStatus(Status.Connected);
188-
expect(hsConnected$.value).toBe(true);
188+
expect(hsConnected.combined$.value).toBe(true);
189189

190190
// Introduce probablyLeft -> false
191191
session.setProbablyLeft(true);
192-
expect(hsConnected$.value).toBe(false);
192+
expect(hsConnected.combined$.value).toBe(false);
193193

194194
// Restore notProbablyLeft -> true again
195195
session.setProbablyLeft(false);
196-
expect(hsConnected$.value).toBe(true);
196+
expect(hsConnected.combined$.value).toBe(true);
197197

198198
// Drop sync -> false
199199
client.setSyncState(SyncState.Error);
200-
expect(hsConnected$.value).toBe(false);
200+
expect(hsConnected.combined$.value).toBe(false);
201201
});
202202
});

src/state/CallViewModel/localMember/HomeserverConnected.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import { type NodeStyleEventEmitter } from "../../../utils/test";
2525
*/
2626
const logger = rootLogger.getChild("[HomeserverConnected]");
2727

28+
export interface HomeserverConnected {
29+
combined$: Behavior<boolean>;
30+
rtsSession$: Behavior<Status>;
31+
}
32+
2833
/**
2934
* Behavior representing whether we consider ourselves connected to the Matrix homeserver
3035
* for the purposes of a MatrixRTC session.
@@ -39,20 +44,23 @@ export function createHomeserverConnected$(
3944
client: NodeStyleEventEmitter & Pick<MatrixClient, "getSyncState">,
4045
matrixRTCSession: NodeStyleEventEmitter &
4146
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
42-
): Behavior<boolean> {
47+
): HomeserverConnected {
4348
const syncing$ = (
4449
fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]>
4550
).pipe(
4651
startWith([client.getSyncState()]),
4752
map(([state]) => state === SyncState.Syncing),
4853
);
4954

50-
const membershipConnected$ = fromEvent(
51-
matrixRTCSession,
52-
MembershipManagerEvent.StatusChanged,
53-
).pipe(
54-
startWith(null),
55-
map(() => matrixRTCSession.membershipStatus === Status.Connected),
55+
const rtsSession$ = scope.behavior<Status>(
56+
fromEvent(matrixRTCSession, MembershipManagerEvent.StatusChanged).pipe(
57+
map(() => matrixRTCSession.membershipStatus ?? Status.Unknown),
58+
),
59+
Status.Unknown,
60+
);
61+
62+
const membershipConnected$ = rtsSession$.pipe(
63+
map((status) => status === Status.Connected),
5664
);
5765

5866
// This is basically notProbablyLeft$
@@ -71,15 +79,13 @@ export function createHomeserverConnected$(
7179
map(() => matrixRTCSession.probablyLeft !== true),
7280
);
7381

74-
const connectedCombined$ = and$(
75-
syncing$,
76-
membershipConnected$,
77-
certainlyConnected$,
78-
).pipe(
79-
tap((connected) => {
80-
logger.info(`Homeserver connected update: ${connected}`);
81-
}),
82+
const combined$ = scope.behavior(
83+
and$(syncing$, membershipConnected$, certainlyConnected$).pipe(
84+
tap((connected) => {
85+
logger.info(`Homeserver connected update: ${connected}`);
86+
}),
87+
),
8288
);
8389

84-
return scope.behavior(connectedCombined$);
90+
return { combined$, rtsSession$ };
8591
}

0 commit comments

Comments
 (0)