2694. Event Emitter


Problem Description

The problem requires designing an EventEmitter class that mimics the basic behavior of event emitter systems found in many programming environments such as Node.js or the web's Event Target interface.

An EventEmitter is essentially a mechanism that allows different parts of an application to communicate with each other by emitting events to which other parts have subscribed.

Two core functionalities are required:

  • Subscribing to events: The subscribe method should allow clients to register a callback function that is to be executed when a particular event is emitted. Each event can have multiple subscribers, and upon the event being emitted, the callbacks should be executed in the order they were added.
  • Emitting events: The emit method should trigger all the callbacks associated with the event name it receives as an argument. If the event has associated arguments, they should be passed to the callback functions.

Additionally, when a client subscribes to an event, a subscription object with an unsubscribe method should be returned. This method, when called, will remove the callback from the event's list of subscribers.

Intuition

To achieve the functionality described, the EventEmitter class must maintain a data structure that can keep track of all the callbacks associated with each event. To do this in an efficient and orderly way, a Map can be used where the key is the event name, and the value is a Set of callback functions. A Set is chosen because it inherently avoids duplicate entries, which is desirable as it's stated that no callbacks passed to subscribe are referentially identical.

For the subscribe method, we implement the following steps:

  1. Retrieve the set of callbacks for the event from the map, or create a new set if it doesn't exist.
  2. Add the callback to the set.
  3. Return an object with an unsubscribe method that, when invoked, will delete the callback from the set of callbacks associated with the event.

For the emit method, we need to:

  1. Retrieve the set of callbacks for the given event name.
  2. If the event has no callbacks, we return an empty array as specified.
  3. Iterate over the set of callbacks, executing each with the provided arguments, and collect the results.
  4. Return the array with the results from all the executed callbacks.

This implementation ensures that all the requirements for the EventEmitter class are fulfilled, allowing for flexible event-driven programming patterns.

Solution Approach

The EventEmitter class is implemented using TypeScript which provides type-safety over JavaScript. Here's an approach to how the solution is crafted:

Data Structures

JavaScript's Map and Set are used to implement the subscription mechanism.

  • A Map is used to associate each event with a set of callbacks (Set<Callback>). This allows efficient retrieval and management of callbacks per event.
  • Set is used to store callbacks for each event to ensure each callback is unique.

Implementation Details

subscribe Method

  1. When a subscriber calls subscribe, it passes the eventName and the callback.
  2. The method retrieves the callbacks set associated with eventName using this.d.get(eventName), or initializes a new Set if none exists.
  3. It adds the callback to the set with .add(callback).
  4. The method returns an object containing an unsubscribe method. Inside this method, it removes the callback from the callbacks set when called.

The use of a Set here ensures that the order of callbacks will be preserved, and duplicates are prevented without extra logic.

emit Method

  1. This method takes eventName and optionally an array of args.
  2. It fetches the set of callbacks associated with eventName. If none are found, it returns an empty array.
  3. It uses the spread syntax [...callbacks] to convert the set into an array that can be iterated over.
  4. map is used to invoke each callback with the provided args, collecting the results in an array that is returned.

The spread and map operations ensure that callbacks are called in the order they were added.

To summarize, the algorithm for this solution is straightforward: use Map for event-to-callback(s) association, and Set for maintaining callbacks. Subscribing adds the callback to the set, and emitting iterates over and invokes these callbacks, while unsubscribing removes a callback from the set.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's demonstrate the solution approach with a small example. Consider we want our EventEmitter to handle 'data' and 'error' events.

First, we create an instance of EventEmitter.

let emitter = new EventEmitter();

Now, let's subscribe to the 'data' event with two different callbacks.

let dataSubscription1 = emitter.subscribe('data', (data) => `Received ${data}`);
let dataSubscription2 = emitter.subscribe('data', (data) => `Processed ${data}`);

According to the solution approach, the subscribe method will add each callback to a set associated with the 'data' event in the emitter's internal map. Each subscription returns an object with an unsubscribe method.

Next, we will emit the 'data' event with a piece of data, let's say 'example data'.

let result = emitter.emit('data', 'example data');

When the emit method is called, it will look up the associated set of callbacks for the 'data' event and invoke each callback in the order they were added, passing 'example data' to them. The result will be an array of callback outputs:

console.log(result); // Output: ["Received example data", "Processed example data"]

Finally, let's demonstrate unsubscribing from an event. We decide to unsubscribe dataSubscription1.

dataSubscription1.unsubscribe();

Now, if we emit the 'data' event again:

result = emitter.emit('data', 'new example data');

This time, the output will only reflect the remaining subscription:

console.log(result); // Output: ["Processed new example data"]

The unsubscribe method removed dataSubscription1 from the emitter's callback set, so invoking the 'data' event no longer triggers the first callback.

This example has shown how the EventEmitter class can subscribe to events with callbacks, emit events which trigger those callbacks, and unsubscribe from events to remove callbacks—all following the explained solution approach using JavaScript's Map and Set.

Solution Implementation

