Skip to content

Commit 79e5689

Browse files
Copilotcubap
andauthored
Add EventDispatcher.one() for auto-unregistering listeners (#382)
* Initial plan * Implement EventDispatcher.one() method with comprehensive tests Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> * Simplify one() implementation using native { once: true } option Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cubap <1119165+cubap@users.noreply.github.com>
1 parent 9b69b24 commit 79e5689

2 files changed

Lines changed: 200 additions & 1 deletion

File tree

api/__tests__/events.test.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { EventDispatcher } from "../events.js"
2+
import { describe, it } from 'node:test'
3+
import assert from 'node:assert'
4+
5+
describe("EventDispatcher", () => {
6+
describe("on()", () => {
7+
it("should register an event listener that fires multiple times", () => {
8+
const dispatcher = new EventDispatcher()
9+
let count = 0
10+
11+
dispatcher.on('test-event', () => {
12+
count++
13+
})
14+
15+
dispatcher.dispatch('test-event')
16+
dispatcher.dispatch('test-event')
17+
dispatcher.dispatch('test-event')
18+
19+
assert.equal(count, 3)
20+
})
21+
22+
it("should pass event detail to the listener", () => {
23+
const dispatcher = new EventDispatcher()
24+
let receivedDetail = null
25+
26+
dispatcher.on('test-event', (event) => {
27+
receivedDetail = event.detail
28+
})
29+
30+
dispatcher.dispatch('test-event', { foo: 'bar' })
31+
32+
assert.deepEqual(receivedDetail, { foo: 'bar' })
33+
})
34+
})
35+
36+
describe("one()", () => {
37+
it("should register an event listener that fires only once", () => {
38+
const dispatcher = new EventDispatcher()
39+
let count = 0
40+
41+
dispatcher.one('test-event', () => {
42+
count++
43+
})
44+
45+
dispatcher.dispatch('test-event')
46+
dispatcher.dispatch('test-event')
47+
dispatcher.dispatch('test-event')
48+
49+
assert.equal(count, 1)
50+
})
51+
52+
it("should pass event detail to the one-time listener", () => {
53+
const dispatcher = new EventDispatcher()
54+
let receivedDetail = null
55+
56+
dispatcher.one('test-event', (event) => {
57+
receivedDetail = event.detail
58+
})
59+
60+
dispatcher.dispatch('test-event', { foo: 'bar' })
61+
62+
assert.deepEqual(receivedDetail, { foo: 'bar' })
63+
})
64+
65+
it("should auto-remove the listener after first execution", () => {
66+
const dispatcher = new EventDispatcher()
67+
let callCount = 0
68+
69+
dispatcher.one('test-event', () => {
70+
callCount++
71+
})
72+
73+
// First dispatch - should trigger
74+
dispatcher.dispatch('test-event')
75+
assert.equal(callCount, 1)
76+
77+
// Second dispatch - should not trigger
78+
dispatcher.dispatch('test-event')
79+
assert.equal(callCount, 1)
80+
81+
// Third dispatch - should still not trigger
82+
dispatcher.dispatch('test-event')
83+
assert.equal(callCount, 1)
84+
})
85+
86+
it("should work with multiple one() listeners on the same event", () => {
87+
const dispatcher = new EventDispatcher()
88+
let count1 = 0
89+
let count2 = 0
90+
91+
dispatcher.one('test-event', () => {
92+
count1++
93+
})
94+
95+
dispatcher.one('test-event', () => {
96+
count2++
97+
})
98+
99+
dispatcher.dispatch('test-event')
100+
assert.equal(count1, 1)
101+
assert.equal(count2, 1)
102+
103+
dispatcher.dispatch('test-event')
104+
assert.equal(count1, 1)
105+
assert.equal(count2, 1)
106+
})
107+
108+
it("should work alongside regular on() listeners", () => {
109+
const dispatcher = new EventDispatcher()
110+
let onCount = 0
111+
let oneCount = 0
112+
113+
dispatcher.on('test-event', () => {
114+
onCount++
115+
})
116+
117+
dispatcher.one('test-event', () => {
118+
oneCount++
119+
})
120+
121+
dispatcher.dispatch('test-event')
122+
assert.equal(onCount, 1)
123+
assert.equal(oneCount, 1)
124+
125+
dispatcher.dispatch('test-event')
126+
assert.equal(onCount, 2)
127+
assert.equal(oneCount, 1)
128+
129+
dispatcher.dispatch('test-event')
130+
assert.equal(onCount, 3)
131+
assert.equal(oneCount, 1)
132+
})
133+
134+
it("should allow removing a one-time listener with off() before it fires", () => {
135+
const dispatcher = new EventDispatcher()
136+
let count = 0
137+
138+
const listener = () => {
139+
count++
140+
}
141+
142+
dispatcher.one('test-event', listener)
143+
dispatcher.off('test-event', listener)
144+
145+
dispatcher.dispatch('test-event')
146+
assert.equal(count, 0)
147+
})
148+
})
149+
150+
describe("off()", () => {
151+
it("should remove a registered event listener", () => {
152+
const dispatcher = new EventDispatcher()
153+
let count = 0
154+
155+
const listener = () => {
156+
count++
157+
}
158+
159+
dispatcher.on('test-event', listener)
160+
dispatcher.dispatch('test-event')
161+
assert.equal(count, 1)
162+
163+
dispatcher.off('test-event', listener)
164+
dispatcher.dispatch('test-event')
165+
assert.equal(count, 1)
166+
})
167+
})
168+
169+
describe("dispatch()", () => {
170+
it("should dispatch an event with no detail", () => {
171+
const dispatcher = new EventDispatcher()
172+
let fired = false
173+
174+
dispatcher.on('test-event', () => {
175+
fired = true
176+
})
177+
178+
dispatcher.dispatch('test-event')
179+
assert.equal(fired, true)
180+
})
181+
182+
it("should dispatch an event with detail", () => {
183+
const dispatcher = new EventDispatcher()
184+
let receivedDetail = null
185+
186+
dispatcher.on('test-event', (event) => {
187+
receivedDetail = event.detail
188+
})
189+
190+
dispatcher.dispatch('test-event', { test: 'data' })
191+
assert.deepEqual(receivedDetail, { test: 'data' })
192+
})
193+
})
194+
})

api/events.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class EventDispatcher extends EventTarget {
88
this.addEventListener(event, listener)
99
}
1010

11+
// Method to add a one-time event listener that auto-removes after first execution
12+
one(event, listener) {
13+
this.addEventListener(event, listener, { once: true })
14+
}
15+
1116
// Method to remove an event listener
1217
off(event, listener) {
1318
this.removeEventListener(event, listener)
@@ -21,4 +26,4 @@ class EventDispatcher extends EventTarget {
2126

2227
// Export a shared instance of EventDispatcher
2328
const eventDispatcher = new EventDispatcher()
24-
export { eventDispatcher }
29+
export { eventDispatcher, EventDispatcher }

0 commit comments

Comments
 (0)