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.
- package/CHANGELOG.md +23 -0
- package/lib/@types/PushRules.d.ts +3 -1
- package/lib/@types/PushRules.d.ts.map +1 -1
- package/lib/@types/PushRules.js +2 -0
- package/lib/@types/PushRules.js.map +1 -1
- package/lib/@types/event.d.ts +1 -0
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/@types/matrix-sdk-crypto-wasm.d.js.map +1 -1
- package/lib/client.d.ts +88 -37
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +72 -47
- package/lib/client.js.map +1 -1
- package/lib/crypto-api/index.d.ts +3 -3
- package/lib/crypto-api/index.d.ts.map +1 -1
- package/lib/crypto-api/index.js.map +1 -1
- package/lib/embedded.d.ts +17 -1
- package/lib/embedded.d.ts.map +1 -1
- package/lib/embedded.js +46 -1
- package/lib/embedded.js.map +1 -1
- package/lib/errors.d.ts +1 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +2 -2
- package/lib/errors.js.map +1 -1
- package/lib/http-api/fetch.d.ts +7 -1
- package/lib/http-api/fetch.d.ts.map +1 -1
- package/lib/http-api/fetch.js +14 -5
- package/lib/http-api/fetch.js.map +1 -1
- package/lib/http-api/interface.d.ts +34 -8
- package/lib/http-api/interface.d.ts.map +1 -1
- package/lib/http-api/interface.js +10 -3
- package/lib/http-api/interface.js.map +1 -1
- package/lib/http-api/refresh.d.ts +30 -11
- package/lib/http-api/refresh.d.ts.map +1 -1
- package/lib/http-api/refresh.js +72 -5
- package/lib/http-api/refresh.js.map +1 -1
- package/lib/matrix.d.ts +1 -1
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.js.map +1 -1
- package/lib/matrixrtc/CallMembership.d.ts.map +1 -1
- package/lib/matrixrtc/CallMembership.js +20 -6
- package/lib/matrixrtc/CallMembership.js.map +1 -1
- package/lib/matrixrtc/IMembershipManager.d.ts +5 -0
- package/lib/matrixrtc/IMembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/IMembershipManager.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts +27 -1
- package/lib/matrixrtc/MatrixRTCSession.d.ts.map +1 -1
- package/lib/matrixrtc/MatrixRTCSession.js +58 -36
- package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js +1 -1
- package/lib/matrixrtc/MatrixRTCSessionManager.js.map +1 -1
- package/lib/matrixrtc/MembershipManager.d.ts +76 -0
- package/lib/matrixrtc/MembershipManager.d.ts.map +1 -1
- package/lib/matrixrtc/MembershipManager.js +264 -47
- package/lib/matrixrtc/MembershipManager.js.map +1 -1
- package/lib/matrixrtc/index.d.ts +1 -1
- package/lib/matrixrtc/index.d.ts.map +1 -1
- package/lib/matrixrtc/index.js +1 -1
- package/lib/matrixrtc/index.js.map +1 -1
- package/lib/matrixrtc/membershipData/common.d.ts +8 -0
- package/lib/matrixrtc/membershipData/common.d.ts.map +1 -1
- package/lib/matrixrtc/membershipData/common.js +10 -0
- package/lib/matrixrtc/membershipData/common.js.map +1 -1
- package/lib/matrixrtc/membershipData/index.d.ts +1 -1
- package/lib/matrixrtc/membershipData/index.d.ts.map +1 -1
- package/lib/matrixrtc/membershipData/index.js +1 -1
- package/lib/matrixrtc/membershipData/index.js.map +1 -1
- package/lib/matrixrtc/types.d.ts +104 -11
- package/lib/matrixrtc/types.d.ts.map +1 -1
- package/lib/matrixrtc/types.js +263 -19
- package/lib/matrixrtc/types.js.map +1 -1
- package/lib/matrixrtc/utils.d.ts +22 -1
- package/lib/matrixrtc/utils.d.ts.map +1 -1
- package/lib/matrixrtc/utils.js +38 -0
- package/lib/matrixrtc/utils.js.map +1 -1
- package/lib/models/room-sticky-events.d.ts +7 -0
- package/lib/models/room-sticky-events.d.ts.map +1 -1
- package/lib/models/room-sticky-events.js +27 -0
- package/lib/models/room-sticky-events.js.map +1 -1
- package/lib/models/thread.d.ts.map +1 -1
- package/lib/models/thread.js +8 -0
- package/lib/models/thread.js.map +1 -1
- package/lib/oauth/authorize.js +1 -1
- package/lib/oauth/authorize.js.map +1 -1
- package/lib/oauth/index.d.ts +3 -5
- package/lib/oauth/index.d.ts.map +1 -1
- package/lib/oauth/index.js +7 -7
- package/lib/oauth/index.js.map +1 -1
- package/lib/oauth/tokenRefresher.d.ts +10 -4
- package/lib/oauth/tokenRefresher.d.ts.map +1 -1
- package/lib/oauth/tokenRefresher.js +34 -24
- package/lib/oauth/tokenRefresher.js.map +1 -1
- package/lib/receipt-accumulator.d.ts +10 -11
- package/lib/receipt-accumulator.d.ts.map +1 -1
- package/lib/receipt-accumulator.js +26 -25
- package/lib/receipt-accumulator.js.map +1 -1
- package/lib/room-summary-api.d.ts +71 -0
- package/lib/room-summary-api.d.ts.map +1 -0
- package/lib/room-summary-api.js +82 -0
- package/lib/room-summary-api.js.map +1 -0
- package/lib/rust-crypto/backup.d.ts +7 -0
- package/lib/rust-crypto/backup.d.ts.map +1 -1
- package/lib/rust-crypto/backup.js +41 -25
- package/lib/rust-crypto/backup.js.map +1 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +6 -1
- package/lib/sync.js.map +1 -1
- package/package.json +6 -6
- package/src/@types/PushRules.ts +2 -0
- package/src/@types/event.ts +1 -1
- package/src/@types/matrix-sdk-crypto-wasm.d.ts +15 -12
- package/src/client.ts +105 -62
- package/src/crypto-api/index.ts +3 -3
- package/src/embedded.ts +61 -2
- package/src/errors.ts +2 -1
- package/src/http-api/fetch.ts +14 -5
- package/src/http-api/interface.ts +36 -8
- package/src/http-api/refresh.ts +101 -17
- package/src/matrix.ts +1 -1
- package/src/matrixrtc/CallMembership.ts +22 -4
- package/src/matrixrtc/IMembershipManager.ts +6 -0
- package/src/matrixrtc/MatrixRTCSession.ts +72 -49
- package/src/matrixrtc/MatrixRTCSessionManager.ts +1 -1
- package/src/matrixrtc/MembershipManager.ts +284 -61
- package/src/matrixrtc/index.ts +7 -1
- package/src/matrixrtc/membershipData/common.ts +12 -0
- package/src/matrixrtc/membershipData/index.ts +1 -1
- package/src/matrixrtc/types.ts +351 -23
- package/src/matrixrtc/utils.ts +54 -1
- package/src/models/room-sticky-events.ts +26 -1
- package/src/models/thread.ts +9 -0
- package/src/oauth/authorize.ts +1 -1
- package/src/oauth/index.ts +7 -8
- package/src/oauth/tokenRefresher.ts +15 -5
- package/src/receipt-accumulator.ts +30 -26
- package/src/room-summary-api.ts +114 -0
- package/src/rust-crypto/backup.ts +46 -33
- 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:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
┌────────────┐ │ │ ┌───────────────────┐ │
|
|
54
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
this.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
729
|
-
|
|
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
|
},
|
package/src/matrixrtc/index.ts
CHANGED
|
@@ -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 {
|
|
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";
|