From 4c7db0147e61d979f1d50594679513930c16fc24 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 28 Jan 2026 14:22:21 +0100 Subject: [PATCH] The advertised livekit_alias in membership is deprecated --- src/livekit/openIDSFU.ts | 4 +- src/state/CallViewModel/CallViewModel.ts | 14 +-- .../localMember/LocalTransport.ts | 23 ++--- .../remoteMembers/Connection.test.ts | 12 ++- .../CallViewModel/remoteMembers/Connection.ts | 89 ++++++++++--------- .../remoteMembers/ConnectionFactory.ts | 9 +- .../remoteMembers/ConnectionManager.test.ts | 11 +-- .../remoteMembers/ConnectionManager.ts | 31 +++---- .../remoteMembers/MatrixLivekitMembers.ts | 20 ++--- src/state/SessionBehaviors.ts | 33 ++++--- 10 files changed, 123 insertions(+), 123 deletions(-) diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 0b7c2c78..8360cdc7 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -78,8 +78,8 @@ export type OpenIDClientParts = Pick< * @param membership Our own membership identity parts used to send to jwt service. * @param serviceUrl The URL of the livekit SFU service * @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases. - * @param opts Additional options to modify which endpoint with which data will be used to aquire the jwt token. - * @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination + * @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token. + * @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatenation * instead of a hash. * This function by default uses whatever is possible with the current jwt service installed next to the SFU. * For remote connections this does not matter, since we will not publish there we can rely on the newest option. diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 6d181255..5fbb6054 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -468,6 +468,7 @@ export function createCallViewModel$( const connectionFactory = new ECConnectionFactory( client, + matrixRoom.roomId, mediaDevices, trackProcessorState$, livekitKeyProvider, @@ -496,12 +497,13 @@ export function createCallViewModel$( ownMembershipIdentity, }); - const matrixLivekitMembers$ = createMatrixLivekitMembers$({ - scope: scope, - membershipsWithTransport$: - membershipsAndTransports.membershipsWithTransport$, - connectionManager: connectionManager, - }); + const matrixLivekitMembers$: Behavior> = + createMatrixLivekitMembers$({ + scope: scope, + membershipsWithTransport$: + membershipsAndTransports.membershipsWithTransport$, + connectionManager: connectionManager, + }); const connectOptions$ = scope.behavior( matrixRTCMode$.pipe( diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 726b0b1c..73364094 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -7,10 +7,9 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, - isLivekitTransport, - type LivekitTransport, isLivekitTransportConfig, type Transport, + type LivekitTransportConfig, } from "matrix-js-sdk/lib/matrixrtc"; import { MatrixError, type MatrixClient } from "matrix-js-sdk"; import { @@ -57,6 +56,7 @@ interface Props { "getDomain" | "baseUrl" | "_unstable_getRTCTransports" > & OpenIDClientParts; + // Used by the jwt service to create the livekit room and compute the livekit alias. roomId: string; useOldestMember$: Behavior; forceJwtEndpoint$: Behavior; @@ -90,11 +90,11 @@ export enum JwtEndpointVersion { // 2. // We need to make sure we do not sent livekit_alias in sticky events and that we drop all code for sending state events! export interface LocalTransportWithSFUConfig { - transport: LivekitTransport; + transport: LivekitTransportConfig; sfuConfig: SFUConfig; } export function isLocalTransportWithSFUConfig( - obj: LivekitTransport | LocalTransportWithSFUConfig, + obj: LivekitTransportConfig | LocalTransportWithSFUConfig, ): obj is LocalTransportWithSFUConfig { return "transport" in obj && "sfuConfig" in obj; } @@ -137,11 +137,10 @@ export const createLocalTransport$ = ({ return transport; }), switchMap((transport) => { - if (transport !== null && isLivekitTransport(transport)) { + if (transport !== null && isLivekitTransportConfig(transport)) { // Get the open jwt token to connect to the sfu const computeLocalTransportWithSFUConfig = async (): Promise => { - // await sleep(1000); return { transport, sfuConfig: await getSFUConfigWithOpenID( @@ -288,18 +287,6 @@ async function makeTransport( transport: { type: "livekit", livekit_service_url: url, - // WARNING PLS READ ME!!! - // This looks unintuitive especially considering that `sfuConfig.livekitAlias` exists. - // Why do we not use: `livekit_alias: sfuConfig.livekitAlias` - // - // - This is going to be used for sending our state event transport (focus_preferred) - // - In sticky events it is expected to NOT send this field at all. The transport is only the `type`, `livekit_service_url` - // - If we set it to the hased alias we get from the jwt, we will end up using the hashed alias as the body.roomId field - // in v0.16.0. (It will use oldest member transport. It is using the transport.livekit_alias as the body.roomId) - // - // TLDR this is a temporal field that allow for comaptibilty but the spec expects it to not exists. (but its existance also does not break anything) - // It is just named poorly: It was intetended to be the actual alias. But now we do pseudonymys ids so we use a hashed alias. - livekit_alias: roomId, }, sfuConfig, }; diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index cc430645..2c89eef5 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -26,7 +26,7 @@ import fetchMock from "fetch-mock"; import EventEmitter from "events"; import { type IOpenIDToken } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransport"; +import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc"; import { Connection, @@ -51,8 +51,9 @@ let fakeLivekitRoom: MockedObject; let localParticipantEventEmiter: EventEmitter; let fakeLocalParticipant: MockedObject; -const livekitFocus: LivekitTransport = { - livekit_alias: "!roomID:example.org", +const ROOM_ID = "!roomID:example.org"; + +const livekitFocus: LivekitTransportConfig = { livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", type: "livekit", }; @@ -112,6 +113,7 @@ function setupTest(): void { function setupRemoteConnection(): Connection { const opts: ConnectionOpts = { client: client, + roomId: ROOM_ID, transport: livekitFocus, scope: testScope, ownMembershipIdentity: ownMemberMock, @@ -154,6 +156,7 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, + roomId: ROOM_ID, transport: livekitFocus, scope: testScope, ownMembershipIdentity: ownMemberMock, @@ -170,6 +173,7 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, + roomId: ROOM_ID, transport: livekitFocus, scope: testScope, ownMembershipIdentity: ownMemberMock, @@ -221,6 +225,7 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, + roomId: ROOM_ID, transport: livekitFocus, scope: testScope, ownMembershipIdentity: ownMemberMock, @@ -279,6 +284,7 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, + roomId: ROOM_ID, transport: livekitFocus, scope: testScope, ownMembershipIdentity: ownMemberMock, diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index f649e931..617189d7 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -15,7 +15,7 @@ import { type Room as LivekitRoom, type RemoteParticipant, } from "livekit-client"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc"; import { BehaviorSubject, map } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; @@ -49,9 +49,11 @@ export interface ConnectionOpts { /** The identity parts to use on this connection */ ownMembershipIdentity: CallMembershipIdentityParts; /** The media transport to connect to. */ - transport: LivekitTransport; + transport: LivekitTransportConfig; /** The Matrix client to use for OpenID and SFU config requests. */ client: OpenIDClientParts; + /** The room ID this connection is associated with. */ + roomId: string; /** The observable scope to use for this connection. */ scope: ObservableScope; @@ -102,7 +104,7 @@ export class Connection { /** * The media transport to connect to. */ - public readonly transport: LivekitTransport; + public readonly transport: LivekitTransportConfig; public readonly livekitRoom: LivekitRoom; @@ -131,6 +133,47 @@ export class Connection { * */ protected stopped = false; + // TODO: can we just keep the ConnectionOpts object instead of spreading? + private readonly client: OpenIDClientParts; + private readonly roomId: string; + private readonly logger: Logger; + private readonly ownMembershipIdentity: CallMembershipIdentityParts; + private readonly existingSFUConfig?: SFUConfig; + /** + * Creates a new connection to a matrix RTC LiveKit backend. + * + * @param opts - Connection options {@link ConnectionOpts}. + * + * @param logger - The logger to use. + */ + public constructor(opts: ConnectionOpts, logger: Logger) { + this.ownMembershipIdentity = opts.ownMembershipIdentity; + this.existingSFUConfig = opts.existingSFUConfig; + this.roomId = opts.roomId; + this.logger = logger.getChild( + "[Connection " + opts.transport.livekit_service_url + "]", + ); + this.logger.info( + `constructor: ${opts.transport.livekit_service_url} roomId: ${this.roomId} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`, + ); + const { transport, client, scope } = opts; + + this.scope = scope; + this.livekitRoom = opts.livekitRoomFactory(); + this.transport = transport; + this.client = client; + + this.remoteParticipants$ = scope.behavior( + // Only tracks remote participants + connectedParticipantsObserver(this.livekitRoom), + ); + + scope.onEnd(() => { + this.logger.info(`Connection scope ended, stopping connection`); + void this.stop(); + }); + } + /** * Starts the connection. * @@ -231,7 +274,7 @@ export class Connection { this.client, this.ownMembershipIdentity, this.transport.livekit_service_url, - this.transport.livekit_alias, + this.roomId, // dont pass any custom opts for the subscribe only connections {}, this.logger, @@ -256,42 +299,4 @@ export class Connection { `stop: DONE disconnecing from lk room ${this.transport.livekit_service_url}`, ); } - - private readonly client: OpenIDClientParts; - private readonly logger: Logger; - private readonly ownMembershipIdentity: CallMembershipIdentityParts; - private readonly existingSFUConfig?: SFUConfig; - /** - * Creates a new connection to a matrix RTC LiveKit backend. - * - * @param opts - Connection options {@link ConnectionOpts}. - * - * @param logger - The logger to use. - */ - public constructor(opts: ConnectionOpts, logger: Logger) { - this.ownMembershipIdentity = opts.ownMembershipIdentity; - this.existingSFUConfig = opts.existingSFUConfig; - this.logger = logger.getChild( - "[Connection " + opts.transport.livekit_service_url + "]", - ); - this.logger.info( - `constructor: ${opts.transport.livekit_service_url} alias: ${opts.transport.livekit_alias} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`, - ); - const { transport, client, scope } = opts; - - this.scope = scope; - this.livekitRoom = opts.livekitRoomFactory(); - this.transport = transport; - this.client = client; - - this.remoteParticipants$ = scope.behavior( - // Only tracks remote participants - connectedParticipantsObserver(this.livekitRoom), - ); - - scope.onEnd(() => { - this.logger.info(`Connection scope ended, stopping connection`); - void this.stop(); - }); - } } diff --git a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts index aa20037c..38a09898 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionFactory.ts @@ -16,7 +16,7 @@ import { type Logger } from "matrix-js-sdk/lib/logger"; // imported as inline to support worker when loaded from a cdn (cross domain) import E2EEWorker from "livekit-client/e2ee-worker?worker&inline"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc/LivekitTransport"; +import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc"; import { type ObservableScope } from "../../ObservableScope.ts"; import { Connection } from "./Connection.ts"; @@ -33,7 +33,7 @@ import { defaultLiveKitOptions } from "../../../livekit/options.ts"; export interface ConnectionFactory { createConnection( scope: ObservableScope, - transport: LivekitTransport, + transport: LivekitTransportConfig, ownMembershipIdentity: CallMembershipIdentityParts, logger: Logger, sfuConfig?: SFUConfig, @@ -47,6 +47,7 @@ export class ECConnectionFactory implements ConnectionFactory { * Creates a ConnectionFactory for LiveKit connections. * * @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens. + * @param roomId - The current room ID. * @param devices - Used for video/audio out/in capture options. * @param processorState$ - Effects like background blur (only for publishing connection?) * @param livekitKeyProvider - Optional key provider for end-to-end encryption. @@ -57,6 +58,7 @@ export class ECConnectionFactory implements ConnectionFactory { */ public constructor( private client: OpenIDClientParts, + private readonly roomId: string, private devices: MediaDevices, private processorState$: Behavior, livekitKeyProvider: BaseKeyProvider | undefined, @@ -95,7 +97,7 @@ export class ECConnectionFactory implements ConnectionFactory { */ public createConnection( scope: ObservableScope, - transport: LivekitTransport, + transport: LivekitTransportConfig, ownMembershipIdentity: CallMembershipIdentityParts, logger: Logger, sfuConfig?: SFUConfig, @@ -103,6 +105,7 @@ export class ECConnectionFactory implements ConnectionFactory { return new Connection( { existingSFUConfig: sfuConfig, + roomId: this.roomId, transport, client: this.client, scope: scope, diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts index cf930415..52f825c7 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts @@ -7,7 +7,10 @@ Please see LICENSE in the repository root for full details. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { BehaviorSubject } from "rxjs"; -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { + type LivekitTransport, + type LivekitTransportConfig, +} from "matrix-js-sdk/lib/matrixrtc"; import { type RemoteParticipant } from "livekit-client"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -24,16 +27,14 @@ import { constant, type Behavior } from "../../Behavior.ts"; // Some test constants -const TRANSPORT_1: LivekitTransport = { +const TRANSPORT_1: LivekitTransportConfig = { type: "livekit", livekit_service_url: "https://lk.example.org", - livekit_alias: "!alias:example.org", }; -const TRANSPORT_2: LivekitTransport = { +const TRANSPORT_2: LivekitTransportConfig = { type: "livekit", livekit_service_url: "https://lk.sample.com", - livekit_alias: "!alias:sample.com", }; let fakeConnectionFactory: ConnectionFactory; diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts index 8bc008ea..a7bf00d7 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; +import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, map, of, switchMap } from "rxjs"; import { type Logger } from "matrix-js-sdk/lib/logger"; import { type RemoteParticipant } from "livekit-client"; @@ -42,8 +42,8 @@ export class ConnectionManagerData { } } - private getKey(transport: LivekitTransport): string { - return transport.livekit_service_url + "|" + transport.livekit_alias; + private getKey(transport: LivekitTransportConfig): string { + return transport.livekit_service_url; } public getConnections(): Connection[] { @@ -51,15 +51,15 @@ export class ConnectionManagerData { } public getConnectionForTransport( - transport: LivekitTransport, + transport: LivekitTransportConfig, ): Connection | null { return this.store.get(this.getKey(transport))?.connection ?? null; } public getParticipantsForTransport( - transport: LivekitTransport, + transport: LivekitTransportConfig, ): RemoteParticipant[] { - const key = transport.livekit_service_url + "|" + transport.livekit_alias; + const key = this.getKey(transport); const existing = this.store.get(key); if (existing) { return existing.participants; @@ -72,7 +72,7 @@ interface Props { scope: ObservableScope; connectionFactory: ConnectionFactory; localTransport$: Behavior; - remoteTransports$: Behavior>; + remoteTransports$: Behavior>; logger: Logger; ownMembershipIdentity: CallMembershipIdentityParts; @@ -123,7 +123,7 @@ export function createConnectionManager$({ * externally this is modified via `registerTransports()`. */ const localAndRemoteTransports$: Behavior< - Epoch<(LivekitTransport | LocalTransportWithSFUConfig)[]> + Epoch<(LivekitTransportConfig | LocalTransportWithSFUConfig)[]> > = scope.behavior( combineLatest([remoteTransports$, localTransport$]).pipe( // Combine local and remote transports into one transport array @@ -168,19 +168,13 @@ export function createConnectionManager$({ // This is the local transport only the `LocalTransportWithSFUConfig` has a `sfuConfig` field const { transport, sfuConfig } = transportWithOrWithoutSfuConfig; yield { - keys: [ - transport.livekit_service_url, - transport.livekit_alias, - sfuConfig, - ], + keys: [transport.livekit_service_url, sfuConfig], data: undefined, }; } else { - const transport = transportWithOrWithoutSfuConfig; yield { keys: [ - transport.livekit_service_url, - transport.livekit_alias, + transportWithOrWithoutSfuConfig.livekit_service_url, undefined as undefined | SFUConfig, ], data: undefined, @@ -188,13 +182,12 @@ export function createConnectionManager$({ } } }, - (scope, _data$, serviceUrl, alias, sfuConfig) => { + (scope, _data$, serviceUrl, sfuConfig) => { const connection = connectionFactory.createConnection( scope, { type: "livekit", livekit_service_url: serviceUrl, - livekit_alias: alias, }, ownMembershipIdentity, logger, @@ -254,7 +247,7 @@ export function createConnectionManager$({ return { connectionManagerData$ }; } -function removeDuplicateTransports( +function removeDuplicateTransports( transports: T[], ): T[] { return transports.reduce((acc, transport) => { diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index b54a50df..04c211d9 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -7,8 +7,8 @@ Please see LICENSE in the repository root for full details. import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; import { - type LivekitTransport, type CallMembership, + type LivekitTransportConfig, } from "matrix-js-sdk/lib/matrixrtc"; import { combineLatest, filter, map } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; @@ -62,7 +62,7 @@ export interface RemoteMatrixLivekitMember extends MatrixLivekitMember { interface Props { scope: ObservableScope; membershipsWithTransport$: Behavior< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch<{ membership: CallMembership; transport?: LivekitTransportConfig }[]> >; connectionManager: IConnectionManager; } @@ -147,18 +147,12 @@ export function createMatrixLivekitMembers$({ // TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$) // TODO add this to the JS-SDK -export function areLivekitTransportsEqual( +export function areLivekitTransportsEqual( t1: T | null, t2: T | null, ): boolean { - if (t1 && t2) - return ( - t1.livekit_service_url === t2.livekit_service_url && - // In case we have different lk rooms in the same SFU (depends on the livekit authorization service) - // It is only needed in case the livekit authorization service is not behaving as expected (or custom implementation) - // Also LivekitTransport is planned to become a `ConnectionIdentifier` which moves this equal somewhere else. - t1.livekit_alias === t2.livekit_alias - ); - if (!t1 && !t2) return true; - return false; + if (t1 && t2) { + return t1.livekit_service_url === t2.livekit_service_url; + } + return !t1 && !t2; } diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index e174a1cc..057c068c 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -7,8 +7,7 @@ Please see LICENSE in the repository root for full details. import { type CallMembership, - isLivekitTransport, - type LivekitTransport, + type LivekitTransportConfig, type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; @@ -21,25 +20,33 @@ import { type ObservableScope, } from "./ObservableScope"; import { type Behavior } from "./Behavior"; +import { isLivekitTransportConfig } from "../../../matrix-js-sdk/src/matrixrtc"; export const membershipsAndTransports$ = ( scope: ObservableScope, memberships$: Behavior>, ): { membershipsWithTransport$: Behavior< - Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]> + Epoch<{ membership: CallMembership; transport?: LivekitTransportConfig }[]> >; - transports$: Behavior>; + transports$: Behavior>; } => { /** * Lists the transports used by ourselves, plus all other MatrixRTC session - * members. For completeness this also lists the preferred transport and - * whether we are in multi-SFU mode or sticky events mode (because - * advertisedTransport$ wants to read them at the same time, and bundling data - * together when it might change together is what you have to do in RxJS to - * avoid reading inconsistent state or observing too many changes.) + * members. + * For completeness this also lists the preferred transport and + * whether we are in multi-SFU mode or sticky events mode. + * `advertisedTransport$` reads these values together, so bundling them avoids inconsistent state or + * excessive updates when using RxJS. */ - const membershipsWithTransport$ = scope.behavior( + const membershipsWithTransport$: Behavior< + Epoch< + { + membership: CallMembership; + transport: LivekitTransportConfig | undefined; + }[] + > + > = scope.behavior( memberships$.pipe( mapEpoch((memberships) => { return memberships.map((membership) => { @@ -47,14 +54,16 @@ export const membershipsAndTransports$ = ( const transport = membership.getTransport(oldestMembership); return { membership, - transport: isLivekitTransport(transport) ? transport : undefined, + transport: isLivekitTransportConfig(transport) + ? transport + : undefined, }; }); }), ), ); - const transports$ = scope.behavior( + const transports$: Behavior> = scope.behavior( membershipsWithTransport$.pipe( mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))), ),