Skip to content

Commit a8f92c4

Browse files
authored
Fix emitter leak (#397)
1 parent 8491a71 commit a8f92c4

File tree

2 files changed

+97
-5
lines changed

2 files changed

+97
-5
lines changed

src/util/emitter.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,27 @@ export class Emitter<Events extends UnknownEvents = UnknownEvents> {
4343

4444
if (historic && this.collectable[event]) {
4545
const buffer = this.collectable[event];
46-
this.collectable[event] = [];
46+
delete this.collectable[event];
4747
for (const args of buffer) {
4848
listener(...args);
4949
}
5050
}
5151
}
5252
}
5353

54-
subscribeOnce<Event extends keyof Events>(
54+
async subscribeOnce<Event extends keyof Events>(
5555
event: Event,
5656
historic = false,
5757
): Promise<Events[Event]> {
58+
if (historic && this.collectable[event]) {
59+
const args = this.collectable[event]?.shift();
60+
if (this.collectable[event]?.length === 0) {
61+
delete this.collectable[event];
62+
}
63+
64+
if (args) return args;
65+
}
66+
5867
return new Promise<Events[Event]>((resolve) => {
5968
let resolved = false;
6069
const listener = (...args: Events[Event]) => {
@@ -65,7 +74,7 @@ export class Emitter<Events extends UnknownEvents = UnknownEvents> {
6574
}
6675
};
6776

68-
this.subscribe(event, listener, historic);
77+
this.subscribe(event, listener, false);
6978
});
7079
}
7180

@@ -75,8 +84,11 @@ export class Emitter<Events extends UnknownEvents = UnknownEvents> {
7584
): void {
7685
if (this.listeners[event]) {
7786
const index = this.listeners[event]?.findIndex((v) => v === listener);
78-
if (index) {
87+
if (index >= 0) {
7988
this.listeners[event]?.splice(index, 1);
89+
if (this.listeners[event]?.length === 0) {
90+
delete this.listeners[event];
91+
}
8092
}
8193
}
8294
}
@@ -96,7 +108,10 @@ export class Emitter<Events extends UnknownEvents = UnknownEvents> {
96108
const interceptor = this.interceptors[event];
97109
const computedArgs = interceptor ? await interceptor(...args) : args;
98110

99-
if (this.listeners[event]?.length === 0 && collectable) {
111+
if (
112+
(collectable && !this.listeners[event]) ||
113+
this.listeners[event]?.length === 0
114+
) {
100115
if (!this.collectable[event]) {
101116
this.collectable[event] = [];
102117
}

tests/unit/emitter.test.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { Emitter } from "../../src";
3+
4+
describe("emitter", () => {
5+
test("scan", () => {
6+
const emitter = new Emitter<{ test: []; other: [] }>();
7+
expect(emitter.scanListeners().length).toBe(0);
8+
9+
const listener = () => {};
10+
emitter.subscribe("test", listener);
11+
expect(emitter.scanListeners().length).toBe(1);
12+
13+
const listener2 = () => {};
14+
emitter.subscribe("other", listener2);
15+
expect(emitter.scanListeners((k) => k === "other").length).toBe(1);
16+
17+
expect(emitter.scanListeners().length).toBe(2);
18+
19+
emitter.unSubscribe("test", listener);
20+
expect(emitter.scanListeners().length).toBe(1);
21+
emitter.unSubscribe("other", listener2);
22+
expect(emitter.scanListeners().length).toBe(0);
23+
});
24+
25+
test("listeners are properly cleared out", () => {
26+
const emitter = new Emitter<{ test: [] }>();
27+
const listener = () => {};
28+
const listener2 = () => {};
29+
30+
emitter.subscribe("test", listener);
31+
expect(emitter.scanListeners().length).toBe(1);
32+
33+
emitter.unSubscribe("test", listener);
34+
expect(emitter.scanListeners().length).toBe(0);
35+
});
36+
37+
test("isSubscribed", () => {
38+
const emitter = new Emitter<{ test: [] }>();
39+
const listener = () => {};
40+
const listener2 = () => {};
41+
42+
emitter.subscribe("test", listener);
43+
expect(emitter.isSubscribed("test", listener)).toBe(true);
44+
expect(emitter.isSubscribed("test", listener2)).toBe(false);
45+
});
46+
47+
test("interceptors", async () => {
48+
const emitter = new Emitter<{ test: [number] }>({
49+
interceptors: {
50+
test: async (v) => [v + 1],
51+
},
52+
});
53+
54+
const listener = (v: number) => v;
55+
emitter.subscribe("test", listener);
56+
57+
const p = emitter.subscribeOnce("test");
58+
emitter.emit("test", [1]);
59+
expect(p).resolves.toEqual([2]);
60+
});
61+
62+
test("collectable", async () => {
63+
const emitter = new Emitter<{ test: [number] }>();
64+
65+
await emitter.emit("test", [1], true);
66+
await emitter.emit("test", [2], true);
67+
expect(emitter.scanListeners().length).toBe(0);
68+
69+
const p1 = await emitter.subscribeOnce("test", true);
70+
expect(p1).toEqual([1]);
71+
72+
const p2 = await emitter.subscribeOnce("test", true);
73+
expect(p2).toEqual([2]);
74+
75+
expect(emitter.scanListeners().length).toBe(0);
76+
});
77+
});

0 commit comments

Comments
 (0)