matrix-js-sdk 42.1.0 → 42.2.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 (154) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/@types/beacon.d.ts +1 -1
  3. package/lib/@types/beacon.js +1 -1
  4. package/lib/@types/beacon.js.map +1 -1
  5. package/lib/@types/extensible_events.d.ts.map +1 -1
  6. package/lib/@types/extensible_events.js.map +1 -1
  7. package/lib/autodiscovery.js.map +1 -1
  8. package/lib/browser-index.js.map +1 -1
  9. package/lib/client.d.ts +12 -5
  10. package/lib/client.d.ts.map +1 -1
  11. package/lib/client.js +23 -26
  12. package/lib/client.js.map +1 -1
  13. package/lib/common-crypto/key-passphrase.js +1 -1
  14. package/lib/common-crypto/key-passphrase.js.map +1 -1
  15. package/lib/crypto/store/indexeddb-crypto-store-backend.js +2 -1
  16. package/lib/crypto/store/indexeddb-crypto-store-backend.js.map +1 -1
  17. package/lib/crypto/store/indexeddb-crypto-store.d.ts.map +1 -1
  18. package/lib/crypto/store/indexeddb-crypto-store.js +1 -1
  19. package/lib/crypto/store/indexeddb-crypto-store.js.map +1 -1
  20. package/lib/crypto/store/memory-crypto-store.js.map +1 -1
  21. package/lib/embedded.d.ts +16 -0
  22. package/lib/embedded.d.ts.map +1 -1
  23. package/lib/embedded.js +19 -6
  24. package/lib/embedded.js.map +1 -1
  25. package/lib/matrixrtc/MatrixRTCSession.js.map +1 -1
  26. package/lib/matrixrtc/MembershipManager.js.map +1 -1
  27. package/lib/matrixrtc/MembershipManagerActionScheduler.js.map +1 -1
  28. package/lib/matrixrtc/RTCEncryptionManager.js.map +1 -1
  29. package/lib/matrixrtc/ToDeviceKeyTransport.js.map +1 -1
  30. package/lib/matrixrtc/membershipData/rtc.js.map +1 -1
  31. package/lib/matrixrtc/utils.d.ts +0 -1
  32. package/lib/matrixrtc/utils.d.ts.map +1 -1
  33. package/lib/matrixrtc/utils.js +0 -1
  34. package/lib/matrixrtc/utils.js.map +1 -1
  35. package/lib/models/event-timeline-set.js +3 -3
  36. package/lib/models/event-timeline-set.js.map +1 -1
  37. package/lib/models/event.d.ts.map +1 -1
  38. package/lib/models/event.js.map +1 -1
  39. package/lib/models/invites-ignorer.js.map +1 -1
  40. package/lib/models/poll.js.map +1 -1
  41. package/lib/models/read-receipt.js.map +1 -1
  42. package/lib/models/relations-container.d.ts +10 -0
  43. package/lib/models/relations-container.d.ts.map +1 -1
  44. package/lib/models/relations-container.js +39 -17
  45. package/lib/models/relations-container.js.map +1 -1
  46. package/lib/models/relations.js +1 -1
  47. package/lib/models/relations.js.map +1 -1
  48. package/lib/models/room.js +3 -3
  49. package/lib/models/room.js.map +1 -1
  50. package/lib/models/thread.d.ts +3 -0
  51. package/lib/models/thread.d.ts.map +1 -1
  52. package/lib/models/thread.js +36 -3
  53. package/lib/models/thread.js.map +1 -1
  54. package/lib/oauth/authorize.js +1 -1
  55. package/lib/oauth/authorize.js.map +1 -1
  56. package/lib/oauth/index.js +4 -2
  57. package/lib/oauth/index.js.map +1 -1
  58. package/lib/pollingCachedValue.d.ts +77 -0
  59. package/lib/pollingCachedValue.d.ts.map +1 -0
  60. package/lib/pollingCachedValue.js +160 -0
  61. package/lib/pollingCachedValue.js.map +1 -0
  62. package/lib/pushprocessor.js.map +1 -1
  63. package/lib/receipt-accumulator.d.ts.map +1 -1
  64. package/lib/receipt-accumulator.js.map +1 -1
  65. package/lib/rendezvous/MSC4108SignInWithQR.d.ts.map +1 -1
  66. package/lib/rendezvous/MSC4108SignInWithQR.js +2 -3
  67. package/lib/rendezvous/MSC4108SignInWithQR.js.map +1 -1
  68. package/lib/rtcTransportsCachedValue.d.ts +9 -0
  69. package/lib/rtcTransportsCachedValue.d.ts.map +1 -0
  70. package/lib/rtcTransportsCachedValue.js +38 -0
  71. package/lib/rtcTransportsCachedValue.js.map +1 -0
  72. package/lib/rust-crypto/PerSessionKeyBackupDownloader.js.map +1 -1
  73. package/lib/rust-crypto/backup.d.ts +2 -3
  74. package/lib/rust-crypto/backup.d.ts.map +1 -1
  75. package/lib/rust-crypto/backup.js.map +1 -1
  76. package/lib/rust-crypto/index.d.ts +10 -4
  77. package/lib/rust-crypto/index.d.ts.map +1 -1
  78. package/lib/rust-crypto/index.js +20 -3
  79. package/lib/rust-crypto/index.js.map +1 -1
  80. package/lib/rust-crypto/libolm_migration.js.map +1 -1
  81. package/lib/rust-crypto/rust-crypto.js.map +1 -1
  82. package/lib/sliding-sync-sdk.js.map +1 -1
  83. package/lib/sliding-sync.js.map +1 -1
  84. package/lib/store/indexeddb-local-backend.js +1 -1
  85. package/lib/store/indexeddb-local-backend.js.map +1 -1
  86. package/lib/store/indexeddb-store-worker.d.ts.map +1 -1
  87. package/lib/store/indexeddb-store-worker.js +1 -0
  88. package/lib/store/indexeddb-store-worker.js.map +1 -1
  89. package/lib/sync.d.ts.map +1 -1
  90. package/lib/sync.js.map +1 -1
  91. package/lib/utils.d.ts +1 -1
  92. package/lib/utils.d.ts.map +1 -1
  93. package/lib/utils.js +1 -0
  94. package/lib/utils.js.map +1 -1
  95. package/lib/webrtc/call.js.map +1 -1
  96. package/lib/webrtc/groupCall.js.map +1 -1
  97. package/lib/webrtc/mediaHandler.js.map +1 -1
  98. package/lib/webrtc/stats/callStatsReportGatherer.js.map +1 -1
  99. package/lib/webrtc/stats/transportStatsBuilder.js.map +1 -1
  100. package/lib/wellKnownCachedValue.d.ts +8 -0
  101. package/lib/wellKnownCachedValue.d.ts.map +1 -0
  102. package/lib/wellKnownCachedValue.js +34 -0
  103. package/lib/wellKnownCachedValue.js.map +1 -0
  104. package/package.json +4 -4
  105. package/src/@types/beacon.ts +1 -1
  106. package/src/@types/extensible_events.ts +3 -4
  107. package/src/autodiscovery.ts +2 -2
  108. package/src/browser-index.ts +1 -1
  109. package/src/client.ts +48 -40
  110. package/src/common-crypto/key-passphrase.ts +1 -1
  111. package/src/crypto/store/indexeddb-crypto-store-backend.ts +2 -1
  112. package/src/crypto/store/indexeddb-crypto-store.ts +1 -3
  113. package/src/crypto/store/memory-crypto-store.ts +1 -1
  114. package/src/embedded.ts +34 -11
  115. package/src/matrixrtc/MatrixRTCSession.ts +3 -3
  116. package/src/matrixrtc/MembershipManager.ts +1 -1
  117. package/src/matrixrtc/MembershipManagerActionScheduler.ts +1 -1
  118. package/src/matrixrtc/RTCEncryptionManager.ts +2 -2
  119. package/src/matrixrtc/ToDeviceKeyTransport.ts +3 -3
  120. package/src/matrixrtc/membershipData/rtc.ts +1 -1
  121. package/src/matrixrtc/utils.ts +0 -2
  122. package/src/models/event-timeline-set.ts +3 -3
  123. package/src/models/event.ts +7 -10
  124. package/src/models/invites-ignorer.ts +1 -1
  125. package/src/models/poll.ts +1 -1
  126. package/src/models/read-receipt.ts +1 -1
  127. package/src/models/relations-container.ts +55 -19
  128. package/src/models/relations.ts +1 -1
  129. package/src/models/room.ts +9 -9
  130. package/src/models/thread.ts +49 -4
  131. package/src/oauth/authorize.ts +1 -1
  132. package/src/oauth/index.ts +2 -2
  133. package/src/pollingCachedValue.ts +184 -0
  134. package/src/pushprocessor.ts +3 -3
  135. package/src/receipt-accumulator.ts +1 -2
  136. package/src/rendezvous/MSC4108SignInWithQR.ts +3 -4
  137. package/src/rtcTransportsCachedValue.ts +40 -0
  138. package/src/rust-crypto/PerSessionKeyBackupDownloader.ts +1 -1
  139. package/src/rust-crypto/backup.ts +1 -5
  140. package/src/rust-crypto/index.ts +23 -25
  141. package/src/rust-crypto/libolm_migration.ts +1 -1
  142. package/src/rust-crypto/rust-crypto.ts +3 -3
  143. package/src/sliding-sync-sdk.ts +1 -1
  144. package/src/sliding-sync.ts +1 -1
  145. package/src/store/indexeddb-local-backend.ts +1 -1
  146. package/src/store/indexeddb-store-worker.ts +1 -0
  147. package/src/sync.ts +4 -3
  148. package/src/utils.ts +3 -2
  149. package/src/webrtc/call.ts +6 -6
  150. package/src/webrtc/groupCall.ts +7 -7
  151. package/src/webrtc/mediaHandler.ts +1 -1
  152. package/src/webrtc/stats/callStatsReportGatherer.ts +2 -2
  153. package/src/webrtc/stats/transportStatsBuilder.ts +1 -1
  154. package/src/wellKnownCachedValue.ts +34 -0
