matrix-js-sdk 42.4.0 → 43.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/@types/PushRules.d.ts +3 -1
  3. package/lib/@types/PushRules.d.ts.map +1 -1
  4. package/lib/@types/PushRules.js +2 -0
  5. package/lib/@types/PushRules.js.map +1 -1
  6. package/lib/@types/event.d.ts +1 -0
  7. package/lib/@types/event.d.ts.map +1 -1
  8. package/lib/@types/event.js.map +1 -1
  9. package/lib/@types/matrix-sdk-crypto-wasm.d.js.map +1 -1
  10. package/lib/client.d.ts +88 -37
  11. package/lib/client.d.ts.map +1 -1
  12. package/lib/client.js +72 -47
  13. package/lib/client.js.map +1 -1
  14. package/lib/crypto-api/index.d.ts +3 -3
  15. package/lib/crypto-api/index.d.ts.map +1 -1
  16. package/lib/crypto-api/index.js.map +1 -1
  17. package/lib/embedded.d.ts +17 -1
  18. package/lib/embedded.d.ts.map +1 -1
  19. package/lib/embedded.js +46 -1
  20. package/lib/embedded.js.map +1 -1
  21. package/lib/errors.d.ts +1 -1
  22. package/lib/errors.d.ts.map +1 -1
  23. package/lib/errors.js +2 -2
  24. package/lib/errors.js.map +1 -1
  25. package/lib/http-api/fetch.d.ts +7 -1
  26. package/lib/http-api/fetch.d.ts.map +1 -1
  27. package/lib/http-api/fetch.js +14 -5
  28. package/lib/http-api/fetch.js.map +1 -1
  29. package/lib/http-api/interface.d.ts +34 -8
  30. package/lib/http-api/interface.d.ts.map +1 -1
  31. package/lib/http-api/interface.js +10 -3
  32. package/lib/http-api/interface.js.map +1 -1
  33. package/lib/http-api/refresh.d.ts +30 -11
  34. package/lib/http-api/refresh.d.ts.map +1 -1
  35. package/lib/http-api/refresh.js +72 -5
  36. package/lib/http-api/refresh.js.map +1 -1
  37. package/lib/matrix.d.ts +1 -1
  38. package/lib/matrix.d.ts.map +1 -1
  39. package/lib/matrix.js.map +1 -1
  40. package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
  41. package/lib/matrixrtc/CallMembership.js +20 -6
  42. package/lib/matrixrtc/CallMembership.js.map +1 -1
  43. package/lib/matrixrtc/IMembershipManager.d.ts +5 -0
  44. package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
  45. package/lib/matrixrtc/IMembershipManager.js.map +1 -1
  46. package/lib/matrixrtc/MatrixRTCSession.d.ts +27 -1
  47. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
  48. package/lib/matrixrtc/MatrixRTCSession.js +58 -36
  49. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  50. package/lib/matrixrtc/MatrixRTCSessionManager.js +1 -1
  51. package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
  52. package/lib/matrixrtc/MembershipManager.d.ts +76 -0
  53. package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
  54. package/lib/matrixrtc/MembershipManager.js +264 -47
  55. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  56. package/lib/matrixrtc/index.d.ts +1 -1
  57. package/lib/matrixrtc/index.d.ts.map +1 -1
  58. package/lib/matrixrtc/index.js +1 -1
  59. package/lib/matrixrtc/index.js.map +1 -1
  60. package/lib/matrixrtc/membershipData/common.d.ts +8 -0
  61. package/lib/matrixrtc/membershipData/common.d.ts.map +1 -1
  62. package/lib/matrixrtc/membershipData/common.js +10 -0
  63. package/lib/matrixrtc/membershipData/common.js.map +1 -1
  64. package/lib/matrixrtc/membershipData/index.d.ts +1 -1
  65. package/lib/matrixrtc/membershipData/index.d.ts.map +1 -1
  66. package/lib/matrixrtc/membershipData/index.js +1 -1
  67. package/lib/matrixrtc/membershipData/index.js.map +1 -1
  68. package/lib/matrixrtc/types.d.ts +104 -11
  69. package/lib/matrixrtc/types.d.ts.map +1 -1
  70. package/lib/matrixrtc/types.js +263 -19
  71. package/lib/matrixrtc/types.js.map +1 -1
  72. package/lib/matrixrtc/utils.d.ts +22 -1
  73. package/lib/matrixrtc/utils.d.ts.map +1 -1
  74. package/lib/matrixrtc/utils.js +38 -0
  75. package/lib/matrixrtc/utils.js.map +1 -1
  76. package/lib/models/room-sticky-events.d.ts +7 -0
  77. package/lib/models/room-sticky-events.d.ts.map +1 -1
  78. package/lib/models/room-sticky-events.js +27 -0
  79. package/lib/models/room-sticky-events.js.map +1 -1
  80. package/lib/models/thread.d.ts.map +1 -1
  81. package/lib/models/thread.js +8 -0
  82. package/lib/models/thread.js.map +1 -1
  83. package/lib/oauth/authorize.js +1 -1
  84. package/lib/oauth/authorize.js.map +1 -1
  85. package/lib/oauth/index.d.ts +3 -5
  86. package/lib/oauth/index.d.ts.map +1 -1
  87. package/lib/oauth/index.js +7 -7
  88. package/lib/oauth/index.js.map +1 -1
  89. package/lib/oauth/tokenRefresher.d.ts +10 -4
  90. package/lib/oauth/tokenRefresher.d.ts.map +1 -1
  91. package/lib/oauth/tokenRefresher.js +34 -24
  92. package/lib/oauth/tokenRefresher.js.map +1 -1
  93. package/lib/receipt-accumulator.d.ts +10 -11
  94. package/lib/receipt-accumulator.d.ts.map +1 -1
  95. package/lib/receipt-accumulator.js +26 -25
  96. package/lib/receipt-accumulator.js.map +1 -1
  97. package/lib/room-summary-api.d.ts +71 -0
  98. package/lib/room-summary-api.d.ts.map +1 -0
  99. package/lib/room-summary-api.js +82 -0
  100. package/lib/room-summary-api.js.map +1 -0
  101. package/lib/rust-crypto/backup.d.ts +7 -0
  102. package/lib/rust-crypto/backup.d.ts.map +1 -1
  103. package/lib/rust-crypto/backup.js +41 -25
  104. package/lib/rust-crypto/backup.js.map +1 -1
  105. package/lib/sync.d.ts.map +1 -1
  106. package/lib/sync.js +6 -1
  107. package/lib/sync.js.map +1 -1
  108. package/package.json +6 -6
  109. package/src/@types/PushRules.ts +2 -0
  110. package/src/@types/event.ts +1 -1
  111. package/src/@types/matrix-sdk-crypto-wasm.d.ts +15 -12
  112. package/src/client.ts +105 -62
  113. package/src/crypto-api/index.ts +3 -3
  114. package/src/embedded.ts +61 -2
  115. package/src/errors.ts +2 -1
  116. package/src/http-api/fetch.ts +14 -5
  117. package/src/http-api/interface.ts +36 -8
  118. package/src/http-api/refresh.ts +101 -17
  119. package/src/matrix.ts +1 -1
  120. package/src/matrixrtc/CallMembership.ts +22 -4
  121. package/src/matrixrtc/IMembershipManager.ts +6 -0
  122. package/src/matrixrtc/MatrixRTCSession.ts +72 -49
  123. package/src/matrixrtc/MatrixRTCSessionManager.ts +1 -1
  124. package/src/matrixrtc/MembershipManager.ts +284 -61
  125. package/src/matrixrtc/index.ts +7 -1
  126. package/src/matrixrtc/membershipData/common.ts +12 -0
  127. package/src/matrixrtc/membershipData/index.ts +1 -1
  128. package/src/matrixrtc/types.ts +351 -23
  129. package/src/matrixrtc/utils.ts +54 -1
  130. package/src/models/room-sticky-events.ts +26 -1
  131. package/src/models/thread.ts +9 -0
  132. package/src/oauth/authorize.ts +1 -1
  133. package/src/oauth/index.ts +7 -8
  134. package/src/oauth/tokenRefresher.ts +15 -5
  135. package/src/receipt-accumulator.ts +30 -26
  136. package/src/room-summary-api.ts +114 -0
  137. package/src/rust-crypto/backup.ts +46 -33
  138. package/src/sync.ts +9 -1
