Skip to content

Commit 69c7f05

Browse files
authored
fix: event-handler leakage (#788)
Fixes an issue where event handlers could be leaked if the same handler reference was added multiple times. --------- Signed-off-by: Todd Baert <[email protected]>
1 parent c6a357a commit 69c7f05

File tree

2 files changed

+102
-18
lines changed

2 files changed

+102
-18
lines changed

packages/shared/src/events/generic-event-emitter.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Logger, ManageLogger, SafeLogger } from '../logger';
22
import { EventContext, EventDetails, EventHandler } from './eventing';
3-
import { AnyProviderEvent } from './events';
3+
import { AllProviderEvents, AnyProviderEvent } from './events';
44

55
/**
66
* The GenericEventEmitter should only be used within the SDK. It supports additional properties that can be included
@@ -11,8 +11,13 @@ export abstract class GenericEventEmitter<E extends AnyProviderEvent, Additional
1111
{
1212
protected abstract readonly eventEmitter: PlatformEventEmitter;
1313

14-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15-
private readonly _handlers = new WeakMap<EventHandler, EventHandler>();
14+
private readonly _handlers: { [key in AnyProviderEvent]: WeakMap<EventHandler, EventHandler[]>} = {
15+
[AllProviderEvents.ConfigurationChanged]: new WeakMap<EventHandler, EventHandler[]>(),
16+
[AllProviderEvents.ContextChanged]: new WeakMap<EventHandler, EventHandler[]>(),
17+
[AllProviderEvents.Ready]: new WeakMap<EventHandler, EventHandler[]>(),
18+
[AllProviderEvents.Error]: new WeakMap<EventHandler, EventHandler[]>(),
19+
[AllProviderEvents.Stale]: new WeakMap<EventHandler, EventHandler[]>(),
20+
};
1621
private _eventLogger?: Logger;
1722

1823
constructor(private readonly globalLogger?: () => Logger) {}
@@ -29,19 +34,25 @@ export abstract class GenericEventEmitter<E extends AnyProviderEvent, Additional
2934
await handler(details);
3035
};
3136
// The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
32-
this._handlers.set(handler, asyncHandler);
37+
const existingAsyncHandlers = this._handlers[eventType].get(handler);
38+
39+
// we allow duplicate event handlers, similar to node,
40+
// see: https://nodejs.org/api/events.html#emitteroneventname-listener
41+
// and https://nodejs.org/api/events.html#emitterremovelistenereventname-listener
42+
this._handlers[eventType].set(handler, [...(existingAsyncHandlers || []), asyncHandler]);
3343
this.eventEmitter.on(eventType, asyncHandler);
3444
}
3545

3646
removeHandler(eventType: AnyProviderEvent, handler: EventHandler): void {
3747
// Get the wrapper function for this handler, to delete it from the event emitter
38-
const asyncHandler = this._handlers.get(handler) as EventHandler | undefined;
48+
const existingAsyncHandlers = this._handlers[eventType].get(handler);
3949

40-
if (!asyncHandler) {
41-
return;
50+
if (existingAsyncHandlers) {
51+
const removedAsyncHandler = existingAsyncHandlers.pop();
52+
if (removedAsyncHandler) {
53+
this.eventEmitter.removeListener(eventType, removedAsyncHandler);
54+
}
4255
}
43-
44-
this.eventEmitter.removeListener(eventType, asyncHandler);
4556
}
4657

4758
removeAllHandlers(eventType?: AnyProviderEvent): void {

packages/shared/test/events.spec.ts

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ class TestEventEmitter extends GenericEventEmitter<AnyProviderEvent> {
1313
}
1414
}
1515

16+
// a little function to make sure we're at least waiting for the event loop
17+
// to clear before we start making assertions
18+
const wait = (millis = 0) => {
19+
return new Promise(resolve => {setTimeout(resolve, millis);});
20+
};
21+
1622
describe('GenericEventEmitter', () => {
1723
describe('addHandler should', function () {
18-
it('attach handler for event type', function () {
24+
it('attach handler for event type', async function () {
1925
const emitter = new TestEventEmitter();
2026

2127
const handler1 = jest.fn();
2228
emitter.addHandler(AllProviderEvents.Ready, handler1);
2329
emitter.emit(AllProviderEvents.Ready);
2430

31+
await wait();
32+
2533
expect(handler1).toHaveBeenCalledTimes(1);
2634
});
2735

28-
it('attach several handlers for event type', function () {
36+
it('attach several handlers for event type', async function () {
2937
const emitter = new TestEventEmitter();
3038

3139
const handler1 = jest.fn();
@@ -38,6 +46,8 @@ describe('GenericEventEmitter', () => {
3846

3947
emitter.emit(AllProviderEvents.Ready);
4048

49+
await wait();
50+
4151
expect(handler1).toHaveBeenCalledTimes(1);
4252
expect(handler2).toHaveBeenCalledTimes(1);
4353
expect(handler3).not.toHaveBeenCalled();
@@ -62,28 +72,32 @@ describe('GenericEventEmitter', () => {
6272
emitter.emit(AllProviderEvents.Ready);
6373
});
6474

65-
it('trigger handler for event type', function () {
75+
it('trigger handler for event type', async function () {
6676
const emitter = new TestEventEmitter();
6777

6878
const handler1 = jest.fn();
6979
emitter.addHandler(AllProviderEvents.Ready, handler1);
7080
emitter.emit(AllProviderEvents.Ready);
7181

82+
await wait();
83+
7284
expect(handler1).toHaveBeenCalledTimes(1);
7385
});
7486

75-
it('trigger handler for event type with event data', function () {
87+
it('trigger handler for event type with event data', async function () {
7688
const event: ReadyEvent = { message: 'message' };
7789
const emitter = new TestEventEmitter();
7890

7991
const handler1 = jest.fn();
8092
emitter.addHandler(AllProviderEvents.Ready, handler1);
8193
emitter.emit(AllProviderEvents.Ready, event);
8294

95+
await wait();
96+
8397
expect(handler1).toHaveBeenNthCalledWith(1, event);
8498
});
8599

86-
it('trigger several handlers for event type', function () {
100+
it('trigger several handlers for event type', async function () {
87101
const emitter = new TestEventEmitter();
88102

89103
const handler1 = jest.fn();
@@ -96,14 +110,16 @@ describe('GenericEventEmitter', () => {
96110

97111
emitter.emit(AllProviderEvents.Ready);
98112

113+
await wait();
114+
99115
expect(handler1).toHaveBeenCalledTimes(1);
100116
expect(handler2).toHaveBeenCalledTimes(1);
101117
expect(handler3).not.toHaveBeenCalled();
102118
});
103119
});
104120

105121
describe('removeHandler should', () => {
106-
it('remove single handler', function () {
122+
it('remove single handler', async function () {
107123
const emitter = new TestEventEmitter();
108124

109125
const handler1 = jest.fn();
@@ -113,12 +129,14 @@ describe('GenericEventEmitter', () => {
113129
emitter.removeHandler(AllProviderEvents.Ready, handler1);
114130
emitter.emit(AllProviderEvents.Ready);
115131

132+
await wait();
133+
116134
expect(handler1).toHaveBeenCalledTimes(1);
117135
});
118136
});
119137

120138
describe('removeAllHandlers should', () => {
121-
it('remove all handlers for event type', function () {
139+
it('remove all handlers for event type', async function () {
122140
const emitter = new TestEventEmitter();
123141

124142
const handler1 = jest.fn();
@@ -130,11 +148,62 @@ describe('GenericEventEmitter', () => {
130148
emitter.emit(AllProviderEvents.Ready);
131149
emitter.emit(AllProviderEvents.Error);
132150

151+
await wait();
152+
133153
expect(handler1).toHaveBeenCalledTimes(0);
134154
expect(handler2).toHaveBeenCalledTimes(1);
135155
});
136156

137-
it('remove all handlers only for event type', function () {
157+
it('remove same handler when assigned to multiple events', async function () {
158+
const emitter = new TestEventEmitter();
159+
160+
const handler = jest.fn();
161+
emitter.addHandler(AllProviderEvents.Stale, handler);
162+
emitter.addHandler(AllProviderEvents.ContextChanged, handler);
163+
164+
emitter.removeHandler(AllProviderEvents.Stale, handler);
165+
emitter.removeHandler(AllProviderEvents.ContextChanged, handler);
166+
167+
emitter.emit(AllProviderEvents.Stale);
168+
emitter.emit(AllProviderEvents.ContextChanged);
169+
170+
await wait();
171+
172+
expect(handler).toHaveBeenCalledTimes(0);
173+
});
174+
175+
it('allow addition/removal of duplicate handlers', async function () {
176+
const emitter = new TestEventEmitter();
177+
178+
const handler = jest.fn();
179+
emitter.addHandler(AllProviderEvents.Stale, handler);
180+
emitter.addHandler(AllProviderEvents.Stale, handler);
181+
182+
emitter.removeHandler(AllProviderEvents.Stale, handler);
183+
emitter.removeHandler(AllProviderEvents.Stale, handler);
184+
185+
emitter.emit(AllProviderEvents.Stale);
186+
187+
await wait();
188+
189+
expect(handler).toHaveBeenCalledTimes(0);
190+
});
191+
192+
it('allow duplicate event handlers and call them', async function () {
193+
const emitter = new TestEventEmitter();
194+
195+
const handler = jest.fn();
196+
emitter.addHandler(AllProviderEvents.Stale, handler);
197+
emitter.addHandler(AllProviderEvents.Stale, handler);
198+
199+
emitter.emit(AllProviderEvents.Stale);
200+
201+
await wait();
202+
203+
expect(handler).toHaveBeenCalledTimes(2);
204+
});
205+
206+
it('remove all handlers only for event type', async function () {
138207
const emitter = new TestEventEmitter();
139208

140209
const handler1 = jest.fn();
@@ -146,11 +215,13 @@ describe('GenericEventEmitter', () => {
146215
emitter.removeAllHandlers(AllProviderEvents.Ready);
147216
emitter.emit(AllProviderEvents.Ready);
148217

218+
await wait();
219+
149220
expect(handler1).toHaveBeenCalledTimes(1);
150221
expect(handler2).toHaveBeenCalledTimes(0);
151222
});
152223

153-
it('remove all handlers if no event type is given', function () {
224+
it('remove all handlers if no event type is given', async function () {
154225
const emitter = new TestEventEmitter();
155226

156227
const handler1 = jest.fn();
@@ -164,6 +235,8 @@ describe('GenericEventEmitter', () => {
164235
emitter.emit(AllProviderEvents.Ready);
165236
emitter.emit(AllProviderEvents.Error);
166237

238+
await wait();
239+
167240
expect(handler1).toHaveBeenCalledTimes(1);
168241
expect(handler2).toHaveBeenCalledTimes(1);
169242
});

0 commit comments

Comments
 (0)