2694. Event Emitter
Problem Description
You need to create an EventEmitter
class that manages event subscriptions and emissions. This class allows you to register callback functions for specific events and trigger those callbacks when the event is emitted.
The class must implement two methods:
1. subscribe(eventName, callback)
- Takes an event name (string) and a callback function as parameters
- Registers the callback to be executed when the specified event is emitted
- Multiple callbacks can be registered for the same event
- Returns a subscription object with an
unsubscribe()
method that:- Removes the callback from the event's subscription list
- Returns
undefined
when called
2. emit(eventName, args)
- Takes an event name (string) and an optional array of arguments
- Executes all callbacks registered for that event in the order they were subscribed
- Passes the provided arguments to each callback using spread syntax
- Returns an array containing the results from all callback executions
- If no callbacks are registered for the event, returns an empty array
Key Requirements:
- Callbacks for the same event must execute in subscription order
- Each callback receives the arguments passed to
emit
- The
unsubscribe
method should only remove the specific callback it was created for - Multiple subscriptions to the same event should all work independently
Example Usage:
const emitter = new EventEmitter();
function onClickCallback() { return 99; }
const sub = emitter.subscribe('onClick', onClickCallback);
emitter.emit('onClick'); // Returns [99]
sub.unsubscribe(); // Returns undefined
emitter.emit('onClick'); // Returns []
Intuition
The core challenge is managing multiple callbacks for different events while maintaining their order and allowing individual unsubscription. Let's think through what data structure would work best.
We need to store callbacks grouped by event names. A Map
is perfect for this - it gives us O(1) lookup by event name. For each event, we need to store multiple callbacks. While an array seems natural for maintaining order, it has a problem: removing a specific callback requires O(n) search time and array manipulation.
Here's the key insight: JavaScript's Set
maintains insertion order (since ES2015) while providing O(1) deletion. This gives us the best of both worlds - we can maintain subscription order and efficiently remove specific callbacks.
The structure becomes: Map<eventName, Set<callbacks>>
.
For the subscribe
method, we add the callback to the event's Set. If the event doesn't exist yet, we create a new Set first. The clever part is returning an object with an unsubscribe
method that captures the specific callback in its closure. This way, each subscription has its own unsubscribe function that knows exactly which callback to remove.
For the emit
method, we retrieve the Set of callbacks for the given event. Since Sets are iterable and maintain insertion order, we can spread it into an array [...callbacks]
and map over it to execute each callback with the provided arguments. The spread operator ...args
ensures each callback receives the arguments as individual parameters rather than as an array.
The beauty of using Set
is that it automatically handles duplicate prevention (though the problem states callbacks won't be referentially identical) and gives us efficient operations. The delete
method on Set returns a boolean, but we don't need to track whether the deletion was successful - we just call it when unsubscribe is invoked.
Solution Approach
Let's implement the EventEmitter
class step by step:
1. Class Structure and Data Storage
class EventEmitter {
private d: Map<string, Set<Callback>> = new Map();
}
We use a Map
called d
to store our events. Each key is an event name (string), and each value is a Set
of callback functions. This gives us O(1) access to any event's callbacks and O(1) addition/removal of callbacks.
2. Subscribe Method Implementation
subscribe(eventName: string, callback: Callback): Subscription {
this.d.set(eventName, (this.d.get(eventName) || new Set()).add(callback));
return {
unsubscribe: () => {
this.d.get(eventName)?.delete(callback);
},
};
}
The subscription logic works as follows:
this.d.get(eventName) || new Set()
- Get the existing Set of callbacks for this event, or create a new Set if this is the first subscription.add(callback)
- Add the new callback to the Set (Set'sadd
method returns the Set itself, enabling chaining)this.d.set(eventName, ...)
- Store the updated Set back in the Map
The returned subscription object contains an unsubscribe
method that:
- Uses optional chaining
?.
to safely access the Set (in case it was somehow removed) - Calls
delete(callback)
to remove the specific callback from the Set - The closure captures both
eventName
andcallback
, so each subscription knows exactly what to unsubscribe
3. Emit Method Implementation
emit(eventName: string, args: any[] = []): any {
const callbacks = this.d.get(eventName);
if (!callbacks) {
return [];
}
return [...callbacks].map(callback => callback(...args));
}
The emission process:
- Retrieve the Set of callbacks for the given event name
- If no callbacks exist (event was never subscribed to or all were unsubscribed), return an empty array
- Convert the Set to an array using spread syntax
[...callbacks]
to maintain insertion order - Map over each callback, executing it with the provided arguments using spread
...args
- Collect and return all the callback return values in an array
Type Definitions
type Callback = (...args: any[]) => any;
type Subscription = {
unsubscribe: () => void;
};
These types ensure type safety:
Callback
accepts any number of arguments and can return anythingSubscription
defines the shape of the object returned bysubscribe
This implementation efficiently handles all requirements: maintains subscription order, allows multiple listeners per event, supports individual unsubscription, and properly passes arguments to callbacks.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the EventEmitter works internally:
const emitter = new EventEmitter();
// Step 1: Subscribe first callback to 'click' event
const callback1 = (x) => x * 2;
const sub1 = emitter.subscribe('click', callback1);
// Internal state: Map { 'click' => Set { callback1 } }
// Step 2: Subscribe second callback to same event
const callback2 = (x) => x + 10;
const sub2 = emitter.subscribe('click', callback2);
// Internal state: Map { 'click' => Set { callback1, callback2 } }
// Note: Set maintains insertion order
// Step 3: Subscribe to a different event
const callback3 = () => 'hello';
const sub3 = emitter.subscribe('hover', callback3);
// Internal state: Map {
// 'click' => Set { callback1, callback2 },
// 'hover' => Set { callback3 }
// }
// Step 4: Emit 'click' event with argument [5]
const result1 = emitter.emit('click', [5]);
// Process:
// 1. Get Set for 'click': Set { callback1, callback2 }
// 2. Convert to array: [callback1, callback2] (preserves order)
// 3. Execute each: callback1(5) = 10, callback2(5) = 15
// Returns: [10, 15]
// Step 5: Unsubscribe first callback
sub1.unsubscribe();
// The closure remembers 'click' and callback1
// Executes: this.d.get('click').delete(callback1)
// Internal state: Map {
// 'click' => Set { callback2 },
// 'hover' => Set { callback3 }
// }
// Step 6: Emit 'click' again
const result2 = emitter.emit('click', [5]);
// Only callback2 remains in the Set
// Returns: [15]
// Step 7: Emit event with no subscribers
const result3 = emitter.emit('missing', []);
// No Set exists for 'missing'
// Returns: []
This example demonstrates:
- How the Map stores Sets of callbacks for each event name
- How Set maintains insertion order for callbacks
- How the unsubscribe closure captures specific callback references
- How emit converts the Set to an array while preserving order
- How the solution handles edge cases like non-existent events
Solution Implementation
1from typing import Any, Callable, List, Dict, Set
2
3# Type definition for callback functions that can accept any arguments and return any value
4Callback = Callable[..., Any]
5
6class Subscription:
7 """Subscription object that provides unsubscribe functionality"""
8
9 def __init__(self, unsubscribe_func: Callable[[], None]):
10 """
11 Initialize subscription with an unsubscribe function
12
13 Args:
14 unsubscribe_func: Function to call when unsubscribing
15 """
16 self._unsubscribe_func = unsubscribe_func
17
18 def unsubscribe(self) -> None:
19 """Unsubscribe from the event"""
20 self._unsubscribe_func()
21
22
23# Map to store event names and their associated callback functions
24# Key: event name, Value: Set of callback functions
25event_registry: Dict[str, Set[Callback]] = {}
26
27
28def subscribe(eventName: str, callback: Callback) -> Subscription:
29 """
30 Subscribes a callback function to a specific event
31
32 Args:
33 eventName: The name of the event to subscribe to
34 callback: The callback function to execute when the event is emitted
35
36 Returns:
37 A subscription object with an unsubscribe method
38 """
39 # Get existing callbacks for this event or create a new Set
40 if eventName not in event_registry:
41 event_registry[eventName] = set()
42
43 existing_callbacks = event_registry[eventName]
44
45 # Add the new callback to the Set
46 existing_callbacks.add(callback)
47
48 # Define unsubscribe function
49 def unsubscribe_func():
50 # Remove the callback from the Set when unsubscribing
51 if eventName in event_registry:
52 callbacks = event_registry[eventName]
53 callbacks.discard(callback)
54
55 # Optional: Clean up empty Sets to prevent memory leaks
56 if len(callbacks) == 0:
57 del event_registry[eventName]
58
59 # Return subscription object with unsubscribe functionality
60 return Subscription(unsubscribe_func)
61
62
63def emit(eventName: str, args: List[Any] = None) -> List[Any]:
64 """
65 Emits an event by executing all subscribed callbacks
66
67 Args:
68 eventName: The name of the event to emit
69 args: List of arguments to pass to each callback function
70
71 Returns:
72 List of results from all executed callbacks, or empty list if no callbacks exist
73 """
74 # Default args to empty list if not provided
75 if args is None:
76 args = []
77
78 # Get all callbacks for the specified event
79 callbacks = event_registry.get(eventName)
80
81 # Return empty list if no callbacks are registered
82 if not callbacks or len(callbacks) == 0:
83 return []
84
85 # Execute each callback with the provided arguments and collect results
86 results = []
87 for callback in callbacks:
88 results.append(callback(*args))
89
90 return results
91
1import java.util.*;
2import java.util.function.Function;
3
4/**
5 * Event emitter implementation that allows subscribing to and emitting events
6 */
7public class EventEmitter {
8
9 /**
10 * Functional interface for callback functions that can accept variable arguments
11 */
12 @FunctionalInterface
13 public interface Callback {
14 Object apply(Object... args);
15 }
16
17 /**
18 * Subscription class that provides unsubscribe functionality
19 */
20 public static class Subscription {
21 private final Runnable unsubscribeAction;
22
23 public Subscription(Runnable unsubscribeAction) {
24 this.unsubscribeAction = unsubscribeAction;
25 }
26
27 /**
28 * Unsubscribes the associated callback from the event
29 */
30 public void unsubscribe() {
31 unsubscribeAction.run();
32 }
33 }
34
35 // Map to store event names and their associated callback functions
36 // Key: event name, Value: Set of callback functions
37 private final Map<String, Set<Callback>> eventRegistry = new HashMap<>();
38
39 /**
40 * Subscribes a callback function to a specific event
41 * @param eventName The name of the event to subscribe to
42 * @param callback The callback function to execute when the event is emitted
43 * @return A subscription object with an unsubscribe method
44 */
45 public Subscription subscribe(String eventName, Callback callback) {
46 // Get existing callbacks for this event or create a new Set
47 Set<Callback> existingCallbacks = eventRegistry.computeIfAbsent(
48 eventName,
49 k -> new HashSet<>()
50 );
51
52 // Add the new callback to the Set
53 existingCallbacks.add(callback);
54
55 // Return subscription object with unsubscribe functionality
56 return new Subscription(() -> {
57 // Remove the callback from the Set when unsubscribing
58 Set<Callback> callbacks = eventRegistry.get(eventName);
59 if (callbacks != null) {
60 callbacks.remove(callback);
61
62 // Clean up empty Sets to prevent memory leaks
63 if (callbacks.isEmpty()) {
64 eventRegistry.remove(eventName);
65 }
66 }
67 });
68 }
69
70 /**
71 * Emits an event by executing all subscribed callbacks
72 * @param eventName The name of the event to emit
73 * @param args Array of arguments to pass to each callback function
74 * @return List of results from all executed callbacks, or empty list if no callbacks exist
75 */
76 public List<Object> emit(String eventName, Object... args) {
77 // Get all callbacks for the specified event
78 Set<Callback> callbacks = eventRegistry.get(eventName);
79
80 // Return empty list if no callbacks are registered
81 if (callbacks == null || callbacks.isEmpty()) {
82 return new ArrayList<>();
83 }
84
85 // Execute each callback with the provided arguments and collect results
86 List<Object> results = new ArrayList<>();
87 for (Callback callback : callbacks) {
88 results.add(callback.apply(args));
89 }
90
91 return results;
92 }
93}
94
1#include <string>
2#include <unordered_map>
3#include <set>
4#include <vector>
5#include <functional>
6#include <memory>
7
8// Type definition for callback functions that can accept any arguments and return any value
9// Using std::function with variable arguments through a vector
10using Callback = std::function<std::any(const std::vector<std::any>&)>;
11
12// Forward declaration for Subscription class
13class Subscription;
14
15// Global map to store event names and their associated callback functions
16// Key: event name, Value: Set of callback function pointers
17static std::unordered_map<std::string, std::set<Callback*>> eventRegistry;
18
19/**
20 * Class representing a subscription object that provides unsubscribe functionality
21 */
22class Subscription {
23private:
24 std::string eventName;
25 Callback* callbackPtr;
26
27public:
28 /**
29 * Constructor to initialize subscription with event name and callback pointer
30 * @param name - The name of the event
31 * @param ptr - Pointer to the callback function
32 */
33 Subscription(const std::string& name, Callback* ptr)
34 : eventName(name), callbackPtr(ptr) {}
35
36 /**
37 * Unsubscribes the callback from the event
38 */
39 void unsubscribe() {
40 // Find the callbacks for this event
41 auto it = eventRegistry.find(eventName);
42 if (it != eventRegistry.end()) {
43 // Remove the callback from the set
44 it->second.erase(callbackPtr);
45
46 // Clean up empty sets to prevent memory leaks
47 if (it->second.empty()) {
48 eventRegistry.erase(it);
49 }
50
51 // Delete the dynamically allocated callback
52 delete callbackPtr;
53 callbackPtr = nullptr;
54 }
55 }
56
57 /**
58 * Destructor - ensures cleanup if subscription object is destroyed
59 */
60 ~Subscription() {
61 if (callbackPtr != nullptr) {
62 unsubscribe();
63 }
64 }
65};
66
67/**
68 * Subscribes a callback function to a specific event
69 * @param eventName - The name of the event to subscribe to
70 * @param callback - The callback function to execute when the event is emitted
71 * @return A unique pointer to a Subscription object with an unsubscribe method
72 */
73std::unique_ptr<Subscription> subscribe(const std::string& eventName, const Callback& callback) {
74 // Create a new callback on the heap
75 Callback* callbackPtr = new Callback(callback);
76
77 // Get existing callbacks for this event or create a new set
78 auto& callbacks = eventRegistry[eventName];
79
80 // Add the new callback pointer to the set
81 callbacks.insert(callbackPtr);
82
83 // Return subscription object with unsubscribe functionality
84 return std::make_unique<Subscription>(eventName, callbackPtr);
85}
86
87/**
88 * Emits an event by executing all subscribed callbacks
89 * @param eventName - The name of the event to emit
90 * @param args - Vector of arguments to pass to each callback function
91 * @return Vector of results from all executed callbacks, or empty vector if no callbacks exist
92 */
93std::vector<std::any> emit(const std::string& eventName, const std::vector<std::any>& args = {}) {
94 // Initialize results vector
95 std::vector<std::any> results;
96
97 // Find callbacks for the specified event
98 auto it = eventRegistry.find(eventName);
99
100 // Return empty vector if no callbacks are registered
101 if (it == eventRegistry.end() || it->second.empty()) {
102 return results;
103 }
104
105 // Execute each callback with the provided arguments and collect results
106 for (Callback* callbackPtr : it->second) {
107 if (callbackPtr != nullptr) {
108 // Dereference the pointer and call the function
109 results.push_back((*callbackPtr)(args));
110 }
111 }
112
113 return results;
114}
115
1// Type definition for callback functions that can accept any arguments and return any value
2type Callback = (...args: any[]) => any;
3
4// Type definition for subscription object that provides unsubscribe functionality
5type Subscription = {
6 unsubscribe: () => void;
7};
8
9// Map to store event names and their associated callback functions
10// Key: event name, Value: Set of callback functions
11const eventRegistry: Map<string, Set<Callback>> = new Map();
12
13/**
14 * Subscribes a callback function to a specific event
15 * @param eventName - The name of the event to subscribe to
16 * @param callback - The callback function to execute when the event is emitted
17 * @returns A subscription object with an unsubscribe method
18 */
19function subscribe(eventName: string, callback: Callback): Subscription {
20 // Get existing callbacks for this event or create a new Set
21 const existingCallbacks = eventRegistry.get(eventName) || new Set<Callback>();
22
23 // Add the new callback to the Set
24 existingCallbacks.add(callback);
25
26 // Update the registry with the updated Set
27 eventRegistry.set(eventName, existingCallbacks);
28
29 // Return subscription object with unsubscribe functionality
30 return {
31 unsubscribe: () => {
32 // Remove the callback from the Set when unsubscribing
33 const callbacks = eventRegistry.get(eventName);
34 if (callbacks) {
35 callbacks.delete(callback);
36
37 // Optional: Clean up empty Sets to prevent memory leaks
38 if (callbacks.size === 0) {
39 eventRegistry.delete(eventName);
40 }
41 }
42 }
43 };
44}
45
46/**
47 * Emits an event by executing all subscribed callbacks
48 * @param eventName - The name of the event to emit
49 * @param args - Array of arguments to pass to each callback function
50 * @returns Array of results from all executed callbacks, or empty array if no callbacks exist
51 */
52function emit(eventName: string, args: any[] = []): any[] {
53 // Get all callbacks for the specified event
54 const callbacks = eventRegistry.get(eventName);
55
56 // Return empty array if no callbacks are registered
57 if (!callbacks || callbacks.size === 0) {
58 return [];
59 }
60
61 // Execute each callback with the provided arguments and collect results
62 const results: any[] = [];
63 callbacks.forEach(callback => {
64 results.push(callback(...args));
65 });
66
67 return results;
68}
69
Time and Space Complexity
Time Complexity:
-
subscribe(eventName, callback)
:O(1)
amortizedMap.get()
operation:O(1)
average caseSet.add()
operation:O(1)
average case- Creating the subscription object with unsubscribe function:
O(1)
-
unsubscribe()
:O(1)
amortizedMap.get()
operation:O(1)
average caseSet.delete()
operation:O(1)
average case
-
emit(eventName, args)
:O(n)
wheren
is the number of callbacks subscribed to the eventMap.get()
operation:O(1)
average case- Spreading the Set into an array
[...callbacks]
:O(n)
- Mapping over callbacks and executing each:
O(n)
- Each callback execution time depends on the callback implementation (not counted here)
Space Complexity:
-
Overall EventEmitter instance:
O(m * k)
wherem
is the number of unique event names andk
is the average number of callbacks per event- The Map stores up to
m
event names - Each event name maps to a Set containing its callbacks
- The Map stores up to
-
subscribe(eventName, callback)
:O(1)
additional space- May create a new Set if the event doesn't exist yet:
O(1)
- Adds one callback reference to the Set:
O(1)
- Creates a subscription object:
O(1)
- May create a new Set if the event doesn't exist yet:
-
emit(eventName, args)
:O(n)
additional space wheren
is the number of callbacks for the event- Creates a new array from the Set:
O(n)
- Creates a result array from map operation:
O(n)
- The
args
array is passed by reference (not copied)
- Creates a new array from the Set:
-
unsubscribe()
:O(1)
additional space- Only removes a reference, no additional space allocated
Common Pitfalls
1. Maintaining Insertion Order with Sets
Pitfall: The Python implementation uses a Set
to store callbacks, but standard Python sets don't guarantee insertion order preservation (though since Python 3.7+, dict insertion order is preserved, sets still don't officially guarantee this). This violates the requirement that callbacks must execute in the order they were subscribed.
Example of the problem:
# If callbacks aren't executed in subscription order: emitter = EventEmitter() results = [] emitter.subscribe('test', lambda: results.append(1)) emitter.subscribe('test', lambda: results.append(2)) emitter.subscribe('test', lambda: results.append(3)) emitter.emit('test') # Expected: results = [1, 2, 3] # Possible with Set: results could be [2, 1, 3] or any order
Solution: Use a list or OrderedDict to maintain insertion order:
from collections import OrderedDict
class EventEmitter:
def __init__(self):
# Use dict with callback as key and True as value to maintain order
self.events = {} # eventName -> OrderedDict of callbacks
def subscribe(self, eventName, callback):
if eventName not in self.events:
self.events[eventName] = OrderedDict()
# Use callback as key to enable O(1) removal
self.events[eventName][callback] = True
def unsubscribe():
if eventName in self.events and callback in self.events[eventName]:
del self.events[eventName][callback]
if not self.events[eventName]:
del self.events[eventName]
return type('Subscription', (), {'unsubscribe': unsubscribe})()
def emit(self, eventName, args=None):
if args is None:
args = []
if eventName not in self.events:
return []
# Callbacks execute in insertion order
return [callback(*args) for callback in self.events[eventName].keys()]
2. Multiple Subscriptions of the Same Callback
Pitfall: Using a Set or dict with the callback as a key prevents subscribing the same callback function multiple times to the same event. This might be desired behavior in some cases.
Example of the problem:
def onClick():
return 1
emitter = EventEmitter()
sub1 = emitter.subscribe('click', onClick)
sub2 = emitter.subscribe('click', onClick) # This won't add a second subscription
result = emitter.emit('click')
# Expected if allowing duplicates: [1, 1]
# Actual with Set/dict: [1]
Solution: If duplicate subscriptions are needed, use a list with unique subscription IDs:
class EventEmitter:
def __init__(self):
self.events = {} # eventName -> list of (id, callback) tuples
self.next_id = 0
def subscribe(self, eventName, callback):
if eventName not in self.events:
self.events[eventName] = []
subscription_id = self.next_id
self.next_id += 1
self.events[eventName].append((subscription_id, callback))
def unsubscribe():
if eventName in self.events:
self.events[eventName] = [
(sid, cb) for sid, cb in self.events[eventName]
if sid != subscription_id
]
if not self.events[eventName]:
del self.events[eventName]
return type('Subscription', (), {'unsubscribe': unsubscribe})()
def emit(self, eventName, args=None):
if args is None:
args = []
if eventName not in self.events:
return []
return [callback(*args) for _, callback in self.events[eventName]]
3. Modifying the Subscription List During Emission
Pitfall: If a callback unsubscribes itself or other callbacks during execution, iterating over the original collection can cause issues.
Example of the problem:
emitter = EventEmitter()
sub = None
def callback1():
sub.unsubscribe() # Unsubscribes itself during execution
return 1
def callback2():
return 2
sub = emitter.subscribe('test', callback1)
emitter.subscribe('test', callback2)
result = emitter.emit('test') # Might cause iteration issues
Solution: Create a copy of the callbacks list before iteration:
def emit(self, eventName, args=None):
if args is None:
args = []
if eventName not in self.events:
return []
# Create a snapshot of callbacks to avoid modification during iteration
callbacks_snapshot = list(self.events[eventName].keys())
return [callback(*args) for callback in callbacks_snapshot if callback in self.events.get(eventName, {})]
Which data structure is used to implement priority queue?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!