From 65762c4ae5c2f03fd59b992220fb149485e50b3f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Sep 2024 14:31:12 +0100 Subject: [PATCH 01/34] Switch sliding sync support to simplified sliding sync Experimental PR to test js-sdk with simlified sliding sync. This does not maintain support for regulaer sliding sync. --- src/client.ts | 2 +- src/sliding-sync.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client.ts b/src/client.ts index 5bc6ab07120..3dfb030fe13 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9929,7 +9929,7 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, "/sync", qps, req, { - prefix: "/_matrix/client/unstable/org.matrix.msc3575", + prefix: "/_matrix/client/unstable/org.matrix.simplified_msc3575", baseUrl: proxyBaseUrl, localTimeoutMs: clientTimeout, abortSignal, diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 487c447a262..d49ac4efa15 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -461,13 +461,13 @@ export class SlidingSync extends TypedEventEmitter { + public setListRanges(key: string, ranges: number[][]): void { const list = this.lists.get(key); if (!list) { - return Promise.reject(new Error("no list with key " + key)); + throw new Error("no list with key " + key); } list.updateListRange(ranges); - return this.resend(); + //return this.resend(); } /** @@ -479,7 +479,7 @@ export class SlidingSync extends TypedEventEmitter { + public setList(key: string, list: MSC3575List): void { const existingList = this.lists.get(key); if (existingList) { existingList.replaceList(list); @@ -488,7 +488,7 @@ export class SlidingSync extends TypedEventEmitter = {}; this.lists.forEach((l: SlidingList, key: string) => { - reqLists[key] = l.getList(false); + reqLists[key] = l.getList(true); }); const reqBody: MSC3575SlidingSyncRequest = { lists: reqLists, From 259399852bab9d78fbba4ca6be35c8e2a2afaa68 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:36:27 +0100 Subject: [PATCH 02/34] Remove txn_id handling, ensure we always resend when req params change --- spec/integ/sliding-sync.spec.ts | 241 +------------------------------- src/sliding-sync.ts | 79 ++--------- 2 files changed, 11 insertions(+), 309 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 29cd3443bfe..c387c1b2de1 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -42,7 +42,7 @@ describe("SlidingSync", () => { const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; const proxyBaseUrl = "http://localhost:8008"; - const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.msc3575/sync"; + const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"; // assign client/httpBackend globals const setupClient = () => { @@ -1018,245 +1018,6 @@ describe("SlidingSync", () => { }); }); - describe("transaction IDs", () => { - beforeAll(setupClient); - afterAll(teardownClient); - const roomId = "!foo:bar"; - - let slidingSync: SlidingSync; - - // really this applies to them all but it's easier to just test one - it("should resolve modifyRoomSubscriptions after SlidingSync.start() is called", async () => { - const roomSubInfo = { - timeline_limit: 1, - required_state: [["m.room.name", ""]], - }; - // add the subscription - slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1); - // modification before SlidingSync.start() - const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check(function (req) { - const body = req.data; - logger.debug("got ", body); - expect(body.room_subscriptions).toBeTruthy(); - expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo); - expect(body.txn_id).toBeTruthy(); - txnId = body.txn_id; - }) - .respond(200, function () { - return { - pos: "aaa", - txn_id: txnId, - lists: {}, - extensions: {}, - rooms: { - [roomId]: { - name: "foo bar", - required_state: [], - timeline: [], - }, - }, - }; - }); - slidingSync.start(); - await httpBackend!.flushAllExpected(); - await subscribePromise; - }); - it("should resolve setList during a connection", async () => { - const newList = { - ranges: [[0, 20]], - }; - const promise = slidingSync.setList("a", newList); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check(function (req) { - const body = req.data; - logger.debug("got ", body); - expect(body.room_subscriptions).toBeFalsy(); - expect(body.lists["a"]).toEqual(newList); - expect(body.txn_id).toBeTruthy(); - txnId = body.txn_id; - }) - .respond(200, function () { - return { - pos: "bbb", - txn_id: txnId, - lists: { a: { count: 5 } }, - extensions: {}, - }; - }); - await httpBackend!.flushAllExpected(); - await promise; - expect(txnId).toBeDefined(); - }); - it("should resolve setListRanges during a connection", async () => { - const promise = slidingSync.setListRanges("a", [[20, 40]]); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check(function (req) { - const body = req.data; - logger.debug("got ", body); - expect(body.room_subscriptions).toBeFalsy(); - expect(body.lists["a"]).toEqual({ - ranges: [[20, 40]], - }); - expect(body.txn_id).toBeTruthy(); - txnId = body.txn_id; - }) - .respond(200, function () { - return { - pos: "ccc", - txn_id: txnId, - lists: { a: { count: 5 } }, - extensions: {}, - }; - }); - await httpBackend!.flushAllExpected(); - await promise; - expect(txnId).toBeDefined(); - }); - it("should resolve modifyRoomSubscriptionInfo during a connection", async () => { - const promise = slidingSync.modifyRoomSubscriptionInfo({ - timeline_limit: 99, - }); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check(function (req) { - const body = req.data; - logger.debug("got ", body); - expect(body.room_subscriptions).toBeTruthy(); - expect(body.room_subscriptions[roomId]).toEqual({ - timeline_limit: 99, - }); - expect(body.txn_id).toBeTruthy(); - txnId = body.txn_id; - }) - .respond(200, function () { - return { - pos: "ddd", - txn_id: txnId, - extensions: {}, - }; - }); - await httpBackend!.flushAllExpected(); - await promise; - expect(txnId).toBeDefined(); - }); - it("should reject earlier pending promises if a later transaction is acknowledged", async () => { - // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. - const gotTxnIds: any[] = []; - const pushTxn = function (req: MockHttpBackend["requests"][0]) { - gotTxnIds.push(req.data.txn_id); - }; - const failPromise = slidingSync.setListRanges("a", [[20, 40]]); - httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id - await httpBackend!.flushAllExpected(); - const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]); - httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id - await httpBackend!.flushAllExpected(); - - const okPromise = slidingSync.setListRanges("a", [[0, 20]]); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check((req) => { - txnId = req.data.txn_id; - }) - .respond(200, () => { - // include the txn_id, earlier requests should now be reject()ed. - return { - pos: "g", - txn_id: txnId, - }; - }); - await Promise.all([ - expect(failPromise).rejects.toEqual(gotTxnIds[0]), - expect(failPromise2).rejects.toEqual(gotTxnIds[1]), - httpBackend!.flushAllExpected(), - okPromise, - ]); - - expect(txnId).toBeDefined(); - }); - it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { - // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. - const gotTxnIds: any[] = []; - const pushTxn = function (req: MockHttpBackend["requests"][0]) { - gotTxnIds.push(req.data?.txn_id); - }; - const A = slidingSync.setListRanges("a", [[20, 40]]); - httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" }); - await httpBackend!.flushAllExpected(); - const B = slidingSync.setListRanges("a", [[60, 70]]); - httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id - await httpBackend!.flushAllExpected(); - - // attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection - // which is a fail. - - const C = slidingSync.setListRanges("a", [[0, 20]]); - let pendingC = true; - C.finally(() => { - pendingC = false; - }); - httpBackend! - .when("POST", syncUrl) - .check(pushTxn) - .respond(200, () => { - // include the txn_id for B, so C's promise is outstanding - return { - pos: "C", - txn_id: gotTxnIds[1], - }; - }); - await Promise.all([ - expect(A).rejects.toEqual(gotTxnIds[0]), - httpBackend!.flushAllExpected(), - // A is rejected, see above - expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved - ]); - expect(pendingC).toBe(true); // C is pending still - }); - it("should do nothing for unknown txn_ids", async () => { - const promise = slidingSync.setListRanges("a", [[20, 40]]); - let pending = true; - promise.finally(() => { - pending = false; - }); - let txnId: string | undefined; - httpBackend! - .when("POST", syncUrl) - .check(function (req) { - const body = req.data; - logger.debug("got ", body); - expect(body.room_subscriptions).toBeFalsy(); - expect(body.lists["a"]).toEqual({ - ranges: [[20, 40]], - }); - expect(body.txn_id).toBeTruthy(); - txnId = body.txn_id; - }) - .respond(200, function () { - return { - pos: "ccc", - txn_id: "bogus transaction id", - lists: { a: { count: 5 } }, - extensions: {}, - }; - }); - await httpBackend!.flushAllExpected(); - expect(txnId).toBeDefined(); - expect(pending).toBe(true); - slidingSync.stop(); - }); - }); - describe("custom room subscriptions", () => { beforeAll(setupClient); afterAll(teardownClient); diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index d49ac4efa15..b41d31e06ea 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -347,11 +347,6 @@ export class SlidingSync extends TypedEventEmitter & { txnId: string })[] = []; // map of extension name to req/resp handler private extensions: Record> = {}; @@ -467,7 +462,7 @@ export class SlidingSync extends TypedEventEmitter): Promise { + public modifyRoomSubscriptions(s: Set) { this.desiredRoomSubscriptions = s; - return this.resend(); + this.resend(); } /** @@ -521,10 +516,10 @@ export class SlidingSync extends TypedEventEmitter { + public modifyRoomSubscriptionInfo(rs: MSC3575RoomSubscription) { this.roomSubscriptionInfo = rs; this.confirmedRoomSubscriptions = new Set(); - return this.resend(); + this.resend(); } /** @@ -745,53 +740,12 @@ export class SlidingSync extends TypedEventEmitter { - if (this.needsResend && this.txnIdDefers.length > 0) { - // we already have a resend queued, so just return the same promise - return this.txnIdDefers[this.txnIdDefers.length - 1].promise; - } + public resend(): void { this.needsResend = true; - this.txnId = this.client.makeTxnId(); - const d = defer(); - this.txnIdDefers.push({ - ...d, - txnId: this.txnId, - }); this.abortController?.abort(); this.abortController = new AbortController(); - return d.promise; - } - - private resolveTransactionDefers(txnId?: string): void { - if (!txnId) { - return; - } - // find the matching index - let txnIndex = -1; - for (let i = 0; i < this.txnIdDefers.length; i++) { - if (this.txnIdDefers[i].txnId === txnId) { - txnIndex = i; - break; - } - } - if (txnIndex === -1) { - // this shouldn't happen; we shouldn't be seeing txn_ids for things we don't know about, - // whine about it. - logger.warn(`resolveTransactionDefers: seen ${txnId} but it isn't a pending txn, ignoring.`); - return; - } - // This list is sorted in time, so if the input txnId ACKs in the middle of this array, - // then everything before it that hasn't been ACKed yet never will and we should reject them. - for (let i = 0; i < txnIndex; i++) { - this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId); - } - this.txnIdDefers[txnIndex].resolve(txnId); - // clear out settled promises, including the one we resolved. - this.txnIdDefers = this.txnIdDefers.slice(txnIndex + 1); } /** @@ -811,20 +765,13 @@ export class SlidingSync extends TypedEventEmitter { - d.reject(d.txnId); - }); - this.txnIdDefers = []; // resend sticky params and de-confirm all subscriptions this.lists.forEach((l) => { l.setModified(true); }); this.confirmedRoomSubscriptions = new Set(); // leave desired ones alone though! // reset the connection as we might be wedged - this.needsResend = true; - this.abortController?.abort(); - this.abortController = new AbortController(); + this.resend(); } /** @@ -868,10 +815,6 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 13 Sep 2024 09:52:38 +0100 Subject: [PATCH 03/34] Fix some tests --- spec/integ/sliding-sync.spec.ts | 37 +++++++++++++-------------------- src/sliding-sync.ts | 2 ++ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index c387c1b2de1..bd66bbd276e 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -144,16 +144,14 @@ describe("SlidingSync", () => { }); await httpBackend!.flushAllExpected(); - // expect nothing but ranges and non-initial extensions to be sent + // expect all params to be sent TODO: check MSC4186 httpBackend! .when("POST", syncUrl) .check(function (req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); - expect(body.lists["a"]).toEqual({ - ranges: [[0, 10]], - }); + expect(body.lists["a"]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: false }); expect(req.queryParams!["pos"]).toEqual("11"); @@ -390,18 +388,19 @@ describe("SlidingSync", () => { [3, 5], ]; + // request first 3 rooms + const listReq = { + ranges: [[0, 2]], + sort: ["by_name"], + timeline_limit: 1, + required_state: [["m.room.topic", ""]], + filters: { + is_dm: true, + }, + }; + let slidingSync: SlidingSync; it("should be possible to subscribe to a list", async () => { - // request first 3 rooms - const listReq = { - ranges: [[0, 2]], - sort: ["by_name"], - timeline_limit: 1, - required_state: [["m.room.topic", ""]], - filters: { - is_dm: true, - }, - }; slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1); httpBackend! .when("POST", syncUrl) @@ -468,10 +467,7 @@ describe("SlidingSync", () => { const body = req.data; logger.log("next ranges", body.lists["a"].ranges); expect(body.lists).toBeTruthy(); - expect(body.lists["a"]).toEqual({ - // only the ranges should be sent as the rest are unchanged and sticky - ranges: newRanges, - }); + expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186 }) .respond(200, { pos: "b", @@ -514,10 +510,7 @@ describe("SlidingSync", () => { const body = req.data; logger.log("extra list", body); expect(body.lists).toBeTruthy(); - expect(body.lists["a"]).toEqual({ - // only the ranges should be sent as the rest are unchanged and sticky - ranges: newRanges, - }); + expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186 expect(body.lists["b"]).toEqual(extraListReq); }) .respond(200, { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index b41d31e06ea..565b3a21bc9 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -877,6 +877,7 @@ export class SlidingSync extends TypedEventEmitter = new Set(); if (!doNotUpdateList) { for (const [key, list] of Object.entries(resp.lists)) { + // TODO: Remove as MSC4186 does not have this. list.ops = list.ops || []; if (list.ops.length > 0) { listKeysWithUpdates.add(key); @@ -891,6 +892,7 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 13 Sep 2024 09:57:31 +0100 Subject: [PATCH 04/34] Fix remaining tests --- spec/integ/sliding-sync.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index bd66bbd276e..e97e0941c7d 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -467,6 +467,8 @@ describe("SlidingSync", () => { const body = req.data; logger.log("next ranges", body.lists["a"].ranges); expect(body.lists).toBeTruthy(); + // list range should be changed + listReq.ranges = newRanges; expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186 }) .respond(200, { @@ -492,7 +494,7 @@ describe("SlidingSync", () => { await httpBackend!.flushAllExpected(); await responseProcessed; // setListRanges for an invalid list key returns an error - await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy(); + expect(() => {slidingSync.setListRanges("idontexist", newRanges)}).toThrow(); }); it("should be possible to add an extra list", async () => { From c75c06aaef37b09108e9bf109f36aada068713f7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:00:39 +0100 Subject: [PATCH 05/34] Mark TODOs on tests which need to die --- spec/integ/sliding-sync.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index e97e0941c7d..99366c4fb2d 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -331,6 +331,7 @@ describe("SlidingSync", () => { await p; }); + // TODO: this does not exist in MSC4186 it("should be able to unsubscribe from a room", async () => { httpBackend! .when("POST", syncUrl) @@ -551,6 +552,7 @@ describe("SlidingSync", () => { await responseProcessed; }); + // TODO: this does not exist in MSC4186 it("should be possible to get list DELETE/INSERTs", async () => { // move C (2) to A (0) httpBackend!.when("POST", syncUrl).respond(200, { @@ -637,6 +639,7 @@ describe("SlidingSync", () => { await listPromise; }); + // TODO: this does not exist in MSC4186 it("should ignore invalid list indexes", async () => { httpBackend!.when("POST", syncUrl).respond(200, { pos: "e", @@ -677,6 +680,7 @@ describe("SlidingSync", () => { await listPromise; }); + // TODO: this does not exist in MSC4186 it("should be possible to update a list", async () => { httpBackend!.when("POST", syncUrl).respond(200, { pos: "g", @@ -728,6 +732,7 @@ describe("SlidingSync", () => { await listPromise; }); + // TODO: this does not exist in MSC4186 // this refers to a set of operations where the end result is no change. it("should handle net zero operations correctly", async () => { const indexToRoomId = { @@ -780,6 +785,7 @@ describe("SlidingSync", () => { await listPromise; }); + // TODO: this does not exist in MSC4186 it("should handle deletions correctly", async () => { expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ 0: roomB, @@ -822,6 +828,7 @@ describe("SlidingSync", () => { await listPromise; }); + // TODO: this does not exist in MSC4186 it("should handle insertions correctly", async () => { expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ 0: roomC, @@ -901,6 +908,7 @@ describe("SlidingSync", () => { slidingSync.stop(); }); + // TODO: this does not exist in MSC4186 // Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't // end up losing room IDs. it("should handle insertions with a spurious DELETE correctly", async () => { From 744409477a0c20f1e2133adcb9ab6fab6cf0626a Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:02:29 +0100 Subject: [PATCH 06/34] Linting --- spec/integ/sliding-sync.spec.ts | 4 +++- src/sliding-sync.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 99366c4fb2d..ee063e5d385 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -495,7 +495,9 @@ describe("SlidingSync", () => { await httpBackend!.flushAllExpected(); await responseProcessed; // setListRanges for an invalid list key returns an error - expect(() => {slidingSync.setListRanges("idontexist", newRanges)}).toThrow(); + expect(() => { + slidingSync.setListRanges("idontexist", newRanges); + }).toThrow(); }); it("should be possible to add an extra list", async () => { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 565b3a21bc9..250437e0d07 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -18,7 +18,7 @@ import { logger } from "./logger.ts"; import { MatrixClient } from "./client.ts"; import { IRoomEvent, IStateEvent } from "./sync-accumulator.ts"; import { TypedEventEmitter } from "./models/typed-event-emitter.ts"; -import { sleep, IDeferred, defer } from "./utils.ts"; +import { sleep } from "./utils.ts"; import { HTTPError } from "./http-api/index.ts"; // /sync requests allow you to set a timeout= but the request may continue From bf7be485aae6ce7c45027a1e2367c5d8e029b73a Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:03:35 +0100 Subject: [PATCH 07/34] Make comments lie less --- src/sliding-sync.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 250437e0d07..79a6e2ecea4 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -499,9 +499,6 @@ export class SlidingSync extends TypedEventEmitter) { this.desiredRoomSubscriptions = s; @@ -512,9 +509,6 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 13 Sep 2024 10:08:00 +0100 Subject: [PATCH 08/34] void --- src/sliding-sync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 79a6e2ecea4..02e6d2f9ed1 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -500,7 +500,7 @@ export class SlidingSync extends TypedEventEmitter) { + public modifyRoomSubscriptions(s: Set): void { this.desiredRoomSubscriptions = s; this.resend(); } @@ -510,7 +510,7 @@ export class SlidingSync extends TypedEventEmitter(); this.resend(); From b8c341918db5255c1170c5f5fd26df17307d6b14 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:26:35 +0100 Subject: [PATCH 09/34] Always sent full extension request --- src/sliding-sync.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 02e6d2f9ed1..1005bb7f7b4 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -527,10 +527,10 @@ export class SlidingSync extends TypedEventEmitter { + private getExtensionRequest(): Record { const ext: Record = {}; Object.keys(this.extensions).forEach((extName) => { - ext[extName] = this.extensions[extName].onRequest(isInitial); + ext[extName] = this.extensions[extName].onRequest(true); }); return ext; } @@ -790,7 +790,7 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 13 Sep 2024 11:28:00 +0100 Subject: [PATCH 10/34] Fix test --- spec/integ/sliding-sync.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index ee063e5d385..e985c8bc347 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -153,7 +153,7 @@ describe("SlidingSync", () => { expect(body.room_subscriptions).toBeFalsy(); expect(body.lists["a"]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); - expect(body.extensions["custom_extension"]).toEqual({ initial: false }); + expect(body.extensions["custom_extension"]).toEqual({ initial: true }); expect(req.queryParams!["pos"]).toEqual("11"); }) .respond(200, function () { From c44818fe543a4bd732221aeae723729e78fa36f8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:41:05 +0100 Subject: [PATCH 11/34] Remove usage of deprecated field --- src/sliding-sync-sdk.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 4c07984732f..135f5928ae0 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -30,7 +30,6 @@ import { SetPresence, } from "./sync.ts"; import { MatrixEvent } from "./models/event.ts"; -import { Crypto } from "./crypto/index.ts"; import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator.ts"; import { MatrixError } from "./http-api/index.ts"; import { @@ -66,7 +65,7 @@ type ExtensionE2EEResponse = Pick< >; class ExtensionE2EE implements Extension { - public constructor(private readonly crypto: Crypto) {} + public constructor(private readonly crypto: SyncCryptoCallbacks) {} public name(): string { return "e2ee"; @@ -373,8 +372,8 @@ export class SlidingSyncSdk { new ExtensionTyping(this.client), new ExtensionReceipts(this.client), ]; - if (this.syncOpts.crypto) { - extensions.push(new ExtensionE2EE(this.syncOpts.crypto)); + if (this.syncOpts.cryptoCallbacks) { + extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks)); } extensions.forEach((ext) => { this.slidingSync.registerExtension(ext); From aeeb25f5a3938cdaa701a6c668da91bb1dd4ad47 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:23:21 +0100 Subject: [PATCH 12/34] Hopefully fix DM names --- src/sliding-sync-sdk.ts | 14 ++++++++++++++ src/sliding-sync.ts | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 135f5928ae0..c377a32196c 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -721,6 +721,20 @@ export class SlidingSyncSdk { // synchronous execution prior to emitting SlidingSyncState.Complete room.updateMyMembership(KnownMembership.Join); + // JS SDK expects m.heroes to be a list of user IDs, which it then looks up the display + // name via the current state (expecting the m.room.member events to exist). In SSS these + // events will not exist. Instead, we will calculate the name of each hero up front and + // insert that into the m.heroes array. This only works because the Room will do: + // otherNames.push(member ? member.name : userId); + // i.e default to whatever string we give it if the member does not exist. + room.setSummary({ + "m.heroes": roomData.heroes.map((h) => { + return h.displayname ? h.displayname : h.user_id; + }), + "m.invited_member_count": roomData.invited_count, + "m.joined_member_count": roomData.joined_count, + }); + room.recalculate(); if (roomData.initial) { client.store.storeRoom(room); diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 1005bb7f7b4..3a732e74131 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -82,10 +82,16 @@ export interface MSC3575SlidingSyncRequest { clientTimeout?: number; } +export interface MSC4186Hero { + user_id: string; + displayname: string; +} + export interface MSC3575RoomData { name: string; required_state: IStateEvent[]; timeline: (IRoomEvent | IStateEvent)[]; + heroes: MSC4186Hero[]; notification_count?: number; highlight_count?: number; joined_count?: number; From 244ca621bd6eb53f3dff11817ef0ffb86a169b79 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 08:38:42 +0100 Subject: [PATCH 13/34] Refactor how heroes are handled in Room --- src/models/room.ts | 62 ++++++++++++++++++++++++++--------------- src/sliding-sync-sdk.ts | 2 ++ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index b013e76afaa..b0753495ae1 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -172,6 +172,18 @@ export type RoomEmittedEvents = | BeaconEvent.LivenessChange | PollEvent.New; +// A Hero is a stripped m.room.member event which contains the key renderable fields from the event. +// It is used in MSC4186 (Simplified Sliding Sync) as a replacement for the old 'summary' field. +// The old form simply contained the hero's user ID, which forced clients to then look up the +// m.room.member event in the current state. This is entirely decoupled in SSS. To ensure this +// works in a backwards compatible way, we lazily populate the displayName/avatarUrl when heroes +// are used. We lazily do this to ensure that the hero list updates with the latest profile values. +export type Hero = { + userId: string; + displayName?: string; + avatarUrl?: string; +} + export type RoomEventHandlerMap = { /** * Fires when the logged in user's membership in the room is updated. @@ -355,7 +367,7 @@ export class Room extends ReadReceipt { // read by megolm via getter; boolean value - null indicates "use global value" private blacklistUnverifiedDevices?: boolean; private selfMembership?: Membership; - private summaryHeroes: string[] | null = null; + private heroes: Hero[] | null = null; // flags to stop logspam about missing m.room.create events private getTypeWarning = false; private getVersionWarning = false; @@ -873,7 +885,7 @@ export class Room extends ReadReceipt { // fall back to summary information const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount === 2) { - return this.summaryHeroes?.[0]; + return this.heroes?.[0]?.userId; } } } @@ -891,8 +903,8 @@ export class Room extends ReadReceipt { } } // Remember, we're assuming this room is a DM, so returning the first member we find should be fine - if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) { - return this.summaryHeroes[0]; + if (Array.isArray(this.heroes) && this.heroes.length) { + return this.heroes[0].userId; } const members = this.currentState.getMembers(); const anyMember = members.find((m) => m.userId !== this.myUserId); @@ -934,12 +946,12 @@ export class Room extends ReadReceipt { if (nonFunctionalMemberCount > 2) return; // Prefer the list of heroes, if present. It should only include the single other user in the DM. - const nonFunctionalHeroes = this.summaryHeroes?.filter((h) => !functionalMembers.includes(h)); + const nonFunctionalHeroes = this.heroes?.filter((h) => !functionalMembers.includes(h.userId)); const hasHeroes = Array.isArray(nonFunctionalHeroes) && nonFunctionalHeroes.length; if (hasHeroes) { const availableMember = nonFunctionalHeroes - .map((userId) => { - return this.getMember(userId); + .map((hero) => { + return this.getMember(hero.userId); }) .find((member) => !!member); if (availableMember) { @@ -964,8 +976,8 @@ export class Room extends ReadReceipt { // trust and try falling back to a hero, creating a one-off member for it if (hasHeroes) { const availableUser = nonFunctionalHeroes - .map((userId) => { - return this.client.getUser(userId); + .map((hero) => { + return this.client.getUser(hero.userId); }) .find((user) => !!user); if (availableUser) { @@ -1610,8 +1622,15 @@ export class Room extends ReadReceipt { return this.setUnreadNotificationCount(type, count); } + public setSummaryHeroes(heroes: Hero[]): void { + // filter out ourselves just in case + this.heroes = heroes.filter((h) => { + return h.userId != this.myUserId; + }); + } + public setSummary(summary: IRoomSummary): void { - const heroes = summary["m.heroes"]; + const heroUserIds = summary["m.heroes"]; const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; if (Number.isInteger(joinedCount)) { @@ -1620,13 +1639,12 @@ export class Room extends ReadReceipt { if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount!); } - if (Array.isArray(heroes)) { - // be cautious about trusting server values, - // and make sure heroes doesn't contain our own id - // just to be sure - this.summaryHeroes = heroes.filter((userId) => { - return userId !== this.myUserId; - }); + if (Array.isArray(heroUserIds)) { + this.setSummaryHeroes(heroUserIds.map((userId): Hero => { + return { + userId: userId, + }; + })) } this.emit(RoomEvent.Summary, summary); @@ -3424,16 +3442,16 @@ export class Room extends ReadReceipt { // get members that are NOT ourselves and are actually in the room. let otherNames: string[] = []; - if (this.summaryHeroes) { + if (this.heroes) { // if we have a summary, the member state events should be in the room state - this.summaryHeroes.forEach((userId) => { + this.heroes.forEach((hero) => { // filter service members - if (excludedUserIds.includes(userId)) { + if (excludedUserIds.includes(hero.userId)) { inviteJoinCount--; return; } - const member = this.getMember(userId); - otherNames.push(member ? member.name : userId); + const member = this.getMember(hero.userId); + otherNames.push(member ? member.name : hero.userId); }); } else { let otherMembers = this.currentState.getMembers().filter((m) => { diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index c377a32196c..e50d110865c 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -727,6 +727,8 @@ export class SlidingSyncSdk { // insert that into the m.heroes array. This only works because the Room will do: // otherNames.push(member ? member.name : userId); // i.e default to whatever string we give it if the member does not exist. + // In practice, we should change the API shape of setSummary to accept the displayname + // up-front. room.setSummary({ "m.heroes": roomData.heroes.map((h) => { return h.displayname ? h.displayname : h.user_id; From bd3f9f733aabeb2b7a666d460a618e816d86bf3c Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:09:51 +0100 Subject: [PATCH 14/34] Fix how heroes work --- src/models/room-summary.ts | 7 ++++ src/models/room.ts | 77 ++++++++++++++++++++++++++++---------- src/sliding-sync-sdk.ts | 16 +++----- src/sliding-sync.ts | 1 + 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/models/room-summary.ts b/src/models/room-summary.ts index 0877ba7970b..6398d047fda 100644 --- a/src/models/room-summary.ts +++ b/src/models/room-summary.ts @@ -14,11 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Hero } from "./room"; + export interface IRoomSummary { "m.heroes": string[]; "m.joined_member_count"?: number; "m.invited_member_count"?: number; } +export interface IRoomSummaryMSC4186 { + "m.heroes": Hero[]; + "m.joined_member_count"?: number; + "m.invited_member_count"?: number; +} interface IInfo { /** The title of the room (e.g. `m.room.name`) */ diff --git a/src/models/room.ts b/src/models/room.ts index b0753495ae1..c28c6524fcf 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -29,7 +29,7 @@ import { normalize, noUnsafeEventProps } from "../utils.ts"; import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event.ts"; import { EventStatus } from "./event-status.ts"; import { RoomMember } from "./room-member.ts"; -import { IRoomSummary, RoomSummary } from "./room-summary.ts"; +import { IRoomSummary, IRoomSummaryMSC4186, RoomSummary } from "./room-summary.ts"; import { logger } from "../logger.ts"; import { TypedReEmitter } from "../ReEmitter.ts"; import { @@ -176,8 +176,10 @@ export type RoomEmittedEvents = // It is used in MSC4186 (Simplified Sliding Sync) as a replacement for the old 'summary' field. // The old form simply contained the hero's user ID, which forced clients to then look up the // m.room.member event in the current state. This is entirely decoupled in SSS. To ensure this -// works in a backwards compatible way, we lazily populate the displayName/avatarUrl when heroes -// are used. We lazily do this to ensure that the hero list updates with the latest profile values. +// works in a backwards compatible way, we will A) only set displayName/avatarUrl with server-provided +// values, B) always prefer the hero values if they are set, over calling `.getMember`. This means +// in SSS mode we will always use the heroes if they exist, but in sync v2 mode these fields will +// never be set and hence we will always do getMember lookups (at the right time as well). export type Hero = { userId: string; displayName?: string; @@ -949,6 +951,35 @@ export class Room extends ReadReceipt { const nonFunctionalHeroes = this.heroes?.filter((h) => !functionalMembers.includes(h.userId)); const hasHeroes = Array.isArray(nonFunctionalHeroes) && nonFunctionalHeroes.length; if (hasHeroes) { + // use first hero which exists + for (const hero of nonFunctionalHeroes) { + if (!hero.displayName && !hero.avatarUrl) { + // attempt to look up renderable fields from the m.room.member event if it exists + const member = this.getMember(hero.userId); + if (member) { + return member; + } + } + else { + // use the Hero supplied values for the room member. + // TODO: It's unfortunate that this function, which clearly only cares about the + // avatar url, returns the entire RoomMember event. We need to fake an event + // to meet this API shape. + const heroMember = new RoomMember(this.roomId, hero.userId); + // set the display name and avatar url + heroMember.setMembershipEvent(new MatrixEvent({ + // ensure it's unique even if we hit the same millisecond + event_id: "$" + this.roomId + hero.userId + new Date().getTime(), + type: EventType.RoomMember, + state_key: hero.userId, + content: { + displayname: hero.displayName, + avatar_url: hero.avatarUrl, + } + })); + return heroMember; + } + } const availableMember = nonFunctionalHeroes .map((hero) => { return this.getMember(hero.userId); @@ -1622,15 +1653,16 @@ export class Room extends ReadReceipt { return this.setUnreadNotificationCount(type, count); } - public setSummaryHeroes(heroes: Hero[]): void { - // filter out ourselves just in case - this.heroes = heroes.filter((h) => { - return h.userId != this.myUserId; + public setSummary(summary: IRoomSummary | IRoomSummaryMSC4186): void { + // map heroes onto the MSC4186 form as that has more data + const heroes = summary["m.heroes"]?.map((h) => { + if (typeof h === "string") { + return { + userId: h, + }; + } + return h; }); - } - - public setSummary(summary: IRoomSummary): void { - const heroUserIds = summary["m.heroes"]; const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; if (Number.isInteger(joinedCount)) { @@ -1639,15 +1671,16 @@ export class Room extends ReadReceipt { if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount!); } - if (Array.isArray(heroUserIds)) { - this.setSummaryHeroes(heroUserIds.map((userId): Hero => { - return { - userId: userId, - }; - })) + if (Array.isArray(heroes)) { + // filter out ourselves just in case + this.heroes = heroes.filter((h) => { + return h.userId != this.myUserId; + }); } - this.emit(RoomEvent.Summary, summary); + summary["m.heroes"] = heroes?.map((h) => h.userId); + + this.emit(RoomEvent.Summary, summary as IRoomSummary); } /** @@ -3450,8 +3483,12 @@ export class Room extends ReadReceipt { inviteJoinCount--; return; } - const member = this.getMember(hero.userId); - otherNames.push(member ? member.name : hero.userId); + if (hero.displayName) { + otherNames.push(hero.displayName); + } else { + const member = this.getMember(hero.userId); + otherNames.push(member ? member.name : hero.userId); + } }); } else { let otherMembers = this.currentState.getMembers().filter((m) => { diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index e50d110865c..3e6b81bc3b0 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -721,17 +721,13 @@ export class SlidingSyncSdk { // synchronous execution prior to emitting SlidingSyncState.Complete room.updateMyMembership(KnownMembership.Join); - // JS SDK expects m.heroes to be a list of user IDs, which it then looks up the display - // name via the current state (expecting the m.room.member events to exist). In SSS these - // events will not exist. Instead, we will calculate the name of each hero up front and - // insert that into the m.heroes array. This only works because the Room will do: - // otherNames.push(member ? member.name : userId); - // i.e default to whatever string we give it if the member does not exist. - // In practice, we should change the API shape of setSummary to accept the displayname - // up-front. room.setSummary({ - "m.heroes": roomData.heroes.map((h) => { - return h.displayname ? h.displayname : h.user_id; + "m.heroes": roomData.heroes?.map((h) => { + return { + userId: h.user_id, + avatarUrl: h.avatar_url, + displayName: h.displayname, + }; }), "m.invited_member_count": roomData.invited_count, "m.joined_member_count": roomData.joined_count, diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 3a732e74131..7c2b81c7747 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -85,6 +85,7 @@ export interface MSC3575SlidingSyncRequest { export interface MSC4186Hero { user_id: string; displayname: string; + avatar_url: string; } export interface MSC3575RoomData { From 581b419341f9a52d91807daf56eec414f90e712c Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:17:10 +0100 Subject: [PATCH 15/34] Linting --- src/models/room.ts | 27 ++++++++++++++------------- src/sliding-sync-sdk.ts | 16 +++++++++------- src/sliding-sync.ts | 2 +- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index c28c6524fcf..9adb4edc89d 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -184,7 +184,7 @@ export type Hero = { userId: string; displayName?: string; avatarUrl?: string; -} +}; export type RoomEventHandlerMap = { /** @@ -959,24 +959,25 @@ export class Room extends ReadReceipt { if (member) { return member; } - } - else { + } else { // use the Hero supplied values for the room member. // TODO: It's unfortunate that this function, which clearly only cares about the // avatar url, returns the entire RoomMember event. We need to fake an event // to meet this API shape. const heroMember = new RoomMember(this.roomId, hero.userId); // set the display name and avatar url - heroMember.setMembershipEvent(new MatrixEvent({ - // ensure it's unique even if we hit the same millisecond - event_id: "$" + this.roomId + hero.userId + new Date().getTime(), - type: EventType.RoomMember, - state_key: hero.userId, - content: { - displayname: hero.displayName, - avatar_url: hero.avatarUrl, - } - })); + heroMember.setMembershipEvent( + new MatrixEvent({ + // ensure it's unique even if we hit the same millisecond + event_id: "$" + this.roomId + hero.userId + new Date().getTime(), + type: EventType.RoomMember, + state_key: hero.userId, + content: { + displayname: hero.displayName, + avatar_url: hero.avatarUrl, + }, + }), + ); return heroMember; } } diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 3e6b81bc3b0..1234ad0fa21 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -722,13 +722,15 @@ export class SlidingSyncSdk { room.updateMyMembership(KnownMembership.Join); room.setSummary({ - "m.heroes": roomData.heroes?.map((h) => { - return { - userId: h.user_id, - avatarUrl: h.avatar_url, - displayName: h.displayname, - }; - }), + "m.heroes": roomData.heroes + ? roomData.heroes.map((h) => { + return { + userId: h.user_id, + avatarUrl: h.avatar_url, + displayName: h.displayname, + }; + }) + : [], "m.invited_member_count": roomData.invited_count, "m.joined_member_count": roomData.joined_count, }); diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 7c2b81c7747..f0acdf4b621 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -92,7 +92,7 @@ export interface MSC3575RoomData { name: string; required_state: IStateEvent[]; timeline: (IRoomEvent | IStateEvent)[]; - heroes: MSC4186Hero[]; + heroes?: MSC4186Hero[]; notification_count?: number; highlight_count?: number; joined_count?: number; From 39b4e9d37d5bb9813adc8c78e8443761a78b4fcc Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:23:14 +0100 Subject: [PATCH 16/34] Ensure that when SSS omits heroes we don't forget we had heroes Otherwise when the room next appears the name/avatar reset to 'Empty Room' with no avatar. --- src/models/room.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.ts b/src/models/room.ts index 9adb4edc89d..663c0694384 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1672,7 +1672,7 @@ export class Room extends ReadReceipt { if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount!); } - if (Array.isArray(heroes)) { + if (Array.isArray(heroes) && heroes.length > 0) { // filter out ourselves just in case this.heroes = heroes.filter((h) => { return h.userId != this.myUserId; From 11e7a3da2160ee2dae96615fe5f3c250f9ecadcf Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:29:58 +0100 Subject: [PATCH 17/34] Check the right flag when doing timeline trickling --- src/sliding-sync-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 1234ad0fa21..9cd15fb2a2b 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -573,7 +573,7 @@ export class SlidingSyncSdk { // TODO: handle threaded / beacon events - if (roomData.initial) { + if (roomData.limited || roomData.initial) { // we should not know about any of these timeline entries if this is a genuinely new room. // If we do, then we've effectively done scrollback (e.g requesting timeline_limit: 1 for // this room, then timeline_limit: 50). From 511a8737de16bbda25200ec6199a900c10d89cdb Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:08:34 +0100 Subject: [PATCH 18/34] Also change when the backpagination token is set --- src/sliding-sync-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 9cd15fb2a2b..e75bed0bdb6 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -653,7 +653,7 @@ export class SlidingSyncSdk { return; } - if (roomData.initial) { + if (roomData.limited) { // set the back-pagination token. Do this *before* adding any // events so that clients can start back-paginating. room.getLiveTimeline().setPaginationToken(roomData.prev_batch ?? null, EventTimeline.BACKWARDS); From 181d786fa867e885d944f8c1db43b399c4b028b9 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:56:49 +0100 Subject: [PATCH 19/34] Remove list ops and server-provided sort positions SSS doesn't have them. --- spec/integ/sliding-sync.spec.ts | 483 -------------------------------- src/sliding-sync.ts | 246 +--------------- 2 files changed, 3 insertions(+), 726 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index e985c8bc347..22d64fe423c 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -453,11 +453,6 @@ describe("SlidingSync", () => { expect(slidingSync.getListData("b")).toBeNull(); const syncData = slidingSync.getListData("a")!; expect(syncData.joinedCount).toEqual(500); // from previous test - expect(syncData.roomIndexToRoomId).toEqual({ - 0: roomA, - 1: roomB, - 2: roomC, - }); }); it("should be possible to adjust list ranges", async () => { @@ -536,16 +531,6 @@ describe("SlidingSync", () => { }, }, }); - listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("b"); - expect(joinedCount).toEqual(50); - expect(roomIndexToRoomId).toEqual({ - 0: roomA, - 1: roomB, - 2: roomC, - }); - return true; - }); const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); @@ -553,474 +538,6 @@ describe("SlidingSync", () => { await httpBackend!.flushAllExpected(); await responseProcessed; }); - - // TODO: this does not exist in MSC4186 - it("should be possible to get list DELETE/INSERTs", async () => { - // move C (2) to A (0) - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "e", - lists: { - a: { - count: 500, - ops: [ - { - op: "DELETE", - index: 2, - }, - { - op: "INSERT", - index: 0, - room_id: roomC, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - let listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(500); - expect(roomIndexToRoomId).toEqual({ - 0: roomC, - 1: roomA, - 2: roomB, - }); - return true; - }, - ); - let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - - // move C (0) back to A (2) - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "f", - lists: { - a: { - count: 500, - ops: [ - { - op: "DELETE", - index: 0, - }, - { - op: "INSERT", - index: 2, - room_id: roomC, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(500); - expect(roomIndexToRoomId).toEqual({ - 0: roomA, - 1: roomB, - 2: roomC, - }); - return true; - }); - responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - }); - - // TODO: this does not exist in MSC4186 - it("should ignore invalid list indexes", async () => { - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "e", - lists: { - a: { - count: 500, - ops: [ - { - op: "DELETE", - index: 2324324, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - const listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(500); - expect(roomIndexToRoomId).toEqual({ - 0: roomA, - 1: roomB, - 2: roomC, - }); - return true; - }, - ); - const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - }); - - // TODO: this does not exist in MSC4186 - it("should be possible to update a list", async () => { - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "g", - lists: { - a: { - count: 42, - ops: [ - { - op: "INVALIDATE", - range: [0, 2], - }, - { - op: "SYNC", - range: [0, 1], - room_ids: [roomB, roomC], - }, - ], - }, - b: { - count: 50, - }, - }, - }); - // update the list with a new filter - slidingSync.setList("a", { - filters: { - is_encrypted: true, - }, - ranges: [[0, 100]], - }); - const listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(42); - expect(roomIndexToRoomId).toEqual({ - 0: roomB, - 1: roomC, - }); - return true; - }, - ); - const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - }); - - // TODO: this does not exist in MSC4186 - // this refers to a set of operations where the end result is no change. - it("should handle net zero operations correctly", async () => { - const indexToRoomId = { - 0: roomB, - 1: roomC, - }; - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId); - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "f", - // currently the list is [B,C] so we will insert D then immediately delete it - lists: { - a: { - count: 500, - ops: [ - { - op: "DELETE", - index: 2, - }, - { - op: "INSERT", - index: 0, - room_id: roomA, - }, - { - op: "DELETE", - index: 0, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - const listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(500); - expect(roomIndexToRoomId).toEqual(indexToRoomId); - return true; - }, - ); - const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - }); - - // TODO: this does not exist in MSC4186 - it("should handle deletions correctly", async () => { - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ - 0: roomB, - 1: roomC, - }); - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "g", - lists: { - a: { - count: 499, - ops: [ - { - op: "DELETE", - index: 0, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - const listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(499); - expect(roomIndexToRoomId).toEqual({ - 0: roomC, - }); - return true; - }, - ); - const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - }); - - // TODO: this does not exist in MSC4186 - it("should handle insertions correctly", async () => { - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ - 0: roomC, - }); - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "h", - lists: { - a: { - count: 500, - ops: [ - { - op: "INSERT", - index: 1, - room_id: roomA, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - let listPromise = listenUntil( - slidingSync, - "SlidingSync.List", - (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(500); - expect(roomIndexToRoomId).toEqual({ - 0: roomC, - 1: roomA, - }); - return true; - }, - ); - let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "h", - lists: { - a: { - count: 501, - ops: [ - { - op: "INSERT", - index: 1, - room_id: roomB, - }, - ], - }, - b: { - count: 50, - }, - }, - }); - listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => { - expect(listKey).toEqual("a"); - expect(joinedCount).toEqual(501); - expect(roomIndexToRoomId).toEqual({ - 0: roomC, - 1: roomB, - 2: roomA, - }); - return true; - }); - responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { - return state === SlidingSyncState.Complete; - }); - await httpBackend!.flushAllExpected(); - await responseProcessed; - await listPromise; - slidingSync.stop(); - }); - - // TODO: this does not exist in MSC4186 - // Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't - // end up losing room IDs. - it("should handle insertions with a spurious DELETE correctly", async () => { - slidingSync = new SlidingSync( - proxyBaseUrl, - new Map([ - [ - "a", - { - ranges: [[0, 20]], - }, - ], - ]), - {}, - client!, - 1, - ); - // initially start with nothing - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "a", - lists: { - a: { - count: 0, - ops: [], - }, - }, - }); - slidingSync.start(); - await httpBackend!.flushAllExpected(); - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({}); - - // insert a room - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "b", - lists: { - a: { - count: 1, - ops: [ - { - op: "DELETE", - index: 0, - }, - { - op: "INSERT", - index: 0, - room_id: roomA, - }, - ], - }, - }, - }); - await httpBackend!.flushAllExpected(); - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ - 0: roomA, - }); - - // insert another room - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "c", - lists: { - a: { - count: 1, - ops: [ - { - op: "DELETE", - index: 1, - }, - { - op: "INSERT", - index: 0, - room_id: roomB, - }, - ], - }, - }, - }); - await httpBackend!.flushAllExpected(); - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ - 0: roomB, - 1: roomA, - }); - - // insert a final room - httpBackend!.when("POST", syncUrl).respond(200, { - pos: "c", - lists: { - a: { - count: 1, - ops: [ - { - op: "DELETE", - index: 2, - }, - { - op: "INSERT", - index: 0, - room_id: roomC, - }, - ], - }, - }, - }); - await httpBackend!.flushAllExpected(); - expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({ - 0: roomC, - 1: roomB, - 2: roomA, - }); - slidingSync.stop(); - }); }); describe("custom room subscriptions", () => { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index f0acdf4b621..893dadfacd6 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -103,41 +103,13 @@ export interface MSC3575RoomData { is_dm?: boolean; prev_batch?: string; num_live?: number; + bump_stamp?: number; } interface ListResponse { count: number; - ops: Operation[]; } -interface BaseOperation { - op: string; -} - -interface DeleteOperation extends BaseOperation { - op: "DELETE"; - index: number; -} - -interface InsertOperation extends BaseOperation { - op: "INSERT"; - index: number; - room_id: string; -} - -interface InvalidateOperation extends BaseOperation { - op: "INVALIDATE"; - range: [number, number]; -} - -interface SyncOperation extends BaseOperation { - op: "SYNC"; - range: [number, number]; - room_ids: string[]; -} - -type Operation = DeleteOperation | InsertOperation | InvalidateOperation | SyncOperation; - /** * A complete Sliding Sync response */ @@ -170,7 +142,6 @@ class SlidingList { private isModified?: boolean; // returned data - public roomIndexToRoomId: Record = {}; public joinedCount = 0; /** @@ -211,9 +182,6 @@ class SlidingList { // reset values as the join count may be very different (if filters changed) including the rooms // (e.g. sort orders or sliding window ranges changed) - // the constantly changing sliding window ranges. Not an array for performance reasons - // E.g. tracking ranges 0-99, 500-599, we don't want to have a 600 element array - this.roomIndexToRoomId = {}; // the total number of joined rooms according to the server, always >= len(roomIndexToRoomId) this.joinedCount = 0; } @@ -233,26 +201,6 @@ class SlidingList { } return list; } - - /** - * Check if a given index is within the list range. This is required even though the /sync API - * provides explicit updates with index positions because of the following situation: - * 0 1 2 3 4 5 6 7 8 indexes - * a b c d e f COMMANDS: SYNC 0 2 a b c; SYNC 6 8 d e f; - * a b c d _ f COMMAND: DELETE 7; - * e a b c d f COMMAND: INSERT 0 e; - * c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it - * @param i - The index to check - * @returns True if the index is within a sliding window - */ - public isIndexInRange(i: number): boolean { - for (const r of this.list.ranges) { - if (r[0] <= i && i <= r[1]) { - return true; - } - } - return false; - } } /** @@ -320,16 +268,9 @@ export enum SlidingSyncEvent { * - SlidingSyncState.RequestFinished: Fires after we receive a valid response but before the * response has been processed. Perform any pre-process steps here. If there was a problem syncing, * `err` will be set (e.g network errors). - * - SlidingSyncState.Complete: Fires after all SlidingSyncEvent.RoomData have been fired but before - * SlidingSyncEvent.List. + * - SlidingSyncState.Complete: Fires after the response has been processed. */ Lifecycle = "SlidingSync.Lifecycle", - /** - * This event fires whenever there has been a change to this list index. It fires exactly once - * per list, even if there were multiple operations for the list. - * It fires AFTER Lifecycle and RoomData events. - */ - List = "SlidingSync.List", } export type SlidingSyncEventHandlerMap = { @@ -339,7 +280,6 @@ export type SlidingSyncEventHandlerMap = { resp: MSC3575SlidingSyncResponse | null, err?: Error, ) => void; - [SlidingSyncEvent.List]: (listKey: string, joinedCount: number, roomIndexToRoomId: Record) => void; }; /** @@ -428,14 +368,13 @@ export class SlidingSync extends TypedEventEmitter } | null { + public getListData(key: string): { joinedCount: number; } | null { const data = this.lists.get(key); if (!data) { return null; } return { joinedCount: data.joinedCount, - roomIndexToRoomId: Object.assign({}, data.roomIndexToRoomId), }; } @@ -591,155 +530,6 @@ export class SlidingSync extends TypedEventEmitter low; i--) { - if (list.isIndexInRange(i)) { - list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i - 1]; - } - } - } - - private shiftLeft(listKey: string, hi: number, low: number): void { - const list = this.lists.get(listKey); - if (!list) { - return; - } - // l h - // 0,1,2,3,4 <- before - // 0,1,3,4,4 <- after, low is deleted and hi is duplicated - for (let i = low; i < hi; i++) { - if (list.isIndexInRange(i)) { - list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i + 1]; - } - } - } - - private removeEntry(listKey: string, index: number): void { - const list = this.lists.get(listKey); - if (!list) { - return; - } - // work out the max index - let max = -1; - for (const n in list.roomIndexToRoomId) { - if (Number(n) > max) { - max = Number(n); - } - } - if (max < 0 || index > max) { - return; - } - // Everything higher than the gap needs to be shifted left. - this.shiftLeft(listKey, max, index); - delete list.roomIndexToRoomId[max]; - } - - private addEntry(listKey: string, index: number): void { - const list = this.lists.get(listKey); - if (!list) { - return; - } - // work out the max index - let max = -1; - for (const n in list.roomIndexToRoomId) { - if (Number(n) > max) { - max = Number(n); - } - } - if (max < 0 || index > max) { - return; - } - // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element - this.shiftRight(listKey, max + 1, index); - } - - private processListOps(list: ListResponse, listKey: string): void { - let gapIndex = -1; - const listData = this.lists.get(listKey); - if (!listData) { - return; - } - list.ops.forEach((op: Operation) => { - if (!listData) { - return; - } - switch (op.op) { - case "DELETE": { - logger.debug("DELETE", listKey, op.index, ";"); - delete listData.roomIndexToRoomId[op.index]; - if (gapIndex !== -1) { - // we already have a DELETE operation to process, so process it. - this.removeEntry(listKey, gapIndex); - } - gapIndex = op.index; - break; - } - case "INSERT": { - logger.debug("INSERT", listKey, op.index, op.room_id, ";"); - if (listData.roomIndexToRoomId[op.index]) { - // something is in this space, shift items out of the way - if (gapIndex < 0) { - // we haven't been told where to shift from, so make way for a new room entry. - this.addEntry(listKey, op.index); - } else if (gapIndex > op.index) { - // the gap is further down the list, shift every element to the right - // starting at the gap so we can just shift each element in turn: - // [A,B,C,_] gapIndex=3, op.index=0 - // [A,B,C,C] i=3 - // [A,B,B,C] i=2 - // [A,A,B,C] i=1 - // Terminate. We'll assign into op.index next. - this.shiftRight(listKey, gapIndex, op.index); - } else if (gapIndex < op.index) { - // the gap is further up the list, shift every element to the left - // starting at the gap so we can just shift each element in turn - this.shiftLeft(listKey, op.index, gapIndex); - } - } - // forget the gap, we don't need it anymore. This is outside the check for - // a room being present in this index position because INSERTs always universally - // forget the gap, not conditionally based on the presence of a room in the INSERT - // position. Without this, DELETE 0; INSERT 0; would do the wrong thing. - gapIndex = -1; - listData.roomIndexToRoomId[op.index] = op.room_id; - break; - } - case "INVALIDATE": { - const startIndex = op.range[0]; - for (let i = startIndex; i <= op.range[1]; i++) { - delete listData.roomIndexToRoomId[i]; - } - logger.debug("INVALIDATE", listKey, op.range[0], op.range[1], ";"); - break; - } - case "SYNC": { - const startIndex = op.range[0]; - for (let i = startIndex; i <= op.range[1]; i++) { - const roomId = op.room_ids[i - startIndex]; - if (!roomId) { - break; // we are at the end of list - } - listData.roomIndexToRoomId[i] = roomId; - } - logger.debug("SYNC", listKey, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";"); - break; - } - } - }); - if (gapIndex !== -1) { - // we already have a DELETE operation to process, so process it - // Everything higher than the gap needs to be shifted left. - this.removeEntry(listKey, gapIndex); - } - } - /** * Resend a Sliding Sync request. Used when something has changed in the request. */ @@ -757,7 +547,6 @@ export class SlidingSync extends TypedEventEmitter = {}; this.lists.forEach((l: SlidingList, key: string) => { reqLists[key] = l.getList(true); @@ -826,13 +613,6 @@ export class SlidingSync extends TypedEventEmitter { l.setModified(false); @@ -874,28 +654,8 @@ export class SlidingSync extends TypedEventEmitter = new Set(); - if (!doNotUpdateList) { - for (const [key, list] of Object.entries(resp.lists)) { - // TODO: Remove as MSC4186 does not have this. - list.ops = list.ops || []; - if (list.ops.length > 0) { - listKeysWithUpdates.add(key); - } - this.processListOps(list, key); - } - } this.invokeLifecycleListeners(SlidingSyncState.Complete, resp); await this.onPostExtensionsResponse(resp.extensions); - listKeysWithUpdates.forEach((listKey: string) => { - const list = this.lists.get(listKey); - if (!list) { - return; - } - // TODO: Remove as there is no concept of list updates in MSC4186 - this.emit(SlidingSyncEvent.List, listKey, list.joinedCount, Object.assign({}, list.roomIndexToRoomId)); - }); } } } From 679a984fdcd72465328b3be354a2933a25ce4e43 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:58:51 +0100 Subject: [PATCH 20/34] Linting --- src/sliding-sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 893dadfacd6..b741ac011f0 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -368,7 +368,7 @@ export class SlidingSync extends TypedEventEmitter Date: Tue, 17 Sep 2024 12:07:39 +0100 Subject: [PATCH 21/34] Add Room.bumpStamp --- src/models/room.ts | 19 +++++++++++++++++++ src/sliding-sync-sdk.ts | 3 +++ src/sliding-sync.ts | 2 -- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index 663c0694384..e84cdb616b2 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -343,6 +343,7 @@ export class Room extends ReadReceipt { public readonly reEmitter: TypedReEmitter; private txnToEvent: Map = new Map(); // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; + private bumpStamp: number | undefined = undefined; private readonly threadNotifications = new Map(); public readonly cachedThreadReadReceipts = new Map(); // Useful to know at what point the current user has started using threads in this room @@ -1640,6 +1641,24 @@ export class Room extends ReadReceipt { this.emit(RoomEvent.UnreadNotifications); } + /** + * Set the bump stamp for this room. This can be used for sorting rooms when the timeline + * entries are unknown. Used in MSC4186: Simplified Sliding Sync. + * @param bumpStamp The bump_stamp value from the server + */ + public setBumpStamp(bumpStamp: number): void { + this.bumpStamp = bumpStamp; + } + + /** + * Get the bump stamp for this room. This can be used for sorting rooms when the timeline + * entries are unknown. Used in MSC4186: Simplified Sliding Sync. + * @returns The bump stamp for the room, if it exists. + */ + public getBumpStamp(): number | undefined { + return this.bumpStamp; + } + /** * Set one of the notification counts for this room * @param type - The type of notification count to set. diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index e75bed0bdb6..0b54177f69c 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -630,6 +630,9 @@ export class SlidingSyncSdk { room.setUnreadNotificationCount(NotificationCountType.Highlight, roomData.highlight_count); } } + if (roomData.bump_stamp) { + room.setBumpStamp(roomData.bump_stamp); + } if (Number.isInteger(roomData.invited_count)) { room.currentState.setInvitedMemberCount(roomData.invited_count!); diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index b741ac011f0..5e9d9c67cf9 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -250,12 +250,10 @@ export interface Extension { * of information when processing sync responses. * - RoomData: concerns rooms, useful for SlidingSyncSdk to update its knowledge of rooms. * - Lifecycle: concerns callbacks at various well-defined points in the sync process. - * - List: concerns lists, useful for UI layers to re-render room lists. * Specifically, the order of event invocation is: * - Lifecycle (state=RequestFinished) * - RoomData (N times) * - Lifecycle (state=Complete) - * - List (at most once per list) */ export enum SlidingSyncEvent { /** From 426aa9366ee34ed7b6f6d6ac02c29bc314b18212 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:29:47 +0100 Subject: [PATCH 22/34] Update crypto wasm lib For new functions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb652a728f2..4586faf2a9f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0", + "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", "@matrix-org/olm": "3.2.15", "another-json": "^0.2.0", "bs58": "^6.0.0", From 63a636fe08052e1d618c4ceb787f121b8e925cb3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:34:45 +0100 Subject: [PATCH 23/34] Add performance logging --- src/sliding-sync-sdk.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 0b54177f69c..bbc9cb39779 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -381,6 +381,7 @@ export class SlidingSyncSdk { } private async onRoomData(roomId: string, roomData: MSC3575RoomData): Promise { + const start = new Date().getTime(); let room = this.client.store.getRoom(roomId); if (!room) { if (!roomData.initial) { @@ -390,6 +391,7 @@ export class SlidingSyncSdk { room = _createAndReEmitRoom(this.client, roomId, this.opts); } await this.processRoomData(this.client, room!, roomData); + logger.log(`onRoomData ${roomId} took ${new Date().getTime() - start}ms`); } private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error): void { @@ -398,6 +400,7 @@ export class SlidingSyncSdk { } switch (state) { case SlidingSyncState.Complete: + logger.log(`SlidingSyncState.Complete with ${Object.keys(resp?.rooms || []).length} rooms`); this.purgeNotifications(); if (!resp) { break; @@ -435,6 +438,7 @@ export class SlidingSyncSdk { } } else { this.failCount = 0; + logger.log(`SlidingSyncState.RequestFinished with ${Object.keys(resp?.rooms || []).length} rooms`); } break; } From f8d4161ded923bcdd4546276bbcce9be4d3c0c6f Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:51:29 +0100 Subject: [PATCH 24/34] Fix breaking change in crypto wasm v8 --- src/rust-crypto/RoomEncryptor.ts | 6 ++++-- yarn.lock | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts index 3b6a6c80cc0..7e82982819b 100644 --- a/src/rust-crypto/RoomEncryptor.ts +++ b/src/rust-crypto/RoomEncryptor.ts @@ -254,9 +254,11 @@ export class RoomEncryptor { // When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used // See Room#getBlacklistUnverifiedDevices if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) { - rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyOnlyTrustedDevices; + // TODO XXX : CONFIRM THIS WITH CRYPTO TEAM BEFORE LANDING PR + rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(true, true); } else { - rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyAllDevices; + // TODO XXX : CONFIRM THIS WITH CRYPTO TEAM BEFORE LANDING PR + rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(false, false); } await logDuration(this.prefixedLogger, "shareRoomKey", async () => { diff --git a/yarn.lock b/yarn.lock index b259daa83dc..7fbe9235875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,10 +1446,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@matrix-org/matrix-sdk-crypto-wasm@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-7.0.0.tgz#8d6abdb9ded8656cc9e2a7909913a34bf3fc9b3a" - integrity sha512-MOencXiW/gI5MuTtCNsuojjwT5DXCrjMqv9xOslJC9h2tPdLFFFMGr58dY5Lis4DRd9MRWcgrGowUIHOqieWTA== +"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.0.0.tgz#293fe8fcb9bc4d577c5f6cf2cbffa151c6e11329" + integrity sha512-dz4dkYXj6BeOQuw52XQj8dMuhi85pSFhfFeFlNRAO7JdRPhE9CHBrfK8knkZV5Zux5vvf3Ub4E7myoLeJgZoEw== "@matrix-org/olm@3.2.15": version "3.2.15" From 1fd66756fea2d46011dd67271a940ea62605bb11 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:59:05 +0100 Subject: [PATCH 25/34] Update crypto wasm for breaking changes See https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/releases/tag/v8.0.0 for how this was mapped from the previous API. --- src/rust-crypto/RoomEncryptor.ts | 4 +--- src/rust-crypto/rust-crypto.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts index 7e82982819b..fcda295eb89 100644 --- a/src/rust-crypto/RoomEncryptor.ts +++ b/src/rust-crypto/RoomEncryptor.ts @@ -254,10 +254,8 @@ export class RoomEncryptor { // When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used // See Room#getBlacklistUnverifiedDevices if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) { - // TODO XXX : CONFIRM THIS WITH CRYPTO TEAM BEFORE LANDING PR - rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(true, true); + rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(true, false); } else { - // TODO XXX : CONFIRM THIS WITH CRYPTO TEAM BEFORE LANDING PR rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(false, false); } diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 2a4ba09c5eb..040a1d3f75f 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -1406,7 +1406,6 @@ export class RustCrypto extends TypedEventEmitter { + await this.olmMachine.markAllTrackedUsersAsDirty(); + } + /** * Handle an incoming m.key.verification.request event, received either in-room or in a to-device message. * @@ -1742,6 +1748,7 @@ class EventDecryptor { const res = (await this.olmMachine.decryptRoomEvent( stringifyEvent(event), new RustSdkCryptoJs.RoomId(event.getRoomId()!), + new RustSdkCryptoJs.DecryptionSettings(RustSdkCryptoJs.TrustRequirement.Untrusted), )) as RustSdkCryptoJs.DecryptedRoomEvent; // Success. We can remove the event from the pending list, if From 86162947d829d6114863cd1dc0cf75a888f700be Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:59:53 +0100 Subject: [PATCH 26/34] Mark all tracked users as dirty on expired SSS connections See https://github.com/matrix-org/matrix-rust-sdk/pull/3965 for more information. Requires `Extension.onRequest` to be `async`. --- spec/integ/sliding-sync-sdk.spec.ts | 42 +++++++++++++++++---------- spec/integ/sliding-sync.spec.ts | 14 ++++----- src/common-crypto/CryptoBackend.ts | 9 ++++++ src/crypto/index.ts | 7 +++++ src/sliding-sync-sdk.ts | 45 +++++++++++++---------------- src/sliding-sync.ts | 14 ++++----- 6 files changed, 77 insertions(+), 54 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index aaf4a1a04a3..fdb69b3975b 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -640,11 +640,13 @@ describe("SlidingSyncSdk", () => { client!.crypto!.stop(); }); - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ + it("gets enabled all the time", async () => { + expect(await ext.onRequest(true)).toEqual({ + enabled: true, + }); + expect(await ext.onRequest(false)).toEqual({ enabled: true, }); - expect(ext.onRequest(false)).toEqual(undefined); }); it("can update device lists", () => { @@ -686,11 +688,13 @@ describe("SlidingSyncSdk", () => { ext = findExtension("account_data"); }); - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ + it("gets enabled all the time", async () => { + expect(await ext.onRequest(true)).toEqual({ + enabled: true, + }); + expect(await ext.onRequest(false)).toEqual({ enabled: true, }); - expect(ext.onRequest(false)).toEqual(undefined); }); it("processes global account data", async () => { @@ -814,8 +818,12 @@ describe("SlidingSyncSdk", () => { ext = findExtension("to_device"); }); - it("gets enabled with a limit on the initial request only", () => { - const reqJson: any = ext.onRequest(true); + it("gets enabled all the time", async () => { + let reqJson: any = await ext.onRequest(true); + expect(reqJson.enabled).toEqual(true); + expect(reqJson.limit).toBeGreaterThan(0); + expect(reqJson.since).toBeUndefined(); + reqJson = await ext.onRequest(false); expect(reqJson.enabled).toEqual(true); expect(reqJson.limit).toBeGreaterThan(0); expect(reqJson.since).toBeUndefined(); @@ -826,7 +834,7 @@ describe("SlidingSyncSdk", () => { next_batch: "12345", events: [], }); - expect(ext.onRequest(false)).toEqual({ + expect(await ext.onRequest(false)).toMatchObject({ since: "12345", }); }); @@ -910,11 +918,13 @@ describe("SlidingSyncSdk", () => { ext = findExtension("typing"); }); - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ + it("gets enabled all the time", async () => { + expect(await ext.onRequest(true)).toEqual({ + enabled: true, + }); + expect(await ext.onRequest(false)).toEqual({ enabled: true, }); - expect(ext.onRequest(false)).toEqual(undefined); }); it("processes typing notifications", async () => { @@ -1035,11 +1045,13 @@ describe("SlidingSyncSdk", () => { ext = findExtension("receipts"); }); - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ + it("gets enabled all the time", async () => { + expect(await ext.onRequest(true)).toEqual({ + enabled: true, + }); + expect(await ext.onRequest(false)).toEqual({ enabled: true, }); - expect(ext.onRequest(false)).toEqual(undefined); }); it("processes receipts", async () => { diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 22d64fe423c..d81cf1c7eb6 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -104,8 +104,8 @@ describe("SlidingSync", () => { }; const ext: Extension = { name: () => "custom_extension", - onRequest: (initial) => { - return { initial: initial }; + onRequest: async (_) => { + return { initial: true }; }, onResponse: async (res) => { return; @@ -827,7 +827,7 @@ describe("SlidingSync", () => { const extPre: Extension = { name: () => preExtName, - onRequest: (initial) => { + onRequest: async (initial) => { return onPreExtensionRequest(initial); }, onResponse: (res) => { @@ -837,7 +837,7 @@ describe("SlidingSync", () => { }; const extPost: Extension = { name: () => postExtName, - onRequest: (initial) => { + onRequest: async (initial) => { return onPostExtensionRequest(initial); }, onResponse: (res) => { @@ -852,7 +852,7 @@ describe("SlidingSync", () => { const callbackOrder: string[] = []; let extensionOnResponseCalled = false; - onPreExtensionRequest = () => { + onPreExtensionRequest = async () => { return extReq; }; onPreExtensionResponse = async (resp) => { @@ -892,7 +892,7 @@ describe("SlidingSync", () => { }); it("should be able to send nothing in an extension request/response", async () => { - onPreExtensionRequest = () => { + onPreExtensionRequest = async () => { return undefined; }; let responseCalled = false; @@ -927,7 +927,7 @@ describe("SlidingSync", () => { it("is possible to register extensions after start() has been called", async () => { slidingSync.registerExtension(extPost); - onPostExtensionRequest = () => { + onPostExtensionRequest = async () => { return extReq; }; let responseCalled = false; diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index bbd6e5ec682..529ea1235ed 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -177,6 +177,15 @@ export interface SyncCryptoCallbacks { * @param syncState - information about the completed sync. */ onSyncCompleted(syncState: OnSyncCompletedData): void; + + /** + * Mark all tracked user's device lists as dirty. + * + * This method will cause additional /keys/query requests on the server, so should be used only + * when the client has desynced tracking device list deltas from the server. + * In MSC4186: Simplified Sliding Sync, this can happen when the server expires the connection. + */ + markAllTrackedUsersAsDirty(): Promise; } /** diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 420160c0de4..237f32c9923 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -3445,6 +3445,13 @@ export class Crypto extends TypedEventEmitter { + // no op: we only expect rust crypto to be used in MSC4186. + } + /** * Trigger the appropriate invalidations and removes for a given * device list diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index bbc9cb39779..3a9b9730fb0 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -75,9 +75,16 @@ class ExtensionE2EE implements Extension { + if (isInitial) { + // In SSS, the `?pos=` contains the stream position for device list updates. + // If we do not have a `?pos=` (e.g because we forgot it, or because the server + // invalidated our connection) then we MUST invlaidate all device lists because + // the server will not tell us the delta. This will then cause UTDs as we will fail + // to encrypt for new devices. This is an expensive call, so we should + // really really remember `?pos=` wherever possible. + logger.log("ExtensionE2EE: invalidating all device lists due to missing 'pos'"); + await this.crypto.markAllTrackedUsersAsDirty(); } return { enabled: true, // this is sticky so only send it on the initial request @@ -127,15 +134,12 @@ class ExtensionToDevice implements Extension { + return { since: this.nextBatch !== null ? this.nextBatch : undefined, + limit: 100, + enabled: true, }; - if (isInitial) { - extReq["limit"] = 100; - extReq["enabled"] = true; - } - return extReq; } public async onResponse(data: ExtensionToDeviceResponse): Promise { @@ -209,10 +213,7 @@ class ExtensionAccountData implements Extension { return { enabled: true, }; @@ -279,10 +280,7 @@ class ExtensionTyping implements Extension { return { enabled: true, }; @@ -318,13 +316,10 @@ class ExtensionReceipts implements Extension { + return { + enabled: true, + }; } public async onResponse(data: ExtensionReceiptsResponse): Promise { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 5e9d9c67cf9..a442b7b4a0a 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -229,10 +229,10 @@ export interface Extension { /** * A function which is called when the request JSON is being formed. * Returns the data to insert under this key. - * @param isInitial - True when this is part of the initial request (send sticky params) + * @param isInitial - True when this is part of the initial request. * @returns The request JSON to send. */ - onRequest(isInitial: boolean): Req | undefined; + onRequest(isInitial: boolean): Promise; /** * A function which is called when there is response JSON under this extension. * @param data - The response JSON under the extension name. @@ -471,11 +471,11 @@ export class SlidingSync extends TypedEventEmitter { + private async getExtensionRequest(isInitial: boolean): Promise> { const ext: Record = {}; - Object.keys(this.extensions).forEach((extName) => { - ext[extName] = this.extensions[extName].onRequest(true); - }); + for (const extName in this.extensions) { + ext[extName] = await this.extensions[extName].onRequest(isInitial); + } return ext; } @@ -582,7 +582,7 @@ export class SlidingSync extends TypedEventEmitter Date: Thu, 21 Nov 2024 15:14:00 +0000 Subject: [PATCH 27/34] add ts extension --- src/models/room-summary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-summary.ts b/src/models/room-summary.ts index 6398d047fda..1ea66ecee5f 100644 --- a/src/models/room-summary.ts +++ b/src/models/room-summary.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Hero } from "./room"; +import { Hero } from "./room.ts"; export interface IRoomSummary { "m.heroes": string[]; From c6f8440f872dd1a3da5ce835b2dd8d43aa6b2a77 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2024 15:22:29 +0000 Subject: [PATCH 28/34] Fix typedoc ref --- src/crypto/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 44119f2ad13..306e9cbe495 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -3423,7 +3423,7 @@ export class Crypto extends TypedEventEmitter { // no op: we only expect rust crypto to be used in MSC4186. From 336797e62ed9de9e5f93cc0b763405f5629ad007 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2024 15:53:06 +0000 Subject: [PATCH 29/34] Add method to interface --- src/crypto-api/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index 89ece07acbe..4130daaca00 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -377,6 +377,12 @@ export interface CryptoApi { */ getEncryptionInfoForEvent(event: MatrixEvent): Promise; + /** + * All users *whose device lists we are tracking* are flagged as needing a + * key query. Users whose devices we are not tracking are ignored. + */ + markAllTrackedUsersAsDirty(): Promise; + /** * Encrypts a given payload object via Olm to-device messages to a given * set of devices. From ec83c14944a7b0d1414e60f94cc1edcc05c00b6b Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Dec 2024 18:25:48 +0000 Subject: [PATCH 30/34] Don't force membership to invite The membership was set correctly from the stripped state anyway so this was redundant and was breaking rooms where we'd knocked. --- src/sliding-sync-sdk.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index a4d02592c7b..f72e247dc36 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -651,7 +651,6 @@ export class SlidingSyncSdk { inviteStateEvents.forEach((e) => { this.client.emit(ClientEvent.Event, e); }); - room.updateMyMembership(KnownMembership.Invite); return; } From dec2cb0b2a6dbe728df004feb74ccccff69e5788 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 09:34:25 +0000 Subject: [PATCH 31/34] Missed merge --- spec/integ/sliding-sync-sdk.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 7358144ca5d..cf249b4b49e 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -649,23 +649,11 @@ describe("SlidingSyncSdk", () => { ext = findExtension("e2ee"); }); -<<<<<<< HEAD - afterAll(async () => { - // needed else we do some async operations in the background which can cause Jest to whine: - // "Cannot log after tests are done. Did you forget to wait for something async in your test?" - // Attempted to log "Saving device tracking data null"." - client!.crypto!.stop(); - }); - it("gets enabled all the time", async () => { expect(await ext.onRequest(true)).toEqual({ enabled: true, }); expect(await ext.onRequest(false)).toEqual({ -======= - it("gets enabled on the initial request only", () => { - expect(ext.onRequest(true)).toEqual({ ->>>>>>> develop enabled: true, }); }); From 1634b83ec21f8b9edf6aadb6c55c4287f2a106f6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 09:36:36 +0000 Subject: [PATCH 32/34] Type import --- src/models/room-summary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-summary.ts b/src/models/room-summary.ts index 1ea66ecee5f..c4248bad14d 100644 --- a/src/models/room-summary.ts +++ b/src/models/room-summary.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Hero } from "./room.ts"; +import type { Hero } from "./room.ts"; export interface IRoomSummary { "m.heroes": string[]; From ebef4cdeb8274dc07a12cb39af26e28c88948c63 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 09:56:39 +0000 Subject: [PATCH 33/34] Make coverage happier --- spec/unit/room.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 88f1361a139..69f374b318e 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -4276,4 +4276,9 @@ describe("Room", function () { expect(filteredEvents[0].getContent().body).toEqual("ev2"); }); }); + + it("saves and retrieves the bump stamp", () => { + room.setBumpStamp(123456789); + expect(room.getBumpStamp()).toEqual(123456789); + }); }); From ef87316b59eccfa2205fd91aa6e5299bdb8022df Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Feb 2025 10:24:32 +0000 Subject: [PATCH 34/34] More test coverage --- spec/unit/room.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 69f374b318e..fe283880a84 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -1234,6 +1234,18 @@ describe("Room", function () { expect(room.name).toEqual(nameB); }); + it("supports MSC4186 style heroes", () => { + const nameB = "Bertha Bobbington"; + const nameC = "Clarissa Harissa"; + addMember(userB, KnownMembership.Join, { name: nameB }); + addMember(userC, KnownMembership.Join, { name: nameC }); + room.setSummary({ + "m.heroes": [{ userId: userB }, { userId: userC }], + }); + room.recalculate(); + expect(room.name).toEqual(`${nameB} and ${nameC}`); + }); + it("reverts to empty room in case of self chat", function () { room.setSummary({ "m.heroes": [],