Hone logo
Hone
Problems

JavaScript Event Bus with Wildcard Subscriptions

This challenge asks you to build a robust event bus system in JavaScript that supports wildcard subscriptions. An event bus is a common pattern for decoupling different parts of an application, allowing components to communicate without direct dependencies. Implementing wildcard support adds flexibility, enabling listeners to subscribe to broader categories of events.

Problem Description

You need to implement a JavaScript class named EventBus that facilitates communication between different parts of an application through events. The core functionality includes:

  1. subscribe(eventPattern, listener): Allows a listener function to be registered for a specific eventPattern.
  2. unsubscribe(eventPattern, listener): Removes a specific listener for a given eventPattern.
  3. emit(eventName, ...args): Triggers all listeners subscribed to eventName or any pattern that matches eventName.

The key requirement is the support for wildcard patterns in subscribe and unsubscribe. Wildcards should be able to match parts of the event name. Specifically, we'll support two types of wildcards:

  • * (single asterisk): Matches any single segment of an event name. For example, user.*.updated would match user.profile.updated but not user.updated or user.profile.email.updated. Event names are considered to be strings where segments are separated by dots (.).
  • ** (double asterisk): Matches zero or more segments. For example, user.** would match user, user.profile, user.profile.settings, etc.

Key Requirements:

  • Event Naming Convention: Event names are strings composed of segments separated by dots (e.g., user.login, order.placed.v2).
  • Wildcard Matching Logic:
    • A listener subscribed to a.b.c should only be called for a.b.c.
    • A listener subscribed to a.*.c should be called for a.x.c and a.y.c, but not a.b.c or a.x.y.c.
    • A listener subscribed to a.** should be called for a, a.x, a.x.y, etc.
    • A listener subscribed to **.b should be called for x.b, y.z.b, etc.
    • A listener subscribed to ** should be called for any event.
  • emit behavior: When emit('user.profile.updated', data) is called, it should trigger listeners subscribed to:
    • user.profile.updated
    • user.*.updated
    • user.**
    • **.updated
    • **
    • And any other valid wildcard pattern that matches user.profile.updated.
  • unsubscribe behavior: Should correctly remove the exact listener function associated with a specific eventPattern. If a listener is subscribed to multiple patterns, it should only be removed from the specified one.
  • No duplicate listeners: The same listener function, when subscribed multiple times with the exact same pattern, should not be added multiple times. However, it's valid to subscribe the same function to different patterns.
  • Order of execution: Listeners should be invoked in the order they were subscribed for a given eventName.

Edge Cases to Consider:

  • Empty event names or patterns.
  • Event names or patterns containing only dots or wildcards.
  • Subscribing and unsubscribing the same listener multiple times.
  • Emitting events when no listeners are registered.
  • Listeners that throw errors during execution (though handling these is optional for this challenge, the event bus should ideally not crash).

Examples

Example 1: Basic Subscriptions and Emits

const eventBus = new EventBus();

const loginListener = (user) => console.log(`User logged in: ${user}`);
const logoutListener = () => console.log('User logged out');

eventBus.subscribe('user.login', loginListener);
eventBus.subscribe('user.logout', logoutListener);

eventBus.emit('user.login', 'Alice'); // Output: User logged in: Alice
eventBus.emit('user.logout');       // Output: User logged out
eventBus.emit('user.profile.view');  // No output

Explanation: Standard subscription and emission of exact event names.

Example 2: Single Wildcard (*) Matching

const eventBus = new EventBus();

const profileUpdateListener = (data) => console.log(`Profile updated: ${JSON.stringify(data)}`);
const settingsUpdateListener = (data) => console.log(`Settings updated: ${JSON.stringify(data)}`);

eventBus.subscribe('user.*.updated', profileUpdateListener);
eventBus.subscribe('user.settings.updated', settingsUpdateListener);

eventBus.emit('user.profile.updated', { name: 'Bob' }); // Output: Profile updated: {"name":"Bob"}
eventBus.emit('user.settings.updated', { theme: 'dark' }); // Output: Profile updated: {"name":"Bob"} (from user.*.updated)
                                                        //         Settings updated: {"theme":"dark"} (from user.settings.updated)
eventBus.emit('user.email.updated', { address: 'test@example.com' }); // Output: Profile updated: {"address":"test@example.com"}
eventBus.emit('user.profile.email.updated', { new: 'mail' }); // No output
eventBus.emit('order.updated', { id: 123 }); // No output

Explanation: user.*.updated correctly matches events where the middle segment can be anything (profile, email). user.settings.updated is a specific match and also gets triggered by user.*.updated because settings is a single segment.

Example 3: Double Wildcard (**) Matching

const eventBus = new EventBus();