@@ -25,7 +25,7 @@ import { type Room } from "../models/room.ts";
25
25
  import { type CallMembership, DEFAULT_EXPIRE_DURATION } from "./CallMembership.ts";
26
26
  import { type Transport, isMyMembership, type RTCCallIntent, Status, type SlotDescription } from "./types.ts";
27
27
  import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts";
28
- import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
28
+ import { type Action, ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
29
29
  import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
30
30
  import { UnsupportedDelayedEventsEndpointError } from "../errors.ts";
31
31
  import {
@@ -35,26 +35,27 @@ import {
35
35
  } from "./IMembershipManager.ts";
36
36
  import { type RtcMembershipData, type SessionMembershipData } from "./membershipData/index.ts";
37
37
  import { computeSlotId } from "./utils.ts";
38
+ import { deepCompare } from "../utils.ts";
38
39
  import { isLivekitTransportConfig } from "./LivekitTransport.ts";
39
40
 
40
41
  /* MembershipActionTypes:
41
- On Join: ───────────────┐ ┌───────────────(1)───────────┐
42
- ▼ ▼ │
43
- ┌────────────────┐ │
44
- │SendDelayedEvent│ ──────(2)───┐ │
45
- └────────────────┘ │ │
46
- │(3) │ │
47
- ▼ │ │
48
- ┌─────────────┐ │ │
49
- ┌──────(4)───│SendJoinEvent│────(4)─────┐ │ │
50
- │ └─────────────┘ │ │ │
51
- │ ┌─────┐ ┌──────┐ │ │ │
52
- ▼ ▼ │ │ ▼ ▼ ▼ │
53
- ┌────────────┐ │ │ ┌───────────────────┐ │
54
- │UpdateExpiry│ (s) (s)|RestartDelayedEvent│ │
55
- └────────────┘ │ │ └───────────────────┘ │
56
- │ │ │ │ │ │
57
- └─────┘ └──────┘ └───────┘
42
+ On Join: ───────────────────┐ ┌───────────────(1)───────────┐
43
+ ▼ ▼ │
44
+ ┌────────────────┐ │
45
+ ┌───────(6)───────────▶│SendDelayedEvent│ ──────(2)───┐ │
46
+ │ └────────────────┘ │ │
47
+ │ │(3) │ │
48
+ │ ▼ │ │
49
+ │ ┌─────────────┐ │ │
50
+ │ ┌──────(4)───│SendJoinEvent│────(4)─────┐ │ │
51
+ │ │ └─────────────┘ │ │ │
52
+ │ │ ┌─────┐ ┌──────┐ │ │ │
53
+ │ ▼ ▼ │ │ ▼ ▼ ▼ │
54
+ │ ┌────────────┐ │ │ ┌───────────────────┐ │
55
+ └───│UpdateExpiry│ (s) (s)|RestartDelayedEvent│ │
56
+ └────────────┘ │ │ └───────────────────┘ │
57
+ │ │ │ │ │ │
58
+ └─────┘ └──────┘ └───────┘
58
59
 
59
60
  On Leave: ───────── STOP ALL ABOVE
60
61
  ▼
@@ -75,6 +76,10 @@ On Leave: ───────── STOP ALL ABOVE
75
76
  (4) Both (UpdateExpiry and RestartDelayedEvent) actions are
76
77
  scheduled when successfully sending the state event
77
78
  (5) Only if delayed event sending failed (fallback)
79
+ (6) Before extending `expires` we make sure a delayed leave
80
+ event will outlive the update. If it was only cancelled we
81
+ schedule a new one and retry; if it already fired, or the
82
+ membership expires while we retry, we join again from scratch
78
83
  (s) Successful restart/resend
79
84
  */
80
85
 
@@ -96,7 +101,7 @@ export enum MembershipActionType {
96
101
 
97
102
  SendJoinEvent = "SendJoinEvent",
98
103
  // -> MembershipActionType.SendJoinEvent if we run into a rate limit and need to retry
99
- // -> MembershipActionType.Update if we successfully send the join event then schedule the expire event update
104
+ // -> MembershipActionType.UpdateExpiry if we successfully send the join event then schedule the expire event update
100
105
  // -> DelayedLeaveActionType.RestartDelayedEvent to recheck the delayed event
101
106
 
102
107
  RestartDelayedEvent = "RestartDelayedEvent",
@@ -105,7 +110,10 @@ export enum MembershipActionType {
105
110
  // -> DelayedLeaveActionType.RestartDelayedEvent on success we schedule the next restart
106
111
 
107
112
  UpdateExpiry = "UpdateExpiry",
108
- // -> MembershipActionType.Update if the timeout has passed so the next update is required.
113
+ // -> MembershipActionType.UpdateExpiry if the timeout has passed so the next update is required, or if we first
114
+ // have to wait for a delayed leave event that can protect the updated membership.
115
+ // -> MembershipActionType.SendDelayedEvent if our delayed leave event is gone. Either to schedule a new one for
116
+ // the membership we still have, or to join again from scratch because the server already removed us.
109
117
 
110
118
  SendScheduledDelayedLeaveEvent = "SendScheduledDelayedLeaveEvent",
111
119
  // -> MembershipActionType.SendLeaveEvent on failure (not found) we need to send the leave manually and cannot use the scheduled delayed event
@@ -138,12 +146,25 @@ export interface MembershipManagerState {
138
146
  networkErrorRetries: Map<MembershipActionType, number>;
139
147
  /** The time at which we expect the server to send the delayed leave event. */
140
148
  expectedServerDelayLeaveTs?: number;
149
+ /** The time at which the membership we last sent stops being valid for other clients.
150
+ * Recorded when sending, see `membershipExpiryTsOf`. */
151
+ membershipExpiryTs: number;
141
152
  /** This is used to track if the client expects the scheduled delayed leave event to have
142
153
  * been sent because restarting failed during the available time.
143
154
  * Once we resend the delayed event or successfully restarted it will get unset. */
144
155
  probablyLeft: boolean;
145
156
  }
146
157
 
158
+ /**
159
+ * The outcome of a delayed leave event restart attempt.
160
+ *
161
+ * - `"ok"`: the server restarted it, it will fire `delayedLeaveEventDelayMs` from now.
162
+ * - `"gone"`: the server does not know the delay id anymore. It either sent the event or lost/cancelled it.
163
+ * - `"unsupported"`: the server does not support the delayed events endpoint.
164
+ * - `{ error }`: any other failure, for the caller to turn into a retry or to rethrow.
165
+ */
166
+ type RestartDelayedEventOutcome = "ok" | "gone" | "unsupported" | { error: unknown };
167
+
147
168
  function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
148
169
  return {
149
170
  insert: [{ ts: Date.now() + (offset ?? 0), type }],
@@ -188,6 +209,7 @@ export class MembershipManager
188
209
  private activated = false;
189
210
  private readonly logger: Logger;
190
211
  protected callIntent: RTCCallIntent | undefined;
212
+ protected applicationData: Record<string, unknown> | undefined;
191
213
 
192
214
  public isActivated(): boolean {
193
215
  return this.activated;
@@ -309,6 +331,17 @@ export class MembershipManager
309
331
  await this.sendJoinEvent();
310
332
  }
311
333
 
334
+ public async updateApplicationData(applicationData: Record<string, unknown>): Promise<void> {
335
+ if (!this.activated || !this.ownMembership) {
336
+ throw Error("You cannot update your application data before joining the call");
337
+ }
338
+ if (deepCompare(this.applicationData ?? {}, applicationData)) {
339
+ return; // No-op
340
+ }
341
+ this.applicationData = applicationData;
342
+ await this.sendJoinEvent();
343
+ }
344
+
312
345
  /**
313
346
  * @throws if the client does not return user or device id.
314
347
  * @param joinConfig
@@ -334,6 +367,7 @@ export class MembershipManager
334
367
  this.stateKey = this.makeMembershipStateKey(userId, deviceId);
335
368
  this.state = MembershipManager.defaultState;
336
369
  this.callIntent = joinConfig?.callIntent;
370
+ this.applicationData = joinConfig?.applicationData;
337
371
  this.scheduler = new ActionScheduler((type): Promise<ActionUpdate> => {
338
372
  if (this.oldStatus) {
339
373
  // we put this at the beginning of the actions scheduler loop handle callback since it is a loop this
@@ -369,6 +403,7 @@ export class MembershipManager
369
403
  delayId: undefined,
370
404
 
371
405
  startTime: 0,
406
+ membershipExpiryTs: 0,
372
407
  rateLimitRetries: new Map(),
373
408
  networkErrorRetries: new Map(),
374
409
  expireUpdateIterations: 1,
@@ -586,7 +621,13 @@ export class MembershipManager
586
621
  this.emit(MembershipManagerEvent.DelayIdChanged, this.state.delayId);
587
622
  }
588
623
 
589
- private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
624
+ /**
625
+ * Attempts to restart the delayed leave event and keeps everything that depends on it in sync.
626
+ *
627
+ * @param delayId the id of the delayed leave event to restart
628
+ * @returns the outcome of the attempt.
629
+ */
630
+ private async tryRestartDelayedEvent(delayId: string): Promise<RestartDelayedEventOutcome> {
590
631
  // Compute the duration until we expect the server to send the delayed leave event.
591
632
  const durationUntilServerDelayedLeave = this.state.expectedServerDelayLeaveTs
592
633
  ? this.state.expectedServerDelayLeaveTs - Date.now()
@@ -606,42 +647,59 @@ export class MembershipManager
606
647
  );
607
648
  });
608
649
 
609
- // The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
610
- // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
611
- return await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise])
612
- .then(() => {
613
- // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
614
- // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
615
- // successful restart requests.
616
- this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
617
- this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
618
- this.setAndEmitProbablyLeft(false);
619
- return createInsertActionUpdate(
620
- MembershipActionType.RestartDelayedEvent,
621
- this.delayedLeaveEventRestartMs,
622
- );
623
- })
624
- .catch((e) => {
625
- if (this.state.expectedServerDelayLeaveTs && this.state.expectedServerDelayLeaveTs <= Date.now()) {
626
- // Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
627
- // It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
628
- this.setAndEmitProbablyLeft(true);
629
- }
630
- const repeatActionType = MembershipActionType.RestartDelayedEvent;
631
- if (this.isNotFoundError(e)) {
632
- this.setAndEmitDelayId(undefined);
633
- return createInsertActionUpdate(MembershipActionType.SendDelayedEvent);
634
- }
635
- // If the HS does not support delayed events we wont reschedule.
636
- if (this.isUnsupportedDelayedEndpoint(e)) return {};
650
+ try {
651
+ // The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
652
+ // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
653
+ await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise]);
654
+ } catch (e) {
655
+ if (this.state.expectedServerDelayLeaveTs && this.state.expectedServerDelayLeaveTs <= Date.now()) {
656
+ // Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
657
+ // It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
658
+ this.setAndEmitProbablyLeft(true);
659
+ }
660
+ if (this.isNotFoundError(e)) {
661
+ this.setAndEmitDelayId(undefined);
662
+ return "gone";
663
+ }
664
+ if (this.isUnsupportedDelayedEndpoint(e)) {
665
+ // There is nothing left that we could restart, cancel or send with this delay id, so we forget it
666
+ // instead of making every later action try (and fail) to use it.
667
+ this.setAndEmitDelayId(undefined);
668
+ return "unsupported";
669
+ }
670
+ return { error: e };
671
+ }
637
672
 
638
- // TODO this also needs a test: get rate limit while checking id delayed event is scheduled
639
- const update = this.actionUpdateFromErrors(e, repeatActionType, "restartScheduledDelayedEvent");
640
- if (update) return update;
673
+ // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
674
+ // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
675
+ // successful restart requests.
676
+ this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
677
+ this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
678
+ this.setAndEmitProbablyLeft(false);
679
+ return "ok";
680
+ }
641
681
 
