1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

refactor: sliding sync: swap to lists-as-keys (#3086)

* refactor: sliding sync: swap to lists-as-keys

Update the request/response API shape to match the latest
MSC3575 version, which converts `lists` from being an array
of list objects to being a map of list objects.

* Linting

* prettier

* add extra setListRanges test

* Default to right type
This commit is contained in:
kegsay
2023-01-23 15:26:25 +00:00
committed by GitHub
parent 02aa3edda4
commit 6cf6a0c522
3 changed files with 266 additions and 236 deletions

View File

@ -52,10 +52,9 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => { const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = jest.fn(); s.getListParams = jest.fn();
s.getListData = jest.fn(); s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn(); s.getRoomSubscriptions = jest.fn();
s.listLength = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn(); s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn(); s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn(); s.registerExtension = jest.fn();
@ -115,7 +114,7 @@ describe("SlidingSyncSdk", () => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend; httpBackend = testClient.httpBackend;
client = testClient.client; client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0)); mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) { if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {}); httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto(); await client!.initCrypto();
@ -549,7 +548,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => { it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, { mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "h", pos: "h",
lists: [], lists: {},
rooms: {}, rooms: {},
extensions: {}, extensions: {},
}); });
@ -577,7 +576,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Syncing after a previous SyncState.Error", async () => { it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, { mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "i", pos: "i",
lists: [], lists: {},
rooms: {}, rooms: {},
extensions: {}, extensions: {},
}); });

View File

