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
@@ -26,26 +26,27 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.js";
26
26
  import { UnsupportedDelayedEventsEndpointError } from "../errors.js";
27
27
  import { MembershipManagerEvent } from "./IMembershipManager.js";
28
28
  import { computeSlotId } from "./utils.js";
29
+ import { deepCompare } from "../utils.js";
29
30
  import { isLivekitTransportConfig } from "./LivekitTransport.js";
30
31
 
31
32
  /* MembershipActionTypes:
32
- On Join: ───────────────┐ ┌───────────────(1)───────────┐
33
- ▼ ▼ │
34
- ┌────────────────┐ │
35
- │SendDelayedEvent│ ──────(2)───┐ │
36
- └────────────────┘ │ │
37
- │(3) │ │
38
- ▼ │ │
39
- ┌─────────────┐ │ │
40
- ┌──────(4)───│SendJoinEvent│────(4)─────┐ │ │
41
- │ └─────────────┘ │ │ │
42
- │ ┌─────┐ ┌──────┐ │ │ │
43
- ▼ ▼ │ │ ▼ ▼ ▼ │
44
- ┌────────────┐ │ │ ┌───────────────────┐ │
45
- │UpdateExpiry│ (s) (s)|RestartDelayedEvent│ │
46
- └────────────┘ │ │ └───────────────────┘ │
47
- │ │ │ │ │ │
48
- └─────┘ └──────┘ └───────┘
33
+ On Join: ───────────────────┐ ┌───────────────(1)───────────┐
34
+ ▼ ▼ │
35
+ ┌────────────────┐ │
36
+ ┌───────(6)───────────▶│SendDelayedEvent│ ──────(2)───┐ │
37
+ │ └────────────────┘ │ │
38
+ │ │(3) │ │
39
+ │ ▼ │ │
40
+ │ ┌─────────────┐ │ │
41
+ │ ┌──────(4)───│SendJoinEvent│────(4)─────┐ │ │
42
+ │ │ └─────────────┘ │ │ │
43
+ │ │ ┌─────┐ ┌──────┐ │ │ │
44
+ │ ▼ ▼ │ │ ▼ ▼ ▼ │
45
+ │ ┌────────────┐ │ │ ┌───────────────────┐ │
46
+ └───│UpdateExpiry│ (s) (s)|RestartDelayedEvent│ │
47
+ └────────────┘ │ │ └───────────────────┘ │
48
+ │ │ │ │ │ │
49
+ └─────┘ └──────┘ └───────┘
49
50
 
50
51
  On Leave: ───────── STOP ALL ABOVE
51
52
  ▼
@@ -66,6 +67,10 @@ On Leave: ───────── STOP ALL ABOVE
66
67
  (4) Both (UpdateExpiry and RestartDelayedEvent) actions are
67
68
  scheduled when successfully sending the state event
68
69
  (5) Only if delayed event sending failed (fallback)
70
+ (6) Before extending `expires` we make sure a delayed leave
71
+ event will outlive the update. If it was only cancelled we
72
+ schedule a new one and retry; if it already fired, or the
73
+ membership expires while we retry, we join again from scratch
69
74
  (s) Successful restart/resend
70
75
  */
71
76
 