642
- // In other error cases we have no idea what is happening
643
- throw Error("Could not restart delayed event, even though delayed events are supported. " + e);
644
- });
682
+ private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
683
+ const outcome = await this.tryRestartDelayedEvent(delayId);
684
+ if (outcome === "ok") {
685
+ return createInsertActionUpdate(MembershipActionType.RestartDelayedEvent, this.delayedLeaveEventRestartMs);
686
+ }
687
+ if (outcome === "gone") {
688
+ return createInsertActionUpdate(MembershipActionType.SendDelayedEvent);
689
+ }
690
+ // If the HS does not support delayed events we won't reschedule.
691
+ if (outcome === "unsupported") return {};
692
+
693
+ // TODO this also needs a test: get rate limit while checking if delayed event is scheduled
694
+ const update = this.actionUpdateFromErrors(
695
+ outcome.error,
696
+ MembershipActionType.RestartDelayedEvent,
697
+ "restartScheduledDelayedEvent",
698
+ );
699
+ if (update) return update;
700
+
701
+ // In other error cases we have no idea what is happening
702
+ throw Error("Could not restart delayed event, even though delayed events are supported. " + outcome.error);
645
703
  }
646
704
 
647
705
  private async sendScheduledDelayedLeaveEventOrFallbackToSendLeaveEvent(delayId: string): Promise<ActionUpdate> {
@@ -685,10 +743,12 @@ export class MembershipManager
685
743
  };