1# Global mapping from event names to a set of callbacks
2event_callbacks_map = {}
3
4# Type Alias for callback function
5Callback = callable
6
7# Subscription object returned by subscribe function
8class Subscription:
9    def __init__(self, event_name, callback):
10        self.event_name = event_name
11        self.callback = callback
12  
13    def unsubscribe(self):
14        """Unsubscribe the callback from the event"""
15        if self.event_name in event_callbacks_map:
16            event_callbacks_map[self.event_name].discard(self.callback)
17
18def subscribe(event_name: str, callback: Callback) -> Subscription:
19    """
20    Subscribes to a given event with a callback and returns a subscription object.
21  
22    :param event_name: The name of the event to subscribe to.
23    :param callback: The callback to invoke when the event is emitted.
24    :return: A Subscription object with an unsubscribe method to cancel the subscription.
25    """
26    # Use setdefault to initialize a set if the event name is not already present
27    event_callbacks_map.setdefault(event_name, set()).add(callback)
28  
29    # Return a new Subscription object
30    return Subscription(event_name, callback)
31
32def emit(event_name: str, args: list = None) -> list:
33    """
34    Emits a given event by invoking all subscribed callbacks with the provided arguments.
35  
36    :param event_name: The name of the event to emit.
37    :param args: A list of arguments to pass to the callbacks.
38    :return: A list containing the results of each callback invocation.
39    """
40    # Default args to an empty list if not provided
41    if args is None:
42        args = []
43      
44    # Get the set of callbacks for the event name
45    callbacks = event_callbacks_map.get(event_name, [])
46  
47    # Invoke all callbacks and collect the results
48    return [callback(*args) for callback in callbacks]
49
50# Example usage:
51
52# Subscribes to the 'onClick' event with on_click_callback
53def on_click_callback():
54    """Example callback function"""
55    return 99
56
57# Subscribe to the 'onClick' event
58subscription = subscribe('onClick', on_click_callback)
59
60# Emit the 'onClick' event and print the result
61print(emit('onClick'))  # [99]
62
63# Unsubscribe from the 'onClick' event
64subscription.unsubscribe()
65
66# Attempt to emit the 'onClick' event again and print the result
67print(emit('onClick'))  # []
68
1import java.util.*;
2
3// Functional interface to represent a generic Callback
4@FunctionalInterface
5interface Callback { 
6    Object call(Object... args); 
7}
8
9// Subscription interface to represent the contract for an unsubscribe action
10interface Subscription { 
11    void unsubscribe(); 
12}
13
14// EventManager class to handle subscribing and emitting events
15public class EventManager {
16    // Global mapping from event names to a set of Callbacks
17    private static Map<String, Set<Callback>> eventCallbacksMap = new HashMap<>();
18
19    // Subscribes to a given event with a callback and returns a subscription object
20    public static Subscription subscribe(String eventName, Callback callback) {
21        Set<Callback> callbacks = eventCallbacksMap.computeIfAbsent(eventName, k -> new HashSet<>());
22        callbacks.add(callback);
23
24        return () -> callbacks.remove(callback);
25    }
26
27    // Emits a given event by invoking all subscribed callbacks with the provided arguments
28    public static List<Object> emit(String eventName, Object... args) {
29        Set<Callback> callbacks = eventCallbacksMap.get(eventName);
30        if (callbacks == null) {
31            return Collections.emptyList();
32        }
33        List<Object> results = new ArrayList<>();
34        for (Callback callback : callbacks) {
35            results.add(callback.call(args));
36        }
37        return results;
38    }
39}
40
41// Example usage
42
43public class Example {
44    public static void main(String[] args) {
45        // Subscribes to the 'onClick' event with onClickCallback
46        Callback onClickCallback = () -> 99;
47        Subscription subscription = EventManager.subscribe("onClick", onClickCallback);
48
49        // Emits the 'onClick' event and logs the result
50        System.out.println(EventManager.emit("onClick")); // Output: [99]
51
52        // Unsubscribes from the 'onClick' event
53        subscription.unsubscribe();
54
55        // Attempts to emit the 'onClick' event again and logs the result
56        System.out.println(EventManager.emit("onClick")); // Output: []
57    }
58}
59
1#include <iostream>
2#include <string>
3#include <unordered_map>
4#include <unordered_set>
5#include <functional>
6#include <vector>
7
8// Alias for a function that takes no arguments and returns an int.
9using Callback = std::function<int()>;
10
11// Struct representing a subscription with an unsubscribe method.
12struct Subscription {
13    std::string eventName;
14    Callback callback;
15  
16    void unsubscribe(std::unordered_map<std::string, std::unordered_set<Callback>>& eventCallbacksMap) {
17        // Deletes the callback from the set of callbacks for the given event.
18        if(eventCallbacksMap.count(eventName) > 0) {
19            eventCallbacksMap[eventName].erase(callback);
20        }
21    }
22};
23
24// Global mapping from event names to a set of Callbacks.
25std::unordered_map<std::string, std::unordered_set<Callback>> eventCallbacksMap;
26
27/**
28 * @brief Subscribes to a given event with a callback and returns a subscription object.
29 * 
30 * @param eventName The name of the event to subscribe to.
31 * @param callback The callback to invoke when the event is emitted.
32 * @return Subscription An object with an unsubscribe method to cancel the subscription.
33 */
34Subscription subscribe(const std::string& eventName, const Callback& callback) {
35    // Adds the callback to the set for the given event.
36    eventCallbacksMap[eventName].emplace(callback);
37  
38    // Creates a subscription object for the caller to retain and use for unsubscribing.
39    Subscription subscription{eventName, callback};
40    return subscription;
41}
42
43/**
44 * @brief Emits a given event by invoking all subscribed callbacks.
45 * 
46 * @param eventName The name of the event to emit.
47 * @return std::vector<int> A vector containing the results of each callback invocation.
48 */
49std::vector<int> emit(const std::string& eventName) {
50    std::vector<int> results;
51    auto callbacksIt = eventCallbacksMap.find(eventName);
52  
53    // If no callbacks are registered for this event, return an empty vector.
54    if (callbacksIt == eventCallbacksMap.end()) {
55        return results;
56    }
57
58    // Invokes each callback associated with the event and stores the result in the vector.
59    for (const auto& callback : callbacksIt->second) {
60        results.push_back(callback());
61    }
62  
63    return results;
64}
65
66// Example usage:
67int main() {
68    // Subscribes to the 'onClick' event with onClickCallback
69    auto onClickCallback = []() -> int {
70        return 99;
71    };
72    Subscription subscription = subscribe("onClick", onClickCallback);
73
74    // Emits the 'onClick' event and prints the result.
75    for (int result : emit("onClick")) {
76        std::cout << result << std::endl; // Should print: 99
77    }
78
79    // Unsubscribes from the 'onClick' event.
80    subscription.unsubscribe(eventCallbacksMap);
81
82    // Attempts to emit the 'onClick' event again and prints the result.
83    for (int result : emit("onClick")) {
84        std::cout << result << std::endl; // Should not print anything.
85    }
86
87    return 0;
88}
89
1type Callback = (...args: any[]) => any;
2type Subscription = {
3    unsubscribe: () => void;
4};
5
6// Global mapping from event names to a set of Callbacks
7const eventCallbacksMap: Map<string, Set<Callback>> = new Map();
8
9/**
10 * Subscribes to a given event with a callback and returns a subscription object.
11 * @param eventName - The name of the event to subscribe to.
12 * @param callback - The callback to invoke when the event is emitted.
13 * @returns An object with an unsubscribe method to cancel the subscription.
14 */
15function subscribe(eventName: string, callback: Callback): Subscription {
16    const callbacks = eventCallbacksMap.get(eventName) || new Set<Callback>();
17    callbacks.add(callback);
18    eventCallbacksMap.set(eventName, callbacks);
19
20    return {
21        unsubscribe: () => {
22            eventCallbacksMap.get(eventName)?.delete(callback);
23        }
24    };
25}
26
27/**
28 * Emits a given event by invoking all subscribed callbacks with the provided arguments.
29 * @param eventName - The name of the event to emit.
30 * @param args - An array of arguments to pass to the callbacks.
31 * @returns An array containing the results of each callback invocation.
32 */
33function emit(eventName: string, args: any[] = []): any[] {
34    const callbacks = eventCallbacksMap.get(eventName);
35    if (!callbacks) {
36        return [];
37    }
38    return Array.from(callbacks).map(callback => callback(...args));
39}
40
41// Example usage:
42
43// Subscribes to the 'onClick' event with onClickCallback
44function onClickCallback() {
45    return 99;
46}
47const subscription = subscribe('onClick', onClickCallback);
48
49// Emits the 'onClick' event and logs the result
50console.log(emit('onClick')); // [99]
51
52// Unsubscribes from the 'onClick' event
53subscription.unsubscribe();
54
55// Attempts to emit the 'onClick' event again and logs the result
56console.log(emit('onClick')); // []
57

Time and Space Complexity

Time Complexity

  • subscribe(eventName: string, callback: Callback): Subscription

    • The time complexity for subscribing is O(1) since adding an event to a Map and adding a callback to a Set are both constant time operations.
  • unsubscribe()

    • The time complexity for unsubscribing is O(1) because it involves a single call to delete on a Set, which is a constant time operation.
  • emit(eventName: string, args: any[] = []): any

    • The time complexity for emitting an event depends on the number of callbacks subscribed to the event. If n is the number of callbacks, the complexity is O(n) because it maps each callback and applies it to the arguments.

Space Complexity

  • Overall space complexity for the EventEmitter class would be O(m * n) where m is the number of unique events and n is the average number of subscribers per event since a Set of callbacks is stored for each event.

Additionally, the space complexity for emitting an event (emit) is also worth noting:

  • emit(eventName: string, args: any[] = []): any
    • The space complexity for emitting is O(n) because it creates an array of the return values from each callback, where n is the number of callbacks.

Do note that this analysis assumes the size of the event names and the footprint of each callback function are not significant compared to the numbers of events and subscribers, which is a common assumption in complexity analysis.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!