@@ -86,14 +91,17 @@ export let MembershipActionType = /*#__PURE__*/function (MembershipActionType) {
86
91
  // -> DelayedLeaveActionType.RestartDelayedEvent on success start updating the delayed event
87
92
  MembershipActionType["SendJoinEvent"] = "SendJoinEvent";
88
93
  // -> MembershipActionType.SendJoinEvent if we run into a rate limit and need to retry
89
- // -> MembershipActionType.Update if we successfully send the join event then schedule the expire event update
94
+ // -> MembershipActionType.UpdateExpiry if we successfully send the join event then schedule the expire event update
90
95
  // -> DelayedLeaveActionType.RestartDelayedEvent to recheck the delayed event
91
96
  MembershipActionType["RestartDelayedEvent"] = "RestartDelayedEvent";
92
97
  // -> DelayedLeaveActionType.SendMainDelayedEvent on missing delay id but there is a rtc state event
93
98
  // -> DelayedLeaveActionType.SendDelayedEvent on missing delay id and there is no state event
94
99
  // -> DelayedLeaveActionType.RestartDelayedEvent on success we schedule the next restart
95
100
  MembershipActionType["UpdateExpiry"] = "UpdateExpiry";
96
- // -> MembershipActionType.Update if the timeout has passed so the next update is required.
101
+ // -> MembershipActionType.UpdateExpiry if the timeout has passed so the next update is required, or if we first
102
+ // have to wait for a delayed leave event that can protect the updated membership.
103
+ // -> MembershipActionType.SendDelayedEvent if our delayed leave event is gone. Either to schedule a new one for
104
+ // the membership we still have, or to join again from scratch because the server already removed us.
97
105
  MembershipActionType["SendScheduledDelayedLeaveEvent"] = "SendScheduledDelayedLeaveEvent";
98
106
  // -> MembershipActionType.SendLeaveEvent on failure (not found) we need to send the leave manually and cannot use the scheduled delayed event
99
107
  // -> DelayedLeaveActionType.SendScheduledDelayedLeaveEvent on error we try again.
@@ -105,6 +113,15 @@ export let MembershipActionType = /*#__PURE__*/function (MembershipActionType) {
105
113
  * @internal
106
114
  */
107
115
 
116
+ /**
117
+ * The outcome of a delayed leave event restart attempt.
118
+ *
119
+ * - `"ok"`: the server restarted it, it will fire `delayedLeaveEventDelayMs` from now.
120
+ * - `"gone"`: the server does not know the delay id anymore. It either sent the event or lost/cancelled it.
121
+ * - `"unsupported"`: the server does not support the delayed events endpoint.
122
+ * - `{ error }`: any other failure, for the caller to turn into a retry or to rethrow.
123
+ */
124
+
108
125
  function createInsertActionUpdate(type, offset) {
109
126
  return {
110
127
  insert: [{
@@ -237,6 +254,16 @@ export class MembershipManager extends TypedEventEmitter {
237
254
  // Kick off a new membership event as a result.
238
255
  await this.sendJoinEvent();
239
256
  }
257
+ async updateApplicationData(applicationData) {
258
+ if (!this.activated || !this.ownMembership) {
259
+ throw Error("You cannot update your application data before joining the call");
260
+ }
261
+ if (deepCompare(this.applicationData ?? {}, applicationData)) {
262
+ return; // No-op
263
+ }
264
+ this.applicationData = applicationData;
265
+ await this.sendJoinEvent();
266
+ }
240
267
 
241
268
  /**
242
269
  * @throws if the client does not return user or device id.
@@ -249,6 +276,7 @@ export class MembershipManager extends TypedEventEmitter {
249
276
  _defineProperty(this, "activated", false);
250
277
  _defineProperty(this, "logger", void 0);
251
278
  _defineProperty(this, "callIntent", void 0);
279
+ _defineProperty(this, "applicationData", void 0);
252
280
  _defineProperty(this, "leavePromiseResolvers", void 0);
253
281
  _defineProperty(this, "_ownMembership", void 0);
254
282
  // scheduler
@@ -287,6 +315,7 @@ export class MembershipManager extends TypedEventEmitter {
287
315
  this.stateKey = this.makeMembershipStateKey(userId, deviceId);
288
316
  this.state = MembershipManager.defaultState;
289
317
  this.callIntent = joinConfig?.callIntent;
318
+ this.applicationData = joinConfig?.applicationData;
290
319
  this.scheduler = new ActionScheduler(type => {
291
320
  if (this.oldStatus) {
292
321
  // we put this at the beginning of the actions scheduler loop handle callback since it is a loop this
@@ -310,6 +339,7 @@ export class MembershipManager extends TypedEventEmitter {
310
339
  hasMemberStateEvent: false,
311
340
  delayId: undefined,
312
341
  startTime: 0,
342
+ membershipExpiryTs: 0,
313
343
  rateLimitRetries: new Map(),
314
344
  networkErrorRetries: new Map(),
315
345
  expireUpdateIterations: 1,
@@ -494,7 +524,14 @@ export class MembershipManager extends TypedEventEmitter {
494
524
  this.state.delayId = delayId;
495
525
  this.emit(MembershipManagerEvent.DelayIdChanged, this.state.delayId);
496
526
  }
497
- async restartDelayedEvent(delayId) {
527
+
528
+ /**
529
+ * Attempts to restart the delayed leave event and keeps everything that depends on it in sync.
530
+ *
531
+ * @param delayId the id of the delayed leave event to restart
532
+ * @returns the outcome of the attempt.
533
+ */
534
+ async tryRestartDelayedEvent(delayId) {
498
535
  // Compute the duration until we expect the server to send the delayed leave event.
499
536
  const durationUntilServerDelayedLeave = this.state.expectedServerDelayLeaveTs ? this.state.expectedServerDelayLeaveTs - Date.now() : undefined;
500
537
  const abortPromise = new Promise((_, reject) => {
@@ -507,38 +544,56 @@ export class MembershipManager extends TypedEventEmitter {
507
544
  // While we are already in probablyLeft state, we use the unaltered delayedLeaveEventRestartLocalTimeoutMs.
508
545
  durationUntilServerDelayedLeave !== undefined && !this.state.probablyLeft ? Math.min(this.delayedLeaveEventRestartLocalTimeoutMs, durationUntilServerDelayedLeave) : this.delayedLeaveEventRestartLocalTimeoutMs);
509
546
  });
510
-
511
- // The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
512
- // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
513
- return await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise]).then(() => {
514
- // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
515
- // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
516
- // successful restart requests.
517
- this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
518
- this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
519
- this.setAndEmitProbablyLeft(false);
520
- return createInsertActionUpdate(MembershipActionType.RestartDelayedEvent, this.delayedLeaveEventRestartMs);
521
- }).catch(e => {
547
+ try {
548
+ // The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
549
+ // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
550
+ await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise]);
551
+ } catch (e) {
522
552
  if (this.state.expectedServerDelayLeaveTs && this.state.expectedServerDelayLeaveTs <= Date.now()) {
523
553
  // Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
524
554
  // It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
525
555
  this.setAndEmitProbablyLeft(true);
526
556
  }
527
- const repeatActionType = MembershipActionType.RestartDelayedEvent;
528
557
  if (this.isNotFoundError(e)) {
529
558
  this.setAndEmitDelayId(undefined);
530
- return createInsertActionUpdate(MembershipActionType.SendDelayedEvent);
559
+ return "gone";
531
560
  }
532
- // If the HS does not support delayed events we wont reschedule.
533
- if (this.isUnsupportedDelayedEndpoint(e)) return {};
561
+ if (this.isUnsupportedDelayedEndpoint(e)) {
562
+ // There is nothing left that we could restart, cancel or send with this delay id, so we forget it
563
+ // instead of making every later action try (and fail) to use it.
564
+ this.setAndEmitDelayId(undefined);
565
+ return "unsupported";
566
+ }
567
+ return {
568
+ error: e
569
+ };
570
+ }
534
571
 
535
- // TODO this also needs a test: get rate limit while checking id delayed event is scheduled
536
- const update = this.actionUpdateFromErrors(e, repeatActionType, "restartScheduledDelayedEvent");
537
- if (update) return update;
572
+ // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
573
+ // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
574
+ // successful restart requests.
575
+ this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
576
+ this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
577
+ this.setAndEmitProbablyLeft(false);
578
+ return "ok";
579
+ }
580
+ async restartDelayedEvent(delayId) {
581
+ const outcome = await this.tryRestartDelayedEvent(delayId);
582
+ if (outcome === "ok") {
583
+ return createInsertActionUpdate(MembershipActionType.RestartDelayedEvent, this.delayedLeaveEventRestartMs);
584
+ }
585
+ if (outcome === "gone") {
586
+ return createInsertActionUpdate(MembershipActionType.SendDelayedEvent);
587
+ }
588
+ // If the HS does not support delayed events we won't reschedule.
589
+ if (outcome === "unsupported") return {};
538
590
 
539
- // In other error cases we have no idea what is happening
540
- throw Error("Could not restart delayed event, even though delayed events are supported. " + e);
541
- });
591
+ // TODO this also needs a test: get rate limit while checking if delayed event is scheduled
592
+ const update = this.actionUpdateFromErrors(outcome.error, MembershipActionType.RestartDelayedEvent, "restartScheduledDelayedEvent");
593
+ if (update) return update;
594
+
595
+ // In other error cases we have no idea what is happening
596
+ throw Error("Could not restart delayed event, even though delayed events are supported. " + outcome.error);
542
597
  }
543
598
  async sendScheduledDelayedLeaveEventOrFallbackToSendLeaveEvent(delayId) {
544
599
  return await this.client._unstable_sendScheduledDelayedEvent(delayId).then(() => {
@@ -564,9 +619,11 @@ export class MembershipManager extends TypedEventEmitter {
564
619
  });
565
620
  }
566
621
  async sendJoinEvent() {
567
- return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs)).then(() => {
622
+ const myMembership = this.makeMyMembership(this.membershipEventExpiryMs);
623
+ return await this.clientSendMembership(myMembership).then(() => {
568
624
  this.setAndEmitProbablyLeft(false);
569
625
  this.state.startTime = Date.now();
626
+ this.state.membershipExpiryTs = this.membershipExpiryTsOf(myMembership);
570
627
  // The next update should already use twice the membershipEventExpiryTimeout
571
628
  this.state.expireUpdateIterations = 1;
572
629
  this.state.hasMemberStateEvent = true;
@@ -598,12 +655,164 @@ export class MembershipManager extends TypedEventEmitter {
598
655
  throw e;
599
656
  });
600
657
  }
658
+
659
+ /**
660
+ * The timestamp at which a membership we send stops being valid for other clients.
661
+ *
662
+ * State events are valid until `created_ts + expires`. `makeMyMembership` inherits `created_ts` from our previous
663
+ * membership when we have one, otherwise the event's own timestamp counts.
664
+ * @param sent the membership as we sent it
665
+ */
666
+ membershipExpiryTsOf(sent) {
667
+ const {
668
+ created_ts: createdTs,
669
+ expires
670
+ } = sent;
671
+ return (createdTs ?? Date.now()) + (expires ?? DEFAULT_EXPIRE_DURATION);
672
+ }
673
+
674
+ /**
675
+ * Gives up on our current membership and starts a fresh join.
676
+ *
677
+ * We forget our own membership so that the next state event does not inherit its `created_ts`. That would keep
678
+ * the identity of the membership we are giving up while `expires` starts over from a single iteration, which can
679
+ * put the absolute expiry in the past.
680
+ * @param reason why we are not keeping the current membership. Only used for logging.
681
+ */
682
+ rejoinFromScratch(reason) {
683
+ this.logger.warn(`Rejoining the session from scratch: ${reason}`);
684
+ this.state.hasMemberStateEvent = false;
685
+ this._ownMembership = undefined;
686
+ return createReplaceActionUpdate(MembershipActionType.SendDelayedEvent);
687
+ }
688
+
689
+ /**
690
+ * Schedules another attempt at the expiry update. Once our membership has expired we stop trying to keep it alive
691
+ * and rejoin instead, so that we never end up silently expired.
692
+ * @param delayMs how long to wait before trying again. Clamped so that we always get a last attempt at the moment
693
+ * the membership expires, and never schedule one beyond it.
694
+ * @param reason why we cannot update the expiry right now. Only used for logging.
695
+ * @param resendDelayedEvent whether to schedule a replacement delayed leave event first, with the retry running
696
+ * right after it.
697
+ */
698
+ deferExpiryUpdate(delayMs, reason, {
699
+ resendDelayedEvent = false
700
+ } = {}) {
701
+ const remainingMs = this.state.membershipExpiryTs - Date.now();
702
+ if (remainingMs <= 0) {
703
+ return this.rejoinFromScratch(`${reason}, and the membership expired while we were trying`);
704
+ }
705
+ const retry = {
706
+ ts: Date.now() + Math.min(delayMs, remainingMs),
707
+ type: MembershipActionType.UpdateExpiry
708
+ };
709
+ if (!resendDelayedEvent) return {
710
+ insert: [retry]
711
+ };
712
+
713
+ // Actions with the same timestamp run in insertion order, so the retry gets its turn after the replacement.
714
+ //
715
+ // The periodic `RestartDelayedEvent` that is still queued has to go, like `sendJoinEvent` does it: sending
716
+ // the delayed event schedules the one restart loop we want, and a leftover one would either start a second
717
+ // loop, or, while the replacement is still pending, queue a second `SendDelayedEvent` that cancels the
718
+ // replacement and `replace`s the retry out of the queue.
719
+ const otherActions = this.scheduler.actions.filter(a => a.type !== MembershipActionType.RestartDelayedEvent && a.type !== MembershipActionType.UpdateExpiry);
720
+ return {
721
+ replace: [...otherActions, {
722
+ ts: Date.now(),
723
+ type: MembershipActionType.SendDelayedEvent
724
+ }, retry]
725
+ };
726
+ }
727
+
728
+ /**
729
+ * Makes sure a delayed leave event is scheduled that will outlive the membership update we are about to send.
730
+ *
731
+ * Re-sending our membership is only safe while our delayed leave event is still scheduled: if the server has
732
+ * already sent it (e.g. because the device was asleep and no restarts reached the server), the membership we
733
+ * would send would come back to life without any delayed leave event to clean it up, and stay until `expires`
734
+ * runs out. So we restart the delayed event first. A success guarantees it will outlive the membership update;
735
+ * a 404 tells us it is gone, and we have to work out why.
736
+ * @returns `undefined` if the membership can be sent, otherwise the action to take instead.
737
+ */
738
+ async ensureDelayedLeaveEventProtectsMembership() {
739
+ if (!this.state.delayId) return this.waitForPendingDelayedLeaveEvent();
740
+ const outcome = await this.tryRestartDelayedEvent(this.state.delayId);
741
+ // `"ok"` and `"unsupported"` both let us go ahead: either the delayed leave event was just restarted, or
742
+ // there is none that could have removed our membership in the first place.
743
+ if (outcome === "ok" || outcome === "unsupported") return undefined;
744
+ if (outcome === "gone") return this.recoverFromGoneDelayedLeaveEvent();
745
+ return this.recoverFromFailedDelayedLeaveEventRestart(outcome.error);
746
+ }
747
+
748
+ /**
749
+ * Without a delay id, a `SendDelayedEvent` action may be queued that has not succeeded yet. Then we wait for it
750
+ * rather than put the membership back on the server with nothing to clean it up. The retry gets the same
751
+ * timestamp, and actions with the same timestamp run in insertion order, so it runs right after that attempt:
752
+ * straight away if the delayed event is there, or to wait for the next attempt if it rescheduled.
753
+ *
754
+ * Without such an action queued we are not using delayed events at all, because the homeserver does not support
755
+ * them. Then there is also no delayed leave event that could have removed our membership, and nothing to protect
756
+ * it from.
757
+ * @returns `undefined` if the membership can be sent, otherwise the deferred expiry update.
758
+ */
759
+ waitForPendingDelayedLeaveEvent() {
760
+ const pending = this.scheduler.actions.find(a => a.type === MembershipActionType.SendDelayedEvent);
761
+ if (!pending) return undefined;
762
+ return this.deferExpiryUpdate(Math.max(0, pending.ts - Date.now()), "no delayed leave event is scheduled");
763
+ }
764
+
765
+ /**
766
+ * The server does not know our delayed leave event anymore. Whether it sent it or lost it decides whether we
767
+ * still have a membership to extend.
768
+ */
769
+ recoverFromGoneDelayedLeaveEvent() {
770
+ if (this.state.expectedServerDelayLeaveTs !== undefined && this.state.expectedServerDelayLeaveTs > Date.now()) {
771
+ // It is too early for the server to have sent the delayed leave event, so it must have been cancelled or
772
+ // lost instead (e.g. by a homeserver lacking https://github.com/element-hq/synapse/pull/17810 when our
773
+ // last state event landed). Our membership is still there and only needs a new delayed event, so we
774
+ // schedule one and pick the expiry update back up right after it.
775
+ return this.deferExpiryUpdate(0, "the delayed leave event was cancelled or lost", {
776
+ resendDelayedEvent: true
777
+ });
778
+ }
779
+ // The delayed leave event has been sent, which also means our membership is gone.
780
+ return this.rejoinFromScratch("the server already sent our delayed leave event");
781
+ }
782
+
783
+ /**
784
+ * Restarting our delayed leave event failed with something other than a 404.
785
+ * @param error what the restart failed with
786
+ * @returns `undefined` if the membership can be sent regardless, otherwise the action to take instead.
787
+ * @throws the error if it is not one we retry on.
788
+ */
789
+ recoverFromFailedDelayedLeaveEventRestart(error) {
790
+ // The retry budget belongs to the restart, but what we need to repeat is the expiry update.
791
+ const update = this.actionUpdateFromErrors(error, MembershipActionType.RestartDelayedEvent, "restartScheduledDelayedEvent");
792
+ if (!update) throw error;
793
+
794
+ // A transient failure does not mean the delayed leave event is not there. What we need for the update to be
795
+ // safe is that it is still pending when the update lands, and `expectedServerDelayLeaveTs` tells us that:
796
+ // while it is comfortably in the future, the restart was only a refresh we can do without, and the
797
+ // `RestartDelayedEvent` loop will make up for it. Only a stale timestamp leaves us guessing.
798
+ const durationUntilServerDelayedLeave = (this.state.expectedServerDelayLeaveTs ?? 0) - Date.now();
799
+ if (durationUntilServerDelayedLeave > this.delayedLeaveEventRestartLocalTimeoutMs) {
800
+ this.logger.warn("Could not restart the delayed leave event before extending `expires`, going ahead since it is still pending", error);
801
+ return undefined;
802
+ }
803
+ const retryTs = "insert" in update ? update.insert[0].ts : Date.now();
804
+ return this.deferExpiryUpdate(Math.max(0, retryTs - Date.now()), "restarting the delayed leave event failed");
805
+ }
601
806
  async updateExpiryOnJoinedEvent() {
807
+ const protection = await this.ensureDelayedLeaveEventProtectsMembership();
808
+ if (protection) return protection;
602
809
  const nextExpireUpdateIteration = this.state.expireUpdateIterations + 1;
603
- return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration)).then(() => {
810
+ const myMembership = this.makeMyMembership(this.membershipEventExpiryMs * nextExpireUpdateIteration);
811
+ return await this.clientSendMembership(myMembership).then(() => {
604
812
  // Success, we reset retries and schedule update.
605
813
  this.resetRateLimitCounter(MembershipActionType.UpdateExpiry);
606
814
  this.state.expireUpdateIterations = nextExpireUpdateIteration;
815
+ this.state.membershipExpiryTs = this.membershipExpiryTsOf(myMembership);
607
816
  return {
608
817
  insert: [{
609
818
  ts: this.computeNextExpiryActionTs(nextExpireUpdateIteration),
@@ -669,7 +878,7 @@ export class MembershipManager extends TypedEventEmitter {
669
878
  },
670
879
  foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])]
671
880
  };
672
- return _objectSpread(_objectSpread({
881
+ return _objectSpread(_objectSpread(_objectSpread({}, this.applicationData), {}, {
673
882
  "application": this.slotDescription.application,
674
883
  // INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
675
884
  // Revert back to "" just for the sending the event.
@@ -883,10 +1092,13 @@ export class StickyEventMembershipManager extends MembershipManager {
883
1092
  _defineProperty(this, "clientSendDelayedDisconnectMembership", () => this.clientWithSticky._unstable_sendStickyDelayedEvent(this.room.roomId, MEMBERSHIP_STICKY_DURATION_MS, {
884
1093
  delay: this.delayedLeaveEventDelayMs
885
1094
  }, null, EventType.RTCMembership, {
1095
+ slot_id: computeSlotId(this.slotDescription),
886
1096
  msc4354_sticky_key: this.memberId
887
1097
  }));
888
1098
  _defineProperty(this, "clientSendMembership", myMembership => {
889
- return this.clientWithSticky._unstable_sendStickyEvent(this.room.roomId, MEMBERSHIP_STICKY_DURATION_MS, null, EventType.RTCMembership, _objectSpread(_objectSpread({}, myMembership), {}, {
1099
+ return this.clientWithSticky._unstable_sendStickyEvent(this.room.roomId, MEMBERSHIP_STICKY_DURATION_MS, null, EventType.RTCMembership, _objectSpread(_objectSpread({
1100
+ slot_id: computeSlotId(this.slotDescription)
1101
+ }, myMembership), {}, {
890
1102
  msc4354_sticky_key: this.memberId
891
1103
  }));
892
1104
  });
@@ -897,6 +1109,11 @@ export class StickyEventMembershipManager extends MembershipManager {
897
1109
  return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
898
1110
  }
899
1111
 
1112
+ /** A sticky event vanishes `MEMBERSHIP_STICKY_DURATION_MS` after it was sent, whatever its content says. */
1113
+ membershipExpiryTsOf() {
1114
+ return Date.now() + MEMBERSHIP_STICKY_DURATION_MS;
1115
+ }
1116
+
900
1117
  /**
901
1118
  *
902
1119
  * @returns Only returns `RtcMembershipData`
@@ -911,7 +1128,7 @@ export class StickyEventMembershipManager extends MembershipManager {
911
1128
  }
912
1129
  } : {};
913
1130
  return _objectSpread({
914
- application: _objectSpread({
1131
+ application: _objectSpread(_objectSpread({}, this.applicationData), {}, {
915
1132
  type: this.slotDescription.application
916
1133
  }, this.callIntent ? {
917
1134
  "m.call.intent": this.callIntent