@@ -20,6 +20,7 @@ import { EventStatus, type MatrixEvent, MatrixEventEvent } from "./event.ts";
20
20
  import { type EventTimelineSet } from "./event-timeline-set.ts";
21
21
  import { type MatrixClient } from "../client.ts";
22
22
  import { type Room } from "./room.ts";
23
+ import { logger } from "../logger.ts";
23
24
 
24
25
  export class RelationsContainer {
25
26
  // A tree of objects to access a set of related children for an event, as in:
@@ -89,29 +90,58 @@ export class RelationsContainer {
89
90
  * @param event - The new child event to be aggregated.
90
91
  * @param timelineSet - The event timeline set within which to search for the related event if any.
91
92
  */
92
- public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void {
93
+ public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void;
94
+ /**
95
+ * Add a relation event using a known target that is not in the timeline yet.
96
+ *
97
+ * @param event - The new child event to be aggregated.
98
+ * @param timelineSet - The event timeline set within which to search for the related event.
99
+ * @param targetEvent - The known relation target.
100
+ * @returns A promise which resolves after aggregation.
101
+ */
102
+ public aggregateChildEvent(
103
+ event: MatrixEvent,
104
+ timelineSet: EventTimelineSet,
105
+ targetEvent: MatrixEvent,
106
+ ): Promise<void>;
107
+ public aggregateChildEvent(
108
+ event: MatrixEvent,
109
+ timelineSet?: EventTimelineSet,
110
+ targetEvent?: MatrixEvent,
111
+ ): void | Promise<void> {
112
+ const aggregation = this.aggregateChildEventInternal(event, timelineSet, targetEvent);
113
+ if (targetEvent) return aggregation;
114
+
115
+ void aggregation.catch((error) => logger.error("Failed to aggregate child event: ", error));
116
+ }
117
+
118
+ private aggregateChildEventInternal(
119
+ event: MatrixEvent,
120
+ timelineSet?: EventTimelineSet,
121
+ targetEvent?: MatrixEvent,
122
+ ): Promise<void> {
93
123
  if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
94
- return;
124
+ return Promise.resolve();
95
125
  }
96
126
 
97
127
  const relation = event.getRelation();
98
- if (!relation) return;
99
-
100
- const onEventDecrypted = (): void => {
101
- if (event.isDecryptionFailure()) {
102
- // This could for example happen if the encryption keys are not yet available.
103
- // The event may still be decrypted later. Register the listener again.
104
- event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
105
- return;
106
- }
107
-
108
- this.aggregateChildEvent(event, timelineSet);
109
- };
128
+ if (!relation) return Promise.resolve();
110
129
 
111
130
  // If the event is currently encrypted, wait until it has been decrypted.
112
131
  if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
113
- event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
114
- return;
132
+ return new Promise<void>((resolve, reject) => {
133
+ const onEventDecrypted = (): void => {
134
+ if (event.isDecryptionFailure()) {
135
+ // This could for example happen if the encryption keys are not yet available.
136
+ // The event may still be decrypted later. Register the listener again.
137
+ event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
138
+ return;
139
+ }
140
+
141
+ void this.aggregateChildEventInternal(event, timelineSet, targetEvent).then(resolve, reject);
142
+ };
143
+ event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
144
+ });
115
145
  }
116
146
 
117
147
  const { event_id: relatesToEventId, rel_type: relationType } = relation;
@@ -130,20 +160,26 @@ export class RelationsContainer {
130
160
  }
131
161
 
132
162
  let relationsWithEventType = relationsWithRelType.get(eventType);
163
+ const isNewRelationCollection = !relationsWithEventType;
164
+ let targetEventUpdate = Promise.resolve();
133
165
  if (!relationsWithEventType) {
134
166
  relationsWithEventType = new Relations(relationType!, eventType, this.client);
135
167
  relationsWithRelType.set(eventType, relationsWithEventType);
168
+ }
136
169
 
170
+ if (isNewRelationCollection || targetEvent) {
137
171
  const room = this.room ?? timelineSet?.room;
138
172
  const relatesToEvent =
139
173
  timelineSet?.findEventById(relatesToEventId!) ??
140
174
  room?.findEventById(relatesToEventId!) ??
141
- room?.getPendingEvent(relatesToEventId!);
175
+ room?.getPendingEvent(relatesToEventId!) ??
176
+ targetEvent;
142
177
  if (relatesToEvent) {
143
- relationsWithEventType.setTargetEvent(relatesToEvent);
178
+ targetEventUpdate = relationsWithEventType.setTargetEvent(relatesToEvent);
144
179
  }
145
180
  }
146
181
 
147
- relationsWithEventType.addEvent(event);
182
+ const addEventUpdate = relationsWithEventType.addEvent(event);
183
+ return Promise.all([targetEventUpdate, addEventUpdate]).then(() => undefined);
148
184
  }
149
185
  }
