diff --git a/spec/unit/matrixrtc/MembershipManager.spec.ts b/spec/unit/matrixrtc/MembershipManager.spec.ts index 482439eca..b5a4e22f1 100644 --- a/spec/unit/matrixrtc/MembershipManager.spec.ts +++ b/spec/unit/matrixrtc/MembershipManager.spec.ts @@ -511,6 +511,32 @@ describe("MembershipManager", () => { expect(client._unstable_updateDelayedEvent).toHaveBeenCalled(); }); + + it("updates the UpdateExpiry entry in the action scheduler", async () => { + const manager = new MembershipManager({}, room, client, () => undefined); + manager.join([focus], focusActive); + await jest.advanceTimersByTimeAsync(1); + // clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` + (client.sendStateEvent as Mock).mockClear(); + (client._unstable_updateDelayedEvent as Mock).mockClear(); + (client._unstable_sendDelayedStateEvent as Mock).mockClear(); + + (client._unstable_updateDelayedEvent as Mock).mockRejectedValueOnce( + new MatrixError({ errcode: "M_NOT_FOUND" }), + ); + + const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent); + await jest.advanceTimersByTimeAsync(10_000); + await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]); + resolve({ delay_id: "id" }); + await jest.advanceTimersByTimeAsync(10_000); + + expect(client.sendStateEvent).toHaveBeenCalled(); + expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled(); + + expect(client._unstable_updateDelayedEvent).toHaveBeenCalled(); + expect(manager.status).toBe(Status.Connected); + }); }); // TODO: Not sure about this name diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts index 9dcd876de..73f2cdf2d 100644 --- a/src/matrixrtc/MembershipManager.ts +++ b/src/matrixrtc/MembershipManager.ts @@ -243,7 +243,7 @@ export class MembershipManager this.logger.warn("Missing own membership: force re-join"); this.state.hasMemberStateEvent = false; - if (this.scheduler.actions.find((a) => sendingMembershipActions.includes(a.type as MembershipActionType))) { + if (this.scheduler.actions.some((a) => sendingMembershipActions.includes(a.type as MembershipActionType))) { this.logger.error( "tried adding another `SendDelayedEvent` actions even though we already have one in the Queue\nActionQueueOnMemberUpdate:", this.scheduler.actions, @@ -624,8 +624,21 @@ export class MembershipManager this.state.expireUpdateIterations = 1; this.state.hasMemberStateEvent = true; this.resetRateLimitCounter(MembershipActionType.SendJoinEvent); + // An UpdateExpiry action might be left over from a previous join event. + // We can reach sendJoinEvent when the delayed leave event gets send by the HS. + // The branch where we might have a leftover UpdateExpiry action is: + // RestartDelayedEvent (cannot find it, server removed it) + // -> SendDelayedEvent (send new delayed event) + // -> SendJoinEvent (here with a still scheduled UpdateExpiry action) + const actionsWithoutUpdateExpiry = this.scheduler.actions.filter( + (a) => + a.type !== MembershipActionType.UpdateExpiry && // A new UpdateExpiry action with an updated will be scheduled, + a.type !== MembershipActionType.SendJoinEvent, // Manually remove the SendJoinEvent action, + ); return { - insert: [ + replace: [ + ...actionsWithoutUpdateExpiry, + // To check if the delayed event is still there or got removed by inserting the stateEvent, we need to restart it. { ts: Date.now(), type: MembershipActionType.RestartDelayedEvent }, { ts: this.computeNextExpiryActionTs(this.state.expireUpdateIterations),