@ -64,10 +64,10 @@ describe("SlidingSync", () => {
let slidingSync: SlidingSync; let slidingSync: SlidingSync;
it("should start the sync loop upon calling start()", async () => { it("should start the sync loop upon calling start()", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
const fakeResp = { const fakeResp = {
pos: "a", pos: "a",
lists: [], lists: {},
rooms: {}, rooms: {},
extensions: {}, extensions: {},
}; };
@ -90,7 +90,7 @@ describe("SlidingSync", () => {
it("should reset the connection on HTTP 400 and send everything again", async () => { it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again // seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
const roomId = "!sub:localhost"; const roomId = "!sub:localhost";
const subInfo = { const subInfo = {
timeline_limit: 42, timeline_limit: 42,
@ -114,7 +114,7 @@ describe("SlidingSync", () => {
}; };
slidingSync.modifyRoomSubscriptions(new Set([roomId])); slidingSync.modifyRoomSubscriptions(new Set([roomId]));
slidingSync.modifyRoomSubscriptionInfo(subInfo); slidingSync.modifyRoomSubscriptionInfo(subInfo);
slidingSync.setList(0, listInfo); slidingSync.setList("a", listInfo);
slidingSync.registerExtension(ext); slidingSync.registerExtension(ext);
slidingSync.start(); slidingSync.start();
@ -128,7 +128,7 @@ describe("SlidingSync", () => {
expect(body.room_subscriptions).toEqual({ expect(body.room_subscriptions).toEqual({
[roomId]: subInfo, [roomId]: subInfo,
}); });
expect(body.lists[0]).toEqual(listInfo); expect(body.lists["a"]).toEqual(listInfo);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true }); expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams!["pos"]).toBeUndefined(); expect(req.queryParams!["pos"]).toBeUndefined();
@ -137,7 +137,7 @@ describe("SlidingSync", () => {
.respond(200, function () { .respond(200, function () {
return { return {
pos: "11", pos: "11",
lists: [{ count: 5 }], lists: { a: { count: 5 } },
extensions: {}, extensions: {},
txn_id: txnId, txn_id: txnId,
}; };
@ -151,7 +151,7 @@ describe("SlidingSync", () => {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({ expect(body.lists["a"]).toEqual({
ranges: [[0, 10]], ranges: [[0, 10]],
}); });
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
@ -161,7 +161,7 @@ describe("SlidingSync", () => {
.respond(200, function () { .respond(200, function () {
return { return {
pos: "12", pos: "12",
lists: [{ count: 5 }], lists: { a: { count: 5 } },
extensions: {}, extensions: {},
}; };
}); });
@ -185,7 +185,7 @@ describe("SlidingSync", () => {
expect(body.room_subscriptions).toEqual({ expect(body.room_subscriptions).toEqual({
[roomId]: subInfo, [roomId]: subInfo,
}); });
expect(body.lists[0]).toEqual(listInfo); expect(body.lists["a"]).toEqual(listInfo);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true }); expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams!["pos"]).toBeUndefined(); expect(req.queryParams!["pos"]).toBeUndefined();
@ -193,7 +193,7 @@ describe("SlidingSync", () => {
.respond(200, function () { .respond(200, function () {
return { return {
pos: "1", pos: "1",
lists: [{ count: 6 }], lists: { a: { count: 6 } },
extensions: {}, extensions: {},
}; };
}); });
@ -221,7 +221,7 @@ describe("SlidingSync", () => {
it("should be able to subscribe to a room", async () => { it("should be able to subscribe to a room", async () => {
// add the subscription // add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
slidingSync.modifyRoomSubscriptions(new Set([roomId])); slidingSync.modifyRoomSubscriptions(new Set([roomId]));
httpBackend! httpBackend!
.when("POST", syncUrl) .when("POST", syncUrl)
@ -233,7 +233,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "a", pos: "a",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: { rooms: {
[roomId]: wantRoomData, [roomId]: wantRoomData,
@ -266,7 +266,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "a", pos: "a",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: { rooms: {
[roomId]: wantRoomData, [roomId]: wantRoomData,
@ -313,7 +313,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: { rooms: {
[anotherRoomID]: anotherRoomData, [anotherRoomID]: anotherRoomData,
@ -344,7 +344,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
}); });
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
@ -402,19 +402,19 @@ describe("SlidingSync", () => {
is_dm: true, is_dm: true,
}, },
}; };
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
httpBackend! httpBackend!
.when("POST", syncUrl) .when("POST", syncUrl)
.check(function (req) { .check(function (req) {
const body = req.data; const body = req.data;
logger.log("list", body); logger.log("list", body);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual(listReq); expect(body.lists["a"]).toEqual(listReq);
}) })
.respond(200, { .respond(200, {
pos: "a", pos: "a",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -424,7 +424,7 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
rooms: rooms, rooms: rooms,
}); });
const listenerData: Record<string, MSC3575RoomData> = {}; const listenerData: Record<string, MSC3575RoomData> = {};
@ -444,15 +444,14 @@ describe("SlidingSync", () => {
expect(listenerData[roomB]).toEqual(rooms[roomB]); expect(listenerData[roomB]).toEqual(rooms[roomB]);
expect(listenerData[roomC]).toEqual(rooms[roomC]); expect(listenerData[roomC]).toEqual(rooms[roomC]);
expect(slidingSync.listLength()).toEqual(1);
slidingSync.off(SlidingSyncEvent.RoomData, dataListener); slidingSync.off(SlidingSyncEvent.RoomData, dataListener);
}); });
it("should be possible to retrieve list data", () => { it("should be possible to retrieve list data", () => {
expect(slidingSync.getList(0)).toBeDefined(); expect(slidingSync.getListParams("a")).toBeDefined();
expect(slidingSync.getList(5)).toBeNull(); expect(slidingSync.getListParams("b")).toBeNull();
expect(slidingSync.getListData(5)).toBeNull(); expect(slidingSync.getListData("b")).toBeNull();
const syncData = slidingSync.getListData(0)!; const syncData = slidingSync.getListData("a")!;
expect(syncData.joinedCount).toEqual(500); // from previous test expect(syncData.joinedCount).toEqual(500); // from previous test
expect(syncData.roomIndexToRoomId).toEqual({ expect(syncData.roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
@ -467,17 +466,17 @@ describe("SlidingSync", () => {
.when("POST", syncUrl) .when("POST", syncUrl)
.check(function (req) { .check(function (req) {
const body = req.data; const body = req.data;
logger.log("next ranges", body.lists[0].ranges); logger.log("next ranges", body.lists["a"].ranges);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual({ expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky // only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges, ranges: newRanges,
}); });
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -487,15 +486,17 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
}); });
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.RequestFinished; return state === SlidingSyncState.RequestFinished;
}); });
slidingSync.setListRanges(0, newRanges); slidingSync.setListRanges("a", newRanges);
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
// setListRanges for an invalid list key returns an error
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
}); });
it("should be possible to add an extra list", async () => { it("should be possible to add an extra list", async () => {
@ -513,19 +514,19 @@ describe("SlidingSync", () => {
const body = req.data; const body = req.data;
logger.log("extra list", body); logger.log("extra list", body);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual({ expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky // only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges, ranges: newRanges,
}); });
expect(body.lists[1]).toEqual(extraListReq); expect(body.lists["b"]).toEqual(extraListReq);
}) })
.respond(200, { .respond(200, {
pos: "c", pos: "c",
lists: [ lists: {
{ a: {
count: 500, count: 500,
}, },
{ b: {
count: 50, count: 50,
ops: [ ops: [
{ {
@ -535,10 +536,10 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
}); });
listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => { listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(1); expect(listKey).toEqual("b");
expect(joinedCount).toEqual(50); expect(joinedCount).toEqual(50);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
@ -550,7 +551,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
slidingSync.setList(1, extraListReq); slidingSync.setList("b", extraListReq);
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
}); });
@ -559,8 +560,8 @@ describe("SlidingSync", () => {
// move C (2) to A (0) // move C (2) to A (0)
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e", pos: "e",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -574,16 +575,16 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
let listPromise = listenUntil( let listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500); expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
@ -603,8 +604,8 @@ describe("SlidingSync", () => {
// move C (0) back to A (2) // move C (0) back to A (2)
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f", pos: "f",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -618,13 +619,13 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => { listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500); expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
@ -644,8 +645,8 @@ describe("SlidingSync", () => {
it("should ignore invalid list indexes", async () => { it("should ignore invalid list indexes", async () => {
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e", pos: "e",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -654,16 +655,16 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
const listPromise = listenUntil( const listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500); expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
@ -684,8 +685,8 @@ describe("SlidingSync", () => {
it("should be possible to update a list", async () => { it("should be possible to update a list", async () => {
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g", pos: "g",
lists: [ lists: {
{ a: {
count: 42, count: 42,
ops: [ ops: [
{ {
@ -699,13 +700,13 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
// update the list with a new filter // update the list with a new filter
slidingSync.setList(0, { slidingSync.setList("a", {
filters: { filters: {
is_encrypted: true, is_encrypted: true,
}, },
@ -714,8 +715,8 @@ describe("SlidingSync", () => {
const listPromise = listenUntil( const listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(42); expect(joinedCount).toEqual(42);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomB, 0: roomB,
@ -738,12 +739,12 @@ describe("SlidingSync", () => {
0: roomB, 0: roomB,
1: roomC, 1: roomC,
}; };
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId); expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f", pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it // currently the list is [B,C] so we will insert D then immediately delete it
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -761,16 +762,16 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
const listPromise = listenUntil( const listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500); expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId); expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true; return true;
@ -785,14 +786,14 @@ describe("SlidingSync", () => {
}); });
it("should handle deletions correctly", async () => { it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB, 0: roomB,
1: roomC, 1: roomC,
}); });
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g", pos: "g",
lists: [ lists: {
{ a: {
count: 499, count: 499,
ops: [ ops: [
{ {
@ -801,16 +802,16 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
const listPromise = listenUntil( const listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(499); expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
@ -827,13 +828,13 @@ describe("SlidingSync", () => {
}); });
it("should handle insertions correctly", async () => { it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
}); });
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h", pos: "h",
lists: [ lists: {
{ a: {
count: 500, count: 500,
ops: [ ops: [
{ {
@ -843,16 +844,16 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
let listPromise = listenUntil( let listPromise = listenUntil(
slidingSync, slidingSync,
"SlidingSync.List", "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => { (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500); expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
@ -870,8 +871,8 @@ describe("SlidingSync", () => {
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h", pos: "h",
lists: [ lists: {
{ a: {
count: 501, count: 501,
ops: [ ops: [
{ {
@ -881,13 +882,13 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
{ b: {
count: 50, count: 50,
}, },
], },
}); });
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => { listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0); expect(listKey).toEqual("a");
expect(joinedCount).toEqual(501); expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({ expect(roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
@ -910,11 +911,14 @@ describe("SlidingSync", () => {
it("should handle insertions with a spurious DELETE correctly", async () => { it("should handle insertions with a spurious DELETE correctly", async () => {
slidingSync = new SlidingSync( slidingSync = new SlidingSync(
proxyBaseUrl, proxyBaseUrl,
[ new Map([
{ [
ranges: [[0, 20]], "a",
}, {
], ranges: [[0, 20]],
},
],
]),
{}, {},
client!, client!,
1, 1,
@ -922,22 +926,22 @@ describe("SlidingSync", () => {
// initially start with nothing // initially start with nothing
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "a", pos: "a",
lists: [ lists: {
{ a: {
count: 0, count: 0,
ops: [], ops: [],
}, },
], },
}); });
slidingSync.start(); slidingSync.start();
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({}); expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
// insert a room // insert a room
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "b", pos: "b",
lists: [ lists: {
{ a: {
count: 1, count: 1,
ops: [ ops: [
{ {
@ -951,18 +955,18 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
}); });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomA, 0: roomA,
}); });
// insert another room // insert another room
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c", pos: "c",
lists: [ lists: {
{ a: {
count: 1, count: 1,
ops: [ ops: [
{ {
@ -976,10 +980,10 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
}); });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB, 0: roomB,
1: roomA, 1: roomA,
}); });
@ -987,8 +991,8 @@ describe("SlidingSync", () => {
// insert a final room // insert a final room
httpBackend!.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c", pos: "c",
lists: [ lists: {
{ a: {
count: 1, count: 1,
ops: [ ops: [
{ {
@ -1002,10 +1006,10 @@ describe("SlidingSync", () => {
}, },
], ],
}, },
], },
}); });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
1: roomB, 1: roomB,
2: roomA, 2: roomA,
@ -1028,7 +1032,7 @@ describe("SlidingSync", () => {
required_state: [["m.room.name", ""]], required_state: [["m.room.name", ""]],
}; };
// add the subscription // add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
// modification before SlidingSync.start() // modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId: string | undefined; let txnId: string | undefined;
@ -1046,7 +1050,7 @@ describe("SlidingSync", () => {
return { return {
pos: "aaa", pos: "aaa",
txn_id: txnId, txn_id: txnId,
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: { rooms: {
[roomId]: { [roomId]: {
@ -1065,7 +1069,7 @@ describe("SlidingSync", () => {
const newList = { const newList = {
ranges: [[0, 20]], ranges: [[0, 20]],
}; };
const promise = slidingSync.setList(0, newList); const promise = slidingSync.setList("a", newList);
let txnId: string | undefined; let txnId: string | undefined;
httpBackend! httpBackend!
.when("POST", syncUrl) .when("POST", syncUrl)
@ -1073,7 +1077,7 @@ describe("SlidingSync", () => {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual(newList); expect(body.lists["a"]).toEqual(newList);
expect(body.txn_id).toBeTruthy(); expect(body.txn_id).toBeTruthy();
txnId = body.txn_id; txnId = body.txn_id;
}) })
@ -1081,7 +1085,7 @@ describe("SlidingSync", () => {
return { return {
pos: "bbb", pos: "bbb",
txn_id: txnId, txn_id: txnId,
lists: [{ count: 5 }], lists: { a: { count: 5 } },
extensions: {}, extensions: {},
}; };
}); });
@ -1090,7 +1094,7 @@ describe("SlidingSync", () => {
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
}); });
it("should resolve setListRanges during a connection", async () => { it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]); const promise = slidingSync.setListRanges("a", [[20, 40]]);
let txnId: string | undefined; let txnId: string | undefined;
httpBackend! httpBackend!
.when("POST", syncUrl) .when("POST", syncUrl)
@ -1098,7 +1102,7 @@ describe("SlidingSync", () => {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({ expect(body.lists["a"]).toEqual({
ranges: [[20, 40]], ranges: [[20, 40]],
}); });
expect(body.txn_id).toBeTruthy(); expect(body.txn_id).toBeTruthy();
@ -1108,7 +1112,7 @@ describe("SlidingSync", () => {
return { return {
pos: "ccc", pos: "ccc",
txn_id: txnId, txn_id: txnId,
lists: [{ count: 5 }], lists: { a: { count: 5 } },
extensions: {}, extensions: {},
}; };
}); });
@ -1150,10 +1154,10 @@ describe("SlidingSync", () => {
const pushTxn = function (req: MockHttpBackend["requests"][0]) { const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data.txn_id); gotTxnIds.push(req.data.txn_id);
}; };
const failPromise = slidingSync.setListRanges(0, [[20, 40]]); const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]); const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
@ -1162,7 +1166,7 @@ describe("SlidingSync", () => {
expect(failPromise).rejects.toEqual(gotTxnIds[0]); expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]); expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]); const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
let txnId: string | undefined; let txnId: string | undefined;
httpBackend! httpBackend!
.when("POST", syncUrl) .when("POST", syncUrl)
@ -1187,10 +1191,10 @@ describe("SlidingSync", () => {
const pushTxn = function (req: MockHttpBackend["requests"][0]) { const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data?.txn_id); gotTxnIds.push(req.data?.txn_id);
}; };
const A = slidingSync.setListRanges(0, [[20, 40]]); const A = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" }); httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
const B = slidingSync.setListRanges(0, [[60, 70]]); const B = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
@ -1198,7 +1202,7 @@ describe("SlidingSync", () => {
// which is a fail. // which is a fail.
expect(A).rejects.toEqual(gotTxnIds[0]); expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges(0, [[0, 20]]); const C = slidingSync.setListRanges("a", [[0, 20]]);
let pendingC = true; let pendingC = true;
C.finally(() => { C.finally(() => {
pendingC = false; pendingC = false;
@ -1219,7 +1223,7 @@ describe("SlidingSync", () => {
expect(pendingC).toBe(true); // C is pending still expect(pendingC).toBe(true); // C is pending still
}); });
it("should do nothing for unknown txn_ids", async () => { it("should do nothing for unknown txn_ids", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]); const promise = slidingSync.setListRanges("a", [[20, 40]]);
let pending = true; let pending = true;
promise.finally(() => { promise.finally(() => {
pending = false; pending = false;
@ -1231,7 +1235,7 @@ describe("SlidingSync", () => {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({ expect(body.lists["a"]).toEqual({
ranges: [[20, 40]], ranges: [[20, 40]],
}); });
expect(body.txn_id).toBeTruthy(); expect(body.txn_id).toBeTruthy();
@ -1241,7 +1245,7 @@ describe("SlidingSync", () => {
return { return {
pos: "ccc", pos: "ccc",
txn_id: "bogus transaction id", txn_id: "bogus transaction id",
lists: [{ count: 5 }], lists: { a: { count: 5 } },
extensions: {}, extensions: {},
}; };
}); });
@ -1279,7 +1283,7 @@ describe("SlidingSync", () => {
}; };
it("should be possible to use custom subscriptions on startup", async () => { it("should be possible to use custom subscriptions on startup", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
// the intention is for clients to set this up at startup // the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1); slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2); slidingSync.addCustomSubscription(customSubName2, customSub2);
@ -1302,7 +1306,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1312,7 +1316,7 @@ describe("SlidingSync", () => {
}); });
it("should be possible to use custom subscriptions mid-connection", async () => { it("should be possible to use custom subscriptions mid-connection", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
// the intention is for clients to set this up at startup // the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1); slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2); slidingSync.addCustomSubscription(customSubName2, customSub2);
@ -1326,7 +1330,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1344,7 +1348,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1363,7 +1367,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1383,7 +1387,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1395,7 +1399,7 @@ describe("SlidingSync", () => {
}); });
it("uses the default subscription for unknown subscription names", async () => { it("uses the default subscription for unknown subscription names", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1); slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.useCustomSubscription(roomA, "unknown name"); slidingSync.useCustomSubscription(roomA, "unknown name");
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA])); slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
@ -1410,7 +1414,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1420,7 +1424,7 @@ describe("SlidingSync", () => {
}); });
it("should not be possible to add/modify an already added custom subscription", async () => { it("should not be possible to add/modify an already added custom subscription", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1); slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName1, customSub2); slidingSync.addCustomSubscription(customSubName1, customSub2);
slidingSync.useCustomSubscription(roomA, customSubName1); slidingSync.useCustomSubscription(roomA, customSubName1);
@ -1436,7 +1440,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1446,7 +1450,7 @@ describe("SlidingSync", () => {
}); });
it("should change the custom subscription if they are different", async () => { it("should change the custom subscription if they are different", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1); slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2); slidingSync.addCustomSubscription(customSubName2, customSub2);
slidingSync.useCustomSubscription(roomA, customSubName1); slidingSync.useCustomSubscription(roomA, customSubName1);
@ -1463,7 +1467,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1484,7 +1488,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1506,7 +1510,7 @@ describe("SlidingSync", () => {
}) })
.respond(200, { .respond(200, {
pos: "b", pos: "b",
lists: [], lists: {},
extensions: {}, extensions: {},
rooms: {}, rooms: {},
}); });
@ -1559,7 +1563,7 @@ describe("SlidingSync", () => {
}; };
it("should be able to register an extension", async () => { it("should be able to register an extension", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
slidingSync.registerExtension(extPre); slidingSync.registerExtension(extPre);
const callbackOrder: string[] = []; const callbackOrder: string[] = [];
@ -1684,7 +1688,7 @@ describe("SlidingSync", () => {
}); });
it("is not possible to register the same extension name twice", async () => { it("is not possible to register the same extension name twice", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
slidingSync.registerExtension(extPre); slidingSync.registerExtension(extPre);
expect(() => { expect(() => {
slidingSync.registerExtension(extPre); slidingSync.registerExtension(extPre);

View File

@ -70,7 +70,7 @@ export interface MSC3575List extends MSC3575RoomSubscription {
*/ */
export interface MSC3575SlidingSyncRequest { export interface MSC3575SlidingSyncRequest {
// json body params // json body params
lists?: MSC3575List[]; lists?: Record<string, MSC3575List>;
unsubscribe_rooms?: string[]; unsubscribe_rooms?: string[];
room_subscriptions?: Record<string, MSC3575RoomSubscription>; room_subscriptions?: Record<string, MSC3575RoomSubscription>;
extensions?: object; extensions?: object;
@ -137,7 +137,7 @@ type Operation = DeleteOperation | InsertOperation | InvalidateOperation | SyncO
export interface MSC3575SlidingSyncResponse { export interface MSC3575SlidingSyncResponse {
pos: string; pos: string;
txn_id?: string; txn_id?: string;
lists: ListResponse[]; lists: Record<string, ListResponse>;
rooms: Record<string, MSC3575RoomData>; rooms: Record<string, MSC3575RoomData>;
extensions: Record<string, object>; extensions: Record<string, object>;
} }
@ -332,11 +332,7 @@ export type SlidingSyncEventHandlerMap = {
resp: MSC3575SlidingSyncResponse | null, resp: MSC3575SlidingSyncResponse | null,
err?: Error, err?: Error,
) => void; ) => void;
[SlidingSyncEvent.List]: ( [SlidingSyncEvent.List]: (listKey: string, joinedCount: number, roomIndexToRoomId: Record<number, string>) => void;
listIndex: number,
joinedCount: number,
roomIndexToRoomId: Record<number, string>,
) => void;
}; };
/** /**
@ -346,7 +342,7 @@ export type SlidingSyncEventHandlerMap = {
* To hook this up with the JS SDK, you need to use SlidingSyncSdk. * To hook this up with the JS SDK, you need to use SlidingSyncSdk.
*/ */
export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSyncEventHandlerMap> { export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSyncEventHandlerMap> {
private lists: SlidingList[]; private lists: Map<string, SlidingList>;
private listModifiedCount = 0; private listModifiedCount = 0;
private terminated = false; private terminated = false;
// flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :( // flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :(
@ -380,13 +376,16 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
*/ */
public constructor( public constructor(
private readonly proxyBaseUrl: string, private readonly proxyBaseUrl: string,
lists: MSC3575List[], lists: Map<string, MSC3575List>,
private roomSubscriptionInfo: MSC3575RoomSubscription, private roomSubscriptionInfo: MSC3575RoomSubscription,
private readonly client: MatrixClient, private readonly client: MatrixClient,
private readonly timeoutMS: number, private readonly timeoutMS: number,
) { ) {
super(); super();
this.lists = lists.map((l) => new SlidingList(l)); this.lists = new Map<string, SlidingList>();
lists.forEach((list, key) => {
this.lists.set(key, new SlidingList(list));
});
} }
/** /**
@ -423,70 +422,70 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
} }
/** /**
* Get the length of the sliding lists. * Get the room index data for a list.
* @returns The number of lists in the sync request * @param key - The list key
*/
public listLength(): number {
return this.lists.length;
}
/**
* Get the room data for a list.
* @param index - The list index
* @returns The list data which contains the rooms in this list * @returns The list data which contains the rooms in this list
*/ */
public getListData(index: number): { joinedCount: number; roomIndexToRoomId: Record<number, string> } | null { public getListData(key: string): { joinedCount: number; roomIndexToRoomId: Record<number, string> } | null {
if (!this.lists[index]) { const data = this.lists.get(key);
if (!data) {
return null; return null;
} }
return { return {
joinedCount: this.lists[index].joinedCount, joinedCount: data.joinedCount,
roomIndexToRoomId: Object.assign({}, this.lists[index].roomIndexToRoomId), roomIndexToRoomId: Object.assign({}, data.roomIndexToRoomId),
}; };
} }
/** /**
* Get the full list parameters for a list index. This function is provided for callers to use * Get the full request list parameters for a list index. This function is provided for callers to use
* in conjunction with setList to update fields on an existing list. * in conjunction with setList to update fields on an existing list.
* @param index - The list index to get the list for. * @param key - The list key to get the params for.
* @returns A copy of the list or undefined. * @returns A copy of the list params or undefined.
*/ */
public getList(index: number): MSC3575List | null { public getListParams(key: string): MSC3575List | null {
if (!this.lists[index]) { const params = this.lists.get(key);
if (!params) {
return null; return null;
} }
return this.lists[index].getList(true); return params.getList(true);
} }
/** /**
* Set new ranges for an existing list. Calling this function when _only_ the ranges have changed * Set new ranges for an existing list. Calling this function when _only_ the ranges have changed
* is more efficient than calling setList(index,list) as this function won't resend sticky params, * is more efficient than calling setList(index,list) as this function won't resend sticky params,
* whereas setList always will. * whereas setList always will.
* @param index - The list index to modify * @param key - The list key to modify
* @param ranges - The new ranges to apply. * @param ranges - The new ranges to apply.
* @returns A promise which resolves to the transaction ID when it has been received down sync * @returns A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request) * immediately after sending, in which case the action will be applied in the subsequent request)
*/ */
public setListRanges(index: number, ranges: number[][]): Promise<string> { public setListRanges(key: string, ranges: number[][]): Promise<string> {
this.lists[index].updateListRange(ranges); const list = this.lists.get(key);
if (!list) {
return Promise.reject(new Error("no list with key " + key));
}
list.updateListRange(ranges);
return this.resend(); return this.resend();
} }
/** /**
* Add or replace a list. Calling this function will interrupt the /sync request to resend new * Add or replace a list. Calling this function will interrupt the /sync request to resend new
* lists. * lists.
* @param index - The index to modify * @param key - The key to modify
* @param list - The new list parameters. * @param list - The new list parameters.
* @returns A promise which resolves to the transaction ID when it has been received down sync * @returns A promise which resolves to the transaction ID when it has been received down sync
* (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled
* immediately after sending, in which case the action will be applied in the subsequent request) * immediately after sending, in which case the action will be applied in the subsequent request)
*/ */
public setList(index: number, list: MSC3575List): Promise<string> { public setList(key: string, list: MSC3575List): Promise<string> {
if (this.lists[index]) { const existingList = this.lists.get(key);
this.lists[index].replaceList(list); if (existingList) {
existingList.replaceList(list);
this.lists.set(key, existingList);
} else { } else {
this.lists[index] = new SlidingList(list); this.lists.set(key, new SlidingList(list));
} }
this.listModifiedCount += 1; this.listModifiedCount += 1;
return this.resend(); return this.resend();
@ -592,32 +591,44 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err); this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
} }
private shiftRight(listIndex: number, hi: number, low: number): void { private shiftRight(listKey: string, hi: number, low: number): void {
const list = this.lists.get(listKey);
if (!list) {
return;
}
// l h // l h
// 0,1,2,3,4 <- before // 0,1,2,3,4 <- before
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated // 0,1,2,2,3 <- after, hi is deleted and low is duplicated
for (let i = hi; i > low; i--) { for (let i = hi; i > low; i--) {
if (this.lists[listIndex].isIndexInRange(i)) { if (list.isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] = this.lists[listIndex].roomIndexToRoomId[i - 1]; list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i - 1];
} }
} }
} }
private shiftLeft(listIndex: number, hi: number, low: number): void { private shiftLeft(listKey: string, hi: number, low: number): void {
const list = this.lists.get(listKey);
if (!list) {
return;
}
// l h // l h
// 0,1,2,3,4 <- before // 0,1,2,3,4 <- before
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated // 0,1,3,4,4 <- after, low is deleted and hi is duplicated
for (let i = low; i < hi; i++) { for (let i = low; i < hi; i++) {
if (this.lists[listIndex].isIndexInRange(i)) { if (list.isIndexInRange(i)) {
this.lists[listIndex].roomIndexToRoomId[i] = this.lists[listIndex].roomIndexToRoomId[i + 1]; list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i + 1];
} }
} }
} }
private removeEntry(listIndex: number, index: number): void { private removeEntry(listKey: string, index: number): void {
const list = this.lists.get(listKey);
if (!list) {
return;
}
// work out the max index // work out the max index
let max = -1; let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) { for (const n in list.roomIndexToRoomId) {
if (Number(n) > max) { if (Number(n) > max) {
max = Number(n); max = Number(n);
} }
@ -626,14 +637,18 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
return; return;
} }
// Everything higher than the gap needs to be shifted left. // Everything higher than the gap needs to be shifted left.
this.shiftLeft(listIndex, max, index); this.shiftLeft(listKey, max, index);
delete this.lists[listIndex].roomIndexToRoomId[max]; delete list.roomIndexToRoomId[max];
} }
private addEntry(listIndex: number, index: number): void { private addEntry(listKey: string, index: number): void {
const list = this.lists.get(listKey);
if (!list) {
return;
}
// work out the max index // work out the max index
let max = -1; let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) { for (const n in list.roomIndexToRoomId) {
if (Number(n) > max) { if (Number(n) > max) {
max = Number(n); max = Number(n);
} }
@ -642,30 +657,37 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
return; return;
} }
// Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
this.shiftRight(listIndex, max + 1, index); this.shiftRight(listKey, max + 1, index);
} }
private processListOps(list: ListResponse, listIndex: number): void { private processListOps(list: ListResponse, listKey: string): void {
let gapIndex = -1; let gapIndex = -1;
const listData = this.lists.get(listKey);
if (!listData) {
return;
}
list.ops.forEach((op: Operation) => { list.ops.forEach((op: Operation) => {
if (!listData) {
return;
}
switch (op.op) { switch (op.op) {
case "DELETE": { case "DELETE": {
logger.debug("DELETE", listIndex, op.index, ";"); logger.debug("DELETE", listKey, op.index, ";");
delete this.lists[listIndex].roomIndexToRoomId[op.index]; delete listData.roomIndexToRoomId[op.index];
if (gapIndex !== -1) { if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it. // we already have a DELETE operation to process, so process it.
this.removeEntry(listIndex, gapIndex); this.removeEntry(listKey, gapIndex);
} }
gapIndex = op.index; gapIndex = op.index;
break; break;
} }
case "INSERT": { case "INSERT": {
logger.debug("INSERT", listIndex, op.index, op.room_id, ";"); logger.debug("INSERT", listKey, op.index, op.room_id, ";");
if (this.lists[listIndex].roomIndexToRoomId[op.index]) { if (listData.roomIndexToRoomId[op.index]) {
// something is in this space, shift items out of the way // something is in this space, shift items out of the way
if (gapIndex < 0) { if (gapIndex < 0) {
// we haven't been told where to shift from, so make way for a new room entry. // we haven't been told where to shift from, so make way for a new room entry.
this.addEntry(listIndex, op.index); this.addEntry(listKey, op.index);
} else if (gapIndex > op.index) { } else if (gapIndex > op.index) {
// the gap is further down the list, shift every element to the right // 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: // starting at the gap so we can just shift each element in turn:
@ -674,11 +696,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// [A,B,B,C] i=2 // [A,B,B,C] i=2
// [A,A,B,C] i=1 // [A,A,B,C] i=1
// Terminate. We'll assign into op.index next. // Terminate. We'll assign into op.index next.
this.shiftRight(listIndex, gapIndex, op.index); this.shiftRight(listKey, gapIndex, op.index);
} else if (gapIndex < op.index) { } else if (gapIndex < op.index) {
// the gap is further up the list, shift every element to the left // 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 // starting at the gap so we can just shift each element in turn
this.shiftLeft(listIndex, op.index, gapIndex); this.shiftLeft(listKey, op.index, gapIndex);
} }
} }
// forget the gap, we don't need it anymore. This is outside the check for // forget the gap, we don't need it anymore. This is outside the check for
@ -686,15 +708,15 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
// forget the gap, not conditionally based on the presence of a room in the INSERT // 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. // position. Without this, DELETE 0; INSERT 0; would do the wrong thing.
gapIndex = -1; gapIndex = -1;
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id; listData.roomIndexToRoomId[op.index] = op.room_id;
break; break;
} }
case "INVALIDATE": { case "INVALIDATE": {
const startIndex = op.range[0]; const startIndex = op.range[0];
for (let i = startIndex; i <= op.range[1]; i++) { for (let i = startIndex; i <= op.range[1]; i++) {
delete this.lists[listIndex].roomIndexToRoomId[i]; delete listData.roomIndexToRoomId[i];
} }
logger.debug("INVALIDATE", listIndex, op.range[0], op.range[1], ";"); logger.debug("INVALIDATE", listKey, op.range[0], op.range[1], ";");
break; break;
} }
case "SYNC": { case "SYNC": {
@ -704,9 +726,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
if (!roomId) { if (!roomId) {
break; // we are at the end of list break; // we are at the end of list
} }
this.lists[listIndex].roomIndexToRoomId[i] = roomId; listData.roomIndexToRoomId[i] = roomId;
} }
logger.debug("SYNC", listIndex, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";"); logger.debug("SYNC", listKey, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";");
break; break;
} }
} }
@ -714,7 +736,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
if (gapIndex !== -1) { if (gapIndex !== -1) {
// we already have a DELETE operation to process, so process it // we already have a DELETE operation to process, so process it
// Everything higher than the gap needs to be shifted left. // Everything higher than the gap needs to be shifted left.
this.removeEntry(listIndex, gapIndex); this.removeEntry(listKey, gapIndex);
} }
} }
@ -814,10 +836,12 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
let resp: MSC3575SlidingSyncResponse | undefined; let resp: MSC3575SlidingSyncResponse | undefined;
try { try {
const listModifiedCount = this.listModifiedCount; const listModifiedCount = this.listModifiedCount;
const reqLists: Record<string, MSC3575List> = {};
this.lists.forEach((l: SlidingList, key: string) => {
reqLists[key] = l.getList(false);
});
const reqBody: MSC3575SlidingSyncRequest = { const reqBody: MSC3575SlidingSyncRequest = {
lists: this.lists.map((l) => { lists: reqLists,
return l.getList(false);
}),
pos: currentPos, pos: currentPos,
timeout: this.timeoutMS, timeout: this.timeoutMS,
clientTimeout: this.timeoutMS + BUFFER_PERIOD_MS, clientTimeout: this.timeoutMS + BUFFER_PERIOD_MS,
@ -866,11 +890,15 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
l.setModified(false); l.setModified(false);
}); });
// set default empty values so we don't need to null check // set default empty values so we don't need to null check
resp.lists = resp.lists || []; resp.lists = resp.lists || {};
resp.rooms = resp.rooms || {}; resp.rooms = resp.rooms || {};
resp.extensions = resp.extensions || {}; resp.extensions = resp.extensions || {};
resp.lists.forEach((val, i) => { Object.keys(resp.lists).forEach((key: string) => {
this.lists[i].joinedCount = val.count; const list = this.lists.get(key);
if (!list || !resp) {
return;
}
list.joinedCount = resp.lists[key].count;
}); });
this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, resp); this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, resp);
} catch (err) { } catch (err) {
@ -899,25 +927,24 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.invokeRoomDataListeners(roomId, resp!.rooms[roomId]); this.invokeRoomDataListeners(roomId, resp!.rooms[roomId]);
}); });
const listIndexesWithUpdates: Set<number> = new Set(); const listKeysWithUpdates: Set<string> = new Set();
if (!doNotUpdateList) { if (!doNotUpdateList) {
resp.lists.forEach((list, listIndex) => { for (const [key, list] of Object.entries(resp.lists)) {
list.ops = list.ops || []; list.ops = list.ops || [];
if (list.ops.length > 0) { if (list.ops.length > 0) {
listIndexesWithUpdates.add(listIndex); listKeysWithUpdates.add(key);
} }
this.processListOps(list, listIndex); this.processListOps(list, key);
}); }
} }
this.invokeLifecycleListeners(SlidingSyncState.Complete, resp); this.invokeLifecycleListeners(SlidingSyncState.Complete, resp);
this.onPostExtensionsResponse(resp.extensions); this.onPostExtensionsResponse(resp.extensions);
listIndexesWithUpdates.forEach((i) => { listKeysWithUpdates.forEach((listKey: string) => {
this.emit( const list = this.lists.get(listKey);
SlidingSyncEvent.List, if (!list) {
i, return;
this.lists[i].joinedCount, }
Object.assign({}, this.lists[i].roomIndexToRoomId), this.emit(SlidingSyncEvent.List, listKey, list.joinedCount, Object.assign({}, list.roomIndexToRoomId));
);
}); });
this.resolveTransactionDefers(resp.txn_id); this.resolveTransactionDefers(resp.txn_id);