Summary
When using wildcard patterns in event listeners (e.g., 'trigger:*', 'user.*'), the handler should receive the actual event name that was fired, not just the payload data.
Current Behavior
const emitter = new Emitter();
// Register wildcard listener
emitter.on('user.*', (data) => {
console.log(data); // { id: 123 }
// ❌ No way to know if this was 'user.login', 'user.logout', etc.
});
// Emit events
emitter.emit('user.login', { id: 123 });
emitter.emit('user.logout', { id: 123 });
Problem: The handler receives only data, with no information about which specific event triggered it.
Desired Behavior
const emitter = new Emitter();
// Wildcard listener receives event name as first parameter
emitter.on('user.*', (eventName, data) => {
console.log(eventName); // 'user.login' or 'user.logout'
console.log(data); // { id: 123 }
// Now we can branch logic based on the event
if (eventName === 'user.login') {
trackLogin(data);
} else if (eventName === 'user.logout') {
trackLogout(data);
}
});
// Exact match listeners work as before (backward compatible)
emitter.on('user.login', (data) => {
console.log(data); // { id: 123 }
// Event name implicit - no need to pass it
});
Use Cases
1. Generic Event Logging
// Log all analytics events with their names
sdk.on('analytics:*', (eventName, data) => {
logger.log(`Analytics Event: ${eventName}`, data);
});
2. Event Routing/Multiplexing
// Route different trigger types to appropriate handlers
sdk.on('trigger:*', (eventName, data) => {
const triggerType = eventName.replace('trigger:', '');
// Update context with the specific trigger
context.triggers[triggerType] = {
triggered: true,
timestamp: Date.now(),
...data
};
evaluate(context);
});
3. Debugging/Monitoring
// Monitor all events in development
if (DEBUG) {
sdk.on('*', (eventName, data) => {
console.log(`[SDK Event] ${eventName}`, data);
});
}
4. Metrics Collection
// Track event frequency by type
const eventCounts = {};
sdk.on('*', (eventName, data) => {
eventCounts[eventName] = (eventCounts[eventName] || 0) + 1;
});
Current Workarounds
Without this feature, developers must:
Workaround 1: Register Multiple Listeners (defeats wildcard purpose)
// Instead of one wildcard, register N listeners
sdk.on('trigger:exitIntent', (data) => handleTrigger('exitIntent', data));
sdk.on('trigger:scrollDepth', (data) => handleTrigger('scrollDepth', data));
sdk.on('trigger:timeDelay', (data) => handleTrigger('timeDelay', data));
// ... repeat for every trigger type
Workaround 2: Duplicate Event Name in Payload (DRY violation)
// Plugins must include the event type in the payload
emit('trigger:exitIntent', {
trigger: 'exitIntent', // Redundant - emitter already knows this!
...data
});
// Handler extracts it from payload
sdk.on('trigger:*', (data) => {
const triggerType = data.trigger; // Had to duplicate it
});
Proposed Implementation
emit(event: string, ...args: any[]): void {
for (const subscription of this.subscriptions) {
if (subscription.compiledPattern.test(event)) {
try {
// Detect if this is a wildcard pattern
const isWildcard = subscription.pattern.includes('*');
// Wildcard handlers get event name as first param
// Exact match handlers get only the data (backward compatible)
const handlerArgs = isWildcard ? [event, ...args] : args;
subscription.handler(...handlerArgs);
} catch (err) {
console.error(`Error in event handler for "${event}":`, err);
}
}
}
}
Backward Compatibility
Fully backward compatible:
- Exact match listeners (
'user.login') continue to receive only data
- Only wildcard listeners (
'user.*', '*') receive the event name
- Existing code without wildcards is unaffected
- New wildcard handlers can opt-in by accepting the first parameter
Industry Precedent
This pattern is standard in event systems with wildcards:
- EventEmitter2/EventEmitter3:
on('user.*', (event, data) => {})
- DOM Events: Event object includes
.type property
- Socket.io: Namespace events include event name
- Redis Pub/Sub: Pattern subscriptions include channel name
Benefits
- Enables powerful patterns - Generic handlers for multiple event types
- Reduces boilerplate - One listener instead of N listeners
- Improves debugging - Clear visibility into which event fired
- Follows standards - Aligns with industry best practices
- Backward compatible - Existing code continues to work
Alternative Considered
Status Quo: Keep current behavior and require workarounds.
Rejected because:
- Defeats the purpose of wildcards (handling multiple events generically)
- Forces code duplication (register N listeners or include type in payload)
- Creates friction for a core feature (wildcards are widely used)
Related
This issue was discovered while building the Experience SDK, which uses trigger events extensively:
- Plugins emit
trigger:exitIntent, trigger:scrollDepth, trigger:timeDelay, etc.
- Runtime needs to know which trigger fired to update context appropriately
- Current workaround: Register 6 separate listeners instead of 1 wildcard
Summary
When using wildcard patterns in event listeners (e.g.,
'trigger:*','user.*'), the handler should receive the actual event name that was fired, not just the payload data.Current Behavior
Problem: The handler receives only
data, with no information about which specific event triggered it.Desired Behavior
Use Cases
1. Generic Event Logging
2. Event Routing/Multiplexing
3. Debugging/Monitoring
4. Metrics Collection
Current Workarounds
Without this feature, developers must:
Workaround 1: Register Multiple Listeners (defeats wildcard purpose)
Workaround 2: Duplicate Event Name in Payload (DRY violation)
Proposed Implementation
Backward Compatibility
Fully backward compatible:
'user.login') continue to receive only data'user.*','*') receive the event nameIndustry Precedent
This pattern is standard in event systems with wildcards:
on('user.*', (event, data) => {}).typepropertyBenefits
Alternative Considered
Status Quo: Keep current behavior and require workarounds.
Rejected because:
Related
This issue was discovered while building the Experience SDK, which uses trigger events extensively:
trigger:exitIntent,trigger:scrollDepth,trigger:timeDelay, etc.