const allUserEventsListener = (eventName, ...args) => console.log(`[User Event] ${eventName}:`, ...args);
const allEventsListener = (eventName, ...args) => console.log(`[Any Event] ${eventName}:`, ...args);
const specificUserListener = (eventName, ...args) => console.log(`[Specific User Action] ${eventName}:`, ...args);

eventBus.subscribe('user.**', allUserEventsListener);
eventBus.subscribe('user.profile.updated', specificUserListener); // This will also be triggered by 'user.**'
eventBus.subscribe('**', allEventsListener);

eventBus.emit('user.login', 'Charlie'); // Output: [User Event] user.login: Charlie
                                    //         [Any Event] user.login: Charlie
eventBus.emit('user.profile.updated', { age: 30 }); // Output: [User Event] user.profile.updated: {"age":30}
                                                //         [Specific User Action] user.profile.updated: {"age":30}
                                                //         [Any Event] user.profile.updated: {"age":30}
eventBus.emit('order.created', { item: 'Book' }); // Output: [Any Event] order.created: {"item":"Book"}
eventBus.emit('user', 'status'); // Output: [User Event] user: status
                               //         [Any Event] user: status

Explanation: user.** catches all events starting with user. ** catches everything. A specific listener for user.profile.updated is still called, and it's also covered by the broader user.** and ** subscriptions.

Example 4: Unsubscribing

const eventBus = new EventBus();

const listenerA = () => console.log('Listener A');
const listenerB = () => console.log('Listener B');

eventBus.subscribe('event.one', listenerA);
eventBus.subscribe('event.one', listenerB);
eventBus.subscribe('event.two', listenerA);

eventBus.emit('event.one'); // Output: Listener A
                           //         Listener B
eventBus.emit('event.two'); // Output: Listener A

eventBus.unsubscribe('event.one', listenerA);

eventBus.emit('event.one'); // Output: Listener B (listenerA is removed)
eventBus.emit('event.two'); // Output: Listener A (still subscribed to event.two)

eventBus.unsubscribe('event.two', listenerA);
eventBus.emit('event.two'); // No output

eventBus.unsubscribe('event.one', listenerB);
eventBus.emit('event.one'); // No output

Explanation: Demonstrates how to remove specific listeners from specific patterns.

Example 5: Wildcard Unsubscribing and Overlap

const eventBus = new EventBus();

const listener = () => console.log('Matched');

eventBus.subscribe('app.*.data', listener);
eventBus.subscribe('app.status', listener);
eventBus.subscribe('**', listener);

// This listener is subscribed 3 times:
// 1. 'app.*.data'
// 2. 'app.status' (also matched by 'app.*.data' and '**')
// 3. '**'

eventBus.emit('app.user.data'); // Output: Matched (from app.*.data)
                               //         Matche (from **)
eventBus.emit('app.status');    // Output: Matched (from app.status)
                               //         Matched (from app.*.data - no, app.*.data doesn't match app.status)
                               //         Matched (from **) -- Correction: app.*.data does NOT match app.status.
                               // Let's re-evaluate the matches for app.status:
                               // - 'app.*.data' does not match 'app.status'
                               // - 'app.status' matches exactly.
                               // - '**' matches.
                               // So, for 'app.status', only 'app.status' and '**' should match.

// Corrected expectation for app.status:
eventBus.emit('app.status');    // Output: Matched (from app.status)
                               //         Matched (from **)

// Let's unsubscribe from 'app.*.data'
eventBus.unsubscribe('app.*.data', listener);

eventBus.emit('app.user.data'); // Output: Matched (from **) - app.*.data is removed
eventBus.emit('app.status');    // Output: Matched (from app.status)
                               //         Matched (from **) - ** is still active

// Let's unsubscribe from '**'
eventBus.unsubscribe('**', listener);

eventBus.emit('app.user.data'); // No output
eventBus.emit('app.status');    // Output: Matched (from app.status) - only app.status is left for this event

Explanation: Shows how unsubscribing a listener from a wildcard pattern only affects that specific subscription, not others. Also clarifies wildcard matching precedence/overlap. The app.*.data pattern does not match app.status because * must match exactly one segment.

Constraints

  • The EventBus class must be implemented in plain JavaScript, without external libraries.
  • Event names and patterns will be strings.
  • Listener functions will be JavaScript functions.
  • The total number of subscriptions for an EventBus instance should not exceed 10,000 at any given time.
  • The number of arguments passed to emit (including the event name) should not exceed 10.

Notes

  • Consider how you will store subscriptions. A Map or an Object might be suitable.
  • Think about the efficiency of your wildcard matching algorithm, especially for **.
  • The unsubscribe method should be robust and handle cases where a listener or pattern doesn't exist.
  • You will need to parse event names and patterns into segments to implement the matching logic effectively.
Loading editor...
javascript