@@ -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,30 @@ 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
+ // setters can't be async, so we call a private function to do the work
69
+ this . updateStickyRoom ( val ) ;
70
+ }
71
+
54
72
protected get hasFilters ( ) : boolean {
55
73
return this . allowedByFilter . size > 0 ;
56
74
}
57
75
58
76
protected set cachedRooms ( val : ITagMap ) {
59
77
this . _cachedRooms = val ;
60
78
this . recalculateFilteredRooms ( ) ;
79
+ this . recalculateStickyRoom ( ) ;
61
80
}
62
81
63
82
protected get cachedRooms ( ) : ITagMap {
83
+ // 🐉 Here be dragons.
84
+ // Note: this is used by the underlying algorithm classes, so don't make it return
85
+ // the sticky room cache. If it ends up returning the sticky room cache, we end up
86
+ // corrupting our caches and confusing them.
64
87
return this . _cachedRooms ;
65
88
}
66
89
@@ -94,6 +117,67 @@ export abstract class Algorithm extends EventEmitter {
94
117
}
95
118
}
96
119
120
+ private async updateStickyRoom ( val : Room ) {
121
+ // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
122
+ // otherwise we risk duplicating rooms.
123
+
124
+ // It's possible to have no selected room. In that case, clear the sticky room
125
+ if ( ! val ) {
126
+ if ( this . _stickyRoom ) {
127
+ // Lie to the algorithm and re-add the room to the algorithm
128
+ await this . handleRoomUpdate ( this . _stickyRoom . room , RoomUpdateCause . NewRoom ) ;
129
+ }
130
+ this . _stickyRoom = null ;
131
+ return ;
132
+ }
133
+
134
+ // When we do have a room though, we expect to be able to find it
135
+ const tag = this . roomIdsToTags [ val . roomId ] [ 0 ] ;
136
+ if ( ! tag ) throw new Error ( `${ val . roomId } does not belong to a tag and cannot be sticky` ) ;
137
+ let position = this . cachedRooms [ tag ] . indexOf ( val ) ;
138
+ if ( position < 0 ) throw new Error ( `${ val . roomId } does not appear to be known and cannot be sticky` ) ;
139
+
140
+ // 🐉 Here be dragons.
141
+ // Before we can go through with lying to the underlying algorithm about a room
142
+ // we need to ensure that when we do we're ready for the innevitable sticky room
143
+ // update we'll receive. To prepare for that, we first remove the sticky room and
144
+ // recalculate the state ourselves so that when the underlying algorithm calls for
145
+ // the same thing it no-ops. After we're done calling the algorithm, we'll issue
146
+ // a new update for ourselves.
147
+ const lastStickyRoom = this . _stickyRoom ;
148
+ console . log ( `Last sticky room:` , lastStickyRoom ) ;
149
+ this . _stickyRoom = null ;
150
+ this . recalculateStickyRoom ( ) ;
151
+
152
+ // When we do have the room, re-add the old room (if needed) to the algorithm
153
+ // and remove the sticky room from the algorithm. This is so the underlying
154
+ // algorithm doesn't try and confuse itself with the sticky room concept.
155
+ if ( lastStickyRoom ) {
156
+ // Lie to the algorithm and re-add the room to the algorithm
157
+ await this . handleRoomUpdate ( lastStickyRoom . room , RoomUpdateCause . NewRoom ) ;
158
+ }
159
+ // Lie to the algorithm and remove the room from it's field of view
160
+ await this . handleRoomUpdate ( val , RoomUpdateCause . RoomRemoved ) ;
161
+
162
+ // Now that we're done lying to the algorithm, we need to update our position
163
+ // marker only if the user is moving further down the same list. If they're switching
164
+ // lists, or moving upwards, the position marker will splice in just fine but if
165
+ // they went downwards in the same list we'll be off by 1 due to the shifting rooms.
166
+ if ( lastStickyRoom && lastStickyRoom . tag === tag && lastStickyRoom . position <= position ) {
167
+ position ++ ;
168
+ }
169
+
170
+ this . _stickyRoom = {
171
+ room : val ,
172
+ position : position ,
173
+ tag : tag ,
174
+ } ;
175
+ this . recalculateStickyRoom ( ) ;
176
+
177
+ // Finally, trigger an update
178
+ this . emit ( LIST_UPDATED_EVENT ) ;
179
+ }
180
+
97
181
protected recalculateFilteredRooms ( ) {
98
182
if ( ! this . hasFilters ) {
99
183
return ;
@@ -154,6 +238,59 @@ export abstract class Algorithm extends EventEmitter {
154
238
console . log ( `[DEBUG] ${ filteredRooms . length } /${ rooms . length } rooms filtered into ${ tagId } ` ) ;
155
239
}
156
240
241
+ /**
242
+ * Recalculate the sticky room position. If this is being called in relation to
243
+ * a specific tag being updated, it should be given to this function to optimize
244
+ * the call.
245
+ * @param updatedTag The tag that was updated, if possible.
246
+ */
247
+ protected recalculateStickyRoom ( updatedTag : TagID = null ) : void {
248
+ // 🐉 Here be dragons.
249
+ // This function does far too much for what it should, and is called by many places.
250
+ // Not only is this responsible for ensuring the sticky room is held in place at all
251
+ // times, it is also responsible for ensuring our clone of the cachedRooms is up to
252
+ // date. If either of these desyncs, we see weird behaviour like duplicated rooms,
253
+ // outdated lists, and other nonsensical issues that aren't necessarily obvious.
254
+
255
+ if ( ! this . _stickyRoom ) {
256
+ // If there's no sticky room, just do nothing useful.
257
+ if ( ! ! this . _cachedStickyRooms ) {
258
+ // Clear the cache if we won't be needing it
259
+ this . _cachedStickyRooms = null ;
260
+ this . emit ( LIST_UPDATED_EVENT ) ;
261
+ }
262
+ return ;
263
+ }
264
+
265
+ if ( ! this . _cachedStickyRooms || ! updatedTag ) {
266
+ console . log ( `Generating clone of cached rooms for sticky room handling` ) ;
267
+ const stickiedTagMap : ITagMap = { } ;
268
+ for ( const tagId of Object . keys ( this . cachedRooms ) ) {
269
+ stickiedTagMap [ tagId ] = this . cachedRooms [ tagId ] . map ( r => r ) ; // shallow clone
270
+ }
271
+ this . _cachedStickyRooms = stickiedTagMap ;
272
+ }
273
+
274
+ if ( updatedTag ) {
275
+ // Update the tag indicated by the caller, if possible. This is mostly to ensure
276
+ // our cache is up to date.
277
+ console . log ( `Replacing cached sticky rooms for ${ updatedTag } ` ) ;
278
+ this . _cachedStickyRooms [ updatedTag ] = this . cachedRooms [ updatedTag ] . map ( r => r ) ; // shallow clone
279
+ }
280
+
281
+ // Now try to insert the sticky room, if we need to.
282
+ // We need to if there's no updated tag (we regenned the whole cache) or if the tag
283
+ // we might have updated from the cache is also our sticky room.
284
+ const sticky = this . _stickyRoom ;
285
+ if ( ! updatedTag || updatedTag === sticky . tag ) {
286
+ console . log ( `Inserting sticky room ${ sticky . room . roomId } at position ${ sticky . position } in ${ sticky . tag } ` ) ;
287
+ this . _cachedStickyRooms [ sticky . tag ] . splice ( sticky . position , 0 , sticky . room ) ;
288
+ }
289
+
290
+ // Finally, trigger an update
291
+ this . emit ( LIST_UPDATED_EVENT ) ;
292
+ }
293
+
157
294
/**
158
295
* Asks the Algorithm to regenerate all lists, using the tags given
159
296
* as reference for which lists to generate and which way to generate
@@ -174,7 +311,7 @@ export abstract class Algorithm extends EventEmitter {
174
311
*/
175
312
public getOrderedRooms ( ) : ITagMap {
176
313
if ( ! this . hasFilters ) {
177
- return this . cachedRooms ;
314
+ return this . _cachedStickyRooms || this . cachedRooms ;
178
315
}
179
316
return this . filteredRooms ;
180
317
}
0 commit comments