Skip to content

Commit 437943a

Browse files
authored
Merge pull request #13736 from nextcloud/fix/noid/event-bus-once-garbage
2 parents f47d21c + a61b1cb commit 437943a

File tree

4 files changed

+330
-2
lines changed

4 files changed

+330
-2
lines changed

src/App.vue

+5
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ export default {
192192
unsubscribe('notifications:action:execute', this.interceptNotificationActions)
193193

194194
window.removeEventListener('beforeunload', this.preventUnload)
195+
196+
EventBus.off('joined-conversation')
197+
EventBus.off('switch-to-conversation')
198+
EventBus.off('conversations-received')
199+
EventBus.off('forbidden-route')
195200
},
196201

197202
beforeMount() {

src/components/LeftSidebar/LeftSidebar.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ export default {
675675
this.debounceHandleScroll.clear?.()
676676

677677
EventBus.off('should-refresh-conversations', this.handleShouldRefreshConversations)
678-
EventBus.off('conversations-received', this.handleUnreadMention)
678+
EventBus.off('conversations-received', this.handleConversationsReceived)
679679
EventBus.off('route-change', this.onRouteChange)
680680

681681
this.cancelSearchPossibleConversations()

src/services/EventBus.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ type GenericEventHandler = Handler<Events[keyof Events]> | WildcardHandler<Event
1111
type ExtendedEmitter = Emitter<Events> & {
1212
once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void
1313
once(type: '*', handler: WildcardHandler<Events>): void
14+
_onceHandlers: Map<keyof Events | '*', Map<GenericEventHandler, GenericEventHandler>>
1415
}
16+
1517
export const EventBus: ExtendedEmitter = mitt() as ExtendedEmitter
1618

19+
EventBus._onceHandlers = new Map()
20+
1721
/**
1822
* Register a one-time event handler for the given type
1923
*
@@ -27,7 +31,43 @@ EventBus.once = function<Key extends keyof Events>(type: Key, handler: GenericEv
2731
const fn = (...args: Parameters<GenericEventHandler>) => {
2832
// @ts-expect-error: Vue: A spread argument must either have a tuple type or be passed to a rest parameter.
2933
handler(...args)
30-
this.off(type, fn)
34+
// @ts-expect-error: Vue: No overload matches this call.
35+
this.off(type, handler)
3136
}
3237
this.on(type, fn)
38+
39+
// Store reference to the original handler to be able to remove it later
40+
if (!EventBus._onceHandlers.has(type)) {
41+
EventBus._onceHandlers.set(type, new Map())
42+
}
43+
EventBus._onceHandlers.get(type)!.set(handler, fn)
44+
}
45+
46+
const off = EventBus.off.bind(EventBus)
47+
/**
48+
* OVERRIDING OF ORIGINAL MITT FUNCTION
49+
* Remove an event handler for the given type.
50+
* If `handler` is omitted, all handlers of the given type are removed.
51+
* @param type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
52+
* @param [handler] Handler function to remove
53+
*/
54+
EventBus.off = function<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
55+
// @ts-expect-error: Vue: No overload matches this call
56+
off(type, handler)
57+
58+
if (!handler) {
59+
EventBus._onceHandlers.delete(type)
60+
return
61+
}
62+
63+
const typeOnceHandlers = EventBus._onceHandlers.get(type)
64+
const onceHandler = typeOnceHandlers?.get(handler)
65+
if (onceHandler) {
66+
typeOnceHandlers!.delete(handler)
67+
if (!typeOnceHandlers!.size) {
68+
EventBus._onceHandlers.delete(type)
69+
}
70+
// @ts-expect-error: Vue: No overload matches this call
71+
off(type, onceHandler)
72+
}
3373
}
+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import { EventBus } from '../EventBus.ts'
6+
7+
describe('EventBus', () => {
8+
const customEvent1 = jest.fn()
9+
const customEvent2 = jest.fn()
10+
const customEvent3 = jest.fn()
11+
const customEventOnce1 = jest.fn()
12+
const customEventOnce2 = jest.fn()
13+
const customEventOnce3 = jest.fn()
14+
15+
const testEventBus = (type, handlers, onceHandlers) => {
16+
expect(EventBus.all.get(type).length).toBe(handlers)
17+
if (!onceHandlers) {
18+
expect(EventBus._onceHandlers.get(type)).toBeUndefined()
19+
} else {
20+
expect(EventBus._onceHandlers.get(type).size).toBe(onceHandlers)
21+
}
22+
}
23+
24+
afterEach(() => {
25+
EventBus.all.clear()
26+
jest.clearAllMocks()
27+
})
28+
29+
describe('on and off', () => {
30+
it('should emit and listen to custom events', () => {
31+
// Arrange
32+
EventBus.on('custom-event', customEvent1)
33+
EventBus.on('custom-event', customEvent2)
34+
35+
// Act
36+
EventBus.emit('custom-event')
37+
38+
// Assert
39+
testEventBus('custom-event', 2)
40+
expect(customEvent1).toHaveBeenCalledTimes(1)
41+
expect(customEvent2).toHaveBeenCalledTimes(1)
42+
})
43+
44+
it('should emit and listen to custom events with wildcard * ', () => {
45+
// Arrange
46+
EventBus.on('*', customEvent1)
47+
EventBus.on('*', customEvent2)
48+
49+
// Act
50+
EventBus.emit('custom-event-1')
51+
52+
// Assert
53+
testEventBus('*', 2)
54+
expect(customEvent1).toHaveBeenCalledTimes(1)
55+
expect(customEvent2).toHaveBeenCalledTimes(1)
56+
})
57+
58+
it('should remove listeners by given type and handler', () => {
59+
// Arrange
60+
EventBus.on('custom-event', customEvent1)
61+
EventBus.on('custom-event', customEvent2)
62+
EventBus.emit('custom-event')
63+
testEventBus('custom-event', 2)
64+
65+
// Act
66+
EventBus.off('custom-event', customEvent1)
67+
EventBus.emit('custom-event')
68+
69+
// Assert
70+
testEventBus('custom-event', 1)
71+
expect(customEvent1).toHaveBeenCalledTimes(1)
72+
expect(customEvent2).toHaveBeenCalledTimes(2)
73+
})
74+
75+
it('should remove listeners by wildcard * and handler', () => {
76+
// Arrange
77+
EventBus.on('custom-event-1', customEvent1)
78+
EventBus.on('custom-event-2', customEvent2)
79+
EventBus.on('*', customEvent3)
80+
EventBus.emit('custom-event-1')
81+
EventBus.emit('custom-event-2')
82+
testEventBus('custom-event-1', 1)
83+
testEventBus('custom-event-2', 1)
84+
testEventBus('*', 1)
85+
expect(customEvent3).toHaveBeenCalledTimes(2)
86+
87+
// Act
88+
EventBus.off('*', customEvent3)
89+
EventBus.emit('custom-event-1')
90+
EventBus.emit('custom-event-2')
91+
92+
// Assert
93+
testEventBus('custom-event-1', 1)
94+
testEventBus('custom-event-2', 1)
95+
testEventBus('*', 0)
96+
expect(customEvent1).toHaveBeenCalledTimes(2)
97+
expect(customEvent2).toHaveBeenCalledTimes(2)
98+
expect(customEvent3).toHaveBeenCalledTimes(2)
99+
})
100+
101+
it('should remove listeners by given type only', () => {
102+
// Arrange
103+
EventBus.on('custom-event', customEvent1)
104+
EventBus.on('custom-event', customEvent2)
105+
EventBus.emit('custom-event')
106+
testEventBus('custom-event', 2)
107+
108+
// Act
109+
EventBus.off('custom-event')
110+
EventBus.emit('custom-event')
111+
112+
// Assert
113+
testEventBus('custom-event', 0)
114+
expect(customEvent1).toHaveBeenCalledTimes(1)
115+
expect(customEvent2).toHaveBeenCalledTimes(1)
116+
})
117+
118+
it('should remove listeners by wildcard * only', () => {
119+
// Arrange
120+
EventBus.on('custom-event-1', customEvent1)
121+
EventBus.on('custom-event-2', customEvent2)
122+
EventBus.on('*', customEvent3)
123+
EventBus.emit('custom-event-1')
124+
EventBus.emit('custom-event-2')
125+
testEventBus('custom-event-1', 1)
126+
testEventBus('custom-event-2', 1)
127+
testEventBus('*', 1)
128+
expect(customEvent3).toHaveBeenCalledTimes(2)
129+
130+
// Act
131+
EventBus.off('*')
132+
EventBus.emit('custom-event-1')
133+
EventBus.emit('custom-event-2')
134+
135+
// Assert
136+
testEventBus('custom-event-1', 1)
137+
testEventBus('custom-event-2', 1)
138+
testEventBus('*', 0)
139+
expect(customEvent1).toHaveBeenCalledTimes(2)
140+
expect(customEvent2).toHaveBeenCalledTimes(2)
141+
expect(customEvent3).toHaveBeenCalledTimes(2)
142+
})
143+
})
144+
145+
describe('once and off', () => {
146+
it('should emit and listen to custom events', () => {
147+
// Arrange
148+
EventBus.on('custom-event', customEvent1)
149+
EventBus.once('custom-event', customEventOnce1)
150+
EventBus.once('custom-event', customEventOnce2)
151+
testEventBus('custom-event', 3, 2)
152+
153+
// Act
154+
EventBus.emit('custom-event')
155+
EventBus.emit('custom-event')
156+
157+
// Assert
158+
expect(customEvent1).toHaveBeenCalledTimes(2)
159+
expect(customEventOnce1).toHaveBeenCalledTimes(1)
160+
expect(customEventOnce2).toHaveBeenCalledTimes(1)
161+
testEventBus('custom-event', 1, 0)
162+
})
163+
164+
it('should emit and listen to custom events with wildcard * ', () => {
165+
// Arrange
166+
EventBus.on('*', customEvent1)
167+
EventBus.once('*', customEventOnce1)
168+
EventBus.once('*', customEventOnce2)
169+
testEventBus('*', 3, 2)
170+
171+
// Act
172+
EventBus.emit('custom-event-1')
173+
EventBus.emit('custom-event-2')
174+
175+
// Assert
176+
expect(customEvent1).toHaveBeenCalledTimes(2)
177+
expect(customEventOnce1).toHaveBeenCalledTimes(1)
178+
expect(customEventOnce2).toHaveBeenCalledTimes(1)
179+
testEventBus('*', 1, 0)
180+
})
181+
182+
it('should remove listeners by given type and handler', () => {
183+
// Arrange
184+
EventBus.on('custom-event', customEvent1)
185+
EventBus.once('custom-event', customEventOnce1)
186+
EventBus.once('custom-event', customEventOnce2)
187+
testEventBus('custom-event', 3, 2)
188+
189+
// Act
190+
EventBus.off('custom-event', customEventOnce1)
191+
testEventBus('custom-event', 2, 1)
192+
EventBus.emit('custom-event')
193+
EventBus.emit('custom-event')
194+
195+
// Assert
196+
expect(customEvent1).toHaveBeenCalledTimes(2)
197+
expect(customEventOnce1).toHaveBeenCalledTimes(0)
198+
expect(customEventOnce2).toHaveBeenCalledTimes(1)
199+
testEventBus('custom-event', 1, 0)
200+
})
201+
202+
it('should remove listeners by wildcard * and handler', () => {
203+
// Arrange
204+
EventBus.once('custom-event-1', customEventOnce1)
205+
EventBus.on('custom-event-2', customEvent2)
206+
EventBus.once('custom-event-2', customEventOnce2)
207+
EventBus.on('*', customEvent3)
208+
EventBus.once('*', customEventOnce3)
209+
testEventBus('custom-event-1', 1, 1)
210+
testEventBus('custom-event-2', 2, 1)
211+
testEventBus('*', 2, 1)
212+
213+
// Act
214+
EventBus.off('*', customEventOnce3)
215+
testEventBus('custom-event-1', 1, 1)
216+
testEventBus('custom-event-2', 2, 1)
217+
testEventBus('*', 1, 0)
218+
EventBus.emit('custom-event-1')
219+
EventBus.emit('custom-event-2')
220+
EventBus.emit('custom-event-3')
221+
222+
// Assert
223+
expect(customEventOnce1).toHaveBeenCalledTimes(1)
224+
expect(customEvent2).toHaveBeenCalledTimes(1)
225+
expect(customEventOnce2).toHaveBeenCalledTimes(1)
226+
expect(customEvent3).toHaveBeenCalledTimes(3)
227+
expect(customEventOnce3).toHaveBeenCalledTimes(0)
228+
testEventBus('custom-event-1', 0, 0)
229+
testEventBus('custom-event-2', 1, 0)
230+
testEventBus('*', 1, 0)
231+
})
232+
233+
it('should remove listeners by given type only', () => {
234+
// Arrange
235+
EventBus.on('custom-event', customEvent1)
236+
EventBus.once('custom-event', customEventOnce1)
237+
EventBus.once('custom-event', customEventOnce2)
238+
testEventBus('custom-event', 3, 2)
239+
240+
// Act
241+
EventBus.off('custom-event')
242+
testEventBus('custom-event', 0, 0)
243+
EventBus.emit('custom-event')
244+
EventBus.emit('custom-event')
245+
246+
// Assert
247+
expect(customEvent1).toHaveBeenCalledTimes(0)
248+
expect(customEventOnce1).toHaveBeenCalledTimes(0)
249+
expect(customEventOnce2).toHaveBeenCalledTimes(0)
250+
})
251+
252+
it('should remove listeners by wildcard * only', () => {
253+
// Arrange
254+
EventBus.once('custom-event-1', customEventOnce1)
255+
EventBus.on('custom-event-2', customEvent2)
256+
EventBus.once('custom-event-2', customEventOnce2)
257+
EventBus.on('*', customEvent3)
258+
EventBus.once('*', customEventOnce3)
259+
testEventBus('custom-event-1', 1, 1)
260+
testEventBus('custom-event-2', 2, 1)
261+
testEventBus('*', 2, 1)
262+
263+
// Act
264+
EventBus.off('*')
265+
testEventBus('custom-event-1', 1, 1)
266+
testEventBus('custom-event-2', 2, 1)
267+
testEventBus('*', 0, 0)
268+
EventBus.emit('custom-event-1')
269+
EventBus.emit('custom-event-2')
270+
EventBus.emit('custom-event-3')
271+
272+
// Assert
273+
expect(customEventOnce1).toHaveBeenCalledTimes(1)
274+
expect(customEvent2).toHaveBeenCalledTimes(1)
275+
expect(customEventOnce2).toHaveBeenCalledTimes(1)
276+
expect(customEvent3).toHaveBeenCalledTimes(0)
277+
expect(customEventOnce3).toHaveBeenCalledTimes(0)
278+
testEventBus('custom-event-1', 0, 0)
279+
testEventBus('custom-event-2', 1, 0)
280+
testEventBus('*', 0, 0)
281+
})
282+
})
283+
})

0 commit comments

Comments
 (0)