2022-02-24 14:14:59 +00:00
|
|
|
/*
|
|
|
|
* This file contains the main sliding sync code.
|
|
|
|
* It defines the core sliding sync types and contains code for handling list deltas.
|
|
|
|
* It doesn't concern itself with storing room data or updating the UI in any way, deferring
|
|
|
|
* to room/lifecycle callbacks for that.
|
|
|
|
*/
|
2022-02-23 17:39:31 +00:00
|
|
|
|
2022-02-23 19:26:38 +00:00
|
|
|
import * as devtools from "./devtools.js";
|
2022-02-23 18:05:09 +00:00
|
|
|
|
2022-02-23 17:39:31 +00:00
|
|
|
// The default range to /always/ track on a list.
|
|
|
|
// When you scroll the list, new windows are added to the first element. E.g [[0,20], [37,45]]
|
|
|
|
// TODO: explain why
|
|
|
|
const DEFAULT_RANGES = [[0, 20]];
|
|
|
|
|
2022-02-24 15:03:10 +00:00
|
|
|
// We'll load these events for every room cell in each list.
|
|
|
|
// Note we do not need any room name information as the server calculates all this for us.
|
2022-02-24 11:10:12 +00:00
|
|
|
const REQUIRED_STATE_EVENTS_IN_LIST = [
|
2022-02-24 15:03:10 +00:00
|
|
|
["m.room.avatar", ""], // need the avatar
|
|
|
|
["m.room.tombstone", ""], // need to know if this room has been replaced
|
2022-02-24 11:10:12 +00:00
|
|
|
];
|
|
|
|
const REQUIRED_STATE_EVENTS_IN_ROOM = [
|
2022-02-24 15:03:10 +00:00
|
|
|
["m.room.avatar", ""], // need the avatar to display next to the room name
|
2022-02-24 16:48:39 +00:00
|
|
|
["m.room.tombstone", ""], // need to know if this room has been replaced
|
2022-02-24 15:03:10 +00:00
|
|
|
["m.room.topic", ""], // need the topic to display on the right-hand-side
|
2022-02-24 11:10:12 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
// Lifecycle state when the /sync response has been fully processed and all room data callbacks
|
|
|
|
// have been invoked. Never contains an error.
|
|
|
|
export const LifecycleSyncComplete = 1;
|
|
|
|
// Lifecycle state when the /sync request has returned. May include an error if there was a problem
|
|
|
|
// talking to the server.
|
|
|
|
export const LifecycleSyncRequestFinished = 2;
|
|
|
|
|
2022-02-23 18:05:09 +00:00
|
|
|
/**
|
|
|
|
* SlidingSyncConnection is a thin wrapper around fetch() which performs a sliding sync request.
|
|
|
|
* The wrapper persists a small amount of extra data including the total number of tx/rx bytes,
|
|
|
|
* as well as the abort controller for interrupting requests. One SlidingSyncConnection should be
|
|
|
|
* made for the lifetime of the connection to ensure tx/rx bytes tally up across multiple requests.
|
|
|
|
*/
|
|
|
|
export class SlidingSyncConnection {
|
|
|
|
constructor() {
|
|
|
|
this.txBytes = 0;
|
|
|
|
this.rxBytes = 0;
|
|
|
|
this.abortController = new AbortController();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Abort an active in-flight sync request if there is one currently. Else does nothing.
|
|
|
|
*/
|
|
|
|
abort() {
|
|
|
|
if (this.abortController) {
|
|
|
|
this.abortController.abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Do a sliding sync request.
|
|
|
|
* @param {string} accessToken The access token for the user.
|
|
|
|
* @param {string} pos The ?pos= value
|
|
|
|
* @param {object} reqBody The request JSON body to send.
|
|
|
|
* @returns {object} The response JSON body
|
|
|
|
*/
|
|
|
|
async doSyncRequest(accessToken, pos, reqBody) {
|
|
|
|
this.abortController = new AbortController();
|
|
|
|
const jsonBody = JSON.stringify(reqBody);
|
2022-02-23 19:26:38 +00:00
|
|
|
let resp = await fetch(
|
2022-03-23 12:24:48 +00:00
|
|
|
"/_matrix/client/v3/sync" + (pos ? "?pos=" + pos : ""),
|
2022-02-23 19:26:38 +00:00
|
|
|
{
|
|
|
|
signal: this.abortController.signal,
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
Authorization: "Bearer " + accessToken,
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
body: jsonBody,
|
|
|
|
}
|
|
|
|
);
|
2022-02-23 18:05:09 +00:00
|
|
|
|
|
|
|
let respBody = await resp.json();
|
|
|
|
if (respBody.ops) {
|
|
|
|
console.log(respBody);
|
|
|
|
}
|
|
|
|
// 0 TX / 0 RX (KB)
|
|
|
|
this.txBytes += jsonBody.length;
|
|
|
|
this.rxBytes += JSON.stringify(respBody).length;
|
|
|
|
|
|
|
|
// TODO: Move?
|
|
|
|
devtools.bandwidth(document.getElementById("txrx"), this);
|
|
|
|
|
|
|
|
if (resp.status != 200) {
|
2022-02-23 19:26:38 +00:00
|
|
|
throw new Error(
|
|
|
|
"/sync returned HTTP " + resp.status + " " + respBody.error
|
|
|
|
);
|
2022-02-23 18:05:09 +00:00
|
|
|
}
|
|
|
|
return respBody;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-23 17:39:31 +00:00
|
|
|
/**
|
|
|
|
* SlidingList represents a single list in sliding sync. The list can have filters, multiple sliding
|
|
|
|
* windows, and maintains the index->room_id mapping.
|
|
|
|
*/
|
|
|
|
export class SlidingList {
|
|
|
|
/**
|
|
|
|
* Construct a new sliding list.
|
2022-02-23 19:26:38 +00:00
|
|
|
* @param {string} name Human-readable name to display on the UI for this list.
|
2022-02-23 17:39:31 +00:00
|
|
|
* @param {object} filters Optional. The sliding sync filters to apply e.g { is_dm: true }.
|
|
|
|
*/
|
|
|
|
constructor(name, filters) {
|
|
|
|
this.name = name;
|
|
|
|
this.filters = filters || {};
|
|
|
|
this.activeRanges = JSON.parse(JSON.stringify(DEFAULT_RANGES));
|
|
|
|
// the constantly changing sliding window ranges. Not an array for performance reasons
|
|
|
|
// E.g tracking ranges 0-99, 500-599, we don't want to have a 600 element array
|
|
|
|
this.roomIndexToRoomId = {};
|
|
|
|
// the total number of joined rooms according to the server, always >= len(roomIndexToRoomId)
|
|
|
|
this.joinedCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the filters for this list.
|
|
|
|
* @returns {object} A copy of the filters
|
|
|
|
*/
|
|
|
|
getFilters() {
|
|
|
|
// return a copy of the filters to ensure the caller cannot modify it without calling setFilters
|
|
|
|
return Object.assign({}, this.filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modify the filters on this list. The filters provided are copied.
|
|
|
|
* @param {object} filters The sliding sync filters to apply e.g { is_dm: true }.
|
|
|
|
*/
|
|
|
|
setFilters(filters) {
|
|
|
|
this.filters = Object.assign({}, filters);
|
|
|
|
// if we are viewing a window at 100-120 and then we filter down to 5 total rooms,
|
|
|
|
// we'll end up showing nothing. Therefore, if the filters change (e.g room name filter)
|
|
|
|
// reset the range back to 0-20.
|
|
|
|
this.activeRanges = JSON.parse(JSON.stringify(DEFAULT_RANGES));
|
|
|
|
// Wipe the index to room ID map as the filters have changed which will invalidate these.
|
|
|
|
this.roomIndexToRoomId = {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 11:10:12 +00:00
|
|
|
/**
|
|
|
|
* SlidingSync is a high-level data structure which controls the majority of sliding sync.
|
|
|
|
*/
|
|
|
|
export class SlidingSync {
|
|
|
|
/**
|
|
|
|
* Create a new sliding sync instance
|
2023-02-13 17:13:42 +00:00
|
|
|
* @param {Record<string, SlidingList>} lists The lists to use for sliding sync.
|
2022-02-24 11:10:12 +00:00
|
|
|
* @param {SlidingSyncConnection} conn The connection to use for /sync calls.
|
|
|
|
*/
|
|
|
|
constructor(lists, conn) {
|
2023-02-13 17:13:42 +00:00
|
|
|
this.lists = lists || {};
|
2022-02-24 11:10:12 +00:00
|
|
|
this.conn = conn;
|
|
|
|
this.terminated = false;
|
|
|
|
this.roomSubscription = "";
|
|
|
|
this.roomDataCallbacks = [];
|
|
|
|
this.lifecycleCallbacks = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Listen for high-level room events on the sync connection
|
|
|
|
* @param {function} callback The callback to invoke.
|
|
|
|
*/
|
|
|
|
addRoomDataListener(callback) {
|
|
|
|
this.roomDataCallbacks.push(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Listen for high-level lifecycle events on the sync connection
|
|
|
|
* @param {function} callback The callback to invoke.
|
|
|
|
*/
|
|
|
|
addLifecycleListener(callback) {
|
|
|
|
this.lifecycleCallbacks.push(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoke all attached room data listeners.
|
|
|
|
* @param {string} roomId The room which received some data.
|
|
|
|
* @param {object} roomData The raw sliding sync response JSON.
|
|
|
|
*/
|
2022-02-24 16:48:39 +00:00
|
|
|
_invokeRoomDataListeners(roomId, roomData) {
|
2022-02-24 11:10:12 +00:00
|
|
|
this.roomDataCallbacks.forEach((callback) => {
|
2022-02-24 16:48:39 +00:00
|
|
|
callback(roomId, roomData);
|
2022-02-24 11:10:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-24 14:28:05 +00:00
|
|
|
/**
|
|
|
|
* Invoke all attached lifecycle listeners.
|
|
|
|
* @param {Number} state The Lifecycle state
|
|
|
|
* @param {object} resp The raw sync response JSON
|
|
|
|
* @param {Error?} err Any error that occurred when making the request e.g network errors.
|
|
|
|
*/
|
2022-02-24 11:10:12 +00:00
|
|
|
_invokeLifecycleListeners(state, resp, err) {
|
|
|
|
this.lifecycleCallbacks.forEach((callback) => {
|
|
|
|
callback(state, resp, err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-23 17:39:31 +00:00
|
|
|
/**
|
2022-02-24 11:10:12 +00:00
|
|
|
* Stop syncing with the server.
|
2022-02-23 17:39:31 +00:00
|
|
|
*/
|
2022-02-24 11:10:12 +00:00
|
|
|
stop() {
|
|
|
|
this.terminated = true;
|
|
|
|
this.conn.abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start syncing with the server. Blocks until stopped.
|
|
|
|
* @param {string} accessToken The access token to sync with.
|
|
|
|
*/
|
|
|
|
async start(accessToken) {
|
|
|
|
let currentPos;
|
|
|
|
let currentSub = "";
|
|
|
|
while (!this.terminated) {
|
|
|
|
let resp;
|
|
|
|
try {
|
|
|
|
// these fields are always required
|
2023-02-13 17:13:42 +00:00
|
|
|
const reqLists = {};
|
|
|
|
Object.keys(this.lists).forEach((listKey) => {
|
|
|
|
const al = this.lists[listKey];
|
|
|
|
let l = {
|
|
|
|
ranges: al.activeRanges,
|
|
|
|
filters: al.getFilters(),
|
|
|
|
};
|
|
|
|
// if this is the first request on this session, send sticky request data which never changes
|
|
|
|
if (!currentPos) {
|
|
|
|
l.required_state = REQUIRED_STATE_EVENTS_IN_LIST;
|
|
|
|
l.timeline_limit = 1;
|
|
|
|
l.sort = [
|
|
|
|
"by_highlight_count",
|
|
|
|
"by_notification_count",
|
|
|
|
"by_recency",
|
|
|
|
];
|
|
|
|
}
|
|
|
|
reqLists[listKey] = l;
|
|
|
|
});
|
2022-02-24 11:10:12 +00:00
|
|
|
let reqBody = {
|
2023-02-13 17:13:42 +00:00
|
|
|
lists: reqLists,
|
2022-02-24 11:10:12 +00:00
|
|
|
};
|
|
|
|
// check if we are (un)subscribing to a room and modify request this one time for it
|
|
|
|
let subscribingToRoom;
|
|
|
|
if (
|
|
|
|
this.roomSubscription &&
|
|
|
|
currentSub !== this.roomSubscription
|
|
|
|
) {
|
|
|
|
if (currentSub) {
|
|
|
|
reqBody.unsubscribe_rooms = [currentSub];
|
|
|
|
}
|
|
|
|
reqBody.room_subscriptions = {
|
|
|
|
[this.roomSubscription]: {
|
|
|
|
required_state: REQUIRED_STATE_EVENTS_IN_ROOM,
|
|
|
|
timeline_limit: 30,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
// hold a ref to the active room ID as it may change by the time we return from doSyncRequest
|
|
|
|
subscribingToRoom = this.roomSubscription;
|
|
|
|
}
|
|
|
|
resp = await this.conn.doSyncRequest(
|
|
|
|
accessToken,
|
|
|
|
currentPos,
|
|
|
|
reqBody
|
|
|
|
);
|
|
|
|
currentPos = resp.pos;
|
|
|
|
// update what we think we're subscribed to.
|
|
|
|
if (subscribingToRoom) {
|
|
|
|
currentSub = subscribingToRoom;
|
|
|
|
}
|
2022-05-27 13:21:03 +01:00
|
|
|
if (!resp.lists) {
|
2023-02-13 17:13:42 +00:00
|
|
|
resp.lists = {};
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
2023-02-13 17:13:42 +00:00
|
|
|
Object.keys(resp.lists).forEach((key) => {
|
|
|
|
const list = this.lists[key];
|
|
|
|
if (!list) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
list.joinedCount = resp.lists[key].count;
|
2022-05-27 13:21:03 +01:00
|
|
|
});
|
2022-02-24 11:10:12 +00:00
|
|
|
this._invokeLifecycleListeners(
|
|
|
|
LifecycleSyncRequestFinished,
|
|
|
|
resp
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
if (err.name !== "AbortError") {
|
|
|
|
this._invokeLifecycleListeners(
|
|
|
|
LifecycleSyncRequestFinished,
|
|
|
|
null,
|
|
|
|
err
|
|
|
|
);
|
|
|
|
await sleep(3000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!resp) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-05-27 13:21:03 +01:00
|
|
|
Object.keys(resp.rooms).forEach((roomId) => {
|
|
|
|
this._invokeRoomDataListeners(roomId, resp.rooms[roomId]);
|
2022-02-24 11:10:12 +00:00
|
|
|
});
|
|
|
|
|
2023-02-13 17:13:42 +00:00
|
|
|
Object.keys(resp.lists).forEach((listKey) => {
|
|
|
|
const list = resp.lists[listKey];
|
2022-05-27 13:21:03 +01:00
|
|
|
let gapIndex = -1;
|
|
|
|
list.ops = list.ops || [];
|
|
|
|
list.ops.forEach((op) => {
|
2022-12-15 10:51:29 +00:00
|
|
|
switch (op.op) {
|
|
|
|
case "DELETE": {
|
2023-02-13 17:13:42 +00:00
|
|
|
console.log("DELETE", listKey, op.index, ";");
|
|
|
|
delete this.lists[listKey].roomIndexToRoomId[op.index];
|
2022-12-15 10:51:29 +00:00
|
|
|
if (gapIndex !== -1) {
|
|
|
|
// we already have a DELETE operation to process, so process it.
|
2023-02-13 17:13:42 +00:00
|
|
|
this.removeEntry(listKey, gapIndex);
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
gapIndex = op.index;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "INSERT": {
|
|
|
|
console.log(
|
|
|
|
"INSERT",
|
2023-02-13 17:13:42 +00:00
|
|
|
listKey,
|
2022-12-15 10:51:29 +00:00
|
|
|
op.index,
|
|
|
|
op.room_id,
|
|
|
|
";",
|
|
|
|
);
|
2023-02-13 17:13:42 +00:00
|
|
|
if (this.lists[listKey].roomIndexToRoomId[op.index]) {
|
2022-12-15 10:51:29 +00:00
|
|
|
// something is in this space, shift items out of the way
|
|
|
|
if (gapIndex < 0) {
|
|
|
|
// we haven't been told where to shift from, so make way for a new room entry.
|
2023-02-13 17:13:42 +00:00
|
|
|
this.addEntry(listKey, op.index);
|
2022-12-15 10:51:29 +00:00
|
|
|
} else if (gapIndex > op.index) {
|
|
|
|
// the gap is further down the list, shift every element to the right
|
|
|
|
// starting at the gap so we can just shift each element in turn:
|
|
|
|
// [A,B,C,_] gapIndex=3, op.index=0
|
|
|
|
// [A,B,C,C] i=3
|
|
|
|
// [A,B,B,C] i=2
|
|
|
|
// [A,A,B,C] i=1
|
|
|
|
// Terminate. We'll assign into op.index next.
|
2023-02-13 17:13:42 +00:00
|
|
|
this.shiftRight(listKey, gapIndex, op.index);
|
2022-12-15 10:51:29 +00:00
|
|
|
} else if (gapIndex < op.index) {
|
|
|
|
// the gap is further up the list, shift every element to the left
|
|
|
|
// starting at the gap so we can just shift each element in turn
|
2023-02-13 17:13:42 +00:00
|
|
|
this.shiftLeft(listKey, op.index, gapIndex);
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
// forget the gap, we don't need it anymore. This is outside the check for
|
|
|
|
// a room being present in this index position because INSERTs always universally
|
|
|
|
// forget the gap, not conditionally based on the presence of a room in the INSERT
|
|
|
|
// position. Without this, DELETE 0; INSERT 0; would do the wrong thing.
|
|
|
|
gapIndex = -1;
|
2023-02-13 17:13:42 +00:00
|
|
|
this.lists[listKey].roomIndexToRoomId[op.index] = op.room_id;
|
2022-12-15 10:51:29 +00:00
|
|
|
break;
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
case "INVALIDATE": {
|
|
|
|
const startIndex = op.range[0];
|
|
|
|
for (let i = startIndex; i <= op.range[1]; i++) {
|
2023-02-13 17:13:42 +00:00
|
|
|
delete this.lists[listKey].roomIndexToRoomId[i];
|
2022-05-27 13:21:03 +01:00
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
console.log(
|
|
|
|
"INVALIDATE",
|
2023-02-13 17:13:42 +00:00
|
|
|
listKey,
|
2022-12-15 10:51:29 +00:00
|
|
|
op.range[0],
|
|
|
|
op.range[1],
|
|
|
|
";",
|
|
|
|
);
|
|
|
|
break;
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
case "SYNC": {
|
|
|
|
const startIndex = op.range[0];
|
|
|
|
for (let i = startIndex; i <= op.range[1]; i++) {
|
|
|
|
const roomId = op.room_ids[i - startIndex];
|
|
|
|
if (!roomId) {
|
|
|
|
break; // we are at the end of list
|
|
|
|
}
|
2023-02-13 17:13:42 +00:00
|
|
|
this.lists[listKey].roomIndexToRoomId[i] = roomId;
|
2022-12-15 10:51:29 +00:00
|
|
|
}
|
|
|
|
console.log(
|
|
|
|
"SYNC",
|
2023-02-13 17:13:42 +00:00
|
|
|
listKey,
|
2022-12-15 10:51:29 +00:00
|
|
|
op.range[0],
|
|
|
|
op.range[1],
|
|
|
|
(op.room_ids || []).join(" "),
|
|
|
|
";",
|
2022-05-27 13:21:03 +01:00
|
|
|
);
|
2022-12-15 10:51:29 +00:00
|
|
|
break;
|
2022-05-27 13:21:03 +01:00
|
|
|
}
|
2022-02-24 11:10:12 +00:00
|
|
|
}
|
2022-05-27 13:21:03 +01:00
|
|
|
});
|
2022-12-15 10:51:29 +00:00
|
|
|
if (gapIndex !== -1) {
|
|
|
|
// we already have a DELETE operation to process, so process it
|
|
|
|
// Everything higher than the gap needs to be shifted left.
|
2023-02-13 17:13:42 +00:00
|
|
|
this.removeEntry(listKey, gapIndex);
|
2022-12-15 10:51:29 +00:00
|
|
|
}
|
2022-02-24 11:10:12 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this._invokeLifecycleListeners(LifecycleSyncComplete, resp);
|
|
|
|
}
|
|
|
|
}
|
2022-12-15 10:51:29 +00:00
|
|
|
|
2023-02-13 17:13:42 +00:00
|
|
|
shiftRight(listKey, hi, low) {
|
2022-12-15 10:51:29 +00:00
|
|
|
// l h
|
|
|
|
// 0,1,2,3,4 <- before
|
|
|
|
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated
|
|
|
|
for (let i = hi; i > low; i--) {
|
2023-02-13 17:13:42 +00:00
|
|
|
if (this.isIndexInRange(this.lists[listKey], i)) {
|
|
|
|
this.lists[listKey].roomIndexToRoomId[i] =
|
|
|
|
this.lists[listKey].roomIndexToRoomId[
|
2022-12-15 10:51:29 +00:00
|
|
|
i - 1
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-13 17:13:42 +00:00
|
|
|
shiftLeft(listKey, hi, low) {
|
2022-12-15 10:51:29 +00:00
|
|
|
// l h
|
|
|
|
// 0,1,2,3,4 <- before
|
|
|
|
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated
|
|
|
|
for (let i = low; i < hi; i++) {
|
2023-02-13 17:13:42 +00:00
|
|
|
if (this.isIndexInRange(this.lists[listKey], i)) {
|
|
|
|
this.lists[listKey].roomIndexToRoomId[i] =
|
|
|
|
this.lists[listKey].roomIndexToRoomId[
|
2022-12-15 10:51:29 +00:00
|
|
|
i + 1
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-13 17:13:42 +00:00
|
|
|
removeEntry(listKey, index) {
|
2022-12-15 10:51:29 +00:00
|
|
|
// work out the max index
|
|
|
|
let max = -1;
|
2023-02-13 17:13:42 +00:00
|
|
|
for (const n in this.lists[listKey].roomIndexToRoomId) {
|
2022-12-15 10:51:29 +00:00
|
|
|
if (Number(n) > max) {
|
|
|
|
max = Number(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (max < 0 || index > max) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Everything higher than the gap needs to be shifted left.
|
2023-02-13 17:13:42 +00:00
|
|
|
this.shiftLeft(listKey, max, index);
|
|
|
|
delete this.lists[listKey].roomIndexToRoomId[max];
|
2022-12-15 10:51:29 +00:00
|
|
|
}
|
|
|
|
|
2023-02-13 17:13:42 +00:00
|
|
|
addEntry(listKey, index) {
|
2022-12-15 10:51:29 +00:00
|
|
|
// work out the max index
|
|
|
|
let max = -1;
|
2023-02-13 17:13:42 +00:00
|
|
|
for (const n in this.lists[listKey].roomIndexToRoomId) {
|
2022-12-15 10:51:29 +00:00
|
|
|
if (Number(n) > max) {
|
|
|
|
max = Number(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (max < 0 || index > max) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
|
2023-02-13 17:13:42 +00:00
|
|
|
this.shiftRight(listKey, max+1, index);
|
2022-12-15 10:51:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
isIndexInRange(list, i) {
|
|
|
|
for (const r of list.activeRanges) {
|
|
|
|
if (r[0] <= i && i <= r[1]) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2022-02-23 19:26:38 +00:00
|
|
|
}
|
2022-02-24 11:10:12 +00:00
|
|
|
|
|
|
|
const sleep = (ms) => {
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
2023-02-13 17:13:42 +00:00
|
|
|
};
|