matrix-react-sdk
Version:
SDK for matrix.org using React
700 lines (649 loc) • 105 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.LIST_UPDATED_EVENT = exports.Algorithm = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _matrix = require("matrix-js-sdk/src/matrix");
var _types = require("matrix-js-sdk/src/types");
var _utils = require("matrix-js-sdk/src/utils");
var _events = require("events");
var _logger = require("matrix-js-sdk/src/logger");
var _DMRoomMap = _interopRequireDefault(require("../../../utils/DMRoomMap"));
var _arrays = require("../../../utils/arrays");
var _models = require("../models");
var _membership = require("../../../utils/membership");
var _listOrdering = require("./list-ordering");
var _VisibilityProvider = require("../filters/VisibilityProvider");
var _CallStore = require("../../CallStore");
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
/**
* Fired when the Algorithm has determined a list has been updated.
*/
const LIST_UPDATED_EVENT = exports.LIST_UPDATED_EVENT = "list_updated_event";
// These are the causes which require a room to be known in order for us to handle them. If
// a cause in this list is raised and we don't know about the room, we don't handle the update.
//
// Note: these typically happen when a new room is coming in, such as the user creating or
// joining the room. For these cases, we need to know about the room prior to handling it otherwise
// we'll make bad assumptions.
const CAUSES_REQUIRING_ROOM = [_models.RoomUpdateCause.Timeline, _models.RoomUpdateCause.ReadReceipt];
/**
* Represents a list ordering algorithm. This class will take care of tag
* management (which rooms go in which tags) and ask the implementation to
* deal with ordering mechanics.
*/
class Algorithm extends _events.EventEmitter {
constructor(...args) {
super(...args);
(0, _defineProperty2.default)(this, "_cachedRooms", {});
(0, _defineProperty2.default)(this, "_cachedStickyRooms", {});
// a clone of the _cachedRooms, with the sticky room
(0, _defineProperty2.default)(this, "_stickyRoom", null);
(0, _defineProperty2.default)(this, "_lastStickyRoom", null);
// only not-null when changing the sticky room
(0, _defineProperty2.default)(this, "sortAlgorithms", null);
(0, _defineProperty2.default)(this, "listAlgorithms", null);
(0, _defineProperty2.default)(this, "algorithms", null);
(0, _defineProperty2.default)(this, "rooms", []);
(0, _defineProperty2.default)(this, "roomIdsToTags", {});
/**
* Set to true to suspend emissions of algorithm updates.
*/
(0, _defineProperty2.default)(this, "updatesInhibited", false);
(0, _defineProperty2.default)(this, "onConnectedCalls", () => {
// In case we're unsticking a room, sort it back into natural order
this.recalculateStickyRoom();
// Update the stickiness of rooms with calls
this.recalculateActiveCallRooms();
if (this.updatesInhibited) return;
// This isn't in response to any particular RoomListStore update,
// so notify the store that it needs to force-update
this.emit(LIST_UPDATED_EVENT, true);
});
}
start() {
_CallStore.CallStore.instance.on(_CallStore.CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
}
stop() {
_CallStore.CallStore.instance.off(_CallStore.CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
}
get stickyRoom() {
return this._stickyRoom ? this._stickyRoom.room : null;
}
get hasTagSortingMap() {
return !!this.sortAlgorithms;
}
set cachedRooms(val) {
this._cachedRooms = val;
this.recalculateStickyRoom();
this.recalculateActiveCallRooms();
}
get cachedRooms() {
// 🐉 Here be dragons.
// Note: this is used by the underlying algorithm classes, so don't make it return
// the sticky room cache. If it ends up returning the sticky room cache, we end up
// corrupting our caches and confusing them.
return this._cachedRooms;
}
/**
* Awaitable version of the sticky room setter.
* @param val The new room to sticky.
*/
setStickyRoom(val) {
try {
this.updateStickyRoom(val);
} catch (e) {
_logger.logger.warn("Failed to update sticky room", e);
}
}
getTagSorting(tagId) {
if (!this.sortAlgorithms) return null;
return this.sortAlgorithms[tagId];
}
setTagSorting(tagId, sort) {
if (!tagId) throw new Error("Tag ID must be defined");
if (!sort) throw new Error("Algorithm must be defined");
if (!this.sortAlgorithms) throw new Error("this.sortAlgorithms must be defined before calling setTagSorting");
if (!this.algorithms) throw new Error("this.algorithms must be defined before calling setTagSorting");
this.sortAlgorithms[tagId] = sort;
const algorithm = this.algorithms[tagId];
algorithm.setSortAlgorithm(sort);
this._cachedRooms[tagId] = algorithm.orderedRooms;
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
this.recalculateActiveCallRooms(tagId);
}
getListOrdering(tagId) {
if (!this.listAlgorithms) return null;
return this.listAlgorithms[tagId];
}
setListOrdering(tagId, order) {
if (!tagId) throw new Error("Tag ID must be defined");
if (!order) throw new Error("Algorithm must be defined");
if (!this.sortAlgorithms) throw new Error("this.sortAlgorithms must be defined before calling setListOrdering");
if (!this.listAlgorithms) throw new Error("this.listAlgorithms must be defined before calling setListOrdering");
if (!this.algorithms) throw new Error("this.algorithms must be defined before calling setListOrdering");
this.listAlgorithms[tagId] = order;
const algorithm = (0, _listOrdering.getListAlgorithmInstance)(order, tagId, this.sortAlgorithms[tagId]);
this.algorithms[tagId] = algorithm;
algorithm.setRooms(this._cachedRooms[tagId]);
this._cachedRooms[tagId] = algorithm.orderedRooms;
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
this.recalculateActiveCallRooms(tagId);
}
updateStickyRoom(val) {
this.doUpdateStickyRoom(val);
this._lastStickyRoom = null; // clear to indicate we're done changing
}
doUpdateStickyRoom(val) {
if (val?.isSpaceRoom() && val.getMyMembership() !== _types.KnownMembership.Invite) {
// no-op sticky rooms for spaces - they're effectively virtual rooms
val = null;
}
if (val && !_VisibilityProvider.VisibilityProvider.instance.isRoomVisible(val)) {
val = null; // the room isn't visible - lie to the rest of this function
}
// Set the last sticky room to indicate that we're in a change. The code throughout the
// class can safely handle a null room, so this should be safe to do as a backup.
this._lastStickyRoom = this._stickyRoom || {};
// It's possible to have no selected room. In that case, clear the sticky room
if (!val) {
if (this._stickyRoom) {
const stickyRoom = this._stickyRoom.room;
this._stickyRoom = null; // clear before we go to update the algorithm
// Lie to the algorithm and re-add the room to the algorithm
this.handleRoomUpdate(stickyRoom, _models.RoomUpdateCause.NewRoom);
return;
}
return;
}
// When we do have a room though, we expect to be able to find it
let tag = this.roomIdsToTags[val.roomId]?.[0];
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
// means we'll be off by 1 when the user is switching rooms. This leads to visual jumping
// when the user is moving south in the list (not north, because of math).
const tagList = this.getOrderedRoomsWithoutSticky()[tag] || []; // can be null if filtering
let position = tagList.indexOf(val);
// We do want to see if a tag change happened though - if this did happen then we'll want
// to force the position to zero (top) to ensure we can properly handle it.
const wasSticky = this._lastStickyRoom.room ? this._lastStickyRoom.room.roomId === val.roomId : false;
if (this._lastStickyRoom.tag && tag !== this._lastStickyRoom.tag && wasSticky && position < 0) {
_logger.logger.warn(`Sticky room ${val.roomId} changed tags during sticky room handling`);
position = 0;
}
// Sanity check the position to make sure the room is qualified for being sticky
if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);
// 🐉 Here be dragons.
// Before we can go through with lying to the underlying algorithm about a room
// we need to ensure that when we do we're ready for the inevitable sticky room
// update we'll receive. To prepare for that, we first remove the sticky room and
// recalculate the state ourselves so that when the underlying algorithm calls for
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
// a new update for ourselves.
const lastStickyRoom = this._stickyRoom;
this._stickyRoom = null; // clear before we update the algorithm
this.recalculateStickyRoom();
// When we do have the room, re-add the old room (if needed) to the algorithm
// and remove the sticky room from the algorithm. This is so the underlying
// algorithm doesn't try and confuse itself with the sticky room concept.
// We don't add the new room if the sticky room isn't changing because that's
// an easy way to cause duplication. We have to do room ID checks instead of
// referential checks as the references can differ through the lifecycle.
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
// Lie to the algorithm and re-add the room to the algorithm
this.handleRoomUpdate(lastStickyRoom.room, _models.RoomUpdateCause.NewRoom);
}
// Lie to the algorithm and remove the room from it's field of view
this.handleRoomUpdate(val, _models.RoomUpdateCause.RoomRemoved);
// handleRoomUpdate may have modified this._stickyRoom. Convince the
// compiler of this fact.
this._stickyRoom = this.stickyRoomMightBeModified();
// Check for tag & position changes while we're here. We also check the room to ensure
// it is still the same room.
if (this._stickyRoom) {
if (this._stickyRoom.room !== val) {
// Check the room IDs just in case
if (this._stickyRoom.room.roomId === val.roomId) {
_logger.logger.warn("Sticky room changed references");
} else {
throw new Error("Sticky room changed while the sticky room was changing");
}
}
_logger.logger.warn(`Sticky room changed tag & position from ${tag} / ${position} ` + `to ${this._stickyRoom.tag} / ${this._stickyRoom.position}`);
tag = this._stickyRoom.tag;
position = this._stickyRoom.position;
}
// Now that we're done lying to the algorithm, we need to update our position
// marker only if the user is moving further down the same list. If they're switching
// lists, or moving upwards, the position marker will splice in just fine but if
// they went downwards in the same list we'll be off by 1 due to the shifting rooms.
if (lastStickyRoom && lastStickyRoom.tag === tag && lastStickyRoom.position <= position) {
position++;
}
this._stickyRoom = {
room: val,
position: position,
tag: tag
};
// We update the filtered rooms just in case, as otherwise users will end up visiting
// a room while filtering and it'll disappear. We don't update the filter earlier in
// this function simply because we don't have to.
this.recalculateStickyRoom();
this.recalculateActiveCallRooms(tag);
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateActiveCallRooms(lastStickyRoom.tag);
// Finally, trigger an update
if (this.updatesInhibited) return;
this.emit(LIST_UPDATED_EVENT);
}
/**
* Hack to prevent Typescript claiming this._stickyRoom is always null.
*/
stickyRoomMightBeModified() {
return this._stickyRoom;
}
initCachedStickyRooms() {
this._cachedStickyRooms = {};
for (const tagId of Object.keys(this.cachedRooms)) {
this._cachedStickyRooms[tagId] = [...this.cachedRooms[tagId]]; // shallow clone
}
}
/**
* Recalculate the sticky room position. If this is being called in relation to
* a specific tag being updated, it should be given to this function to optimize
* the call.
* @param updatedTag The tag that was updated, if possible.
*/
recalculateStickyRoom(updatedTag = null) {
// 🐉 Here be dragons.
// This function does far too much for what it should, and is called by many places.
// Not only is this responsible for ensuring the sticky room is held in place at all
// times, it is also responsible for ensuring our clone of the cachedRooms is up to
// date. If either of these desyncs, we see weird behaviour like duplicated rooms,
// outdated lists, and other nonsensical issues that aren't necessarily obvious.
if (!this._stickyRoom) {
// If there's no sticky room, just do nothing useful.
if (!!this._cachedStickyRooms) {
// Clear the cache if we won't be needing it
this._cachedStickyRooms = null;
if (this.updatesInhibited) return;
this.emit(LIST_UPDATED_EVENT);
}
return;
}
if (!this._cachedStickyRooms || !updatedTag) {
this.initCachedStickyRooms();
}
if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date.
if (this._cachedStickyRooms) {
this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone
}
}
// Now try to insert the sticky room, if we need to.
// We need to if there's no updated tag (we regenned the whole cache) or if the tag
// we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom;
if (sticky && (!updatedTag || updatedTag === sticky.tag) && this._cachedStickyRooms) {
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
}
// Finally, trigger an update
if (this.updatesInhibited) return;
this.emit(LIST_UPDATED_EVENT);
}
/**
* Recalculate the position of any rooms with calls. If this is being called in
* relation to a specific tag being updated, it should be given to this function to
* optimize the call.
*
* This expects to be called *after* the sticky rooms are updated, and sticks the
* room with the currently active call to the top of its tag.
*
* @param updatedTag The tag that was updated, if possible.
*/
recalculateActiveCallRooms(updatedTag = null) {
if (!updatedTag) {
// Assume all tags need updating
// We're not modifying the map here, so can safely rely on the cached values
// rather than the explicitly sticky map.
for (const tagId of Object.keys(this.cachedRooms)) {
if (!tagId) {
throw new Error("Unexpected recursion: falsy tag");
}
this.recalculateActiveCallRooms(tagId);
}
return;
}
if (_CallStore.CallStore.instance.connectedCalls.size) {
// We operate on the sticky rooms map
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
const rooms = this._cachedStickyRooms[updatedTag];
const activeRoomIds = new Set([..._CallStore.CallStore.instance.connectedCalls].map(call => call.roomId));
const activeRooms = [];
const inactiveRooms = [];
for (const room of rooms) {
(activeRoomIds.has(room.roomId) ? activeRooms : inactiveRooms).push(room);
}
// Stick rooms with active calls to the top
this._cachedStickyRooms[updatedTag] = [...activeRooms, ...inactiveRooms];
}
}
/**
* Asks the Algorithm to regenerate all lists, using the tags given
* as reference for which lists to generate and which way to generate
* them.
* @param {ITagSortingMap} tagSortingMap The tags to generate.
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
*/
populateTags(tagSortingMap, listOrderingMap) {
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
if ((0, _arrays.arrayHasDiff)(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
throw new Error(`Both maps must contain the exact same tags`);
}
this.sortAlgorithms = tagSortingMap;
this.listAlgorithms = listOrderingMap;
this.algorithms = {};
for (const tag of Object.keys(tagSortingMap)) {
this.algorithms[tag] = (0, _listOrdering.getListAlgorithmInstance)(this.listAlgorithms[tag], tag, this.sortAlgorithms[tag]);
}
return this.setKnownRooms(this.rooms);
}
/**
* Gets an ordered set of rooms for the all known tags.
* @returns {ITagMap} The cached list of rooms, ordered,
* for each tag. May be empty, but never null/undefined.
*/
getOrderedRooms() {
return this._cachedStickyRooms || this.cachedRooms;
}
/**
* This returns the same as getOrderedRooms(), but without the sticky room
* map as it causes issues for sticky room handling (see sticky room handling
* for more information).
* @returns {ITagMap} The cached list of rooms, ordered,
* for each tag. May be empty, but never null/undefined.
*/
getOrderedRoomsWithoutSticky() {
return this.cachedRooms;
}
/**
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
* previously known information and instead use these rooms instead.
* @param {Room[]} rooms The rooms to force the algorithm to use.
*/
setKnownRooms(rooms) {
if ((0, _utils.isNullOrUndefined)(rooms)) throw new Error(`Array of rooms cannot be null`);
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
if (!this.updatesInhibited) {
// We only log this if we're expecting to be publishing updates, which means that
// this could be an unexpected invocation. If we're inhibited, then this is probably
// an intentional invocation.
_logger.logger.warn("Resetting known rooms, initiating regeneration");
}
// Before we go any further we need to clear (but remember) the sticky room to
// avoid accidentally duplicating it in the list.
const oldStickyRoom = this._stickyRoom;
if (oldStickyRoom) this.updateStickyRoom(null);
this.rooms = rooms;
const newTags = {};
for (const tagId in this.sortAlgorithms) {
// noinspection JSUnfilteredForInLoop
newTags[tagId] = [];
}
// If we can avoid doing work, do so.
if (!rooms.length) {
this.generateFreshTags(newTags); // just in case it wants to do something
this.cachedRooms = newTags;
return;
}
// Split out the easy rooms first (leave and invite)
const memberships = (0, _membership.splitRoomsByMembership)(rooms);
for (const room of memberships[_membership.EffectiveMembership.Invite]) {
newTags[_models.DefaultTagID.Invite].push(room);
}
for (const room of memberships[_membership.EffectiveMembership.Leave]) {
// We may not have had an archived section previously, so make sure its there.
if (newTags[_models.DefaultTagID.Archived] === undefined) newTags[_models.DefaultTagID.Archived] = [];
newTags[_models.DefaultTagID.Archived].push(room);
}
// Now process all the joined rooms. This is a bit more complicated
for (const room of memberships[_membership.EffectiveMembership.Join]) {
const tags = this.getTagsOfJoinedRoom(room);
let inTag = false;
if (tags.length > 0) {
for (const tag of tags) {
if (!(0, _utils.isNullOrUndefined)(newTags[tag])) {
newTags[tag].push(room);
inTag = true;
}
}
}
if (!inTag) {
if (_DMRoomMap.default.shared().getUserIdForRoomId(room.roomId)) {
newTags[_models.DefaultTagID.DM].push(room);
} else {
newTags[_models.DefaultTagID.Untagged].push(room);
}
}
}
this.generateFreshTags(newTags);
this.cachedRooms = newTags; // this recalculates the filtered rooms for us
this.updateTagsFromCache();
// Now that we've finished generation, we need to update the sticky room to what
// it was. It's entirely possible that it changed lists though, so if it did then
// we also have to update the position of it.
if (oldStickyRoom && oldStickyRoom.room) {
this.updateStickyRoom(oldStickyRoom.room);
if (this._stickyRoom && this._stickyRoom.room) {
// just in case the update doesn't go according to plan
if (this._stickyRoom.tag !== oldStickyRoom.tag) {
// We put the sticky room at the top of the list to treat it as an obvious tag change.
this._stickyRoom.position = 0;
this.recalculateStickyRoom(this._stickyRoom.tag);
}
}
}
}
getTagsForRoom(room) {
const tags = [];
if (!(0, _membership.getEffectiveMembership)(room.getMyMembership())) return []; // peeked room has no tags
const membership = (0, _membership.getEffectiveMembershipTag)(room);
if (membership === _membership.EffectiveMembership.Invite) {
tags.push(_models.DefaultTagID.Invite);
} else if (membership === _membership.EffectiveMembership.Leave) {
tags.push(_models.DefaultTagID.Archived);
} else {
tags.push(...this.getTagsOfJoinedRoom(room));
}
if (!tags.length) tags.push(_models.DefaultTagID.Untagged);
return tags;
}
getTagsOfJoinedRoom(room) {
let tags = Object.keys(room.tags || {});
if (tags.length === 0) {
// Check to see if it's a DM if it isn't anything else
if (_DMRoomMap.default.shared().getUserIdForRoomId(room.roomId)) {
tags = [_models.DefaultTagID.DM];
}
}
if (room.isCallRoom() && (room.getJoinRule() === _matrix.JoinRule.Public || room.getJoinRule() === _matrix.JoinRule.Knock)) {
tags.push(_models.DefaultTagID.Conference);
}
return tags;
}
/**
* Updates the roomsToTags map
*/
updateTagsFromCache() {
const newMap = {};
const tags = Object.keys(this.cachedRooms);
for (const tagId of tags) {
const rooms = this.cachedRooms[tagId];
for (const room of rooms) {
if (!newMap[room.roomId]) newMap[room.roomId] = [];
newMap[room.roomId].push(tagId);
}
}
this.roomIdsToTags = newMap;
}
/**
* Called when the Algorithm believes a complete regeneration of the existing
* lists is needed.
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
* will already have the rooms which belong to it - they just need ordering. Must
* be mutated in place.
*/
generateFreshTags(updatedTagMap) {
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
for (const tag of Object.keys(updatedTagMap)) {
const algorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
algorithm.setRooms(updatedTagMap[tag]);
updatedTagMap[tag] = algorithm.orderedRooms;
}
}
/**
* Asks the Algorithm to update its knowledge of a room. For example, when
* a user tags a room, joins/creates a room, or leaves a room the Algorithm
* should be told that the room's info might have changed. The Algorithm
* may no-op this request if no changes are required.
* @param {Room} room The room which might have affected sorting.
* @param {RoomUpdateCause} cause The reason for the update being triggered.
* @returns {Promise<boolean>} A boolean of whether or not getOrderedRooms()
* should be called after processing.
*/
handleRoomUpdate(room, cause) {
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
// Note: check the isSticky against the room ID just in case the reference is wrong
const isSticky = this._stickyRoom?.room?.roomId === room.roomId;
if (cause === _models.RoomUpdateCause.NewRoom) {
const isForLastSticky = this._lastStickyRoom?.room === room;
const roomTags = this.roomIdsToTags[room.roomId];
const hasTags = roomTags && roomTags.length > 0;
// Don't change the cause if the last sticky room is being re-added. If we fail to
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
// lose the room.
if (hasTags && !isForLastSticky) {
_logger.logger.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
cause = _models.RoomUpdateCause.PossibleTagChange;
}
// Check to see if the room is known first
let knownRoomRef = this.rooms.includes(room);
if (hasTags && !knownRoomRef) {
_logger.logger.warn(`${room.roomId} might be a reference change - attempting to update reference`);
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
knownRoomRef = this.rooms.includes(room);
if (!knownRoomRef) {
_logger.logger.warn(`${room.roomId} is still not referenced. It may be sticky.`);
}
}
// If we have tags for a room and don't have the room referenced, something went horribly
// wrong - the reference should have been updated above.
if (hasTags && !knownRoomRef && !isSticky) {
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
}
// Like above, update the reference to the sticky room if we need to
if (hasTags && isSticky && this._stickyRoom) {
// Go directly in and set the sticky room's new reference, being careful not
// to trigger a sticky room update ourselves.
this._stickyRoom.room = room;
}
// If after all that we're still a NewRoom update, add the room if applicable.
// We don't do this for the sticky room (because it causes duplication issues)
// or if we know about the reference (as it should be replaced).
if (cause === _models.RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
this.rooms.push(room);
}
}
let didTagChange = false;
if (cause === _models.RoomUpdateCause.PossibleTagChange) {
const oldTags = this.roomIdsToTags[room.roomId] || [];
const newTags = this.getTagsForRoom(room);
const diff = (0, _arrays.arrayDiff)(oldTags, newTags);
if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) {
const algorithm = this.algorithms[rmTag];
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
algorithm.handleRoomUpdate(room, _models.RoomUpdateCause.RoomRemoved);
this._cachedRooms[rmTag] = algorithm.orderedRooms;
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
this.recalculateActiveCallRooms(rmTag);
}
for (const addTag of diff.added) {
const algorithm = this.algorithms[addTag];
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
algorithm.handleRoomUpdate(room, _models.RoomUpdateCause.NewRoom);
this._cachedRooms[addTag] = algorithm.orderedRooms;
}
// Update the tag map so we don't regen it in a moment
this.roomIdsToTags[room.roomId] = newTags;
cause = _models.RoomUpdateCause.Timeline;
didTagChange = true;
} else {
// This is a tag change update and no tags were changed, nothing to do!
return false;
}
if (didTagChange && isSticky) {
// Manually update the tag for the sticky room without triggering a sticky room
// update. The update will be handled implicitly by the sticky room handling and
// requires no changes on our part, if we're in the middle of a sticky room change.
if (this._lastStickyRoom) {
this._stickyRoom = {
room,
tag: this.roomIdsToTags[room.roomId][0],
position: 0 // right at the top as it changed tags
};
} else {
// We have to clear the lock as the sticky room change will trigger updates.
this.setStickyRoom(room);
}
}
}
// If the update is for a room change which might be the sticky room, prevent it. We
// need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though
// as the sticky room relies on this.
if (cause !== _models.RoomUpdateCause.NewRoom && cause !== _models.RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) {
return false;
}
}
if (!this.roomIdsToTags[room.roomId]) {
if (CAUSES_REQUIRING_ROOM.includes(cause)) {
return false;
}
// Get the tags for the room and populate the cache
const roomTags = this.getTagsForRoom(room).filter(t => !(0, _utils.isNullOrUndefined)(this.cachedRooms[t]));
// "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(),
// which means we should *always* have a tag to go off of.
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
this.roomIdsToTags[room.roomId] = roomTags;
}
const tags = this.roomIdsToTags[room.roomId];
if (!tags) {
_logger.logger.warn(`No tags known for "${room.name}" (${room.roomId})`);
return false;
}
let changed = didTagChange;
for (const tag of tags) {
const algorithm = this.algorithms[tag];
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
algorithm.handleRoomUpdate(room, cause);
this._cachedRooms[tag] = algorithm.orderedRooms;
// Flag that we've done something
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
this.recalculateActiveCallRooms(tag);
changed = true;
}
return changed;
}
}
exports.Algorithm = Algorithm;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfbWF0cml4IiwicmVxdWlyZSIsIl90eXBlcyIsIl91dGlscyIsIl9ldmVudHMiLCJfbG9nZ2VyIiwiX0RNUm9vbU1hcCIsIl9pbnRlcm9wUmVxdWlyZURlZmF1bHQiLCJfYXJyYXlzIiwiX21vZGVscyIsIl9tZW1iZXJzaGlwIiwiX2xpc3RPcmRlcmluZyIsIl9WaXNpYmlsaXR5UHJvdmlkZXIiLCJfQ2FsbFN0b3JlIiwiTElTVF9VUERBVEVEX0VWRU5UIiwiZXhwb3J0cyIsIkNBVVNFU19SRVFVSVJJTkdfUk9PTSIsIlJvb21VcGRhdGVDYXVzZSIsIlRpbWVsaW5lIiwiUmVhZFJlY2VpcHQiLCJBbGdvcml0aG0iLCJFdmVudEVtaXR0ZXIiLCJjb25zdHJ1Y3RvciIsImFyZ3MiLCJfZGVmaW5lUHJvcGVydHkyIiwiZGVmYXVsdCIsInJlY2FsY3VsYXRlU3RpY2t5Um9vbSIsInJlY2FsY3VsYXRlQWN0aXZlQ2FsbFJvb21zIiwidXBkYXRlc0luaGliaXRlZCIsImVtaXQiLCJzdGFydCIsIkNhbGxTdG9yZSIsImluc3RhbmNlIiwib24iLCJDYWxsU3RvcmVFdmVudCIsIkNvbm5lY3RlZENhbGxzIiwib25Db25uZWN0ZWRDYWxscyIsInN0b3AiLCJvZmYiLCJzdGlja3lSb29tIiwiX3N0aWNreVJvb20iLCJyb29tIiwiaGFzVGFnU29ydGluZ01hcCIsInNvcnRBbGdvcml0aG1zIiwiY2FjaGVkUm9vbXMiLCJ2YWwiLCJfY2FjaGVkUm9vbXMiLCJzZXRTdGlja3lSb29tIiwidXBkYXRlU3RpY2t5Um9vbSIsImUiLCJsb2dnZXIiLCJ3YXJuIiwiZ2V0VGFnU29ydGluZyIsInRhZ0lkIiwic2V0VGFnU29ydGluZyIsInNvcnQiLCJFcnJvciIsImFsZ29yaXRobXMiLCJhbGdvcml0aG0iLCJzZXRTb3J0QWxnb3JpdGhtIiwib3JkZXJlZFJvb21zIiwiZ2V0TGlzdE9yZGVyaW5nIiwibGlzdEFsZ29yaXRobXMiLCJzZXRMaXN0T3JkZXJpbmciLCJvcmRlciIsImdldExpc3RBbGdvcml0aG1JbnN0YW5jZSIsInNldFJvb21zIiwiZG9VcGRhdGVTdGlja3lSb29tIiwiX2xhc3RTdGlja3lSb29tIiwiaXNTcGFjZVJvb20iLCJnZXRNeU1lbWJlcnNoaXAiLCJLbm93bk1lbWJlcnNoaXAiLCJJbnZpdGUiLCJWaXNpYmlsaXR5UHJvdmlkZXIiLCJpc1Jvb21WaXNpYmxlIiwiaGFuZGxlUm9vbVVwZGF0ZSIsIk5ld1Jvb20iLCJ0YWciLCJyb29tSWRzVG9UYWdzIiwicm9vbUlkIiwidGFnTGlzdCIsImdldE9yZGVyZWRSb29tc1dpdGhvdXRTdGlja3kiLCJwb3NpdGlvbiIsImluZGV4T2YiLCJ3YXNTdGlja3kiLCJsYXN0U3RpY2t5Um9vbSIsIlJvb21SZW1vdmVkIiwic3RpY2t5Um9vbU1pZ2h0QmVNb2RpZmllZCIsImluaXRDYWNoZWRTdGlja3lSb29tcyIsIl9jYWNoZWRTdGlja3lSb29tcyIsIk9iamVjdCIsImtleXMiLCJ1cGRhdGVkVGFnIiwic3RpY2t5Iiwic3BsaWNlIiwiY29ubmVjdGVkQ2FsbHMiLCJzaXplIiwicm9vbXMiLCJhY3RpdmVSb29tSWRzIiwiU2V0IiwibWFwIiwiY2FsbCIsImFjdGl2ZVJvb21zIiwiaW5hY3RpdmVSb29tcyIsImhhcyIsInB1c2giLCJwb3B1bGF0ZVRhZ3MiLCJ0YWdTb3J0aW5nTWFwIiwibGlzdE9yZGVyaW5nTWFwIiwiYXJyYXlIYXNEaWZmIiwic2V0S25vd25Sb29tcyIsImdldE9yZGVyZWRSb29tcyIsImlzTnVsbE9yVW5kZWZpbmVkIiwib2xkU3RpY2t5Um9vbSIsIm5ld1RhZ3MiLCJsZW5ndGgiLCJnZW5lcmF0ZUZyZXNoVGFncyIsIm1lbWJlcnNoaXBzIiwic3BsaXRSb29tc0J5TWVtYmVyc2hpcCIsIkVmZmVjdGl2ZU1lbWJlcnNoaXAiLCJEZWZhdWx0VGFnSUQiLCJMZWF2ZSIsIkFyY2hpdmVkIiwidW5kZWZpbmVkIiwiSm9pbiIsInRhZ3MiLCJnZXRUYWdzT2ZKb2luZWRSb29tIiwiaW5UYWciLCJETVJvb21NYXAiLCJzaGFyZWQiLCJnZXRVc2VySWRGb3JSb29tSWQiLCJETSIsIlVudGFnZ2VkIiwidXBkYXRlVGFnc0Zyb21DYWNoZSIsImdldFRhZ3NGb3JSb29tIiwiZ2V0RWZmZWN0aXZlTWVtYmVyc2hpcCIsIm1lbWJlcnNoaXAiLCJnZXRFZmZlY3RpdmVNZW1iZXJzaGlwVGFnIiwiaXNDYWxsUm9vbSIsImdldEpvaW5SdWxlIiwiSm9pblJ1bGUiLCJQdWJsaWMiLCJLbm9jayIsIkNvbmZlcmVuY2UiLCJuZXdNYXAiLCJ1cGRhdGVkVGFnTWFwIiwiY2F1c2UiLCJpc1N0aWNreSIsImlzRm9yTGFzdFN0aWNreSIsInJvb21UYWdzIiwiaGFzVGFncyIsIlBvc3NpYmxlVGFnQ2hhbmdlIiwia25vd25Sb29tUmVmIiwiaW5jbHVkZXMiLCJyIiwiZGlkVGFnQ2hhbmdlIiwib2xkVGFncyIsImRpZmYiLCJhcnJheURpZmYiLCJyZW1vdmVkIiwiYWRkZWQiLCJybVRhZyIsImFkZFRhZyIsImZpbHRlciIsInQiLCJuYW1lIiwiY2hhbmdlZCJdLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9zdG9yZXMvcm9vbS1saXN0L2FsZ29yaXRobXMvQWxnb3JpdGhtLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qXG5Db3B5cmlnaHQgMjAyNCBOZXcgVmVjdG9yIEx0ZC5cbkNvcHlyaWdodCAyMDIwLCAyMDIxIFRoZSBNYXRyaXgub3JnIEZvdW5kYXRpb24gQy5JLkMuXG5cblNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBR1BMLTMuMC1vbmx5IE9SIEdQTC0zLjAtb25seVxuUGxlYXNlIHNlZSBMSUNFTlNFIGZpbGVzIGluIHRoZSByZXBvc2l0b3J5IHJvb3QgZm9yIGZ1bGwgZGV0YWlscy5cbiovXG5cbmltcG9ydCB7IEpvaW5SdWxlLCBSb29tIH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL21hdHJpeFwiO1xuaW1wb3J0IHsgS25vd25NZW1iZXJzaGlwIH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL3R5cGVzXCI7XG5pbXBvcnQgeyBpc051bGxPclVuZGVmaW5lZCB9IGZyb20gXCJtYXRyaXgtanMtc2RrL3NyYy91dGlsc1wiO1xuaW1wb3J0IHsgRXZlbnRFbWl0dGVyIH0gZnJvbSBcImV2ZW50c1wiO1xuaW1wb3J0IHsgbG9nZ2VyIH0gZnJvbSBcIm1hdHJpeC1qcy1zZGsvc3JjL2xvZ2dlclwiO1xuXG5pbXBvcnQgRE1Sb29tTWFwIGZyb20gXCIuLi8uLi8uLi91dGlscy9ETVJvb21NYXBcIjtcbmltcG9ydCB7IGFycmF5RGlmZiwgYXJyYXlIYXNEaWZmIH0gZnJvbSBcIi4uLy4uLy4uL3V0aWxzL2FycmF5c1wiO1xuaW1wb3J0IHsgRGVmYXVsdFRhZ0lELCBSb29tVXBkYXRlQ2F1c2UsIFRhZ0lEIH0gZnJvbSBcIi4uL21vZGVsc1wiO1xuaW1wb3J0IHtcbiAgICBJTGlzdE9yZGVyaW5nTWFwLFxuICAgIElPcmRlcmluZ0FsZ29yaXRobU1hcCxcbiAgICBJVGFnTWFwLFxuICAgIElUYWdTb3J0aW5nTWFwLFxuICAgIExpc3RBbGdvcml0aG0sXG4gICAgU29ydEFsZ29yaXRobSxcbn0gZnJvbSBcIi4vbW9kZWxzXCI7XG5pbXBvcnQge1xuICAgIEVmZmVjdGl2ZU1lbWJlcnNoaXAsXG4gICAgZ2V0RWZmZWN0aXZlTWVtYmVyc2hpcCxcbiAgICBnZXRFZmZlY3RpdmVNZW1iZXJzaGlwVGFnLFxuICAgIHNwbGl0Um9vbXNCeU1lbWJlcnNoaXAsXG59IGZyb20gXCIuLi8uLi8uLi91dGlscy9tZW1iZXJzaGlwXCI7XG5pbXBvcnQgeyBPcmRlcmluZ0FsZ29yaXRobSB9IGZyb20gXCIuL2xpc3Qtb3JkZXJpbmcvT3JkZXJpbmdBbGdvcml0aG1cIjtcbmltcG9ydCB7IGdldExpc3RBbGdvcml0aG1JbnN0YW5jZSB9IGZyb20gXCIuL2xpc3Qtb3JkZXJpbmdcIjtcbmltcG9ydCB7IFZpc2liaWxpdHlQcm92aWRlciB9IGZyb20gXCIuLi9maWx0ZXJzL1Zpc2liaWxpdHlQcm92aWRlclwiO1xuaW1wb3J0IHsgQ2FsbFN0b3JlLCBDYWxsU3RvcmVFdmVudCB9IGZyb20gXCIuLi8uLi9DYWxsU3RvcmVcIjtcblxuLyoqXG4gKiBGaXJlZCB3aGVuIHRoZSBBbGdvcml0aG0gaGFzIGRldGVybWluZWQgYSBsaXN0IGhhcyBiZWVuIHVwZGF0ZWQuXG4gKi9cbmV4cG9ydCBjb25zdCBMSVNUX1VQREFURURfRVZFTlQgPSBcImxpc3RfdXBkYXRlZF9ldmVudFwiO1xuXG4vLyBUaGVzZSBhcmUgdGhlIGNhdXNlcyB3aGljaCByZXF1aXJlIGEgcm9vbSB0byBiZSBrbm93biBpbiBvcmRlciBmb3IgdXMgdG8gaGFuZGxlIHRoZW0uIElmXG4vLyBhIGNhdXNlIGluIHRoaXMgbGlzdCBpcyByYWlzZWQgYW5kIHdlIGRvbid0IGtub3cgYWJvdXQgdGhlIHJvb20sIHdlIGRvbid0IGhhbmRsZSB0aGUgdXBkYXRlLlxuLy9cbi8vIE5vdGU6IHRoZXNlIHR5cGljYWxseSBoYXBwZW4gd2hlbiBhIG5ldyByb29tIGlzIGNvbWluZyBpbiwgc3VjaCBhcyB0aGUgdXNlciBjcmVhdGluZyBvclxuLy8gam9pbmluZyB0aGUgcm9vbS4gRm9yIHRoZXNlIGNhc2VzLCB3ZSBuZWVkIHRvIGtub3cgYWJvdXQgdGhlIHJvb20gcHJpb3IgdG8gaGFuZGxpbmcgaXQgb3RoZXJ3aXNlXG4vLyB3ZSdsbCBtYWtlIGJhZCBhc3N1bXB0aW9ucy5cbmNvbnN0IENBVVNFU19SRVFVSVJJTkdfUk9PTSA9IFtSb29tVXBkYXRlQ2F1c2UuVGltZWxpbmUsIFJvb21VcGRhdGVDYXVzZS5SZWFkUmVjZWlwdF07XG5cbmludGVyZmFjZSBJU3RpY2t5Um9vbSB7XG4gICAgcm9vbTogUm9vbTtcbiAgICBwb3NpdGlvbjogbnVtYmVyO1xuICAgIHRhZzogVGFnSUQ7XG59XG5cbi8qKlxuICogUmVwcmVzZW50cyBhIGxpc3Qgb3JkZXJpbmcgYWxnb3JpdGhtLiBUaGlzIGNsYXNzIHdpbGwgdGFrZSBjYXJlIG9mIHRhZ1xuICogbWFuYWdlbWVudCAod2hpY2ggcm9vbXMgZ28gaW4gd2hpY2ggdGFncykgYW5kIGFzayB0aGUgaW1wbGVtZW50YXRpb24gdG9cbiAqIGRlYWwgd2l0aCBvcmRlcmluZyBtZWNoYW5pY3MuXG4gKi9cbmV4cG9ydCBjbGFzcyBBbGdvcml0aG0gZXh0ZW5kcyBFdmVudEVtaXR0ZXIge1xuICAgIHByaXZhdGUgX2NhY2hlZFJvb21zOiBJVGFnTWFwID0ge307XG4gICAgcHJpdmF0ZSBfY2FjaGVkU3RpY2t5Um9vbXM6IElUYWdNYXAgfCBudWxsID0ge307IC8vIGEgY2xvbmUgb2YgdGhlIF9jYWNoZWRSb29tcywgd2l0aCB0aGUgc3RpY2t5IHJvb21cbiAgICBwcml2YXRlIF9zdGlja3lSb29tOiBJU3RpY2t5Um9vbSB8IG51bGwgPSBudWxsO1xuICAgIHByaXZhdGUgX2xhc3RTdGlja3lSb29tOiBJU3RpY2t5Um9vbSB8IG51bGwgPSBudWxsOyAvLyBvbmx5IG5vdC1udWxsIHdoZW4gY2hhbmdpbmcgdGhlIHN0aWNreSByb29tXG4gICAgcHJpdmF0ZSBzb3J0QWxnb3JpdGhtczogSVRhZ1NvcnRpbmdNYXAgfCBudWxsID0gbnVsbDtcbiAgICBwcml2YXRlIGxpc3RBbGdvcml0aG1zOiBJTGlzdE9yZGVyaW5nTWFwIHwgbnVsbCA9IG51bGw7XG4gICAgcHJpdmF0ZSBhbGdvcml0aG1zOiBJT3JkZXJpbmdBbGdvcml0aG1NYXAgfCBudWxsID0gbnVsbDtcbiAgICBwcml2YXRlIHJvb21zOiBSb29tW10gPSBbXTtcbiAgICBwcml2YXRlIHJvb21JZHNUb1RhZ3M6IHtcbiAgICAgICAgW3Jvb21JZDogc3RyaW5nXTogVGFnSURbXTtcbiAgICB9ID0ge307XG5cbiAgICAvKipcbiAgICAgKiBTZXQgdG8gdHJ1ZSB0byBzdXNwZW5kIGVtaXNzaW9ucyBvZiBhbGdvcml0aG0gdXBkYXRlcy5cbiAgICAgKi9cbiAgICBwdWJsaWMgdXBkYXRlc0luaGliaXRlZCA9IGZhbHNlO1xuXG4gICAgcHVibGljIHN0YXJ0KCk6IHZvaWQge1xuICAgICAgICBDYWxsU3RvcmUuaW5zdGFuY2Uub24oQ2FsbFN0b3JlRXZlbnQuQ29ubmVjdGVkQ2FsbHMsIHRoaXMub25Db25uZWN0ZWRDYWxscyk7XG4gICAgfVxuXG4gICAgcHVibGljIHN0b3AoKTogdm9pZCB7XG4gICAgICAgIENhbGxTdG9yZS5pbnN0YW5jZS5vZmYoQ2FsbFN0b3JlRXZlbnQuQ29ubmVjdGVkQ2FsbHMsIHRoaXMub25Db25uZWN0ZWRDYWxscyk7XG4gICAgfVxuXG4gICAgcHVibGljIGdldCBzdGlja3lSb29tKCk6IFJvb20gfCBudWxsIHtcbiAgICAgICAgcmV0dXJuIHRoaXMuX3N0aWNreVJvb20gPyB0aGlzLl9zdGlja3lSb29tLnJvb20gOiBudWxsO1xuICAgIH1cblxuICAgIHB1YmxpYyBnZXQgaGFzVGFnU29ydGluZ01hcCgpOiBib29sZWFuIHtcbiAgICAgICAgcmV0dXJuICEhdGhpcy5zb3J0QWxnb3JpdGhtcztcbiAgICB9XG5cbiAgICBwcm90ZWN0ZWQgc2V0IGNhY2hlZFJvb21zKHZhbDogSVRhZ01hcCkge1xuICAgICAgICB0aGlzLl9jYWNoZWRSb29tcyA9IHZhbDtcbiAgICAgICAgdGhpcy5yZWNhbGN1bGF0ZVN0aWNreVJvb20oKTtcbiAgICAgICAgdGhpcy5yZWNhbGN1bGF0ZUFjdGl2ZUNhbGxSb29tcygpO1xuICAgIH1cblxuICAgIHByb3RlY3RlZCBnZXQgY2FjaGVkUm9vbXMoKTogSVRhZ01hcCB7XG4gICAgICAgIC8vIPCfkIkgSGVyZSBiZSBkcmFnb25zLlxuICAgICAgICAvLyBOb3RlOiB0aGlzIGlzIHVzZWQgYnkgdGhlIHVuZGVybHlpbmcgYWxnb3JpdGhtIGNsYXNzZXMsIHNvIGRvbid0IG1ha2UgaXQgcmV0dXJuXG4gICAgICAgIC8vIHRoZSBzdGlja3kgcm9vbSBjYWNoZS4gSWYgaXQgZW5kcyB1cCByZXR1cm5pbmcgdGhlIHN0aWNreSByb29tIGNhY2hlLCB3ZSBlbmQgdXBcbiAgICAgICAgLy8gY29ycnVwdGluZyBvdXIgY2FjaGVzIGFuZCBjb25mdXNpbmcgdGhlbS5cbiAgICAgICAgcmV0dXJuIHRoaXMuX2NhY2hlZFJvb21zO1xuICAgIH1cblxuICAgIC8qKlxuICAgICAqIEF3YWl0YWJsZSB2ZXJzaW9uIG9mIHRoZSBzdGlja3kgcm9vbSBzZXR0ZXIuXG4gICAgICogQHBhcmFtIHZhbCBUaGUgbmV3IHJvb20gdG8gc3RpY2t5LlxuICAgICAqL1xuICAgIHB1YmxpYyBzZXRTdGlja3lSb29tKHZhbDogUm9vbSB8IG51bGwpOiB2b2lkIHtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHRoaXMudXBkYXRlU3RpY2t5Um9vbSh2YWwpO1xuICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgICBsb2dnZXIud2FybihcIkZhaWxlZCB0byB1cGRhdGUgc3RpY2t5IHJvb21cIiwgZSk7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBwdWJsaWMgZ2V0VGFnU29ydGluZyh0YWdJZDogVGFnSUQpOiBTb3J0QWxnb3JpdGhtIHwgbnVsbCB7XG4gICAgICAgIGlmICghdGhpcy5zb3J0QWxnb3JpdGhtcykgcmV0dXJuIG51bGw7XG4gICAgICAgIHJldHVybiB0aGlzLnNvcnRBbGdvcml0aG1zW3RhZ0lkXTtcbiAgICB9XG5cbiAgICBwdWJsaWMgc2V0VGFnU29ydGluZyh0YWdJZDogVGFnSUQsIHNvcnQ6IFNvcnRBbGdvcml0aG0pOiB2b2lkIHtcbiAgICAgICAgaWYgKCF0YWdJZCkgdGhyb3cgbmV3IEVycm9yKFwiVGFnIElEIG11c3QgYmUgZGVmaW5lZFwiKTtcbiAgICAgICAgaWYgKCFzb3J0KSB0aHJvdyBuZXcgRXJyb3IoXCJBbGdvcml0aG0gbXVzdCBiZSBkZWZpbmVkXCIpO1xuICAgICAgICBpZiAoIXRoaXMuc29ydEFsZ29yaXRobXMpIHRocm93IG5ldyBFcnJvcihcInRoaXMuc29ydEFsZ29yaXRobXMgbXVzdCBiZSBkZWZpbmVkIGJlZm9yZSBjYWxsaW5nIHNldFRhZ1NvcnRpbmdcIik7XG4gICAgICAgIGlmICghdGhpcy5hbGdvcml0aG1zKSB0aHJvdyBuZXcgRXJyb3IoXCJ0aGlzLmFsZ29yaXRobXMgbXVzdCBiZSBkZWZpbmVkIGJlZm9yZSBjYWxsaW5nIHNldFRhZ1NvcnRpbmdcIik7XG4gICAgICAgIHRoaXMuc29ydEFsZ29yaXRobXNbdGFnSWRdID0gc29ydDtcblxuICAgICAgICBjb25zdCBhbGdvcml0aG06IE9yZGVyaW5nQWxnb3JpdGhtID0gdGhpcy5hbGdvcml0aG1zW3RhZ0lkXTtcbiAgICAgICAgYWxnb3JpdGhtLnNldFNvcnRBbGdvcml0aG0oc29ydCk7XG4gICAgICAgIHRoaXMuX2NhY2hlZFJvb21zW3RhZ0lkXSA9IGFsZ29yaXRobS5vcmRlcmVkUm9vbXM7XG4gICAgICAgIHRoaXMucmVjYWxjdWxhdGVTdGlja3lSb29tKHRhZ0lkKTsgLy8gdXBkYXRlIHN0aWNreSByb29tIHRvIG1ha2Ugc3VyZSBpdCBhcHBlYXJzIGlmIG5lZWRlZFxuICAgICAgICB0aGlzLnJlY2FsY3VsYXRlQWN0aXZlQ2FsbFJvb21zKHRhZ0lkKTtcbiAgICB9XG5cbiAgICBwdWJsaWMgZ2V0TGlzdE9yZGVyaW5nKHRhZ0lkOiBUYWdJRCk6IExpc3RBbGdvcml0aG0gfCBudWxsIHtcbiAgICAgICAgaWYgKCF0aGlzLmxpc3RBbGdvcml0aG1zKSByZXR1cm4gbnVsbDtcbiAgICAgICAgcmV0dXJuIHRoaXMubGlzdEFsZ29yaXRobXNbdGFnSWRdO1xuICAgIH1cblxuICAgIHB1YmxpYyBzZXRMaXN0T3JkZXJpbmcodGFnSWQ6IFRhZ0lELCBvcmRlcjogTGlzdEFsZ29yaXRobSk6IHZvaWQge1xuICAgICAgICBpZiAoIXRhZ0lkKSB0aHJvdyBuZXcgRXJyb3IoXCJUYWcgSUQgbXVzdCBiZSBkZWZpbmVkXCIpO1xuICAgICAgICBpZiAoIW9yZGVyKSB0aHJvdyBuZXcgRXJyb3IoXCJBbGdvcml0aG0gbXVzdCBiZSBkZWZpbmVkXCIpO1xuICAgICAgICBpZiAoIXRoaXMuc29ydEFsZ29yaXRobXMpIHRocm93IG5ldyBFcnJvcihcInRoaXMuc29ydEFsZ29yaXRobXMgbXVzdCBiZSBkZWZpbmVkIGJlZm9yZSBjYWxsaW5nIHNldExpc3RPcmRlcmluZ1wiKTtcbiAgICAgICAgaWYgKCF0aGlzLmxpc3RBbGdvcml0aG1zKSB0aHJvdyBuZXcgRXJyb3IoXCJ0aGlzLmxpc3RBbGdvcml0aG1zIG11c3QgYmUgZGVmaW5lZCBiZWZvcmUgY2FsbGluZyBzZXRMaXN0T3JkZXJpbmdcIik7XG4gICAgICAgIGlmICghdGhpcy5hbGdvcml0aG1zKSB0aHJvdyBuZXcgRXJyb3IoXCJ0aGlzLmFsZ29yaXRobXMgbXVzdCBiZSBkZWZpbmVkIGJlZm9yZSBjYWxsaW5nIHNldExpc3RPcmRlcmluZ1wiKTtcbiAgICAgICAgdGhpcy5saXN0QWxnb3JpdGhtc1t0YWdJZF0gPSBvcmRlcjtcblxuICAgICAgICBjb25zdCBhbGdvcml0aG0gPSBnZXRMaXN0QWxnb3JpdGhtSW5zdGFuY2Uob3JkZXIsIHRhZ0lkLCB0aGlzLnNvcnRBbGdvcml0aG1zW3RhZ0lkXSk7XG4gICAgICAgIHRoaXMuYWxnb3JpdGhtc1t0YWdJZF0gPSBhbGdvcml0aG07XG5cbiAgICAgICAgYWxnb3JpdGhtLnNldFJvb21zKHRoaXMuX2NhY2hlZFJvb21zW3RhZ0lkXSk7XG4gICAgICAgIHRoaXMuX2NhY2hlZFJvb21zW3RhZ0lkXSA9IGFsZ29yaXRobS5vcmRlcmVkUm9vbXM7XG4gICAgICAgIHRoaXMucmVjYWxjdWxhdGVTdGlja3lSb29tKHRhZ0lkKTsgLy8gdXBkYXRlIHN0aWNreSByb29tIHRvIG1ha2Ugc3VyZSBpdCBhcHBlYXJzIGlmIG5lZWRlZFxuICAgICAgICB0aGlzLnJlY2FsY3VsYXRlQWN0aXZlQ2FsbFJvb21zKHRhZ0lkKTtcbiAgICB9XG5cbiAgICBwcml2YXRlIHVwZGF0ZVN0aWNreVJvb20odmFsOiBSb29tIHwgbnVsbCk6IHZvaWQge1xuICAgICAgICB0aGlzLmRvVXBkYXRlU3RpY2t5Um9vbSh2YWwpO1xuICAgICAgICB0aGlzLl9sYXN0U3RpY2t5Um9vbSA9IG51bGw7IC8vIGNsZWFyIHRvIGluZGljYXRlIHdlJ3JlIGRvbmUgY2hhbmdpbmdcbiAgICB9XG5cbiAgICBwcml2YXRlIGRvVXBkYXRlU3RpY2t5Um9vbSh2YWw6IFJvb20gfCBudWxsKTogdm9pZCB7XG4gICAgICAgIGlmICh2YWw/LmlzU3BhY2VSb29tKCkgJiYgdmFsLmdldE15TWVtYmVyc2hpcCgpICE9PSBLbm93bk1lbWJlcnNoaXAuSW52aXRlKSB7XG4gICAgICAgICAgICAvLyBuby1vcCBzdGlja3kgcm9vbXMgZm9yIHNwYWNlcyAtIHRoZXkncmUgZWZmZWN0aXZlbHkgdmlydHVhbCByb29tc1xuICAgICAgICAgICAgdmFsID0gbnVsbDtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmICh2YWwgJiYgIVZpc2liaWxpdHlQcm92aWRlci5pbnN0YW5jZS5pc1Jvb21WaXNpYmxlKHZhbCkpIHtcbiAgICAgICAgICAgIHZhbCA9IG51bGw7IC8vIHRoZSByb29tIGlzbid0IHZpc2libGUgLSBsaWUgdG8gdGhlIHJlc3Qgb2YgdGhpcyBmdW5jdGlvblxuICAgICAgICB9XG5cbiAgICAgICAgLy8gU2V0IHRoZSBsYXN0IHN0aWNreSByb29tIHRvIGluZGljYXRlIHRoYXQgd2UncmUgaW4gYSBjaGFuZ2UuIFRoZSBjb2RlIHRocm91Z2hvdXQgdGhlXG4gICAgICAgIC8vIGNsYXNzIGNhbiBzYWZlbHkgaGFuZGxlIGEgbnVsbCByb29tLCBzbyB0aGlzIHNob3VsZCBiZSBzYWZlIHRvIGRvIGFzIGEgYmFja3VwLlxuICAgICAgICB0aGlzLl9sYXN0U3RpY2t5Um9vbSA9IHRoaXMuX3N0aWNreVJvb20gfHwgPElTdGlja3lSb29tPnt9O1xuXG4gICAgICAgIC8vIEl0J3MgcG9zc2libGUgdG8gaGF2ZSBubyBzZWxlY3RlZCByb29tLiBJbiB0aGF0IGNhc2UsIGNsZWFyIHRoZSBzdGlja3kgcm9vbVxuICAgICAgICBpZiAoIXZhbCkge1xuICAgICAgICAgICAgaWYgKHRoaXMuX3N0aWNreVJvb20pIHtcbiAgICAgICAgICAgICAgICBjb25zdCBzdGlja3lSb29tID0gdGhpcy5fc3RpY2t5Um9vbS5yb29tO1xuICAgICAgICAgICAgICAgIHRoaXMuX3N0aWNreVJvb20gPSBudWxsOyAvLyBjbGVhciBiZWZvcmUgd2UgZ28gdG8gdXBkYXRlIHRoZSBhbGdvcml0aG1cblxuICAgICAgICAgICAgICAgIC8vIExpZSB0byB0aGUgYWxnb3JpdGhtIGFuZCByZS1hZGQgdGhlIHJvb20gdG8gdGhlIGFsZ29yaXRobVxuICAgICAgICAgICAgICAgIHRoaXMuaGFuZGxlUm9vbVVwZGF0ZShzdGlja3lSb29tLCBSb29tVXBkYXRlQ2F1c2UuTmV3Um9vbSk7XG4gICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gV2hlbiB3ZSBkbyBoYXZlIGEgcm9vbSB0aG91Z2gsIHdlIGV4cGVjdCB0byBiZSBhYmxlIHRvIGZpbmQgaXRcbiAgICAgICAgbGV0IHRhZyA9IHRoaXMucm9vbUlkc1RvVGFnc1t2YWwucm9vbUlkXT8uWzBdO1xuICAgICAgICBpZiAoIXRhZykgdGhyb3cgbmV3IEVycm9yKGAke3ZhbC5yb29tSWR9IGRvZXMgbm90IGJlbG9uZyB0byBhIHRhZyBhbmQgY2Fubm90IGJlIHN0aWNreWApO1xuXG4gICAgICAgIC8vIFdlIHNwZWNpZmljYWxseSBkbyBOT1QgdXNlIHRoZSBvcmRlcmVkIHJvb21zIHNldCBhcyBpdCBjb250YWlucyB0aGUgc3RpY2t5IHJvb20sIHdoaWNoXG4gICAgICAgIC8vIG1lYW5zIHdlJ2xsIGJlIG9mZiBieSAxIHdoZW4gdGhlIHVzZXIgaXMgc3dpdGNoaW5nIHJvb21zLiBUaGlzIGxlYWRzIHRvIHZpc3VhbCBqdW1waW5nXG4gICAgICAgIC8vIHdoZW4gdGhlIHVzZXIgaXMgbW92aW5nIHNvdXRoIGluIHRoZSBsaXN0IChub3Qgbm9ydGgsIGJlY2F1c2Ugb2YgbWF0aCkuXG4gICAgICAgIGNvbnN0IHRhZ0xpc3QgPSB0aGlzLmdldE9yZGVyZWRSb29tc1dpdGhvdXRTdGlja3koKVt0YWddIHx8IFtdOyAvLyBjYW4gYmUgbnVsbCBpZiBmaWx0ZXJpbmdcbiAgICAgICAgbGV0IHBvc2l0aW9uID0gdGFnTGlzdC5pbmRleE9mKHZhbCk7XG5cbiAgICAgICAgLy8gV2UgZG8gd2FudCB0byBzZWUgaWYgYSB0YWcgY2hhbmdlIGhhcHBlbmVkIHRob3VnaCAtIGlmIHRoaXMgZGlkIGhhcHBlbiB0aGVuIHdlJ2xsIHdhbnRcbiAgICAgICAgLy8gdG8gZm9yY2UgdGhlIHBvc2l0aW9uIHRvIHplcm8gKHRvcCkgdG8gZW5zdXJlIHdlIGNhbiBwcm9wZXJseSBoYW5kbGUgaXQuXG4gICAgICAgIGNvbnN0IHdhc1N0aWNreSA9IHRoaXMuX2xhc3RTdGlja3lSb29tLnJvb20gPyB0aGlzLl9sYXN0U3RpY2t5Um9vbS5yb29tLnJvb21JZCA9PT0gdmFsLnJvb21JZCA6IGZhbHNlO1xuICAgICAgICBpZiAodGhpcy5fbGFzdFN0aWNreVJvb20udGFnICYmIHRhZyAhPT0gdGhpcy5fbGFzdFN0aWNreVJvb20udGFnICYmIHdhc1N0aWNreSAmJiBwb3NpdGlvbiA8IDApIHtcbiAgICAgICAgICAgIGxvZ2dlci53YXJuKGBTdGlja3kgcm9vbSAke3ZhbC5yb29tSWR9IGNoYW5nZWQgdGFncyBkdXJpbmcgc3RpY2t5IHJvb20gaGFuZGxpbmdgKTtcbiAgICAgICAgICAgIHBvc2l0aW9uID0gMDtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIFNhbml0eSBjaGVjayB0aGUgcG9zaXRpb24gdG8gbWFrZSBzdXJlIHRoZSByb29tIGlzIHF1YWxpZmllZCBmb3IgYmVpbmcgc3RpY2t5XG4gICAgICAgIGlmIChwb3NpdGlvbiA8IDApIHRocm93IG5ldyBFcnJvcihgJHt2YWwucm9vbUlkfSBkb2VzIG5vdCBhcHBlYXIgdG8gYmUga25vd24gYW5kIGNhbm5vdCBiZSBzdGlja3lgKTtcblxuICAgICAgICAvLyDwn5CJIEhlcmUgYmUgZHJhZ29ucy5cbiAgICAgICAgLy8gQmVmb3JlIHdlIGNhbiBnbyB0aHJvdWdoIHdpdGggbHlpbmcgdG8gdGhlIHVuZGVybHlpbmcgYWxnb3JpdGhtIGFib3V0IGEgcm9vbVxuICAgICAgICAvLyB3ZSBuZWVkIHRvIGVuc3VyZSB0aGF0IHdoZW4gd2UgZG8gd2UncmUgcmVhZHkgZm9yIHRoZSBpbmV2aXRhYmxlIHN0aWNreSByb29tXG4gICAgICAgIC8vIHVwZGF0ZSB3ZSdsbCByZWNlaXZlLiBUbyBwcmVwYXJlIGZvciB0aGF0LCB3ZSBmaXJzdCByZW1vdmUgdGhlIHN0aWNreSByb29tIGFuZFxuICAgICAgICAvLyByZWNhbGN1bGF0ZSB0aGUgc3RhdGUgb3Vyc2VsdmVzIHNvIHRoYXQgd2hlbiB0aGUgdW5kZXJseWluZyBhbGdvcml0aG0gY2FsbHMgZm9yXG4gICAgICAgIC8vIHRoZSBzYW1lIHRoaW5nIGl0IG5vLW9wcy4gQWZ0ZXIgd2UncmUgZG9uZSBjYWxsaW5nIHRoZSBhbGdvcml0aG0sIHdlJ2xsIGlzc3VlXG4gICAgICAgIC8vIGEgbmV3IHVwZGF0ZSBmb3Igb3Vyc2VsdmVzLlxuICAgICAgICBjb25zdCBsYXN0U3RpY2t5Um9vbSA9IHRoaXMuX3N0aWNreVJvb207XG4gICAgICAgIHRoaXMuX3N0aWNreVJvb20gPSBudWxsOyAvLyBjbGVhciBiZWZvcmUgd2UgdXBkYXRlIHRoZSBhbGdvcml0aG1cbiAgICAgICAgdGhpcy5yZWNhbGN1bGF0ZVN0aWNreVJvb20oKTtcblxuICAgICAgICAvLyBXaGVuIHdlIGRvIGhhdmUgdGhlIHJvb20sIHJlLWFkZCB0aGUgb2xkIHJvb20gKGlmIG5lZWRlZCkgdG8gdGhlIGFsZ29yaXRobVxuICAgICAgICAvLyBhbmQgcmVtb3ZlIHRoZSBzdGlja3kgcm9vbSBmcm9tIHRoZSBhbGdvcml0aG0uIFRoaXMgaXMgc28gdGhlIHVuZGVybHlpbmdcbiAgICAgICAgLy8gYWxnb3JpdGhtIGRvZXNuJ3QgdHJ5IGFuZCBjb25mdXNlIGl0c2VsZiB3aXRoIHRoZSBzdGlja3kgcm9vbSBjb25jZXB0LlxuICAgICAgICAvLyBXZSBkb24ndCBhZGQgdGhlIG5ldyByb29tIGlmIHRoZSBzdGlja3kgcm9vbSBpc24ndCBjaGFuZ2luZyBiZWNhdXNlIHRoYXQnc1xuICAgICAgICAvLyBhbiBlYXN5IHdheSB0byBjYXVzZSBkdXBsaWNhdGlvbi4gV2UgaGF2ZSB0byBkbyByb29tIElEIGNoZWNrcyBpbnN0ZWFkIG9mXG4gICAgICAgIC8vIHJlZmVyZW50aWFsIGNoZWNrcyBhcyB0aGUgcmVmZXJlbmNlcyBjYW4gZGlmZmVyIHRocm91Z2ggdGhlIGxpZmVjeWNsZS5cbiAgICAgICAgaWYgKGxhc3RTdGlja3lSb29tICYmIGxhc3RTdGlja3lSb29tLnJvb20gJiYgbGFzdFN0aWNreVJvb20ucm9vbS5yb29tSWQgIT09IHZhbC5yb29tSWQpIHtcbiAgICAgICAgICAgIC8vIExpZSB0byB0aGUgYWxnb3JpdGhtIGFuZCByZS1hZGQgdGhlIHJvb20gdG8gdGhlIGFsZ29yaXRobVxuICAgICAgICAgICAgdGhpcy5oYW5kbGVSb29tVXBkYXRlKGxhc3RTdGlja3lSb29tLnJvb20sIFJvb21VcGRhdGVDYXVzZS5OZXdSb29tKTtcbiAgICAgICAgfVxuICAgICAgICAvLyBMaWUgdG8gdGhlIGFsZ29yaXRobSBhbmQgcmVtb3ZlIHRoZSByb29tIGZyb20gaXQncyBmaWVsZCBvZiB2aWV3XG4gICAgICAgIHRoaXMuaGFuZGxlUm9vbVVwZGF0ZSh2YWwsIFJvb21VcGRhdGVDYXVzZS5Sb29tUmVtb3ZlZCk7XG5cbiAgICAgICAgLy8gaGFuZGxlUm9vbVVwZGF0ZSBtYXkgaGF2ZSBtb2RpZmllZCB0aGlzLl9zdGlja3lSb29tLiBDb252aW5jZSB0aGVcbiAgICAgICAgLy8gY29tcGlsZXIgb2YgdGhpcyBmYWN0LlxuICAgICAgICB0aGlzLl9zdGlja3lSb29tID0gdGhpcy5zdGlja3lSb29tTWlnaHRCZU1vZGlmaWVkKCk7XG5cbiAgICAgICAgLy8gQ2hlY2sgZm9yIHRhZyAmIHBvc2l0aW9uIGNoYW5nZXMgd2hpbGUgd2UncmUgaGVyZS4gV2UgYWxzbyBjaGVjayB0aGUgcm9vbSB0byBlbnN1cmVcbiAgICAgICAgLy8gaXQgaXMgc3RpbGwgdGhlIHNhbWUgcm9vbS5cbiAgICAgICAgaWYgKHRoaXMuX3N0aWNreVJvb20pIHtcbiAgICAgICAgICAgIGlmICh0aGlzLl9zdGlja3lSb29tLnJvb20gIT09IHZhbCkge1xuICAgICAgICAgICAgICAgIC8vIENoZWNrIHRoZSByb29tIElEcyBqdXN0IGluIGNhc2VcbiAgICAgICAgICAgICAgICBpZiAodGhpcy5fc3RpY2t5Um9vbS5yb29tLnJvb21JZCA9PT0gdmFsLnJvb21JZCkge1xuICAgICAgICAgICAgICAgICAgICBsb2dnZXIud2FybihcIlN0aWNreSByb29tIGNoYW5nZWQgcmVmZXJlbmNlc1wiKTtcbiAgICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXCJTdGlja3kgcm9vbSBjaGFuZ2VkIHdoaWxlIHRoZSBzdGlja3kgcm9vbSB3YXMgY2hhbmdpbmdcIik7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICBsb2dnZXIud2FybihcbiAgICAgICAgICAgICAgICBgU3RpY2t5IHJvb20gY2hhbmdlZCB0YWcgJiBwb3NpdGlvbiBmcm9tICR7dGFnfSAvICR7cG9zaXRpb259IGAgK1xuICAgICAgICAgICAgICAgICAgICBgdG8gJHt0aGlzLl9zdGlja3lSb29tLnRhZ30gLyAke3RoaXMuX3N0aWNreVJvb20ucG9zaXRpb259YCxcbiAgICAgICAgICAgICk7XG5cbiAgICAgICAgICAgIHRhZyA9IHRoaXMuX3N0aWNreVJvb20udGFnO1xuICAgICAgICAgICAgcG9zaXRpb24gPSB0aGlzLl9zdGlja3lSb29tLnBvc2l0aW9uO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gTm93IHRoYXQgd2UncmUgZG9uZSBseWluZyB0byB0aGUgYWxnb3JpdGhtLCB3ZSBuZWVkIHRvIHVwZGF0ZSBvdXIgcG9zaXRpb25cbiAgICAgICAgLy8gbWFya2VyIG9ubHkgaWYgdGhlIHVzZXIgaXM