cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

slidingSync.ts (34296B)


      1 import { MatrixClient } from "./client";
      2 import { ISlidingSyncReq, ISlidingSyncResp, isDeleteOp, isInsertOp, isInvalidateOp, isSyncOp } from './api/slidingSync';
      3 import EventEmitter from "events";
      4 import { Room } from "./room";
      5 import { OwnUser } from "./ownUser";
      6 import { DeviceLists, UserId } from "@mtrnord/matrix-sdk-crypto-js";
      7 import { IRoomEvent, IRoomStateEvent, isRoomStateEvent } from "./api/events";
      8 import { HostnameMissingError, NotLogeedInError, SDKError, SyncError } from "./utils";
      9 
     10 export interface MatrixSlidingSyncEvents {
     11     // Used to notify about changes to the room list
     12     'rooms': (rooms: Set<Room>) => void;
     13     //'delete': (changedCount: number) => void;
     14 }
     15 
     16 export declare interface MatrixSlidingSync {
     17     on<U extends keyof MatrixSlidingSyncEvents>(
     18         event: U, listener: MatrixSlidingSyncEvents[U]
     19     ): this;
     20 
     21     emit<U extends keyof MatrixSlidingSyncEvents>(
     22         event: U, ...args: Parameters<MatrixSlidingSyncEvents[U]>
     23     ): boolean;
     24 }
     25 
     26 export class MatrixSlidingSync extends EventEmitter {
     27     private syncing = false;
     28     private syncPos?: string;
     29     private initialSync = true;
     30     private lastRanges?: { [key: string]: number[][] };
     31     private lastTxnID?: string;
     32     private to_device_since?: string;
     33     public mustUpdateTxnID = true;
     34     public rooms: Set<Room> = new Set();
     35     private abortController = new AbortController();
     36 
     37     constructor(private client: MatrixClient, private user: OwnUser) { super() }
     38 
     39     public applyStoredSyncInfo(syncInfo: {
     40         userId: string;
     41         syncPos?: string;
     42         initialSync: boolean;
     43         lastRanges?: {
     44             [key: string]: number[][];
     45         };
     46         lastTxnID?: string;
     47         to_device_since?: string;
     48     }) {
     49         this.syncPos = syncInfo.syncPos;
     50         this.initialSync = syncInfo.initialSync;
     51         this.lastRanges = syncInfo.lastRanges;
     52         this.lastTxnID = syncInfo.lastTxnID;
     53         this.to_device_since = syncInfo.to_device_since;
     54     }
     55 
     56     public logout() {
     57         this.stopSync();
     58         this.abortController.abort();
     59         this.rooms = new Set();
     60         this.initialSync = true;
     61         this.syncPos = undefined;
     62         this.to_device_since = undefined;
     63     }
     64 
     65     public resetAbortController() {
     66         this.abortController = new AbortController();
     67     }
     68 
     69     public async startSync(): Promise<SDKError | void> {
     70         // @ts-ignore
     71         if (globalThis.IS_STORYBOOK) {
     72             await new Promise(r => setTimeout(r, 5000));
     73         }
     74         if (!this.client.isLoggedIn) {
     75             return new NotLogeedInError();
     76         }
     77         if (!this.client.database) {
     78             await this.client.createDatabase();
     79         }
     80         if (this.syncing) {
     81             return;
     82         }
     83         this.syncing = true;
     84         let retries = 0;
     85         while (this.syncing) {
     86             try {
     87                 await this.sync();
     88             } catch (e) {
     89                 console.error(`Error: ${e}`);
     90                 if (retries > 5) {
     91                     console.error("Too many retries, giving up");
     92                     this.stopSync();
     93                     return;
     94                 }
     95                 // Sleep for 30 seconds
     96                 await new Promise(resolve => setTimeout(resolve, 30000));
     97                 console.warn("Retrying sync");
     98                 retries++;
     99             }
    100         }
    101     }
    102 
    103     public stopSync() {
    104         this.syncing = false;
    105     }
    106 
    107     private isIndexInRange(index: number, ranges: number[][]): boolean {
    108         for (const r of ranges) {
    109             if (r[0] < index && index <= r[1]) {
    110                 return true
    111             }
    112         }
    113         return false
    114     }
    115 
    116     private shiftRight(listKey: string, ranges: number[][], hi: number, low: number) {
    117         //     l   h
    118         // 0,1,2,3,4 <- before
    119         // 0,1,2,2,3 <- after, hi is deleted and low is duplicated
    120         for (let i = hi - 1; i > low - 1; i--) {
    121             if (this.isIndexInRange(i, ranges)) {
    122                 const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i + 1);
    123                 if (roomObj) {
    124                     roomObj.windowPos[listKey] = (i);
    125                 }
    126             }
    127         }
    128     }
    129 
    130     private shiftLeft(listKey: string, ranges: number[][], hi: number, low: number) {
    131         //     l   h
    132         // 0,1,2,3,4 <- before
    133         // 0,1,3,4,4 <- after, low is deleted and hi is duplicated
    134         for (let i = low + 1; i < hi + 1; i++) {
    135             if (this.isIndexInRange(i, ranges)) {
    136                 const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i - 1);
    137                 if (roomObj) {
    138                     roomObj.windowPos[listKey] = (i);
    139                 }
    140             }
    141         }
    142 
    143     }
    144 
    145     private async removeEntry(listKey: string, ranges: number[][], index: number) {
    146         // work out the max index
    147         let max = -1;
    148         const indexes = [...this.rooms].map(room => room.windowPos[listKey]);
    149         for (const n in indexes) {
    150             if (Number(n) > max) {
    151                 max = Number(n);
    152             }
    153         }
    154         // TODO: Unclear if this is needed or working. Probably wrong?
    155         // const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === index);
    156         // if (roomObj) {
    157         //     const tx = this.database?.transaction('rooms', 'readwrite');
    158         //     await tx?.store.delete(roomObj.roomID);
    159         //     await tx?.done;
    160         //     this.rooms.delete(roomObj)
    161         // }
    162         if (max < 0 || index > max) {
    163             return;
    164         }
    165         // Everything higher than the gap needs to be shifted left.
    166         this.shiftLeft(listKey, ranges, max, index);
    167     }
    168 
    169     private addEntry(listKey: string, ranges: number[][], index: number): void {
    170         // work out the max index
    171         let max = -1;
    172         const indexes = [...this.rooms].map(room => room.windowPos[listKey]);
    173         for (const n in indexes) {
    174             if (Number(n) > max) {
    175                 max = Number(n);
    176             }
    177         }
    178         if (max < 0 || index > max) {
    179             return;
    180         }
    181         // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
    182         this.shiftRight(listKey, ranges, max + 1, index);
    183     }
    184 
    185     private async sync(): Promise<SDKError | void> {
    186         if (!this.client.isLoggedIn) {
    187             return new NotLogeedInError();
    188         }
    189         if (!this.user.slidingSyncHostname) {
    190             return new HostnameMissingError();
    191         }
    192 
    193         // TODO: This might cause future issues
    194         Promise.all([this.user.e2ee.sendIdentifyAndOneTimeKeys()]).catch(e => {
    195             console.error("Error sending identify and one time keys", e);
    196         });
    197 
    198         // This is the initial sync case for each list
    199         const lists_ranges: {
    200             "overview": number[][];
    201             "spaces": number[][];
    202             [key: string]: number[][];
    203         } = {
    204             "overview": [[0, 20]],
    205             "spaces": [[0, 20]]
    206         };
    207         for (const space of this.client.spaceOpen) {
    208             if (space === "other") { continue }
    209             if (space === "dm") { continue }
    210             lists_ranges[space] = [[0, 20]];
    211         }
    212 
    213         let timeline_limit = 1;
    214         let subscription_limit = 10;
    215         if (!this.initialSync) {
    216             for (const list in lists_ranges) {
    217                 // Set higher timeline limit for subsequent syncs
    218                 timeline_limit = 10;
    219                 subscription_limit = 50;
    220                 // Calculate overlap between this.roomsInView and this.roomToRoomID and then
    221                 // calculate the ranges for each list
    222                 let rawRangeInView = new Set([...this.rooms]
    223                     .filter(room => this.client.roomsInView.includes(room.roomID))
    224                     .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null))
    225 
    226                 if (this.client.getSpaces().find(r => r.roomID === list)) {
    227                     // If we are syncing the spaces list, we need to use the spaceInView list instead
    228                     rawRangeInView = new Set([...this.rooms]
    229                         .filter(room => this.client.spacesInView.includes(room.roomID))
    230                         .map(room => room.windowPos[list]).sort().filter(x => x !== undefined && x !== null))
    231                 }
    232 
    233                 if (rawRangeInView.size !== 0) {
    234                     const minimum = Math.min(...rawRangeInView);
    235                     const maximum = Math.max(...rawRangeInView);
    236 
    237                     lists_ranges[list] = [[Math.max(minimum - 10, 0), maximum + 10]];
    238                 }
    239             }
    240             lists_ranges["e2ee"] = lists_ranges["overview"];
    241         }
    242 
    243 
    244         if (this.lastRanges && Object.entries(lists_ranges).toString() !== Object.entries(this.lastRanges).toString()) {
    245             console.log("Ranges changed, resetting sync txn_id", lists_ranges)
    246             this.lastRanges = lists_ranges;
    247             this.lastTxnID = Date.now().toString();
    248         }
    249 
    250         if (!this.lastRanges) {
    251             this.lastRanges = lists_ranges;
    252             this.lastTxnID = Date.now().toString();
    253         }
    254 
    255         if (this.mustUpdateTxnID) {
    256             this.lastTxnID = Date.now().toString();
    257             this.mustUpdateTxnID = false;
    258         }
    259 
    260 
    261         let url = `${this.user.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=5000`;
    262         if (this.syncPos) {
    263             url = `${this.user.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=5000&pos=${this.syncPos}`
    264         }
    265 
    266         const body: ISlidingSyncReq = {
    267             // allows clients to know what request params reached the server,
    268             // functionally similar to txn IDs on /send for events.
    269             txn_id: this.lastTxnID,
    270 
    271             // a delta token to remember information between sessions.
    272             // See "Bandwidth optimisations for persistent clients" for more information.
    273             // TODO: This isnt implemented anywhere yet
    274             //delta_token: "opaque-server-provided-string",
    275 
    276             // Sliding Window API
    277             lists: {
    278                 "spaces": {
    279                     ranges: this.lastRanges["spaces"],
    280                     // slow_get_all_rooms: true,
    281                     sort: ["by_name"],
    282                     required_state: [
    283                         // needed to build sections
    284                         ["m.space.child", "*"],
    285                         ["m.space.parent", "*"],
    286                         ["m.room.create", ""],
    287                         ["m.room.tombstone", ""],
    288                         // Room Avatar
    289                         ["m.room.avatar", "*"],
    290                         // Room Topic
    291                         ["m.room.topic", "*"],
    292                         // Request only the m.room.member events required to render events in the timeline.
    293                         // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for
    294                         // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members".
    295                         ["m.room.member", "$LAZY"],
    296                         // E2EE
    297                         ["m.room.encryption", ""],
    298                         ["m.room.history_visibility", ""],
    299                     ],
    300                     timeline_limit: 0,
    301                     filters: {
    302                         room_types: ["m.space"]
    303                     }
    304                 },
    305                 "overview": {
    306                     ranges: this.lastRanges["overview"],
    307                     sort: ["by_notification_level", "by_recency", "by_name"],
    308                     required_state: [
    309                         // needed to build sections
    310                         ["m.space.child", "*"],
    311                         ["m.space.parent", "*"],
    312                         ["m.room.create", ""],
    313                         ["m.room.tombstone", ""],
    314                         // Room Avatar
    315                         ["m.room.avatar", "*"],
    316                         // Room Topic
    317                         ["m.room.topic", "*"],
    318                         // Request only the m.room.member events required to render events in the timeline.
    319                         // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for
    320                         // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members".
    321                         ["m.room.member", "$LAZY"],
    322                         // E2EE
    323                         ["m.room.encryption", ""],
    324                         ["m.room.history_visibility", ""],
    325                     ],
    326                     timeline_limit: timeline_limit,
    327                     filters: {
    328                         not_room_types: ["m.space"],
    329                     }
    330                 },
    331                 "e2ee": {
    332                     ranges: this.lastRanges["overview"],
    333                     sort: ["by_notification_level", "by_recency", "by_name"],
    334                     required_state: [
    335                         // needed to build sections
    336                         ["m.space.child", "*"],
    337                         ["m.space.parent", "*"],
    338                         ["m.room.create", ""],
    339                         ["m.room.tombstone", ""],
    340                         // Room Avatar
    341                         ["m.room.avatar", "*"],
    342                         // Room Topic
    343                         ["m.room.topic", "*"],
    344                         ["m.room.member", "*"],
    345                         // E2EE
    346                         ["m.room.encryption", ""],
    347                         ["m.room.history_visibility", ""],
    348                     ],
    349                     timeline_limit: timeline_limit,
    350                     filters: {
    351                         not_room_types: ["m.space"],
    352                         is_encrypted: true,
    353                     }
    354                 },
    355             },
    356             bump_event_types: ["m.room.message", "m.room.encrypted"],
    357 
    358             extensions: {
    359                 e2ee: {
    360                     enabled: true,
    361                 },
    362                 to_device: {
    363                     enabled: true,
    364                     since: this.to_device_since
    365                 },
    366                 typing: {
    367                     enabled: true,
    368                     lists: ["overview", "e2ee"],
    369                 },
    370                 receipts: {
    371                     enabled: true,
    372                     lists: ["overview", "e2ee"],
    373                 }
    374             },
    375         };
    376 
    377         for (const space of this.client.spaceOpen) {
    378             if (space === "other") { continue }
    379             if (space === "dm") { continue }
    380             if (!body.lists) {
    381                 body.lists = {};
    382             }
    383             body.lists[space] = {
    384                 slow_get_all_rooms: true,
    385                 ranges: this.lastRanges[space],
    386                 sort: ["by_notification_level", "by_recency", "by_name"],
    387                 required_state: [
    388                     // needed to build sections
    389                     ["m.space.child", "*"],
    390                     ["m.space.parent", "*"],
    391                     ["m.room.create", ""],
    392                     ["m.room.tombstone", ""],
    393                     // Room Avatar
    394                     ["m.room.avatar", "*"],
    395                     // Room Topic
    396                     ["m.room.topic", "*"],
    397                     // Request only the m.room.member events required to render events in the timeline.
    398                     // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for
    399                     // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members".
    400                     ["m.room.member", "$LAZY"],
    401                     // E2EE
    402                     ["m.room.encryption", ""],
    403                     ["m.room.history_visibility", ""],
    404                 ],
    405                 timeline_limit: timeline_limit,
    406                 filters: {
    407                     "spaces": [space]
    408                 }
    409             }
    410         }
    411 
    412         if (this.client.currentRoom) {
    413             body.room_subscriptions = {};
    414             body.room_subscriptions[this.client.currentRoom] = {
    415                 sort: ["by_notification_level", "by_recency", "by_name"],
    416                 required_state: [
    417                     // needed to build sections
    418                     ["m.space.child", "*"],
    419                     ["m.space.parent", "*"],
    420                     ["m.room.create", ""],
    421                     ["m.room.tombstone", ""],
    422                     // Room Avatar
    423                     ["m.room.avatar", "*"],
    424                     // Room Topic
    425                     ["m.room.topic", "*"],
    426                     // Request only the m.room.member events required to render events in the timeline.
    427                     // The "$LAZY" value is a special sentinel value meaning "lazy loading" and is only valid for
    428                     // the "m.room.member" event type. For more information on the semantics, see "Lazy-Loading Room Members".
    429                     ["m.room.member", "$LAZY"],
    430                     // E2EE
    431                     ["m.room.encryption", ""],
    432                     ["m.room.history_visibility", ""],
    433                 ],
    434                 timeline_limit: subscription_limit,
    435                 filters: {}
    436             }
    437         }
    438 
    439         const resp = await fetch(url, {
    440             method: "POST",
    441             signal: this.abortController.signal,
    442             headers: {
    443                 "Content-Type": "application/json",
    444                 "Authorization": `Bearer ${this.user.access_token}`
    445             },
    446             body: JSON.stringify(body)
    447         });
    448         if (!resp.ok) {
    449             if (resp.status === 400) {
    450                 if ((await resp.json()).errcode === "M_UNKNOWN_POS") {
    451                     this.syncPos = undefined;
    452                     const syncInfoTX = this.client.database?.transaction('syncInfo', 'readwrite');
    453                     await syncInfoTX?.store.put({
    454                         userId: this.user.mxid!,
    455                         syncPos: this.syncPos,
    456                         initialSync: this.initialSync,
    457                         lastRanges: this.lastRanges,
    458                         lastTxnID: this.lastTxnID,
    459                     });
    460                     await syncInfoTX?.done;
    461                 }
    462                 return;
    463             } else if (resp.status === 401) {
    464                 await this.logout();
    465                 return new SyncError(resp);
    466             } else {
    467                 return new SyncError(resp);
    468             }
    469         }
    470         const json = await resp.json() as ISlidingSyncResp;
    471         this.syncPos = json.pos;
    472 
    473         if (json.extensions?.to_device) {
    474             await this.user.e2ee.receiveSyncData(
    475                 JSON.stringify(json.extensions.to_device.events || []),
    476                 new DeviceLists(
    477                     json.extensions.e2ee?.device_lists ? json.extensions.e2ee?.device_lists?.changed?.map(
    478                         user_id => new UserId(user_id)
    479                     ) : [],
    480                     json.extensions.e2ee?.device_lists ? json.extensions.e2ee?.device_lists?.left?.map(
    481                         user_id => new UserId(user_id)
    482                     ) : []
    483                 ),
    484                 new Map(Object.entries(json.extensions.e2ee?.device_one_time_keys_count || [])),
    485                 new Set(json.extensions.e2ee?.device_unused_fallback_key_types)
    486             )
    487             this.to_device_since = json.extensions.to_device.next_batch;
    488         }
    489 
    490         await this.user.e2ee.sendIdentifyAndOneTimeKeys();
    491 
    492 
    493         const syncInfoTX = this.client.database?.transaction('syncInfo', 'readwrite');
    494         await syncInfoTX?.store.put({
    495             userId: this.user.mxid!,
    496             syncPos: this.syncPos,
    497             initialSync: this.initialSync,
    498             lastRanges: this.lastRanges,
    499             lastTxnID: this.lastTxnID,
    500             to_device_since: this.to_device_since,
    501         });
    502         await syncInfoTX?.done;
    503 
    504         let gapIndex = -1;
    505         for (const listKey in json.lists) {
    506             const list = json.lists[listKey];
    507             if (list.ops) {
    508                 for (const op of list.ops) {
    509                     if (isSyncOp(op)) {
    510                         const tx = this.client.database?.transaction('rooms', 'readwrite');
    511                         for (let i = op.range[0]; i <= op.range[1]; i++) {
    512                             const roomID = op.room_ids[i - op.range[0]];
    513                             if (!roomID) {
    514                                 break; // we are at the end of list
    515                             }
    516 
    517                             // Check if we already know this room and skip if we do. This is needed since we have 2 lists.
    518                             // The db would already do this but the obj list doesn't (even though its a Set. Thats a mystery yet to solve)
    519                             const roomObj = [...this.rooms].find(room => room.roomID === roomID);
    520                             if (roomObj) {
    521                                 roomObj.windowPos[listKey] = i;
    522                                 continue;
    523                             }
    524 
    525                             const newRoom = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee);
    526                             // We start to remember the Room now.
    527                             newRoom.setName(roomID);
    528                             newRoom.windowPos[listKey] = i;
    529 
    530                             this.rooms.add(newRoom);
    531                             await tx?.store.put({
    532                                 windowPos: newRoom.windowPos,
    533                                 roomID: newRoom.roomID,
    534                                 name: newRoom.getName(),
    535                                 notification_count: newRoom.getNotificationCount(),
    536                                 highlight_count: newRoom.getNotificationHighlightCount(),
    537                                 joined_count: newRoom.getJoinedCount(),
    538                                 invited_count: newRoom.getInvitedCount(),
    539                                 avatarUrl: newRoom.getAvatarURL(),
    540                                 isSpace: newRoom.isSpace(),
    541                                 isDM: newRoom.isDM(),
    542                                 stateEvents: newRoom.getStateEvents(),
    543                                 events: newRoom.getEvents(),
    544                             });
    545                         }
    546                         await tx?.done;
    547                     } else if (isInsertOp(op)) {
    548                         console.log("Got INSERT OP", op);
    549                         const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === op.index);
    550                         if (roomObj) {
    551                             if (gapIndex < 0) {
    552                                 // we haven't been told where to shift from, so make way for a new room entry.
    553                                 this.addEntry(listKey, this.lastRanges[listKey], op.index);
    554                             } else if (gapIndex > op.index) {
    555                                 // the gap is further down the list, shift every element to the right
    556                                 // starting at the gap so we can just shift each element in turn:
    557                                 // [A,B,C,_] gapIndex=3, op.index=0
    558                                 // [A,B,C,C] i=3
    559                                 // [A,B,B,C] i=2
    560                                 // [A,A,B,C] i=1
    561                                 // Terminate. We'll assign into op.index next.
    562                                 this.shiftRight(listKey, this.lastRanges[listKey], gapIndex, op.index);
    563                             } else if (gapIndex < op.index) {
    564                                 // the gap is further up the list, shift every element to the left
    565                                 // starting at the gap so we can just shift each element in turn
    566                                 this.shiftLeft(listKey, this.lastRanges[listKey], op.index, gapIndex);
    567                             }
    568                         }
    569                         gapIndex = -1;
    570                         const tx = this.client.database?.transaction('rooms', 'readwrite');
    571                         // We start to remember the Room now.
    572                         const foundRoom = [...this.rooms].find(room => room.roomID === op.room_id);
    573                         if (foundRoom) {
    574                             foundRoom.windowPos[listKey] = op.index;
    575                             await tx?.store.put({
    576                                 windowPos: foundRoom.windowPos,
    577                                 roomID: foundRoom.roomID,
    578                                 name: foundRoom.getName(),
    579                                 notification_count: foundRoom.getNotificationCount(),
    580                                 highlight_count: foundRoom.getNotificationHighlightCount(),
    581                                 joined_count: foundRoom.getJoinedCount(),
    582                                 invited_count: foundRoom.getInvitedCount(),
    583                                 avatarUrl: foundRoom.getAvatarURL(),
    584                                 isSpace: foundRoom.isSpace(),
    585                                 isDM: foundRoom.isDM(),
    586                                 stateEvents: foundRoom.getStateEvents(),
    587                                 events: foundRoom.getEvents(),
    588                             });
    589                         } else {
    590                             const roomFromDB = await tx?.store.get(op.room_id);
    591                             let newRoom = new Room(op.room_id, this.user.hostname!, this.client, this.user.e2ee);
    592                             newRoom.setName(op.room_id);
    593                             newRoom.windowPos[listKey] = op.index;
    594                             if (roomFromDB) {
    595                                 console.warn("Room in db but not in obj list.", op.room_id, "Updating obj list.");
    596                                 newRoom = new Room(op.room_id, this.user.hostname!, this.client, this.user.e2ee)
    597                                 newRoom.setName(roomFromDB.name);
    598                                 newRoom.setNotificationCount(roomFromDB.notification_count);
    599                                 newRoom.setNotificationHighlightCount(roomFromDB.highlight_count);
    600                                 newRoom.setJoinedCount(roomFromDB.joined_count);
    601                                 newRoom.setInvitedCount(roomFromDB.invited_count);
    602                                 newRoom.setDM(roomFromDB.isDM || false);
    603                             }
    604                             this.rooms.add(newRoom);
    605                             await tx?.store.put({
    606                                 windowPos: newRoom.windowPos,
    607                                 roomID: newRoom.roomID,
    608                                 name: newRoom.getName(),
    609                                 notification_count: newRoom.getNotificationCount(),
    610                                 highlight_count: newRoom.getNotificationHighlightCount(),
    611                                 joined_count: newRoom.getJoinedCount(),
    612                                 invited_count: newRoom.getInvitedCount(),
    613                                 avatarUrl: newRoom.getAvatarURL(),
    614                                 isSpace: newRoom.isSpace(),
    615                                 isDM: newRoom.isDM(),
    616                                 stateEvents: newRoom.getStateEvents(),
    617                                 events: newRoom.getEvents(),
    618                             });
    619                         }
    620 
    621                         const roomIDs2 = [...this.rooms].map(room => room.roomID);
    622                         // Check if we generated any duplicates and log them.
    623                         const duplicates = roomIDs2.filter((item, index) => roomIDs2.indexOf(item) != index);
    624                         if (duplicates.length > 0) {
    625                             console.error("Duplicates found", duplicates);
    626                         }
    627                         await tx?.done;
    628                     } else if (isDeleteOp(op)) {
    629                         console.log("Got DELETE OP", op);
    630 
    631                         if (gapIndex !== -1) {
    632                             // we already have a DELETE operation to process, so process it.
    633                             await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex);
    634                         }
    635                         gapIndex = op.index;
    636                     } else if (isInvalidateOp(op)) {
    637                         // TODO: Figure out if this is needed in reality
    638                         // const tx = this.database?.transaction('rooms', 'readwrite');
    639                         // for (let i = op.range[0]; i <= op.range[1]; i++) {
    640                         //     // We shall first forget about these and "startover"
    641                         //     const roomObj = [...this.rooms].find(room => room.windowPos[listKey] === i);
    642                         //     if (roomObj) {
    643                         //         await tx?.store.delete(roomObj.roomID);
    644                         //         this.rooms.delete(roomObj)
    645                         //     }
    646                         // }
    647                         // await tx?.done;
    648                     }
    649                 }
    650                 if (gapIndex !== -1) {
    651                     // we already have a DELETE operation to process, so process it
    652                     // Everything higher than the gap needs to be shifted left.
    653                     await this.removeEntry(listKey, this.lastRanges[listKey], gapIndex);
    654                 }
    655             }
    656         }
    657         for (const roomID in json.rooms) {
    658             const room = json.rooms[roomID];
    659             const name = room.name;
    660             const notification_count = room.notification_count;
    661             const notification_highlight_count = room.highlight_count;
    662             const joined_count = room.joined_count;
    663             const invited_count = room.invited_count;
    664             const events = room.timeline;
    665             const state_events = events?.filter(event => isRoomStateEvent(event)).map(event => event as IRoomStateEvent);
    666             const normal_events = events?.filter(event => !isRoomStateEvent(event)).map(event => event as IRoomEvent);
    667             const required_state = room.required_state;
    668             const is_dm = room.is_dm;
    669 
    670             let roomObj = [...this.rooms].find(room => room.roomID === roomID);
    671             if (!roomObj) {
    672                 // Warn, check in the db and if that fails, create a new one.
    673                 console.warn("Could not find roomObj for roomID:", roomID);
    674 
    675                 const tx = this.client.database?.transaction('rooms', 'readwrite');
    676                 const roomFromDB = await tx?.store.get(roomID);
    677                 await tx?.done;
    678 
    679                 if (roomFromDB) {
    680                     console.warn("Room in db but not in obj list.", roomID, "Updating obj list.");
    681 
    682                     roomObj = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee);
    683                     roomObj.setName(roomFromDB.name);
    684                     roomObj.setNotificationCount(roomFromDB.notification_count);
    685                     roomObj.setNotificationHighlightCount(roomFromDB.highlight_count);
    686                     roomObj.setJoinedCount(roomFromDB.joined_count);
    687                     roomObj.setInvitedCount(roomFromDB.invited_count);
    688                     roomObj.setDM(roomFromDB.isDM || false);
    689                     if (roomFromDB.events) {
    690                         roomObj.addEvents(roomFromDB.events);
    691                     }
    692                     if (roomFromDB.stateEvents) {
    693                         roomObj.addStateEvents(roomFromDB.stateEvents);
    694                     }
    695                     roomObj.windowPos = roomFromDB.windowPos;
    696                 } else {
    697                     console.warn("Could not find room in db. Creating new one.");
    698                     roomObj = new Room(roomID, this.user.hostname!, this.client, this.user.e2ee);
    699                     this.rooms.add(roomObj);
    700                 }
    701             }
    702 
    703             if (name) {
    704                 roomObj.setName(name);
    705             }
    706             roomObj.setNotificationCount(notification_count);
    707             roomObj.setNotificationHighlightCount(notification_highlight_count);
    708             roomObj.setJoinedCount(joined_count);
    709             roomObj.setInvitedCount(invited_count);
    710             if (normal_events) {
    711                 roomObj.addEvents(normal_events);
    712             }
    713             if (required_state) {
    714                 roomObj.addStateEvents(required_state);
    715             }
    716             if (state_events) {
    717                 roomObj.addStateEvents(state_events);
    718             }
    719             if (required_state || state_events) {
    720                 if (roomObj.isEncrypted() && roomObj.isJoined()) {
    721                     const joinEvents = [...(required_state || []), ...(state_events || [])]
    722                         .filter(event => event.type === "m.room.member" && event.content.membership === "join");
    723                     const memberIds = joinEvents.map(event => new UserId(event.state_key));
    724                     await this.user.e2ee.updateTrackedUsers(memberIds);
    725                 }
    726             }
    727             if (is_dm) {
    728                 roomObj.setDM(is_dm);
    729             }
    730 
    731 
    732             const tx = this.client.database?.transaction('rooms', 'readwrite');
    733             // Write to database
    734             await tx?.store.put({
    735                 windowPos: roomObj.windowPos,
    736                 roomID: roomObj.roomID,
    737                 name: roomObj.getName(),
    738                 notification_count: roomObj.getNotificationCount(),
    739                 highlight_count: roomObj.getNotificationHighlightCount(),
    740                 joined_count: roomObj.getJoinedCount(),
    741                 invited_count: roomObj.getInvitedCount(),
    742                 events: roomObj.getPureEvents(),
    743                 stateEvents: roomObj.getStateEvents(),
    744                 avatarUrl: roomObj.getAvatarURL(),
    745                 isSpace: roomObj.isSpace(),
    746                 isDM: roomObj.isDM(),
    747             });
    748             await tx?.done
    749         }
    750 
    751         if (this.initialSync) {
    752             this.initialSync = false;
    753             console.log("initialSyncComplete");
    754         }
    755         if (json.rooms && Object.keys(json.rooms).length > 0) {
    756             this.emit("rooms", this.rooms);
    757         }
    758     }
    759 }