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:
subscribe(eventPattern, listener): Allows a listener function to be registered for a specificeventPattern.unsubscribe(eventPattern, listener): Removes a specific listener for a giveneventPattern.emit(eventName, ...args): Triggers all listeners subscribed toeventNameor any pattern that matcheseventName.
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.*.updatedwould matchuser.profile.updatedbut notuser.updatedoruser.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 matchuser,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.cshould only be called fora.b.c. - A listener subscribed to
a.*.cshould be called fora.x.canda.y.c, but nota.b.cora.x.y.c. - A listener subscribed to
a.**should be called fora,a.x,a.x.y, etc. - A listener subscribed to
**.bshould be called forx.b,y.z.b, etc. - A listener subscribed to
**should be called for any event.
- A listener subscribed to
emitbehavior: Whenemit('user.profile.updated', data)is called, it should trigger listeners subscribed to:user.profile.updateduser.*.updateduser.****.updated**- And any other valid wildcard pattern that matches
user.profile.updated.
unsubscribebehavior: Should correctly remove the exact listener function associated with a specificeventPattern. 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
EventBusclass 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
EventBusinstance 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
unsubscribemethod 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.