@@ -25,7 +25,7 @@ import {
2525import { Direction , EventTimeline } from "./event-timeline.ts" ;
2626import { getHttpUriForMxc } from "../content-repo.ts" ;
2727import * as utils from "../utils.ts" ;
28- import { normalize , noUnsafeEventProps , removeElement } from "../utils.ts" ;
28+ import { normalize , noUnsafeEventProps , removeElement , MultiKeyMap } from "../utils.ts" ;
2929import {
3030 type IEvent ,
3131 type IThreadBundledRelationship ,
@@ -312,7 +312,19 @@ export type RoomEventHandlerMap = {
312312 * @param summary - the room summary object
313313 */
314314 [ RoomEvent . Summary ] : ( summary : IRoomSummary ) => void ;
315- [ RoomEvent . StickyEvents ] : ( stickyEventMap : Map < string , MatrixEvent > , room : Room ) => void ;
315+ /**
316+ * Fires when sticky events are updated for a room.
317+ * For a list of all updated events use:
318+ * `const updated = added.filter(e => removed.includes(e));`
319+ * for a list of all new events use:
320+ * `const addedNew = added.filter(e => !removed.includes(e));`
321+ * for a list of all removed events use:
322+ * `const removedOnly = removed.filter(e => !added.includes(e));`
323+ * @param added - The events that were added to the map of sticky events (can be updated events for existing keys or new keys)
324+ * @param removed - The events that were removed from the map of sticky events (caused by expiration or updated keys)
325+ * @param room - The room containing the sticky events
326+ */
327+ [ RoomEvent . StickyEvents ] : ( added : MatrixEvent [ ] , removed : MatrixEvent [ ] , room : Room ) => void ;
316328 [ ThreadEvent . New ] : ( thread : Thread , toStartOfTimeline : boolean ) => void ;
317329 /**
318330 * Fires when a new poll instance is added to the room state
@@ -378,7 +390,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
378390 private getTypeWarning = false ;
379391 private membersPromise ?: Promise < boolean > ;
380392
381- private stickyEventsMap = new Map < string , MatrixEvent > ( ) ;
393+ private stickyEventsMap = new MultiKeyMap < MatrixEvent & { event : { msc4354_sticky : { duration_ms : number } } } > ( ) ;
394+
382395 // XXX: These should be read-only
383396 /**
384397 * The human-readable display name for this room.
@@ -3417,24 +3430,56 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
34173430 return this . accountData . get ( type ) ;
34183431 }
34193432
3420- public getStickyEventsMap ( ) : Map < string , MatrixEvent > {
3421- return this . stickyEventsMap ;
3433+ public getStickyEvents ( ) : MapIterator < MatrixEvent > {
3434+ return this . stickyEventsMap . values ( ) ;
34223435 }
34233436
34243437 public addStickyEvents ( event : MatrixEvent [ ] ) : void {
3438+ const added = [ ] ;
3439+ const removed = [ ] ;
34253440 for ( const e of event ) {
3426- const stickyKey = Room . stickyKey ( e ) ;
3427- if ( ! stickyKey ) {
3441+ const mapStickyKey = Room . stickyKey ( e ) ;
3442+ if ( ! mapStickyKey ) {
34283443 logger . warn ( "Event from sticky sync section is missing sticky_key, ignoring" , e ) ;
34293444 continue ;
34303445 }
3431- if ( "sticky" in e . event ) {
3446+ // With this we have the guarantee, that all events in stickyEventsMap are correctly formatted
3447+ if (
3448+ ! (
3449+ e . event . msc4354_sticky &&
3450+ "duration_ms" in e . event . msc4354_sticky &&
3451+ e . event . msc4354_sticky . duration_ms !== undefined &&
3452+ typeof e . event . msc4354_sticky . duration_ms === "number"
3453+ )
3454+ ) {
34323455 logger . warn ( "Event from sticky sync section is missing sticky timeout" , e ) ;
34333456 continue ;
34343457 }
3435- this . stickyEventsMap . set ( stickyKey , e ) ;
3436- this . emit ( RoomEvent . StickyEvents , this . stickyEventsMap , this ) ;
3458+ const ev = e as MatrixEvent & { event : { msc4354_sticky : { duration_ms : number } } } ;
3459+ const prevEv = this . stickyEventsMap . get ( mapStickyKey ) ;
3460+ const sender = e . getSender ( ) ;
3461+ if ( ! sender ) {
3462+ logger . warn ( "Event from sticky sync section is missing sender" , e ) ;
3463+ continue ;
3464+ } else if ( prevEv && ev . getTs ( ) < prevEv . getTs ( ) ) {
3465+ logger . info (
3466+ "ignored sticky event with older timestamp for on sticky_key" ,
3467+ ev . getContent ( ) . sticky_key ,
3468+ ev ,
3469+ ) ;
3470+ } else if ( prevEv && ev . getTs ( ) === prevEv . getTs ( ) && ( ev . getId ( ) ?? "" ) < ( prevEv . getId ( ) ?? "" ) ) {
3471+ logger . info (
3472+ "ignored sticky event due to 'id tie break rule' on sticky_key" ,
3473+ ev . getContent ( ) . sticky_key ,
3474+ ev ,
3475+ ) ;
3476+ } else {
3477+ added . push ( ev ) ;
3478+ if ( prevEv ) removed . push ( prevEv ) ;
3479+ this . stickyEventsMap . set ( mapStickyKey , ev ) ;
3480+ }
34373481 }
3482+ if ( added . length ) this . emit ( RoomEvent . StickyEvents , added , removed , this ) ;
34383483 this . checkStickyTimer ( ) ;
34393484 }
34403485 private stickyEventTimer ?: NodeJS . Timeout ;
@@ -3454,7 +3499,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
34543499 private getNextStickyExpiryTs ( ) : number | null {
34553500 let nextExpiry = null ;
34563501 for ( const event of this . stickyEventsMap . values ( ) ) {
3457- const expiry = event . getTs ( ) + event . event . msc4354_sticky ! ;
3502+ const expiry = event . getTs ( ) + event . event . msc4354_sticky . duration_ms ;
34583503 if ( nextExpiry === null || expiry < nextExpiry ) {
34593504 nextExpiry = expiry ;
34603505 }
@@ -3463,29 +3508,31 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
34633508 }
34643509 private cleanExpiredStickyEvents ( ) : void {
34653510 const now = Date . now ( ) ;
3466- const toRemove : string [ ] = [ ] ;
3511+ const toRemove : string [ ] [ ] = [ ] ;
3512+ const removedEvents : MatrixEvent [ ] = [ ] ;
34673513 for ( const [ key , event ] of this . stickyEventsMap . entries ( ) ) {
34683514 const creationTs = event . getTs ( ) ;
34693515 // we only added items with `sticky` into this map so we can assert non-null here
3470- if ( now > creationTs + event . event . msc4354_sticky ! ) {
3516+ if ( now > creationTs + event . event . msc4354_sticky . duration_ms ) {
34713517 toRemove . push ( key ) ;
3518+ removedEvents . push ( event ) ;
34723519 }
34733520 }
34743521 for ( const key of toRemove ) {
34753522 this . stickyEventsMap . delete ( key ) ;
34763523 }
3477- if ( toRemove . length ) {
3478- this . emit ( RoomEvent . StickyEvents , this . stickyEventsMap , this ) ;
3524+ if ( removedEvents . length ) {
3525+ this . emit ( RoomEvent . StickyEvents , [ ] , removedEvents , this ) ;
34793526 }
34803527 this . checkStickyTimer ( ) ;
34813528 }
34823529
3483- private static stickyKey ( event : MatrixEvent ) : string | undefined {
3484- const k = event . getContent ( ) . sticky_key ;
3485- if ( ! k ) {
3530+ private static stickyKey ( event : MatrixEvent ) : string [ ] | undefined {
3531+ const stickyKey = event . getContent ( ) . sticky_key ;
3532+ if ( ! stickyKey ) {
34863533 return undefined ;
34873534 }
3488- return event . getSender ( ) + event . getType ( ) + k ;
3535+ return [ event . getSender ( ) , event . getType ( ) , stickyKey ] ;
34893536 }
34903537
34913538 /**
0 commit comments