66 * running after the client is "stopped", leaking network traffic and
77 * event listeners.
88 *
9- * 2. pruneRoomTimeline (via unsubscribeFromRoom) — when a room is explicitly
10- * unsubscribed (e.g. user leaves the room), its in-memory event chain is
11- * released if it exceeds PRUNE_TIMELINE_THRESHOLD. The recent tail is
12- * persisted to IndexedDB (via store.setSyncData + store.save) so the events
13- * survive an app reload; the full history is always available from the server.
9+ * 2. onMembershipLeave — when the MatrixClient emits a RoomMemberEvent.Membership
10+ * event indicating the local user left or was banned from a room that is
11+ * actively subscribed, unsubscribeFromRoom() should be called automatically.
1412 *
15- * Note: navigation between rooms no longer calls unsubscribeFromRoom —
13+ * Note: navigation between rooms does not call unsubscribeFromRoom —
1614 * subscriptions accumulate across the session so returning to a room is
1715 * instant (matching Element Web's model).
18- *
19- * 3. onMembershipLeave — when the MatrixClient emits a RoomMemberEvent.Membership
20- * event indicating the local user left or was banned from a room that is
21- * actively subscribed, unsubscribeFromRoom() should be called automatically.
2216 */
2317import { describe , it , expect , vi , beforeEach } from 'vitest' ;
2418import { SlidingSyncManager , type SlidingSyncConfig } from './slidingSync' ;
2519
26- /** Flush all pending Promise microtasks. */
27- const flushPromises = ( ) : Promise < void > =>
28- new Promise ( ( r ) => {
29- setTimeout ( r , 0 ) ;
30- } ) ;
31-
3220// ── vi.hoisted mocks ─────────────────────────────────────────────────────────
3321// Must be defined via vi.hoisted so they're available before vi.mock runs
3422// (vi.mock calls are hoisted above all imports by vitest's transformer).
@@ -86,33 +74,10 @@ function makeMockMx(overrides: Record<string, unknown> = {}) {
8674 on : vi . fn ( ) ,
8775 off : vi . fn ( ) ,
8876 removeListener : vi . fn ( ) ,
89- store : {
90- setSyncData : vi . fn ( ) . mockResolvedValue ( undefined ) ,
91- save : vi . fn ( ) . mockResolvedValue ( undefined ) ,
92- removeRoom : vi . fn ( ) ,
93- } ,
9477 ...overrides ,
9578 } as unknown as import ( '$types/matrix-sdk' ) . MatrixClient ;
9679}
9780
98- function makeMockRoom ( eventCount : number ) {
99- const events = Array . from ( { length : eventCount } , ( _ , i ) => ( {
100- getId : ( ) => `$ev${ i } ` ,
101- event : { event_id : `$ev${ i } ` , type : 'm.room.message' , content : { } } ,
102- } ) ) ;
103- const resetLiveTimeline = vi . fn ( ) ;
104- return {
105- getUnfilteredTimelineSet : vi . fn ( ) . mockReturnValue ( {
106- getLiveTimeline : vi . fn ( ) . mockReturnValue ( {
107- getEvents : vi . fn ( ) . mockReturnValue ( events ) ,
108- getPaginationToken : vi . fn ( ) . mockReturnValue ( 't123' ) ,
109- } ) ,
110- resetLiveTimeline,
111- } ) ,
112- _resetLiveTimeline : resetLiveTimeline ,
113- } ;
114- }
115-
11681function makeManager ( mx : ReturnType < typeof makeMockMx > ) : SlidingSyncManager {
11782 const config : SlidingSyncConfig = { } ;
11883 return new SlidingSyncManager ( mx , 'https://sliding.example.com' , config ) ;
@@ -132,90 +97,6 @@ describe('SlidingSyncManager.dispose()', () => {
13297 } ) ;
13398} ) ;
13499
135- // ── pruneRoomTimeline (exercised via unsubscribeFromRoom) ────────────────────
136-
137- // This value must match PRUNE_TIMELINE_THRESHOLD in slidingSync.ts.
138- const PRUNE_THRESHOLD = 100 ;
139-
140- describe ( 'SlidingSyncManager — timeline pruning on unsubscribe' , ( ) => {
141- it ( 'resets the live timeline when event count exceeds the threshold' , ( ) => {
142- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
143- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
144- const manager = makeManager ( mx ) ;
145-
146- manager . unsubscribeFromRoom ( '!room:example.com' ) ;
147-
148- expect ( room . _resetLiveTimeline ) . toHaveBeenCalledOnce ( ) ;
149- } ) ;
150-
151- it ( 'persists events to store.setSyncData and store.save after pruning' , async ( ) => {
152- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
153- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
154- const manager = makeManager ( mx ) ;
155-
156- manager . unsubscribeFromRoom ( '!room:example.com' ) ;
157- await flushPromises ( ) ;
158-
159- const { store } = mx as unknown as {
160- store : { setSyncData : ReturnType < typeof vi . fn > ; save : ReturnType < typeof vi . fn > } ;
161- } ;
162- expect ( store . setSyncData ) . toHaveBeenCalledOnce ( ) ;
163- expect ( store . save ) . toHaveBeenCalledWith ( true ) ;
164-
165- // The payload must target the correct room and use limited:true so the
166- // accumulator replaces any stale timeline rather than appending.
167- const [ payload ] = store . setSyncData . mock . calls [ 0 ] as [
168- { rooms : { join : Record < string , { timeline : { limited : boolean } } > } } ,
169- ] ;
170- expect ( payload . rooms . join [ '!room:example.com' ] . timeline . limited ) . toBe ( true ) ;
171- } ) ;
172-
173- it ( 'evicts room from store cache when persist fails' , async ( ) => {
174- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
175- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
176- // Make setSyncData reject to simulate an IndexedDB write failure.
177- (
178- mx as unknown as { store : { setSyncData : ReturnType < typeof vi . fn > } }
179- ) . store . setSyncData . mockRejectedValue ( new Error ( 'IndexedDB write failed' ) ) ;
180- const manager = makeManager ( mx ) ;
181-
182- manager . unsubscribeFromRoom ( '!room:example.com' ) ;
183- await flushPromises ( ) ;
184-
185- const { store } = mx as unknown as {
186- store : { removeRoom : ReturnType < typeof vi . fn > } ;
187- } ;
188- expect ( store . removeRoom ) . toHaveBeenCalledWith ( '!room:example.com' ) ;
189- } ) ;
190-
191- it ( 'does not reset when event count equals the threshold exactly' , ( ) => {
192- const room = makeMockRoom ( PRUNE_THRESHOLD ) ;
193- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
194- const manager = makeManager ( mx ) ;
195-
196- manager . unsubscribeFromRoom ( '!room:example.com' ) ;
197-
198- expect ( room . _resetLiveTimeline ) . not . toHaveBeenCalled ( ) ;
199- } ) ;
200-
201- it ( 'does not reset for rooms with very few events' , ( ) => {
202- const room = makeMockRoom ( 5 ) ;
203- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
204- const manager = makeManager ( mx ) ;
205-
206- manager . unsubscribeFromRoom ( '!room:example.com' ) ;
207-
208- expect ( room . _resetLiveTimeline ) . not . toHaveBeenCalled ( ) ;
209- } ) ;
210-
211- it ( 'does not throw when the room is not yet in SDK state' , ( ) => {
212- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( null ) } ) ;
213- const manager = makeManager ( mx ) ;
214-
215- expect ( ( ) => manager . unsubscribeFromRoom ( '!room:example.com' ) ) . not . toThrow ( ) ;
216- } ) ;
217- } ) ;
218-
219100// ── onMembershipLeave: auto-unsubscribe on leave/ban ─────────────────────────
220101
221102describe ( 'SlidingSyncManager — membership leave auto-unsubscribe' , ( ) => {
@@ -238,62 +119,58 @@ describe('SlidingSyncManager — membership leave auto-unsubscribe', () => {
238119 }
239120
240121 it ( 'unsubscribes when the local user leaves an active room' , ( ) => {
241- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
242- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
122+ const mx = makeMockMx ( ) ;
243123 const manager = makeManager ( mx ) ;
244124 manager . attach ( ) ;
245- // Manually add to active subscriptions to simulate having visited the room.
246125 manager . subscribeToRoom ( '!room:example.com' ) ;
247126
248127 fireMembershipEvent ( mx , 'leave' ) ;
249128
250- expect ( room . _resetLiveTimeline ) . toHaveBeenCalledOnce ( ) ;
129+ // subscribeToRoom + unsubscribeFromRoom = 2 calls
130+ expect ( mocks . slidingSyncInstance . modifyRoomSubscriptions ) . toHaveBeenCalledTimes ( 2 ) ;
251131 } ) ;
252132
253133 it ( 'unsubscribes when the local user is banned from an active room' , ( ) => {
254- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
255- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
134+ const mx = makeMockMx ( ) ;
256135 const manager = makeManager ( mx ) ;
257136 manager . attach ( ) ;
258137 manager . subscribeToRoom ( '!room:example.com' ) ;
259138
260139 fireMembershipEvent ( mx , 'ban' ) ;
261140
262- expect ( room . _resetLiveTimeline ) . toHaveBeenCalledOnce ( ) ;
141+ expect ( mocks . slidingSyncInstance . modifyRoomSubscriptions ) . toHaveBeenCalledTimes ( 2 ) ;
263142 } ) ;
264143
265144 it ( 'does nothing when a different user leaves' , ( ) => {
266- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
267- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
145+ const mx = makeMockMx ( ) ;
268146 const manager = makeManager ( mx ) ;
269147 manager . attach ( ) ;
270148 manager . subscribeToRoom ( '!room:example.com' ) ;
271149
272150 fireMembershipEvent ( mx , 'leave' , '!room:example.com' , '@other:example.com' ) ;
273151
274- expect ( room . _resetLiveTimeline ) . not . toHaveBeenCalled ( ) ;
152+ // Only the initial subscribe — no unsubscribe
153+ expect ( mocks . slidingSyncInstance . modifyRoomSubscriptions ) . toHaveBeenCalledTimes ( 1 ) ;
275154 } ) ;
276155
277156 it ( 'does nothing when membership is join' , ( ) => {
278- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
279- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
157+ const mx = makeMockMx ( ) ;
280158 const manager = makeManager ( mx ) ;
281159 manager . attach ( ) ;
282160 manager . subscribeToRoom ( '!room:example.com' ) ;
283161
284162 fireMembershipEvent ( mx , 'join' ) ;
285163
286- expect ( room . _resetLiveTimeline ) . not . toHaveBeenCalled ( ) ;
164+ expect ( mocks . slidingSyncInstance . modifyRoomSubscriptions ) . toHaveBeenCalledTimes ( 1 ) ;
287165 } ) ;
288166
289167 it ( 'does nothing for a room that was never subscribed' , ( ) => {
290- const room = makeMockRoom ( PRUNE_THRESHOLD + 1 ) ;
291- const mx = makeMockMx ( { getRoom : vi . fn ( ) . mockReturnValue ( room ) } ) ;
168+ const mx = makeMockMx ( ) ;
292169 const manager = makeManager ( mx ) ;
293170 manager . attach ( ) ; // registers the listener, but no subscribeToRoom call
294171
295172 fireMembershipEvent ( mx , 'leave' ) ;
296173
297- expect ( room . _resetLiveTimeline ) . not . toHaveBeenCalled ( ) ;
174+ expect ( mocks . slidingSyncInstance . modifyRoomSubscriptions ) . not . toHaveBeenCalled ( ) ;
298175 } ) ;
299176} ) ;
0 commit comments