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.

LeetCode Problem 2694

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:

  1. Subscribing callback functions to named events
  2. 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:

  1. Look up the event name in the hash map
  2. Execute all callbacks in order
  3. 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 subscriptions
  • k = 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:

  1. Check whether the event already exists in the map
  2. If not, create an empty list
  3. 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:

  1. Check whether the event exists
  2. If not, return an empty list
  3. Otherwise, iterate through the callback list in order
  4. Execute each callback with the provided arguments
  5. 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:

  • EventEmitter
  • Subscription
  • Callback

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 returns 5
  • cb2() executes second and returns 6

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:

  • n is the total number of subscriptions
  • k is 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.