Skip to content

Conversation

toger5
Copy link
Contributor

@toger5 toger5 commented Oct 6, 2025

This PR changes the construction of the CallMembership object.
It will add an optional relatedEvent parameter which will store initial connect event for this call participation.

This is then used to fetch metadata (like application) and the origin_server_ts of the initial join event.

This requires that MatrixRTCSession.sessionMembershipsForSlot becomes async since it now also needs to fetch related events.

This change needs a bit of test adjustments and other places of the codebase need updating.

Checklist

  • Tests written for new code (and old code if feasible).
  • New or updated public/exported symbols have accurate TSDoc documentation.
  • Linter and other CI checks pass.
  • Sign-off given on the changes (see CONTRIBUTING.md).

@toger5 toger5 changed the title Toger5/use relation based call membership create ts [MatrixRTC] use relation based call membership create ts Oct 6, 2025
@toger5 toger5 force-pushed the toger5/use-relation-based-CallMembership-create-ts branch from 9e977fb to aa1cbe9 Compare October 7, 2025 13:20
Signed-off-by: Timo K <[email protected]>
Signed-off-by: Timo K <[email protected]>
Copy link
Contributor Author

@toger5 toger5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if two MatrixRTCSession.sessionMembershipsForSlot are running at the same time?
The second one could finish before the other.... Is this an issue?

@toger5 toger5 marked this pull request as ready for review October 7, 2025 18:25
@toger5 toger5 requested a review from a team as a code owner October 7, 2025 18:25
@toger5 toger5 requested review from BillCarsonFr and removed request for a team October 7, 2025 18:25
@robintown
Copy link
Member

@toger5 Is the diff meant to be as big as it is? I think I'm looking at a lot of lint fixes, or something.

@toger5
Copy link
Contributor Author

toger5 commented Oct 8, 2025

Is the diff meant to be as big as it is?
Yes the diff is meant to be this big. There is a very large amount of lines which add async and await

On top of this, the construct for callMembership has a signature change which requires a change in makeMockEvent.

before
new CallMembership(event, event.content, ....) now new CallMembership(event, relatedEvent,...)event.content will be aquired indirectly by reading event.getContent().relatedEvent` is new.

Base automatically changed from voip-team/multi-SFU to develop October 8, 2025 19:28
@sonarqubecloud sonarqubecloud bot requested a review from a team as a code owner October 8, 2025 19:28
Copy link
Member

@BillCarsonFr BillCarsonFr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks to be doind more than what the title suggests ^^
Nice to see all the tests

expect(membership.transports).toEqual([mockFocus]);
});

describe("getTransport", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like in all the tests foci_preferred is always an array of 1. Can the test clarify how this works?
I think it is a non multi-sfu thing to avoid switching focus, but for multi-sfu it is always one?
=> maybe a bug in the spec? the well-known also gives a list :/ should we pick one random?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment:

...membershipTemplate,
                        // The list of foci_preferred provided by the homeserver. (in the test example we just provide one)
                        // The oldest member logic will use the first item in this list.
                        // The multi-sfu logic will (theoretically) also use all the items in the list at once
                        // (currently the js-sdk sets it to only one item in multi-sfu mode).
                        foci_preferred: [mockFocus],
                        focus_active: { type: "livekit", focus_selection: "oldest_membership" },

I hope that clarifies things.

const membership = new CallMembership(makeMockEvent(0, membershipTemplate));

// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit lost, the transport is only { type: livekit }? shouldn't there be a livekit_service_url and the alias?
Maybe we can keep "realistic" test data, and then use expect with expect.objectContaining({ .. }) to just test the field you want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the tests to contain more realistic data.

expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
});
});
describe("correct values from computed fields", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bonus point to have a test.each() that would try different configuration without reapeating code

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => {
  expect(a + b).toBe(expected)
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean using different membership objects?
I am not sure what additional stability this would result in. The code just gets fields from different branches of the object but does not do things with the values. (except the expiration which we do in a dedicated test section)

client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
client.sendEvent = jest.fn().mockResolvedValue({ event_id: "success" });
client.decryptEventIfNeeded = jest.fn();
client.fetchRoomEvent = jest.fn().mockResolvedValue(undefined);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly this PR, but shouldn't we use getMockClientWithEventEmitter({}) insteaf of overriding the properties like that?
Currently this could be doing more thing than we think and polute the test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definilty worth a discussion. I remember we discussed that it can also be benefitital to not use too many helpers so the tests are more accessible if devs are not familiar with the helper methods.

getId: jest.fn().mockReturnValue(secureRandomString(8)),
isDecryptionFailure: jest.fn().mockReturnValue(false),
} as unknown as MatrixEvent;
return new MatrixEvent({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a makeMockEvent utility in mock.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just updating this method?
Or do you mean test-utils.ts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right, I was expecting to see a vi.mock()

* Start sending all necessary events to make this user participate in the RTC session.
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
* If multiSfuFocus is set, this is only needed if this client wants to publish to multiple transports simultaneously.
* @param multiSfuFocus the active focus to use in the joined RTC membership event. Setting this implies the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still confused by that. My understanding was that there is a list of FOCI that my homeserver recommends. But if I join an existing call, there is already an active FOCI people are on, and we want to keep this one on top of my list in case I become the oldest member later, so that we don't switch focus.

So foci_active made sense to me, it was the existing Foci people are already using that I should connect on but also add on top of my list to avoid future jumps.

Now this signature do not make sense for me :/
Maybe if we change it to fociPreferred: Transport[], oldestMembership?: MembershipData, ..

Also I don't get why fociPreferred is an array

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So foci_active made sense to me, it was the existing Foci people are already using that I should connect on but also add on top of my list to avoid future jumps.

focus_active was just the description what system a specific user was using to decide what transport to use.

Copy link
Contributor Author

@toger5 toger5 Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really is only used with m.call.member events (not with m.rtc.member).

So it is basically depreacted anyways.

Also I don't get why fociPreferred is an array

This was so multiple foci could be defined as fallbacks. All this is entirely depreacted due to multi-sfu.
In the new spec there is onlly rtc_transports that is the list of currently in use publishing transports.

if (!this.state.delayId) {
// Delay id got reset. This action was used to check if the hs canceled the delayed event when the join state got sent.
return createInsertActionUpdate(MembershipActionType.SendDelayedEvent);
return Promise.resolve(createInsertActionUpdate(MembershipActionType.SendDelayedEvent));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed? just returning the value is doing the same thing no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the method is not async. I had issues with tests if this became an async method... It also is nto really async but just a wrapper to construct a promise.

Copy link
Member

@t3chguy t3chguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the merge conflicts before review

@toger5 toger5 requested a review from t3chguy October 9, 2025 09:49
Copy link
Member

@t3chguy t3chguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please be a little more mindful to reviewers, there's little point in reviewing something with so many CI failures given it'll need re-reviewing after fixing.

@toger5 toger5 marked this pull request as draft October 9, 2025 12:31
@toger5
Copy link
Contributor Author

toger5 commented Oct 9, 2025

Could we please be a little more mindful to reviewers, there's little point in reviewing something with so many CI failures given it'll need re-reviewing after fixing.

Converted it to draft. As it should be until ci is sorted. Sorry for the confusion/overhead.

Signed-off-by: Timo K <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants