mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-24 19:19:21 +00:00
Add support for a grace period for /sync (aka homeserver disconnected) interruptions
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"feature_use_device_session_member_events": true
|
"feature_use_device_session_member_events": true
|
||||||
},
|
},
|
||||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
|
"sync_disconnect_grace_period_ms": 60000,
|
||||||
"matrix_rtc_session": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ export interface ConfigOptions {
|
|||||||
enable_video?: boolean;
|
enable_video?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grace period in milliseconds to wait before reporting the sync loop as disconnected.
|
||||||
|
* This allows brief sync interruptions without triggering a reconnection message.
|
||||||
|
* Default is 60000ms (60 seconds). Set to 0 to disable the grace period.
|
||||||
|
*/
|
||||||
|
sync_disconnect_grace_period_ms?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are low level options that are used to configure the MatrixRTC session.
|
* These are low level options that are used to configure the MatrixRTC session.
|
||||||
* Take care when changing these options.
|
* Take care when changing these options.
|
||||||
@@ -168,5 +175,6 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
|||||||
features: {
|
features: {
|
||||||
feature_use_device_session_member_events: true,
|
feature_use_device_session_member_events: true,
|
||||||
},
|
},
|
||||||
|
sync_disconnect_grace_period_ms: 60000,
|
||||||
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import { E2eeType } from "../../e2ee/e2eeType";
|
|||||||
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
||||||
import { type MuteStates } from "../MuteStates";
|
import { type MuteStates } from "../MuteStates";
|
||||||
import { getUrlParams, HeaderStyle } from "../../UrlParams";
|
import { getUrlParams, HeaderStyle } from "../../UrlParams";
|
||||||
|
import { Config } from "../../config/Config";
|
||||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||||
import { ElementWidgetActions, widget } from "../../widget";
|
import { ElementWidgetActions, widget } from "../../widget";
|
||||||
import {
|
import {
|
||||||
@@ -536,6 +537,7 @@ export function createCallViewModel$(
|
|||||||
scope,
|
scope,
|
||||||
client,
|
client,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
|
Config.get().sync_disconnect_grace_period_ms,
|
||||||
),
|
),
|
||||||
muteStates,
|
muteStates,
|
||||||
joinMatrixRTC: (transport: LivekitTransportConfig) => {
|
joinMatrixRTC: (transport: LivekitTransportConfig) => {
|
||||||
|
|||||||
@@ -96,19 +96,20 @@ describe("createHomeserverConnected$", () => {
|
|||||||
|
|
||||||
// LLM generated test cases. They are a bit overkill but I improved the mocking so it is
|
// LLM generated test cases. They are a bit overkill but I improved the mocking so it is
|
||||||
// easy enough to read them so I think they can stay.
|
// easy enough to read them so I think they can stay.
|
||||||
|
// Note: gracePeriodMs is set to 0 to avoid debouncing delays in tests
|
||||||
it("is false when sync state is not Syncing", () => {
|
it("is false when sync state is not Syncing", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
expect(hsConnected.combined$.value).toBe(false);
|
expect(hsConnected.combined$.value).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remains false while membership status is not Connected even if sync is Syncing", () => {
|
it("remains false while membership status is not Connected even if sync is Syncing", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
expect(hsConnected.combined$.value).toBe(false); // membership still disconnected
|
expect(hsConnected.combined$.value).toBe(false); // membership still disconnected
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is false when membership status transitions to Connected but ProbablyLeft is true", () => {
|
it("is false when membership status transitions to Connected but ProbablyLeft is true", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
// Make sync loop OK
|
// Make sync loop OK
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
// Indicate probable leave before connection
|
// Indicate probable leave before connection
|
||||||
@@ -118,7 +119,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("becomes true only when all three conditions are satisfied", () => {
|
it("becomes true only when all three conditions are satisfied", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
// 1. Sync loop connected
|
// 1. Sync loop connected
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
expect(hsConnected.combined$.value).toBe(false); // not yet membership connected
|
expect(hsConnected.combined$.value).toBe(false); // not yet membership connected
|
||||||
@@ -128,7 +129,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("drops back to false when sync loop leaves Syncing", () => {
|
it("drops back to false when sync loop leaves Syncing", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
// Reach connected state
|
// Reach connected state
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
session.setMembershipStatus(Status.Connected);
|
session.setMembershipStatus(Status.Connected);
|
||||||
@@ -140,7 +141,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("drops back to false when membership status becomes disconnected", () => {
|
it("drops back to false when membership status becomes disconnected", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
session.setMembershipStatus(Status.Connected);
|
session.setMembershipStatus(Status.Connected);
|
||||||
expect(hsConnected.combined$.value).toBe(true);
|
expect(hsConnected.combined$.value).toBe(true);
|
||||||
@@ -150,7 +151,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("drops to false when ProbablyLeft is emitted after being true", () => {
|
it("drops to false when ProbablyLeft is emitted after being true", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
session.setMembershipStatus(Status.Connected);
|
session.setMembershipStatus(Status.Connected);
|
||||||
expect(hsConnected.combined$.value).toBe(true);
|
expect(hsConnected.combined$.value).toBe(true);
|
||||||
@@ -160,7 +161,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("recovers to true if ProbablyLeft becomes false again while other conditions remain true", () => {
|
it("recovers to true if ProbablyLeft becomes false again while other conditions remain true", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
client.setSyncState(SyncState.Syncing);
|
client.setSyncState(SyncState.Syncing);
|
||||||
session.setMembershipStatus(Status.Connected);
|
session.setMembershipStatus(Status.Connected);
|
||||||
expect(hsConnected.combined$.value).toBe(true);
|
expect(hsConnected.combined$.value).toBe(true);
|
||||||
@@ -174,7 +175,7 @@ describe("createHomeserverConnected$", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("composite sequence reflects each individual failure reason", () => {
|
it("composite sequence reflects each individual failure reason", () => {
|
||||||
const hsConnected = createHomeserverConnected$(scope, client, session);
|
const hsConnected = createHomeserverConnected$(scope, client, session, 0);
|
||||||
|
|
||||||
// Initially false (sync error + disconnected + not probably left)
|
// Initially false (sync error + disconnected + not probably left)
|
||||||
expect(hsConnected.combined$.value).toBe(false);
|
expect(hsConnected.combined$.value).toBe(false);
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import {
|
|||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk";
|
import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk";
|
||||||
import { fromEvent, startWith, map, tap, type Observable } from "rxjs";
|
import { fromEvent, startWith, map, tap, type Observable, debounceTime } from "rxjs";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
|
import { Config } from "../../../config/Config";
|
||||||
|
|
||||||
import { type ObservableScope } from "../../ObservableScope";
|
import { type ObservableScope } from "../../ObservableScope";
|
||||||
import { type Behavior } from "../../Behavior";
|
import { type Behavior } from "../../Behavior";
|
||||||
import { and$ } from "../../../utils/observable";
|
import { and$ } from "../../../utils/observable";
|
||||||
@@ -35,21 +37,29 @@ export interface HomeserverConnected {
|
|||||||
* for the purposes of a MatrixRTC session.
|
* for the purposes of a MatrixRTC session.
|
||||||
*
|
*
|
||||||
* Becomes FALSE if ANY sub-condition is fulfilled:
|
* Becomes FALSE if ANY sub-condition is fulfilled:
|
||||||
* 1. Sync loop is not in SyncState.Syncing
|
* 1. Sync loop is not in SyncState.Syncing (after grace period)
|
||||||
* 2. membershipStatus !== Status.Connected
|
* 2. membershipStatus !== Status.Connected
|
||||||
* 3. probablyLeft === true
|
* 3. probablyLeft === true
|
||||||
|
*
|
||||||
|
* @param gracePeriodMs - Grace period in milliseconds to wait before reporting sync disconnect.
|
||||||
|
* If not provided, uses the config value (default 60000ms).
|
||||||
*/
|
*/
|
||||||
export function createHomeserverConnected$(
|
export function createHomeserverConnected$(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
client: NodeStyleEventEmitter & Pick<MatrixClient, "getSyncState">,
|
client: NodeStyleEventEmitter & Pick<MatrixClient, "getSyncState">,
|
||||||
matrixRTCSession: NodeStyleEventEmitter &
|
matrixRTCSession: NodeStyleEventEmitter &
|
||||||
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
|
Pick<MatrixRTCSession, "membershipStatus" | "probablyLeft">,
|
||||||
|
gracePeriodMs?: number,
|
||||||
): HomeserverConnected {
|
): HomeserverConnected {
|
||||||
|
// Get grace period from parameter or config (default 60000ms)
|
||||||
|
const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 60000;
|
||||||
|
|
||||||
const syncing$ = (
|
const syncing$ = (
|
||||||
fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]>
|
fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]>
|
||||||
).pipe(
|
).pipe(
|
||||||
startWith([client.getSyncState()]),
|
startWith([client.getSyncState()]),
|
||||||
map(([state]) => state === SyncState.Syncing),
|
map(([state]) => state === SyncState.Syncing),
|
||||||
|
debounceTime(graceMs),
|
||||||
);
|
);
|
||||||
|
|
||||||
const rtsSession$ = scope.behavior<Status>(
|
const rtsSession$ = scope.behavior<Status>(
|
||||||
|
|||||||
Reference in New Issue
Block a user