diff --git a/CHANGELOG.md b/CHANGELOG.md index 00691cac0..1951ec102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31) +================================================================================================== + +## ✨ Features + * Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)). + * Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)). + * Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)). + +## 🐛 Bug Fixes + * Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)). + * Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni. + * Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto. + * fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)). + * Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald. + * Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027. + * fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)). + Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16) ================================================================================================== diff --git a/package.json b/package.json index b991e763c..954496c7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "19.3.0", + "version": "19.4.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=12.9.0" @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./src/index.ts", + "main": "./lib/index.js", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -125,5 +125,6 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - } + }, + "typings": "./lib/index.d.ts" } diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 9aa3c5c78..b9f16c742 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; -const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; -const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; +const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); +const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); const ROOM_ID = '!ROOM:ID'; diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index 6759fe161..8e2647404 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src'; const Olm = global.Olm; -const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; +const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); const ROOM_ID = '!ROOM:ID'; diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index 22bd4505d..898a04dd8 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -34,7 +34,7 @@ import { IRoomEncryption } from "../RoomList"; * * @type {Object.} */ -export const ENCRYPTION_CLASSES: Record EncryptionAlgorithm> = {}; +export const ENCRYPTION_CLASSES = new Map EncryptionAlgorithm>(); type DecryptionClassParams = Omit; @@ -44,7 +44,7 @@ type DecryptionClassParams = Omit; * * @type {Object.} */ -export const DECRYPTION_CLASSES: Record DecryptionAlgorithm> = {}; +export const DECRYPTION_CLASSES = new Map DecryptionAlgorithm>(); export interface IParams { userId: string; @@ -297,6 +297,6 @@ export function registerAlgorithm( encryptor: new (params: IParams) => EncryptionAlgorithm, decryptor: new (params: Omit) => DecryptionAlgorithm, ): void { - ENCRYPTION_CLASSES[algorithm] = encryptor; - DECRYPTION_CLASSES[algorithm] = decryptor; + ENCRYPTION_CLASSES.set(algorithm, encryptor); + DECRYPTION_CLASSES.set(algorithm, decryptor); } diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 1807905ba..122164f44 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1191,7 +1191,7 @@ class MegolmEncryption extends EncryptionAlgorithm { class MegolmDecryption extends DecryptionAlgorithm { // events which we couldn't decrypt due to unknown sessions / indexes: map from // senderKey|sessionId to Set of MatrixEvents - private pendingEvents: Record>> = {}; + private pendingEvents = new Map>>(); // this gets stubbed out by the unit tests. private olmlib = olmlib; @@ -1343,10 +1343,10 @@ class MegolmDecryption extends DecryptionAlgorithm { const content = event.getWireContent(); const senderKey = content.sender_key; const sessionId = content.session_id; - if (!this.pendingEvents[senderKey]) { - this.pendingEvents[senderKey] = new Map(); + if (!this.pendingEvents.has(senderKey)) { + this.pendingEvents.set(senderKey, new Map>()); } - const senderPendingEvents = this.pendingEvents[senderKey]; + const senderPendingEvents = this.pendingEvents.get(senderKey); if (!senderPendingEvents.has(sessionId)) { senderPendingEvents.set(sessionId, new Set()); } @@ -1364,7 +1364,7 @@ class MegolmDecryption extends DecryptionAlgorithm { const content = event.getWireContent(); const senderKey = content.sender_key; const sessionId = content.session_id; - const senderPendingEvents = this.pendingEvents[senderKey]; + const senderPendingEvents = this.pendingEvents.get(senderKey); const pendingEvents = senderPendingEvents?.get(sessionId); if (!pendingEvents) { return; @@ -1375,7 +1375,7 @@ class MegolmDecryption extends DecryptionAlgorithm { senderPendingEvents.delete(sessionId); } if (senderPendingEvents.size === 0) { - delete this.pendingEvents[senderKey]; + this.pendingEvents.delete(senderKey); } } @@ -1711,7 +1711,7 @@ class MegolmDecryption extends DecryptionAlgorithm { * @return {Boolean} whether all messages were successfully decrypted */ private async retryDecryption(senderKey: string, sessionId: string): Promise { - const senderPendingEvents = this.pendingEvents[senderKey]; + const senderPendingEvents = this.pendingEvents.get(senderKey); if (!senderPendingEvents) { return true; } @@ -1732,16 +1732,16 @@ class MegolmDecryption extends DecryptionAlgorithm { })); // If decrypted successfully, they'll have been removed from pendingEvents - return !this.pendingEvents[senderKey]?.has(sessionId); + return !this.pendingEvents.get(senderKey)?.has(sessionId); } public async retryDecryptionFromSender(senderKey: string): Promise { - const senderPendingEvents = this.pendingEvents[senderKey]; + const senderPendingEvents = this.pendingEvents.get(senderKey); if (!senderPendingEvents) { return true; } - delete this.pendingEvents[senderKey]; + this.pendingEvents.delete(senderKey); await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => { await Promise.all([...pending].map(async (ev) => { @@ -1753,7 +1753,7 @@ class MegolmDecryption extends DecryptionAlgorithm { })); })); - return !this.pendingEvents[senderKey]; + return !this.pendingEvents.has(senderKey); } public async sendSharedHistoryInboundSessions(devicesByUser: Record): Promise { diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 451d2d8c8..653916481 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -278,9 +278,9 @@ export class Crypto extends TypedEventEmitter = {}; + private roomEncryptors = new Map(); // map from algorithm to DecryptionAlgorithm instance, for each room - private roomDecryptors: Record> = {}; + private roomDecryptors = new Map>(); private deviceKeys: Record = {}; // type: key @@ -422,7 +422,7 @@ export class Crypto extends TypedEventEmitter { const trackMembers = async () => { // not an encrypted room - if (!this.roomEncryptors[roomId]) { + if (!this.roomEncryptors.has(roomId)) { return; } const room = this.clientStore.getRoom(roomId); @@ -2785,7 +2785,7 @@ export class Crypto extends TypedEventEmitter { // check for rooms with encryption enabled - const alg = this.roomEncryptors[room.roomId]; + const alg = this.roomEncryptors.get(room.roomId); if (!alg) { return false; } @@ -3533,7 +3533,7 @@ export class Crypto extends TypedEventEmitter; + let decryptors: Map; let alg: DecryptionAlgorithm; roomId = roomId || null; if (roomId) { - decryptors = this.roomDecryptors[roomId]; + decryptors = this.roomDecryptors.get(roomId); if (!decryptors) { - this.roomDecryptors[roomId] = decryptors = {}; + decryptors = new Map(); + this.roomDecryptors.set(roomId, decryptors); } - alg = decryptors[algorithm]; + alg = decryptors.get(algorithm); if (alg) { return alg; } } - const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; + const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm); if (!AlgClass) { throw new algorithms.DecryptionError( 'UNKNOWN_ENCRYPTION_ALGORITHM', @@ -3777,7 +3778,7 @@ export class Crypto extends TypedEventEmitter; + private _eventIdToTimeline = new Map(); private filter?: Filter; /** @@ -138,7 +138,7 @@ export class EventTimelineSet extends TypedEventEmitter(); this.filter = opts.filter; @@ -210,7 +210,7 @@ export class EventTimelineSet extends TypedEventEmitter(); } else { this.timelines.push(newTimeline); } @@ -288,7 +288,7 @@ export class EventTimelineSet extends TypedEventEmitter = {}; -function intern(str: string): string { - if (!interns[str]) { - interns[str] = str; - } - return interns[str]; -} - /* eslint-disable camelcase */ export interface IContent { [key: string]: any; @@ -326,17 +318,17 @@ export class MatrixEvent extends TypedEventEmitter { if (typeof event[prop] !== "string") return; - event[prop] = intern(event[prop]); + event[prop] = internaliseString(event[prop]); }); ["membership", "avatar_url", "displayname"].forEach((prop) => { if (typeof event.content?.[prop] !== "string") return; - event.content[prop] = intern(event.content[prop]); + event.content[prop] = internaliseString(event.content[prop]); }); ["rel_type"].forEach((prop) => { if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; - event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]); + event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]); }); this.txnId = event.txn_id || null; @@ -796,6 +788,8 @@ export class MatrixEvent extends TypedEventEmitter>>(); constructor(private readonly client: MatrixClient, private readonly room?: Room) { } @@ -57,14 +51,15 @@ export class RelationsContainer { relationType: RelationType | string, eventType: EventType | string, ): Relations | undefined { - return this.relations[eventId]?.[relationType]?.[eventType]; + return this.relations.get(eventId)?.get(relationType)?.get(eventType); } public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] { - const relationsForEvent = this.relations[parentEventId] ?? {}; + const relationsForEvent = this.relations.get(parentEventId) + ?? new Map>(); const events: MatrixEvent[] = []; - for (const relationsRecord of Object.values(relationsForEvent)) { - for (const relations of Object.values(relationsRecord)) { + for (const relationsRecord of relationsForEvent.values()) { + for (const relations of relationsRecord.values()) { events.push(...relations.getRelations()); } } @@ -79,11 +74,11 @@ export class RelationsContainer { * @param {MatrixEvent} event The event to check as relation target. */ public aggregateParentEvent(event: MatrixEvent): void { - const relationsForEvent = this.relations[event.getId()]; + const relationsForEvent = this.relations.get(event.getId()); if (!relationsForEvent) return; - for (const relationsWithRelType of Object.values(relationsForEvent)) { - for (const relationsWithEventType of Object.values(relationsWithRelType)) { + for (const relationsWithRelType of relationsForEvent.values()) { + for (const relationsWithEventType of relationsWithRelType.values()) { relationsWithEventType.setTargetEvent(event); } } @@ -123,23 +118,26 @@ export class RelationsContainer { const { event_id: relatesToEventId, rel_type: relationType } = relation; const eventType = event.getType(); - let relationsForEvent = this.relations[relatesToEventId]; + let relationsForEvent = this.relations.get(relatesToEventId); if (!relationsForEvent) { - relationsForEvent = this.relations[relatesToEventId] = {}; + relationsForEvent = new Map>(); + this.relations.set(relatesToEventId, relationsForEvent); } - let relationsWithRelType = relationsForEvent[relationType]; + let relationsWithRelType = relationsForEvent.get(relationType); if (!relationsWithRelType) { - relationsWithRelType = relationsForEvent[relationType] = {}; + relationsWithRelType = new Map(); + relationsForEvent.set(relationType, relationsWithRelType); } - let relationsWithEventType = relationsWithRelType[eventType]; + let relationsWithEventType = relationsWithRelType.get(eventType); if (!relationsWithEventType) { - relationsWithEventType = relationsWithRelType[eventType] = new Relations( + relationsWithEventType = new Relations( relationType, eventType, this.client, ); + relationsWithRelType.set(eventType, relationsWithEventType); const room = this.room ?? timelineSet?.room; const relatesToEvent = timelineSet?.findEventById(relatesToEventId) diff --git a/src/models/room-state.ts b/src/models/room-state.ts index c7d3ac325..b0104cf70 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -79,7 +79,7 @@ export class RoomState extends TypedEventEmitter public readonly reEmitter = new TypedReEmitter(this); private sentinels: Record = {}; // userId: RoomMember // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) - private displayNameToUserIds: Record = {}; + private displayNameToUserIds = new Map(); private userIdsToDisplayNames: Record = {}; private tokenToInvite: Record = {}; // 3pid invite state_key to m.room.member invite private joinedMemberCount: number = null; // cache of the number of joined members @@ -709,7 +709,7 @@ export class RoomState extends TypedEventEmitter * @return {string[]} An array of user IDs or an empty array. */ public getUserIdsWithDisplayName(displayName: string): string[] { - return this.displayNameToUserIds[utils.removeHiddenChars(displayName)] || []; + return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? []; } /** @@ -941,11 +941,11 @@ export class RoomState extends TypedEventEmitter // the lot. const strippedOldName = utils.removeHiddenChars(oldName); - const existingUserIds = this.displayNameToUserIds[strippedOldName]; + const existingUserIds = this.displayNameToUserIds.get(strippedOldName); if (existingUserIds) { // remove this user ID from this array const filteredUserIDs = existingUserIds.filter((id) => id !== userId); - this.displayNameToUserIds[strippedOldName] = filteredUserIDs; + this.displayNameToUserIds.set(strippedOldName, filteredUserIDs); } } @@ -954,10 +954,9 @@ export class RoomState extends TypedEventEmitter const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js if (strippedDisplayname) { - if (!this.displayNameToUserIds[strippedDisplayname]) { - this.displayNameToUserIds[strippedDisplayname] = []; - } - this.displayNameToUserIds[strippedDisplayname].push(userId); + const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? []; + arr.push(userId); + this.displayNameToUserIds.set(strippedDisplayname, arr); } } } diff --git a/src/utils.ts b/src/utils.ts index 5dfeffece..f9c087fd9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,6 +28,30 @@ import { MatrixClient, MatrixEvent } from "."; import { M_TIMESTAMP } from "./@types/location"; import { ReceiptType } from "./@types/read_receipts"; +const interns = new Map(); + +/** + * Internalises a string, reusing a known pointer or storing the pointer + * if needed for future strings. + * @param str The string to internalise. + * @returns The internalised string. + */ +export function internaliseString(str: string): string { + // Unwrap strings before entering the map, if we somehow got a wrapped + // string as our input. This should only happen from tests. + if ((str as unknown) instanceof String) { + str = str.toString(); + } + + // Check the map to see if we can store the value + if (!interns.has(str)) { + interns.set(str, str); + } + + // Return any cached string reference + return interns.get(str); +} + /** * Encode a dictionary of query parameters. * Omits any undefined/null values.