686
744
 
687
745
  private async sendJoinEvent(): Promise<ActionUpdate> {
688
- return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs))
746
+ const myMembership = this.makeMyMembership(this.membershipEventExpiryMs);
747
+ return await this.clientSendMembership(myMembership)
689
748
  .then(() => {
690
749
  this.setAndEmitProbablyLeft(false);
691
750
  this.state.startTime = Date.now();
751
+ this.state.membershipExpiryTs = this.membershipExpiryTsOf(myMembership);
692
752
  // The next update should already use twice the membershipEventExpiryTimeout
693
753
  this.state.expireUpdateIterations = 1;
694
754
  this.state.hasMemberStateEvent = true;
@@ -723,15 +783,168 @@ export class MembershipManager
723
783
  });
724
784
  }
725
785
 
786
+ /**
787
+ * The timestamp at which a membership we send stops being valid for other clients.
788
+ *
789
+ * State events are valid until `created_ts + expires`. `makeMyMembership` inherits `created_ts` from our previous
790
+ * membership when we have one, otherwise the event's own timestamp counts.
791
+ * @param sent the membership as we sent it
792
+ */
793
+ protected membershipExpiryTsOf(sent: SessionMembershipData | RtcMembershipData): number {
794
+ const { created_ts: createdTs, expires } = sent as SessionMembershipData;
795
+ return (createdTs ?? Date.now()) + (expires ?? DEFAULT_EXPIRE_DURATION);
796
+ }
797
+
798
+ /**
799
+ * Gives up on our current membership and starts a fresh join.
800
+ *
801
+ * We forget our own membership so that the next state event does not inherit its `created_ts`. That would keep
802
+ * the identity of the membership we are giving up while `expires` starts over from a single iteration, which can
803
+ * put the absolute expiry in the past.
804
+ * @param reason why we are not keeping the current membership. Only used for logging.
805
+ */
806
+ private rejoinFromScratch(reason: string): ActionUpdate {
807
+ this.logger.warn(`Rejoining the session from scratch: ${reason}`);
808
+ this.state.hasMemberStateEvent = false;
809
+ this._ownMembership = undefined;
810
+ return createReplaceActionUpdate(MembershipActionType.SendDelayedEvent);
811
+ }
812
+
813
+ /**
814
+ * Schedules another attempt at the expiry update. Once our membership has expired we stop trying to keep it alive
815
+ * and rejoin instead, so that we never end up silently expired.
816
+ * @param delayMs how long to wait before trying again. Clamped so that we always get a last attempt at the moment
817
+ * the membership expires, and never schedule one beyond it.
818
+ * @param reason why we cannot update the expiry right now. Only used for logging.
819
+ * @param resendDelayedEvent whether to schedule a replacement delayed leave event first, with the retry running
820
+ * right after it.
821
+ */
822
+ private deferExpiryUpdate(
823
+ delayMs: number,
824
+ reason: string,
825
+ { resendDelayedEvent = false }: { resendDelayedEvent?: boolean } = {},
826
+ ): ActionUpdate {
827
+ const remainingMs = this.state.membershipExpiryTs - Date.now();
828
+ if (remainingMs <= 0) {
829
+ return this.rejoinFromScratch(`${reason}, and the membership expired while we were trying`);
830
+ }
831
+ const retry: Action = {
832
+ ts: Date.now() + Math.min(delayMs, remainingMs),
833
+ type: MembershipActionType.UpdateExpiry,
834
+ };
835
+ if (!resendDelayedEvent) return { insert: [retry] };
836
+
837
+ // Actions with the same timestamp run in insertion order, so the retry gets its turn after the replacement.
838
+ //
839
+ // The periodic `RestartDelayedEvent` that is still queued has to go, like `sendJoinEvent` does it: sending
840
+ // the delayed event schedules the one restart loop we want, and a leftover one would either start a second
841
+ // loop, or, while the replacement is still pending, queue a second `SendDelayedEvent` that cancels the
842
+ // replacement and `replace`s the retry out of the queue.
843
+ const otherActions = this.scheduler.actions.filter(
844
+ (a) => a.type !== MembershipActionType.RestartDelayedEvent && a.type !== MembershipActionType.UpdateExpiry,
845
+ );
846
+ return { replace: [...otherActions, { ts: Date.now(), type: MembershipActionType.SendDelayedEvent }, retry] };
847
+ }
848
+
849
+ /**
850
+ * Makes sure a delayed leave event is scheduled that will outlive the membership update we are about to send.
851
+ *
852
+ * Re-sending our membership is only safe while our delayed leave event is still scheduled: if the server has
853
+ * already sent it (e.g. because the device was asleep and no restarts reached the server), the membership we
854
+ * would send would come back to life without any delayed leave event to clean it up, and stay until `expires`
855
+ * runs out. So we restart the delayed event first. A success guarantees it will outlive the membership update;
856
+ * a 404 tells us it is gone, and we have to work out why.
857
+ * @returns `undefined` if the membership can be sent, otherwise the action to take instead.
858
+ */
859
+ private async ensureDelayedLeaveEventProtectsMembership(): Promise<ActionUpdate | undefined> {
860
+ if (!this.state.delayId) return this.waitForPendingDelayedLeaveEvent();
861
+
862
+ const outcome = await this.tryRestartDelayedEvent(this.state.delayId);
863
+ // `"ok"` and `"unsupported"` both let us go ahead: either the delayed leave event was just restarted, or
864
+ // there is none that could have removed our membership in the first place.
865
+ if (outcome === "ok" || outcome === "unsupported") return undefined;
866
+ if (outcome === "gone") return this.recoverFromGoneDelayedLeaveEvent();
867
+ return this.recoverFromFailedDelayedLeaveEventRestart(outcome.error);
868
+ }
869
+
870
+ /**
871
+ * Without a delay id, a `SendDelayedEvent` action may be queued that has not succeeded yet. Then we wait for it
872
+ * rather than put the membership back on the server with nothing to clean it up. The retry gets the same
873
+ * timestamp, and actions with the same timestamp run in insertion order, so it runs right after that attempt:
874
+ * straight away if the delayed event is there, or to wait for the next attempt if it rescheduled.
875
+ *
876
+ * Without such an action queued we are not using delayed events at all, because the homeserver does not support
877
+ * them. Then there is also no delayed leave event that could have removed our membership, and nothing to protect
878
+ * it from.
879
+ * @returns `undefined` if the membership can be sent, otherwise the deferred expiry update.
880
+ */
881
+ private waitForPendingDelayedLeaveEvent(): ActionUpdate | undefined {
882
+ const pending = this.scheduler.actions.find((a) => a.type === MembershipActionType.SendDelayedEvent);
883
+ if (!pending) return undefined;
884
+ return this.deferExpiryUpdate(Math.max(0, pending.ts - Date.now()), "no delayed leave event is scheduled");
885
+ }
886
+
887
+ /**
888
+ * The server does not know our delayed leave event anymore. Whether it sent it or lost it decides whether we
889
+ * still have a membership to extend.
890
+ */
891
+ private recoverFromGoneDelayedLeaveEvent(): ActionUpdate {
892
+ if (this.state.expectedServerDelayLeaveTs !== undefined && this.state.expectedServerDelayLeaveTs > Date.now()) {
893
+ // It is too early for the server to have sent the delayed leave event, so it must have been cancelled or
894
+ // lost instead (e.g. by a homeserver lacking https://github.com/element-hq/synapse/pull/17810 when our
895
+ // last state event landed). Our membership is still there and only needs a new delayed event, so we
896
+ // schedule one and pick the expiry update back up right after it.
897
+ return this.deferExpiryUpdate(0, "the delayed leave event was cancelled or lost", {
898
+ resendDelayedEvent: true,
899
+ });
900
+ }
901
+ // The delayed leave event has been sent, which also means our membership is gone.
902
+ return this.rejoinFromScratch("the server already sent our delayed leave event");
903
+ }
904
+
905
+ /**
906
+ * Restarting our delayed leave event failed with something other than a 404.
907
+ * @param error what the restart failed with
908
+ * @returns `undefined` if the membership can be sent regardless, otherwise the action to take instead.
909
+ * @throws the error if it is not one we retry on.
910
+ */
911
+ private recoverFromFailedDelayedLeaveEventRestart(error: unknown): ActionUpdate | undefined {
912
+ // The retry budget belongs to the restart, but what we need to repeat is the expiry update.
913
+ const update = this.actionUpdateFromErrors(
914
+ error,
915
+ MembershipActionType.RestartDelayedEvent,
916
+ "restartScheduledDelayedEvent",
917
+ );
918
+ if (!update) throw error;
919
+
920
+ // A transient failure does not mean the delayed leave event is not there. What we need for the update to be
921
+ // safe is that it is still pending when the update lands, and `expectedServerDelayLeaveTs` tells us that:
922
+ // while it is comfortably in the future, the restart was only a refresh we can do without, and the
923
+ // `RestartDelayedEvent` loop will make up for it. Only a stale timestamp leaves us guessing.
924
+ const durationUntilServerDelayedLeave = (this.state.expectedServerDelayLeaveTs ?? 0) - Date.now();
925
+ if (durationUntilServerDelayedLeave > this.delayedLeaveEventRestartLocalTimeoutMs) {
926
+ this.logger.warn(
927
+ "Could not restart the delayed leave event before extending `expires`, going ahead since it is still pending",
928
+ error,
929
+ );
930
+ return undefined;
931
+ }
932
+ const retryTs = "insert" in update ? update.insert[0].ts : Date.now();
933
+ return this.deferExpiryUpdate(Math.max(0, retryTs - Date.now()), "restarting the delayed leave event failed");
934
+ }
935
+
726
936
  private async updateExpiryOnJoinedEvent(): Promise<ActionUpdate> {
937
+ const protection = await this.ensureDelayedLeaveEventProtectsMembership();
938
+ if (protection) return protection;
939
+
727
940
  const nextExpireUpdateIteration = this.state.expireUpdateIterations + 1;
728
- return await this.clientSendMembership(
729
- this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration),
730
- )
941
+ const myMembership = this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration);
942
+ return await this.clientSendMembership(myMembership)
731
943
  .then(() => {
732
944
  // Success, we reset retries and schedule update.
733
945
  this.resetRateLimitCounter(MembershipActionType.UpdateExpiry);
734
946
  this.state.expireUpdateIterations = nextExpireUpdateIteration;
947
+ this.state.membershipExpiryTs = this.membershipExpiryTsOf(myMembership);
735
948
  return {
736
949
  insert: [
737
950
  {
@@ -801,6 +1014,10 @@ export class MembershipManager
801
1014
  foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])],
802
1015
  };
803
1016
  return {
1017
+ // Legacy memberships have no application object, so the
1018
+ // application's data sits at the top level, under the fields
1019
+ // this manager owns
1020
+ ...this.applicationData,
804
1021
  "application": this.slotDescription.application,
805
1022
  // INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
806
1023
  // Revert back to "" just for the sending the event.
@@ -1071,7 +1288,7 @@ export class StickyEventMembershipManager extends MembershipManager {
1071
1288
  { delay: this.delayedLeaveEventDelayMs },
1072
1289
  null,
1073
1290
  EventType.RTCMembership,
1074
- { msc4354_sticky_key: this.memberId },
1291
+ { slot_id: computeSlotId(this.slotDescription), msc4354_sticky_key: this.memberId },
1075
1292
  );
1076
1293
 
1077
1294
  protected clientSendMembership: (
@@ -1082,7 +1299,7 @@ export class StickyEventMembershipManager extends MembershipManager {
1082
1299
  MEMBERSHIP_STICKY_DURATION_MS,
1083
1300
  null,
1084
1301
  EventType.RTCMembership,
1085
- { ...myMembership, msc4354_sticky_key: this.memberId },
1302
+ { slot_id: computeSlotId(this.slotDescription), ...myMembership, msc4354_sticky_key: this.memberId },
1086
1303
  );
1087
1304
  };
1088
1305
 
@@ -1094,6 +1311,11 @@ export class StickyEventMembershipManager extends MembershipManager {
1094
1311
  return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
1095
1312
  }
1096
1313
 
1314
+ /** A sticky event vanishes `MEMBERSHIP_STICKY_DURATION_MS` after it was sent, whatever its content says. */
1315
+ protected membershipExpiryTsOf(): number {
1316
+ return Date.now() + MEMBERSHIP_STICKY_DURATION_MS;
1317
+ }
1318
+
1097
1319
  /**
1098
1320
  *
1099
1321
  * @returns Only returns `RtcMembershipData`
@@ -1107,6 +1329,7 @@ export class StickyEventMembershipManager extends MembershipManager {
1107
1329
  : {};
1108
1330
  return {
1109
1331
  application: {
1332
+ ...this.applicationData,
1110
1333
  type: this.slotDescription.application,
1111
1334
  ...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
1112
1335
  },
@@ -20,5 +20,11 @@ export * from "./MatrixRTCSession.ts";
20
20
  export * from "./MatrixRTCSessionManager.ts";
21
21
  export type * from "./types.ts";
22
22
  export { type SessionMembershipData, type RtcMembershipData } from "./membershipData/index.ts";
23
- export { Status, parseCallNotificationContent, isMyMembership, RTC_SLOT_ENCRYPTION_PER_MEMBER } from "./types.ts";
23
+ export {
24
+ Status,
25
+ parseCallNotificationContent,
26
+ getCallNotificationExpiry,
27
+ isMyMembership,
28
+ RTC_SLOT_ENCRYPTION_PER_MEMBER,
29
+ } from "./types.ts";
24
30
  export { MembershipManagerEvent } from "./IMembershipManager.ts";
@@ -14,6 +14,18 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import type { IContent } from "../../models/event.ts";
18
+
19
+ /**
20
+ * Whether the content of an `m.rtc.member` or `m.call.member` event represents a left membership.
21
+ *
22
+ * Leaving a slot is expressed by emptying the event's content, so nothing is left beyond the slot the
23
+ * membership applied to and the MSC4354 sticky key.
24
+ */
25
+ export function isLeftMembershipContent(content: IContent): boolean {
26
+ return Object.keys(content).every((key) => key === "slot_id" || key === "msc4354_sticky_key");
27
+ }
28
+
17
29
  /**
18
30
  * Thrown when an event is not valid for use with MatrixRTC.
19
31
  */
@@ -16,4 +16,4 @@ limitations under the License.
16
16
 
17
17
  export { type SessionMembershipData, checkSessionsMembershipData } from "./session.ts";
18
18
  export { type RtcMembershipData, computeRtcIdentityRaw, checkRtcMembershipData } from "./rtc.ts";
19
- export { MatrixRTCMembershipParseError } from "./common.ts";
19
+ export { MatrixRTCMembershipParseError, isLeftMembershipContent } from "./common.ts";