@@ -22,6 +22,7 @@ import { ITagMap, ITagSortingMap } from "../models";
22
22
import DMRoomMap from "../../../../utils/DMRoomMap" ;
23
23
import { FILTER_CHANGED , IFilterCondition } from "../../filters/IFilterCondition" ;
24
24
import { EventEmitter } from "events" ;
25
+ import { UPDATE_EVENT } from "../../../AsyncStore" ;
25
26
26
27
// TODO: Add locking support to avoid concurrent writes?
27
28
@@ -30,14 +31,22 @@ import { EventEmitter } from "events";
30
31
*/
31
32
export const LIST_UPDATED_EVENT = "list_updated_event" ;
32
33
34
+ interface IStickyRoom {
35
+ room : Room ;
36
+ position : number ;
37
+ tag : TagID ;
38
+ }
39
+
33
40
/**
34
41
* Represents a list ordering algorithm. This class will take care of tag
35
42
* management (which rooms go in which tags) and ask the implementation to
36
43
* deal with ordering mechanics.
37
44
*/
38
45
export abstract class Algorithm extends EventEmitter {
39
46
private _cachedRooms : ITagMap = { } ;
47
+ private _cachedStickyRooms : ITagMap = { } ; // a clone of the _cachedRooms, with the sticky room
40
48
private filteredRooms : ITagMap = { } ;
49
+ private _stickyRoom : IStickyRoom = null ;
41
50
42
51
protected sortAlgorithms : ITagSortingMap ;
43
52
protected rooms : Room [ ] = [ ] ;
@@ -51,16 +60,88 @@ export abstract class Algorithm extends EventEmitter {
51
60
super ( ) ;
52
61
}
53
62
63
+ public get stickyRoom ( ) : Room {
64
+ return this . _stickyRoom ? this . _stickyRoom . room : null ;
65
+ }
66
+
67
+ public set stickyRoom ( val : Room ) {
68
+ // We wrap this in a closure because we can't use async setters.
69
+ // We need async so we can wait for handleRoomUpdate() to do its thing, otherwise
70
+ // we risk duplicating rooms.
71
+ ( async ( ) => {
72
+ // It's possible to have no selected room. In that case, clear the sticky room
73
+ if ( ! val ) {
74
+ if ( this . _stickyRoom ) {
75
+ // Lie to the algorithm and re-add the room to the algorithm
76
+ await this . handleRoomUpdate ( this . _stickyRoom . room , RoomUpdateCause . NewRoom ) ;
77
+ }
78
+ this . _stickyRoom = null ;
79
+ return ;
80
+ }
81
+
82
+ // When we do have a room though, we expect to be able to find it
83
+ const tag = this . roomIdsToTags [ val . roomId ] [ 0 ] ;
84
+ if ( ! tag ) throw new Error ( `${ val . roomId } does not belong to a tag and cannot be sticky` ) ;
85
+ let position = this . cachedRooms [ tag ] . indexOf ( val ) ;
86
+ if ( position < 0 ) throw new Error ( `${ val . roomId } does not appear to be known and cannot be sticky` ) ;
87
+
88
+ // 🐉 Here be dragons.
89
+ // Before we can go through with lying to the underlying algorithm about a room
90
+ // we need to ensure that when we do we're ready for the innevitable sticky room
91
+ // update we'll receive. To prepare for that, we first remove the sticky room and
92
+ // recalculate the state ourselves so that when the underlying algorithm calls for
93
+ // the same thing it no-ops. After we're done calling the algorithm, we'll issue
94
+ // a new update for ourselves.
95
+ const lastStickyRoom = this . _stickyRoom ;
96
+ console . log ( `Last sticky room:` , lastStickyRoom ) ;
97
+ this . _stickyRoom = null ;
98
+ this . recalculateStickyRoom ( ) ;
99
+
100
+ // When we do have the room, re-add the old room (if needed) to the algorithm
101
+ // and remove the sticky room from the algorithm. This is so the underlying
102
+ // algorithm doesn't try and confuse itself with the sticky room concept.
103
+ if ( lastStickyRoom ) {
104
+ // Lie to the algorithm and re-add the room to the algorithm
105
+ await this . handleRoomUpdate ( lastStickyRoom . room , RoomUpdateCause . NewRoom ) ;
106
+ }
107
+ // Lie to the algorithm and remove the room from it's field of view
108
+ await this . handleRoomUpdate ( val , RoomUpdateCause . RoomRemoved ) ;
109
+
110
+ // Now that we're done lying to the algorithm, we need to update our position
111
+ // marker only if the user is moving further down the same list. If they're switching
112
+ // lists, or moving upwards, the position marker will splice in just fine but if
113
+ // they went downwards in the same list we'll be off by 1 due to the shifting rooms.
114
+ if ( lastStickyRoom && lastStickyRoom . tag === tag && lastStickyRoom . position <= position ) {
115
+ position ++ ;
116
+ }
117
+
118
+ this . _stickyRoom = {
119
+ room : val ,
120
+ position : position ,
121
+ tag : tag ,
122
+ } ;
123
+ this . recalculateStickyRoom ( ) ;
124
+
125
+ // Finally, trigger an update
126
+ this . emit ( LIST_UPDATED_EVENT ) ;
127
+ } ) ( ) ;
128
+ }
129
+
54
130
protected get hasFilters ( ) : boolean {
55
131
return this . allowedByFilter . size > 0 ;
56
132
}
57
133
58
134
protected set cachedRooms ( val : ITagMap ) {
59
135
this . _cachedRooms = val ;
60
136
this . recalculateFilteredRooms ( ) ;
137
+ this . recalculateStickyRoom ( ) ;
61
138
}
62
139
63
140
protected get cachedRooms ( ) : ITagMap {
141
+ // 🐉 Here be dragons.
142
+ // Note: this is used by the underlying algorithm classes, so don't make it return
143
+ // the sticky room cache. If it ends up returning the sticky room cache, we end up
144
+ // corrupting our caches and confusing them.
64
145
return this . _cachedRooms ;
65
146
}
66
147
@@ -154,6 +235,59 @@ export abstract class Algorithm extends EventEmitter {
154
235
console . log ( `[DEBUG] ${ filteredRooms . length } /${ rooms . length } rooms filtered into ${ tagId } ` ) ;
155
236
}
156
237
238
+ /**
239
+ * Recalculate the sticky room position. If this is being called in relation to
240
+ * a specific tag being updated, it should be given to this function to optimize
241
+ * the call.
242
+ * @param updatedTag The tag that was updated, if possible.
243
+ */
244
+ protected recalculateStickyRoom ( updatedTag : TagID = null ) : void {
245
+ // 🐉 Here be dragons.
246
+ // This function does far too much for what it should, and is called by many places.
247
+ // Not only is this responsible for ensuring the sticky room is held in place at all
248
+ // times, it is also responsible for ensuring our clone of the cachedRooms is up to
249
+ // date. If either of these desyncs, we see weird behaviour like duplicated rooms,
250
+ // outdated lists, and other nonsensical issues that aren't necessarily obvious.
251
+
252
+ if ( ! this . _stickyRoom ) {
253
+ // If there's no sticky room, just do nothing useful.
254
+ if ( ! ! this . _cachedStickyRooms ) {
255
+ // Clear the cache if we won't be needing it
256
+ this . _cachedStickyRooms = null ;
257
+ this . emit ( LIST_UPDATED_EVENT ) ;
258
+ }
259
+ return ;
260
+ }
261
+
262
+ if ( ! this . _cachedStickyRooms || ! updatedTag ) {
263
+ console . log ( `Generating clone of cached rooms for sticky room handling` ) ;
264
+ const stickiedTagMap : ITagMap = { } ;
265
+ for ( const tagId of Object . keys ( this . cachedRooms ) ) {
266
+ stickiedTagMap [ tagId ] = this . cachedRooms [ tagId ] . map ( r => r ) ; // shallow clone
267
+ }
268
+ this . _cachedStickyRooms = stickiedTagMap ;
269
+ }
270
+
271
+ if ( updatedTag ) {
272
+ // Update the tag indicated by the caller, if possible. This is mostly to ensure
273
+ // our cache is up to date.
274
+ console . log ( `Replacing cached sticky rooms for ${ updatedTag } ` ) ;
275
+ this . _cachedStickyRooms [ updatedTag ] = this . cachedRooms [ updatedTag ] . map ( r => r ) ; // shallow clone
276
+ }
277
+
278
+ // Now try to insert the sticky room, if we need to.
279
+ // We need to if there's no updated tag (we regenned the whole cache) or if the tag
280
+ // we might have updated from the cache is also our sticky room.
281
+ const sticky = this . _stickyRoom ;
282
+ if ( ! updatedTag || updatedTag === sticky . tag ) {
283
+ console . log ( `Inserting sticky room ${ sticky . room . roomId } at position ${ sticky . position } in ${ sticky . tag } ` ) ;
284
+ this . _cachedStickyRooms [ sticky . tag ] . splice ( sticky . position , 0 , sticky . room ) ;
285
+ }
286
+
287
+ // Finally, trigger an update
288
+ this . emit ( LIST_UPDATED_EVENT ) ;
289
+ }
290
+
157
291
/**
158
292
* Asks the Algorithm to regenerate all lists, using the tags given
159
293
* as reference for which lists to generate and which way to generate
@@ -174,7 +308,7 @@ export abstract class Algorithm extends EventEmitter {
174
308
*/
175
309
public getOrderedRooms ( ) : ITagMap {
176
310
if ( ! this . hasFilters ) {
177
- return this . cachedRooms ;
311
+ return this . _cachedStickyRooms || this . cachedRooms ;
178
312
}
179
313
return this . filteredRooms ;
180
314
}
0 commit comments