@@ -336,7 +336,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
336
336
  * @param targetEvent - the event the relations are related to.
337
337
  */
338
338
  public async setTargetEvent(event: MatrixEvent): Promise<void> {
339
- if (this.targetEvent) {
339
+ if (this.targetEvent === event) {
340
340
  return;
341
341
  }
342
342
  this.targetEvent = event;
@@ -939,7 +939,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
939
939
  private getFunctionalMembers(): string[] {
940
940
  const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
941
941
  if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
942
- return mFunctionalMembers!.getContent().service_members;
942
+ return mFunctionalMembers.getContent().service_members;
943
943
  }
944
944
  return [];
945
945
  }
@@ -949,7 +949,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
949
949
 
950
950
  // Only generate a fallback avatar if the conversation is with a single specific other user (a "DM").
951
951
  let nonFunctionalMemberCount = 0;
952
- this.getMembers()!.forEach((m) => {
952
+ this.getMembers().forEach((m) => {
953
953
  if (m.membership !== "join" && m.membership !== "invite") return;
954
954
  if (functionalMembers.includes(m.userId)) return;
955
955
  nonFunctionalMemberCount++;
@@ -1070,7 +1070,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
1070
1070
  if (rawMembersEvents === null || this.hasEncryptionStateEvent()) {
1071
1071
  fromServer = true;
1072
1072
  rawMembersEvents = await this.loadMembersFromServer();
1073
- logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
1073
+ logger.log(`LL: got ${rawMembersEvents.length} members from server for room ${this.roomId}`);
1074
1074
  }
1075
1075
  const memberEvents = rawMembersEvents.filter(noUnsafeEventProps).map(this.client.getEventMapper());
1076
1076
  return { memberEvents, fromServer };
@@ -1129,7 +1129,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
1129
1129
  .getMembers()
1130
1130
  .filter((m) => m.isOutOfBand())
1131
1131
  .map((m) => m.events.member?.event as IStateEventWithRoomId);
1132
- logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`);
1132
+ logger.log(`LL: telling store to write ${oobMembers.length} members for room ${this.roomId}`);
1133
1133
  const store = this.client.store;
1134
1134
  return (
1135
1135
  store
@@ -1171,7 +1171,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
1171
1171
  */
1172
1172
  private cleanupAfterLeaving(): void {
1173
1173
  this.clearLoadedMembersIfNeeded().catch((err) => {
1174
- logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
1174
+ logger.error(`error after clearing loaded members from room ${this.roomId} after leaving`);
1175
1175
  logger.log(err);
1176
1176
  });
1177
1177
  }
@@ -2078,7 +2078,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
2078
2078
  };
2079
2079
 
2080
2080
  if (filterType === ThreadFilterType.My) {
2081
- definition!.room!.timeline![FILTER_RELATED_BY_SENDERS.name] = [myUserId];
2081
+ definition.room!.timeline![FILTER_RELATED_BY_SENDERS.name] = [myUserId];
2082
2082
  }
2083
2083
 
2084
2084
  filter.setDefinition(definition);
@@ -2410,7 +2410,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
2410
2410
  const threadRootId = event.threadRootId;
2411
2411
 
2412
2412
  // Where the parent is the thread root and this is a non-thread relation this should live only in the main timeline
2413
- if (!!parentEventId && !isThreadRelation && (threadRootId === parentEventId || roots?.has(parentEventId!))) {
2413
+ if (!!parentEventId && !isThreadRelation && (threadRootId === parentEventId || roots?.has(parentEventId))) {
2414
2414
  return {
2415
2415
  shouldLiveInRoom: true,
2416
2416
  shouldLiveInThread: false,
@@ -3208,7 +3208,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
3208
3208
  );
3209
3209
  } else {
3210
3210
  // When `threadSupport` is disabled treat all events as timelineEvents
3211
- return [events as MatrixEvent[], [] as MatrixEvent[], [] as MatrixEvent[]];
3211
+ return [events, [] as MatrixEvent[], [] as MatrixEvent[]];
3212
3212
  }
3213
3213
  }
3214
3214
 
@@ -3240,7 +3240,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
3240
3240
  Object.keys(content).forEach((eventId: string) => {
3241
3241
  Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
3242
3242
  Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
3243
- const receipt = content[eventId][receiptType][userId] as Receipt;
3243
+ const receipt = content[eventId][receiptType][userId];
3244
3244
  const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE;
3245
3245
  const receiptDestination: Thread | this | undefined = receiptForMainTimeline
3246
3246
  ? this
@@ -132,6 +132,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
132
132
  */
133
133
  public initialEventsFetched = !Thread.hasServerSideSupport;
134
134
  private initalEventFetchProm: Promise<boolean> | undefined;
135
+ private preInitEventTargets = new Map<string, MatrixEvent>();
136
+ private preInitObserverState = { active: true };
135
137
 
136
138
  /**
137
139
  * An array of events to add to the timeline once the thread has been initialised
@@ -170,6 +172,9 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
170
172
  this.reEmitter = new TypedReEmitter(this);
171
173
 
172
174
  this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
175
+ this.once(ThreadEvent.Delete, () => {
176
+ this.preInitObserverState.active = false;
177
+ });
173
178
 
174
179
  this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
175
180
  this.room.on(RoomEvent.Redaction, this.onRedaction);
@@ -364,10 +369,19 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
364
369
  // Modify this event to point at our room's state, and mark its thread
365
370
  // as this.
366
371
  this.setEventMetadata(event);
372
+ const eventId = event.getId();
373
+ if (
374
+ !this.initialEventsFetched &&
375
+ eventId &&
376
+ !event.isRelation(RelationType.Annotation) &&
377
+ !event.isRelation(RelationType.Replace)
378
+ ) {
379
+ this.preInitEventTargets.set(eventId, event);
380
+ }
367
381
 
368
382
  // Decide whether this event is going to be added at the end of the timeline.
369
383
  const lastReply = this.lastReply();
370
- const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp;
384
+ const isNewestReply = !lastReply || event.localTimestamp >= lastReply.localTimestamp;
371
385
 
372
386
  if (!Thread.hasServerSideSupport) {
373
387
  // When there's no server-side support, just add it to the end of the timeline.
@@ -438,12 +452,33 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
438
452
  */
439
453
  this.replayEvents?.push(event);
440
454
 
441
- // For annotations (reactions), aggregate immediately (pre-init) to keep
442
- // reaction counts/summary visible while the thread is still initialising.
443
- // Only aggregate as child: parent aggregation is unnecessary here.
455
+ // Aggregate annotations immediately to keep reaction counts visible.
444
456
  if (event.isRelation(RelationType.Annotation)) {
445
457
  this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
446
458
  }
459
+
460
+ // Edits can also be aggregated immediately when their target is already
461
+ // known. If it is not known yet, replay still handles them after pagination.
462
+ if (event.isRelation(RelationType.Replace)) {
463
+ const targetEventId = event.getRelation()?.event_id;
464
+ const targetEvent = targetEventId ? this.findPreInitTargetEvent(targetEventId) : undefined;
465
+ if (targetEvent && targetEventId) {
466
+ const weakThread = new WeakRef(this);
467
+ const observerState = this.preInitObserverState;
468
+ const eventId = event.getId();
469
+ void this.timelineSet.relations
470
+ .aggregateChildEvent(event, this.timelineSet, targetEvent)
471
+ .then(() => {
472
+ const thread = weakThread.deref();
473
+ if (!thread || !observerState.active) return;
474
+ const effectiveTarget = thread.findPreInitTargetEvent(targetEventId);
475
+ if (effectiveTarget?.replacingEvent()?.getId() === eventId) {
476
+ thread.emit(ThreadEvent.Update, thread);
477
+ }
478
+ })
479
+ .catch((error) => logger.error("Failed to aggregate pre-initialization thread edit: ", error));
480
+ }
481
+ }
447
482
  } else {
448
483
  // Case 2: this is happening later, and we have a timeline. In
449
484
  // this case, these events might be out-of order.
@@ -474,6 +509,15 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
474
509
  }
475
510
  }
476
511
 
512
+ private findPreInitTargetEvent(eventId: string): MatrixEvent | undefined {
513
+ return (
514
+ this.timelineSet.findEventById(eventId) ??
515
+ this.room.findEventById(eventId) ??
516
+ this.preInitEventTargets.get(eventId) ??
517
+ (this.lastEvent?.getId() === eventId ? this.lastEvent : undefined)
518
+ );
519
+ }
520
+
477
521
  public async processEvent(event: MatrixEvent | null | undefined): Promise<void> {
478
522
  if (event) {
479
523
  this.setEventMetadata(event);
@@ -642,6 +686,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
642
686
  this.addEvent(event, false);
643
687
  }
644
688
  this.replayEvents = null;
689
+ this.preInitEventTargets.clear();
645
690
  // just to make sure that, if we've created a timeline window for this thread before the thread itself
646
691
  // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
647
692
  this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
@@ -24,7 +24,7 @@ import {
24
24
  hasRequiredStringProperty,
25
25
  isRecord,
26
26
  } from "../@types/type-guards.ts";
27
- import { Method } from "../http-api";
27
+ import { Method } from "../http-api/index.ts";
28
28
  import { OAuthGrantType } from "./register.ts";
29
29
  import { sleep } from "../utils.ts";
30
30
 
@@ -34,7 +34,7 @@ import {
34
34
  } from "./register.ts";
35
35
  import { encodeUnpaddedBase64Url } from "../base64.ts";
36
36
  import { sha256 } from "../digest.ts";
37
- import { HTTPError, Method } from "../http-api";
37
+ import { HTTPError, Method } from "../http-api/index.ts";
38
38
  import { logger } from "../logger.ts";
39
39
  import { OAuth2Error } from "./error.ts";
40
40
  import { secureRandomString } from "../randomstring.ts";
@@ -126,7 +126,7 @@ export class OAuth2 {
126
126
  throw error;
127
127
  } else {
128
128
  logger.error("Dynamic registration request failed", error);
129
- throw new Error(OAuth2Error.DynamicRegistrationFailed);
129
+ throw new Error(OAuth2Error.DynamicRegistrationFailed, { cause: error });
130
130
  }
131
131
  }
132
132
  }
@@ -0,0 +1,184 @@
1
+ /*
2
+ Copyright 2026 The Matrix.org Foundation C.I.C.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { calculateRetryBackoff } from "./http-api/index.ts";
18
+ import { type Logger } from "./logger.ts";
19
+ import { sleep } from "./utils.ts";
20
+
21
+ /**
22
+ * How to fetch and cache a given value, as passed to {@link PollingCachedValue}.
23
+ */
24
+ export interface PollingCachedValueOptions<ValueType> {
25
+ /** The name of the cached value (for tracing/logs). */
26
+ name: string;
27
+ /** The logger to derive this value's child logger from. */
28
+ logger: Logger;
29
+ /** Fetches a fresh value. Rejections are retried according to the retry policy. */
30
+ fetch: () => Promise<ValueType>;
31
+ /**
32
+ * The default time-to-live before the value gets refreshed, overridable per run
33
+ * via {@link PollingCachedValue.start}.
34
+ * If undefined the value never expires; use {@link PollingCachedValue.refresh} to clear the cache.
35
+ */
36
+ ttlMillis?: number;
37
+ /** Called when a new value is cached. Use to emit changes if needed. */
38
+ onValueCached?: (value: ValueType) => void;
39
+ /**
40
+ * Determines whether a non-transient error should be cached.
41
+ * By default errors are not cached, i.e. the next attempt retries the fetch.
42
+ */
43
+ shouldCacheError?: (error: unknown) => boolean;
44
+ }
45
+
46
+ /**
47
+ * Defines a generic mechanism to fetch and cache a value, refreshing it periodically.
48
+ */
49
+ export class PollingCachedValue<ValueType> {
50
+ private cached?: ValueType;
51
+ private fetchPromise?: Promise<ValueType>;
52
+ private ttlTimeoutHandle?: ReturnType<typeof setTimeout>;
53
+
54
+ private isStopped = false;
55
+ private ttlMillis?: number;
56
+
57
+ private readonly logger: Logger;
58
+
59
+ /**
60
+ * Build a generic mechanism allowing to fetch and cache a given value.
61
+ * @param opts - Describes what to fetch and how to cache it, see {@link PollingCachedValueOptions}.
62
+ */
63
+ public constructor(private readonly opts: PollingCachedValueOptions<ValueType>) {
64
+ this.ttlMillis = opts.ttlMillis;
65
+ this.logger = opts.logger.getChild(`PollingCachedValue<${opts.name}>`);
66
+ }
67
+
68
+ /**
69
+ * Gets the cached value if any.
70
+ */
71
+ public get(): ValueType | undefined {
72
+ return this.cached;
73
+ }
74
+
75
+ /**
76
+ * Ensures that the value was fetched once before getting the actual cached value.
77
+ */
78
+ public async wait(): Promise<ValueType | undefined> {
79
+ await this.doFetch();
80
+ return this.cached;
81
+ }
82
+
83
+ /**
84
+ * Call this as early as possible and when the value can already be fetched.
85
+ * @param ttlMillis - The time-to-live before the value gets refreshed, overriding the one
86
+ * given at construction. If omitted the configured default is kept.
87
+ */
88
+ public start(ttlMillis?: number): void {
89
+ if (ttlMillis !== undefined) {
90
+ this.ttlMillis = ttlMillis;
91
+ }
92
+ this.isStopped = false;
93
+ // Request a fetch as soon as possible
94
+ this.doFetch().catch((err) => {
95
+ this.logger.debug("Cached value fetch on start did fail", err);
96
+ });
97
+ }
98
+ /**
99
+ * Call when the client is stopped, or when the cached value is no longer needed.
100
+ */
101
+ public stop(): void {
102
+ if (this.ttlTimeoutHandle) {
103
+ clearTimeout(this.ttlTimeoutHandle);
104
+ }
105
+ this.isStopped = true;
106
+ }
107
+
108
+ /**
109
+ * Force to refresh.
110
+ * @throws
111
+ */
112
+ public async refresh(): Promise<ValueType | undefined> {
113
+ await this.doFetch(true);
114
+ return this.cached;
115
+ }
116
+
117
+ /**
118
+ * Internal fetch method
119
+ * @param force - if set to true will force a fetch of the value even if a fetch is already in progress.
120
+ * @private
121
+ */
122
+ private async doFetch(force: boolean = false): Promise<void> {
123
+ if (this.fetchPromise && !force) {
124
+ await this.fetchPromise;
125
+ return;
126
+ }
127
+
128
+ this.fetchPromise = this.fetchWithRetryPolicy();
129
+
130
+ try {
131
+ this.cached = await this.fetchPromise;
132
+ if (this.isStopped) {
133
+ this.fetchPromise = undefined;
134
+ return;
135
+ }
136
+ this.logger.trace(`New cachedValue: ${this.cached}`);
137
+ this.opts.onValueCached?.(this.cached);
138
+ // The value is cached only for a given time.
139
+ if (this.ttlMillis) {
140
+ this.ttlTimeoutHandle = setTimeout(() => {
141
+ this.fetchPromise = undefined;
142
+ void this.doFetch();
143
+ }, this.ttlMillis);
144
+ }
145
+ } catch (error) {
146
+ this.logger.debug(`Error fetching server value for ${error}`);
147
+ if (this.isStopped) {
148
+ this.fetchPromise = undefined;
149
+ return;
150
+ }
151
+ if (!this.opts.shouldCacheError?.(error)) {
152
+ // clear the promise, i.e next tentative will retry to fetch
153
+ this.fetchPromise = undefined;
154
+ }
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Gracefully handle transient error retries, respect retry_after_millis for rate limits.
161
+ * @private
162
+ */
163
+ private async fetchWithRetryPolicy(): Promise<ValueType> {
164
+ let currentRetryCount = 0;
165
+ while (true) {
166
+ try {
167
+ return await this.opts.fetch();
168
+ } catch (e) {
169
+ this.logger.trace(`Failed to fetch retry: ${e}`);
170
+ if (this.isStopped) {
171
+ throw e;
172
+ }
173
+ currentRetryCount++;
174
+ const backoff = calculateRetryBackoff(e, currentRetryCount, true);
175
+ if (backoff < 0) {
176
+ // Max number of retries reached, or error is not retryable. rethrow the error
177
+ throw e;
178
+ }
179
+ // wait for the specified time and then retry the request
180
+ await sleep(backoff);
181
+ }
182
+ }
183
+ }
184
+ }
@@ -294,7 +294,7 @@ export class PushProcessor {
294
294
  // These lines are mostly to make the tests happy. We shouldn't run into these
295
295
  // properties missing in practice.
296
296
  if (!newRules) newRules = {} as IPushRules;
297
- if (!newRules.global) newRules.global = {} as PushRuleSet;
297
+ if (!newRules.global) newRules.global = {};
298
298
  if (!newRules.global.override) newRules.global.override = [];
299
299
  if (!newRules.global.underride) newRules.global.underride = [];
300
300
 
@@ -349,7 +349,7 @@ export class PushProcessor {
349
349
  // These lines are mostly to make the tests happy. We shouldn't run into these
350
350
  // properties missing in practice.
351
351
  if (!newRules) newRules = {} as IPushRules;
352
- if (!newRules.global) newRules.global = {} as PushRuleSet;
352
+ if (!newRules.global) newRules.global = {};
353
353
  if (!newRules.global.override) newRules.global.override = [];
354
354
  if (!newRules.global.room) newRules.global.room = [];
355
355
  if (!newRules.global.sender) newRules.global.sender = [];
@@ -846,7 +846,7 @@ export class PushProcessor {
846
846
  for (const kind of RULEKINDS_IN_ORDER) {
847
847
  if (this.client.pushRules[scope][kind] === undefined) continue;
848
848
 
849
- for (const rule of this.client.pushRules[scope][kind]!) {
849
+ for (const rule of this.client.pushRules[scope][kind]) {
850
850
  if (rule.rule_id === ruleId) return { rule, kind };
851
851
  }
852
852
  }
@@ -17,7 +17,6 @@ limitations under the License.
17
17
  import { type IMinimalEvent } from "./sync-accumulator.ts";
18
18
  import { EventType } from "./@types/event.ts";
19
19
  import { isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils.ts";
20
- import { type IContent } from "./models/event.ts";
21
20
  import { type ReceiptContent, type ReceiptType } from "./@types/read_receipts.ts";
22
21
 
23
22
  interface AccumulatedReceipt {
@@ -162,7 +161,7 @@ export class ReceiptAccumulator {
162
161
  room_id: roomId,
163
162
  content: {
164
163
  // $event_id: { "m.read": { $user_id: $json } }
165
- } as IContent,
164
+ },
166
165
  };
167
166
 
168
167
  const receiptEventContent: MapWithDefault<
@@ -385,7 +385,7 @@ export class MSC4108SignInWithQR {
385
385
  );
386
386
  }
387
387
  if (payload.type === PayloadType.Failure) {
388
- throw new RendezvousError("Failed", (payload as FailurePayload).reason);
388
+ throw new RendezvousError("Failed", payload.reason);
389
389
  }
390
390
  if (payload.type !== PayloadType.ProtocolAccepted) {
391
391
  throw new RendezvousError("Unexpected message received", MSC4108FailureReason.UnexpectedMessageReceived);
@@ -412,11 +412,10 @@ export class MSC4108SignInWithQR {
412
412
  } else if (res.error === "access_denied") {
413
413
  reason = MSC4108FailureReason.UserCancelled;
414
414
  }
415
- const payload: FailurePayload = {
415
+ await this.send({
416
416
  type: PayloadType.Failure,
417
417
  reason,
418
- };
419
- await this.send(payload);
418
+ } satisfies FailurePayload);
420
419
 
421
420
  throw new RendezvousError("Rejection from device authorization endpoint", reason);
422
421
  }
@@ -0,0 +1,40 @@
1
+ /*
2
+ Copyright 2026 The Matrix.org Foundation C.I.C.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { PollingCachedValue } from "./pollingCachedValue.ts";
18
+ import { type Transport } from "./matrixrtc/index.ts";
19
+ import { ClientEvent, type MatrixClient } from "./client.ts";
20
+ import { type Logger } from "./logger.ts";
21
+ import { MatrixError } from "./http-api/index.ts";
22
+
23
+ const TRANSPORT_CACHE_TTL_MILLISECONDS = 1000 * 60 * 60 * 24; // 1 day
24
+
25
+ /**
26
+ * Builds a cache for the rtc/transport discovery end-point of the given client.
27
+ */
28
+ export function createRtcTransportsCachedValue(client: MatrixClient, logger: Logger): PollingCachedValue<Transport[]> {
29
+ return new PollingCachedValue({
30
+ name: "RTCTransports",
31
+ logger,
32
+ ttlMillis: TRANSPORT_CACHE_TTL_MILLISECONDS,
33
+ fetch: () => client._unstable_getRTCTransports(),
34
+ onValueCached: (transports) => client.emit(ClientEvent.RtcTransportsUpdated, transports),
35
+ // If the end point is not supported by the homeserver it will be a 404 error.
36
+ // This is a final error, it can be cached, no need to retry everytime.
37
+ // It will retry when ttl has expired.
38
+ shouldCacheError: (error) => error instanceof MatrixError && error.httpStatus === 404,
39
+ });
40
+ }
@@ -394,7 +394,7 @@ export class PerSessionKeyBackupDownloader {
394
394
  ): Promise<void> {
395
395
  const sessionsToImport: Record<string, KeyBackupSession> = { [sessionInfo.megolmSessionId]: data };
396
396
 
397
- const keys = await configuration!.decryptor.decryptSessions(sessionsToImport);
397
+ const keys = await configuration.decryptor.decryptSessions(sessionsToImport);
398
398
  for (const k of keys) {
399
399
  k.room_id = sessionInfo.roomId;
400
400
  }
@@ -23,7 +23,6 @@ import {
23
23
  type KeyBackupCheck,
24
24
  type KeyBackupInfo,
25
25
  type KeyBackupSession,
26
- type Curve25519SessionData,
27
26
  type KeyBackupRestoreOpts,
28
27
  type KeyBackupRestoreResult,
29
28
  type KeyBackupRoomSessions,
@@ -42,7 +41,6 @@ import {
42
41
  CryptoEvent,
43
42
  ImportRoomKeyStage,
44
43
  } from "../crypto-api/index.ts";
45
- import { type AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
46
44
  import { type IMegolmSessionData } from "../@types/crypto.ts";
47
45
 
48
46
  /** Authentification of the backup info, depends on algorithm */
@@ -901,9 +899,7 @@ export class RustBackupDecryptor implements BackupDecryptor {
901
899
  /**
902
900
  * Implements {@link BackupDecryptor#decryptSessions}
903
901
  */
904
- public async decryptSessions(
905
- ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | AESEncryptedSecretStoragePayload>>,
906
- ): Promise<IMegolmSessionData[]> {
902
+ public async decryptSessions(ciphertexts: Record<string, KeyBackupSession>): Promise<IMegolmSessionData[]> {
907
903
  const keys: IMegolmSessionData[] = [];
908
904
  for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
909
905
  try {