LeetCode 2694 - Event Emitter
This problem asks us to design a simplified event system similar to the one used in environments like Node.js or browser DOM events. The goal is to implement an EventEmitter class that supports two operations: 1. Subscribing callback functions to named events 2.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
This problem asks us to design a simplified event system similar to the one used in environments like Node.js or browser DOM events. The goal is to implement an EventEmitter class that supports two operations:
- Subscribing callback functions to named events
- Emitting events so that all subscribed callbacks execute
The class behaves like a publish-subscribe system. Different event names can exist independently, and each event may have multiple callback functions attached to it.
The subscribe method takes:
- An event name, represented as a string
- A callback function
When a callback is subscribed, it should be stored so that it can later be executed when the corresponding event is emitted.
The method must also return an object containing an unsubscribe method. Calling this method removes the callback from the event's subscription list.
The emit method takes:
- An event name
- Optionally, an array of arguments
When the event is emitted, every callback subscribed to that event should execute in the exact order they were added. The arguments array should be passed into each callback. The method should return an array containing the return values of all executed callbacks.
For example, if two callbacks return 5 and 6, the emitted result should be:
[5, 6]
If no callbacks are subscribed to an event, emitting that event should simply return an empty array.
The constraints are intentionally small, with at most 10 actions. This means performance is not a major concern for correctness. However, the problem is fundamentally about designing clean data structures and maintaining correct event behavior.
Several edge cases are important:
- Emitting an event with no listeners
- Multiple listeners for the same event
- Ensuring listener execution order is preserved
- Correctly removing only the intended callback during unsubscribe
- Supporting optional arguments during emit
- Allowing multiple independent event names
The problem guarantees that all operations are valid, so we do not need to handle invalid unsubscriptions or malformed input.
Approaches
Brute Force Approach
A naive implementation could store every subscription in a single global list containing pairs like:
(eventName, callback)
Whenever emit is called, we would scan the entire list and execute callbacks whose event name matches the emitted event.
Similarly, unsubscribing would require scanning the list to locate and remove the target callback.
This approach works because every subscription is explicitly stored, and all matching callbacks can always be found by iterating through the entire collection.
However, this design becomes inefficient as the number of events and subscriptions grows. Every emit operation would unnecessarily examine unrelated subscriptions belonging to different events.
Optimal Approach
The key observation is that callbacks naturally group by event name.
Instead of storing all subscriptions in one large collection, we use a hash map where:
- The key is the event name
- The value is the list of callbacks subscribed to that event
Conceptually:
{
"click": [cb1, cb2],
"submit": [cb3]
}
This allows:
- Fast lookup of all callbacks for a specific event
- Preserving insertion order naturally with a list
- Easy callback removal during unsubscribe
When emitting an event:
- Look up the event name in the hash map
- Execute all callbacks in order
- Collect their return values
This avoids scanning unrelated events entirely.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) per emit | O(n) | Scans all subscriptions regardless of event |
| Optimal | O(k) per emit | O(n) | Only processes callbacks for the target event |
Here:
n= total number of subscriptionsk= number of callbacks subscribed to a specific event
Algorithm Walkthrough
Step 1: Create a hash map for events
We maintain a dictionary where:
- Keys are event names
- Values are lists of callback functions
Example:
{
"firstEvent": [cb1, cb2]
}
This structure allows direct access to all listeners for an event.
Step 2: Implement subscribe
When subscribing:
- Check whether the event already exists in the map
- If not, create an empty list
- Append the callback to the event's list
Appending preserves subscription order automatically.
We then return an object containing an unsubscribe method.
Step 3: Implement unsubscribe
The unsubscribe method removes the callback from the corresponding event list.
Since the problem guarantees valid operations, we know the callback exists and can safely remove it.
This ensures future emits no longer invoke the removed callback.
Step 4: Implement emit
When emitting:
- Check whether the event exists
- If not, return an empty list
- Otherwise, iterate through the callback list in order
- Execute each callback with the provided arguments
- Store each return value in a results array
Finally, return the results array.
Step 5: Handle optional arguments
The second parameter to emit may be omitted.
To support this cleanly, we default arguments to an empty list.
This ensures callbacks can always be invoked consistently using unpacking.
Why it works
The correctness comes from maintaining one ordered callback list per event. Subscriptions append callbacks in order, so emits naturally execute callbacks in the same order. Unsubscribe removes exactly one callback from the corresponding event list, guaranteeing removed callbacks are no longer executed. Because each event is isolated in the hash map, emitting one event cannot accidentally invoke callbacks from another event.
Python Solution
from typing import Any, Callable, Dict, List
class EventEmitter:
def __init__(self):
self.events: Dict[str, List[Callable]] = {}
def subscribe(self, eventName: str, callback: Callable) -> Any:
if eventName not in self.events:
self.events[eventName] = []
self.events[eventName].append(callback)
emitter = self
class Subscription:
def unsubscribe(self) -> None:
emitter.events[eventName].remove(callback)
return Subscription()
def emit(self, eventName: str, args: List[Any] = []) -> List[Any]:
if eventName not in self.events:
return []
results = []
for callback in self.events[eventName]:
results.append(callback(*args))
return results
The implementation begins by initializing a dictionary named events. Each key is an event name, and each value is a list of callback functions.
Inside subscribe, we first ensure the event exists in the dictionary. If it does not, we create an empty list. We then append the callback to preserve subscription order.
The method returns a nested Subscription object. This object captures the current callback and event name through closure variables. When unsubscribe executes, it removes the callback from the corresponding event list.
The emit method first checks whether the event exists. If no listeners are registered, it immediately returns an empty list.
Otherwise, it iterates through the callbacks in order, invokes each one using argument unpacking with *args, and stores the return values in results.
Finally, the collected results are returned.
Go Solution
package main
type Callback func(args ...interface{}) interface{}
type Subscription struct {
unsubscribe func()
}
func (s Subscription) Unsubscribe() {
s.unsubscribe()
}
type EventEmitter struct {
events map[string][]Callback
}
func Constructor() EventEmitter {
return EventEmitter{
events: make(map[string][]Callback),
}
}
func (this *EventEmitter) Subscribe(eventName string, callback Callback) Subscription {
this.events[eventName] = append(this.events[eventName], callback)
return Subscription{
unsubscribe: func() {
callbacks := this.events[eventName]
for i, cb := range callbacks {
if &cb == &callback || cb == callback {
this.events[eventName] = append(callbacks[:i], callbacks[i+1:]...)
break
}
}
},
}
}
func (this *EventEmitter) Emit(eventName string, args []interface{}) []interface{} {
callbacks, exists := this.events[eventName]
if !exists {
return []interface{}{}
}
results := []interface{}{}
for _, callback := range callbacks {
results = append(results, callback(args...))
}
return results
}
The Go implementation follows the same overall structure as the Python version, but there are several language-specific differences.
Go does not have first-class classes like Python, so we explicitly define:
EventEmitterSubscriptionCallback
The callback type is declared using:
type Callback func(args ...interface{}) interface{}
This allows callbacks with variable argument counts.
The events map stores slices of callbacks grouped by event name.
For unsubscribe functionality, the Subscription struct stores a closure function that removes the callback from the slice.
Slices are modified using:
append(slice[:i], slice[i+1:]...)
which removes the callback at index i.
Go also distinguishes between nil and empty slices. The implementation returns an empty slice instead of nil, matching the expected problem behavior.
Worked Examples
Example 1
Input:
subscribe("firstEvent", cb1)
subscribe("firstEvent", cb2)
emit("firstEvent")
Where:
cb1() -> 5
cb2() -> 6
State Evolution
| Step | Operation | Event Map | Result |
|---|---|---|---|
| 1 | Create emitter | {} |
|
| 2 | Subscribe cb1 | {"firstEvent": [cb1]} |
|
| 3 | Subscribe cb2 | {"firstEvent": [cb1, cb2]} |
|
| 4 | Emit firstEvent | same | [5, 6] |
During emit:
cb1()executes first and returns5cb2()executes second and returns6
The returned array preserves subscription order.
Example 2
Input:
cb1(*args) => ",".join(args)
emit("firstEvent", [1,2,3])
State Evolution
| Step | Operation | Event Map | Result |
|---|---|---|---|
| 1 | Subscribe cb1 | {"firstEvent": [cb1]} |
|
| 2 | Emit with args | same | ["1,2,3"] |
The arguments array is unpacked into the callback:
cb1(1, 2, 3)
Example 3
State Evolution
| Step | Operation | Event Map | Result |
|---|---|---|---|
| 1 | Subscribe cb1 | {"firstEvent": [cb1]} |
|
| 2 | Emit | same | ["1,2,3"] |
| 3 | Unsubscribe | {"firstEvent": []} |
|
| 4 | Emit again | same | [] |
After unsubscribe, the callback list becomes empty.
Example 4
State Evolution
| Step | Operation | Event Map | Result |
|---|---|---|---|
| 1 | Subscribe x+1 | {"firstEvent": [cb1]} |
|
| 2 | Subscribe x+2 | {"firstEvent": [cb1, cb2]} |
|
| 3 | Unsubscribe cb1 | {"firstEvent": [cb2]} |
|
| 4 | Emit(5) | same | [7] |
Only the second callback remains:
5 + 2 = 7
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(k) | Emit processes only callbacks for one event |
| Space | O(n) | Stores all subscribed callbacks |
Here:
nis the total number of subscriptionskis the number of callbacks attached to a particular emitted event
The dominant operation is iterating through callbacks during emit. Since only listeners for the target event are processed, the runtime depends on the number of listeners for that specific event rather than the total number of subscriptions globally.
The space complexity comes from storing every subscribed callback in the event map.
Test Cases
from typing import Any, Callable, Dict, List
class EventEmitter:
def __init__(self):
self.events: Dict[str, List[Callable]] = {}
def subscribe(self, eventName: str, callback: Callable) -> Any:
if eventName not in self.events:
self.events[eventName] = []
self.events[eventName].append(callback)
emitter = self
class Subscription:
def unsubscribe(self) -> None:
emitter.events[eventName].remove(callback)
return Subscription()
def emit(self, eventName: str, args: List[Any] = []) -> List[Any]:
if eventName not in self.events:
return []
return [callback(*args) for callback in self.events[eventName]]
# Example 1
emitter = EventEmitter()
assert emitter.emit("firstEvent") == [] # emitting with no listeners
emitter.subscribe("firstEvent", lambda: 5)
emitter.subscribe("firstEvent", lambda: 6)
assert emitter.emit("firstEvent") == [5, 6] # multiple listeners
# Example 2
emitter = EventEmitter()
emitter.subscribe(
"firstEvent",
lambda *args: ",".join(map(str, args))
)
assert emitter.emit("firstEvent", [1, 2, 3]) == ["1,2,3"] # optional args
assert emitter.emit("firstEvent", [3, 4, 6]) == ["3,4,6"] # repeated emit
# Example 3
emitter = EventEmitter()
sub = emitter.subscribe(
"firstEvent",
lambda *args: ",".join(map(str, args))
)
assert emitter.emit("firstEvent", [1, 2, 3]) == ["1,2,3"]
sub.unsubscribe()
assert emitter.emit("firstEvent", [4, 5, 6]) == [] # unsubscribed listener
# Example 4
emitter = EventEmitter()
sub1 = emitter.subscribe("firstEvent", lambda x: x + 1)
sub2 = emitter.subscribe("firstEvent", lambda x: x + 2)
sub1.unsubscribe()
assert emitter.emit("firstEvent", [5]) == [7] # only remaining listener
# Multiple event names
emitter = EventEmitter()
emitter.subscribe("a", lambda: "A")
emitter.subscribe("b", lambda: "B")
assert emitter.emit("a") == ["A"] # independent event storage
assert emitter.emit("b") == ["B"]
# Emit with empty argument list
emitter = EventEmitter()
emitter.subscribe("test", lambda: 42)
assert emitter.emit("test", []) == [42] # explicit empty args
# Multiple unsubscribes across events
emitter = EventEmitter()
sub1 = emitter.subscribe("x", lambda: 1)
sub2 = emitter.subscribe("y", lambda: 2)
sub1.unsubscribe()
assert emitter.emit("x") == [] # removed correctly
assert emitter.emit("y") == [2] # unrelated event unaffected
Test Summary
| Test | Why |
|---|---|
| Emit without listeners | Verifies empty array behavior |
| Multiple listeners | Ensures order preservation |
| Optional arguments | Confirms argument unpacking |
| Unsubscribe behavior | Verifies listener removal |
| Multiple event names | Ensures event isolation |
| Explicit empty args | Tests empty argument handling |
| Cross-event unsubscribe | Confirms unrelated events are unaffected |
Edge Cases
Emitting an Event With No Subscribers
One common source of bugs is assuming every emitted event has listeners. If the event does not exist in the map, attempting to iterate through callbacks could fail.
The implementation avoids this by checking:
if eventName not in self.events:
return []
This guarantees correct empty-array behavior.
Multiple Subscribers for the Same Event
Another subtle issue is preserving subscription order. Some incorrect implementations may use unordered collections such as sets.
The problem explicitly requires callbacks to execute in the order they were subscribed.
Using a list solves this naturally because append operations preserve insertion order.
Unsubscribing One Callback Without Affecting Others
Removing listeners can easily introduce bugs if the wrong callback is removed or all callbacks are accidentally cleared.
The implementation removes only the specific callback associated with the returned subscription object:
emitter.events[eventName].remove(callback)
This guarantees that unrelated listeners remain intact.
Supporting Optional Arguments
The emit method may be called with or without an argument list. A naive implementation might assume arguments always exist.
The implementation safely defaults to an empty list:
args: List[Any] = []
This allows callbacks with zero parameters to execute correctly while still supporting variable-length argument lists.