Event Pattern Matching
In Happen, we've taken a different approach to event pattern matching. Instead of providing complex pattern syntax with its own parsing rules and limitations, we leverage JavaScript's first-class functions to give you complete freedom in defining how events should be matched.
Function-Based Matchers
At its core, a pattern in Happen is simply a function that decides whether an event should be handled:
// A pattern is a function that returns true or false for an event type
node.on(eventType => eventType === 'order.submitted', (event, context) => {
// Process order submission
// ...
// Return next function or result
return processPayment;
});
This function receives the event's type string and returns a boolean: true if the event should be handled, false if it should be ignored.
This simple approach provides extraordinary flexibility:
// Match exact event type
node.on(type => type === 'order.submitted', handleOrderSubmission);
// Match events by domain prefix
node.on(type => type.startsWith('order.'), (event, context) => {
// Log all order events
logOrderEvent(event);
// Continue to domain-specific handler
return getDomainSpecificHandler(event.type);
});
// Match multiple specific events
node.on(type => ['payment.succeeded', 'payment.failed'].includes(type),
function(event, context) {
// Process payment result
processPaymentResult(event);
// Branch based on success or failure
return event.type === 'payment.succeeded' ?
handlePaymentSuccess : handlePaymentFailure;
}
);
// Match with regular expressions
node.on(type => /^user\.(created|updated|deleted)$/.test(type),
function(event, context) {
// Handle user lifecycle event
updateUserCache(event);
// Determine next step based on event type
const actions = {
'user.created': notifyUserCreated,
'user.updated': notifyUserUpdated,
'user.deleted': notifyUserDeleted
};
// Return appropriate next function
return actions[event.type];
}
);
// Complex conditional matching
node.on(type => {
const [domain, action] = type.split('.');
return domain === 'inventory' && action.includes('level');
}, function(event, context) {
// Process inventory level event
processInventoryLevel(event);
// Check if restock needed
if (isRestockNeeded(event.payload)) {
return createRestockOrder;
}
// Otherwise complete
return { processed: true };
});
Creating Your Own Pattern System
With function-based matchers, you can build any pattern matching system that suits your needs. Here's an example of how you might create your own pattern utilities:
// Create a domain matcher
function domain(name) {
return type => type.startsWith(`${name}.`);
}
// Create an exact matcher
function exact(fullType) {
return type => type === fullType;
}
// Create a wildcard matcher
function wildcard(pattern) {
// Convert the pattern to a regex
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
return type => regex.test(type);
}
// Create a matcher for multiple patterns
function oneOf(...patterns) {
return type => patterns.some(pattern =>
typeof pattern === 'function' ? pattern(type) : type === pattern
);
}
// Create a matcher that excludes patterns
function not(pattern) {
const matcher = typeof pattern === 'function' ? pattern : type => type === pattern;
return type => !matcher(type);
}
Now you can use these utilities to create expressive, reusable matchers:
// Match all order events
node.on(domain('order'), (event, context) => {
// Process all order events
logOrderEvent(event);
// Continue to specific handler based on event type
const actionType = event.type.split('.')[1];
return getHandlerForAction(actionType);
});
// Match a specific event exactly
node.on(exact('payment.succeeded'), (event, context) => {
// Process successful payment
updateOrderPaymentStatus(event.payload);
// Continue to shipping
return createShipment;
});
// Match using wildcards
node.on(wildcard('user.*.completed'), (event, context) => {
// Handle user completion events
trackUserCompletion(event);
// Determine next steps
return selectNextUserAction;
});
// Match any of multiple patterns
node.on(oneOf(
exact('order.submitted'),
exact('payment.succeeded'),
wildcard('shipping.*')
), (event, context) => {
// Handle important events
notifyAdmins(event);
// Continue to regular processing
return processNormally;
});
// Match one domain but exclude specific events
node.on(
type => domain('user')(type) && not(exact('user.password-changed'))(type),
(event, context) => {
// Handle non-sensitive user events
logUserAction(event);
// Continue to specific handler
return getHandlerForUserAction(event.type);
}
);
String Pattern Support
For convenience, Happen also supports string patterns which are converted to matcher functions internally:
// These are equivalent
node.on('order.submitted', handleOrderSubmission);
node.on(type => type === 'order.submitted', handleOrderSubmission);
// These are equivalent too
node.on('order.*', handleAllOrderEvents);
node.on(type => type.startsWith('order.') && !type.includes('.', type.indexOf('.') + 1), handleAllOrderEvents);
String patterns support several features:
Exact matching: 'order.submitted'
Wildcards: 'order.*'
Alternative patterns: '{order,payment}.created'
Multiple segments: 'user.profile.*'
However, function matchers provide greater flexibility and expressiveness when you need more complex matching logic.
Building Domain-Specific Pattern Systems
For larger applications, you might want to build a more structured pattern system. Here's an example of a domain-oriented approach:
// Create a domain builder
function createDomain(name) {
// Basic event matcher creator
function eventMatcher(action) {
return type => type === `${name}.${action}`;
}
// Return a function that can be called directly or accessed for utilities
const domain = eventMatcher;
// Add utility methods
domain.all = () => type => type.startsWith(`${name}.`);
domain.oneOf = (...actions) => type => {
const prefix = `${name}.`;
return type.startsWith(prefix) &&
actions.includes(type.slice(prefix.length));
};
return domain;
}
// Create domains for your application
const order = createDomain('order');
const payment = createDomain('payment');
const user = createDomain('user');
// Use them in your handlers
node.on(order('submitted'), (event, context) => {
// Handle order submission
processOrder(event.payload);
// Return next function
return validateInventory;
});
node.on(order.all(), (event, context) => {
// Log all order events
logOrderActivity(event);
// Continue normal processing
return context.next || null;
});
node.on(payment.oneOf('succeeded', 'failed'), (event, context) => {
// Process payment results
updateOrderWithPayment(event);
// Branch based on result
return event.type === 'payment.succeeded' ?
handleSuccessfulPayment : handleFailedPayment;
});
Benefits of Function-Based Pattern Matching
This approach to pattern matching offers several advantages:
Unlimited Flexibility: Any matching logic can be implemented
Zero Parse-Time Overhead: Patterns are just functions, no parsing needed
Type Safety: TypeScript can fully type your pattern functions
Testability: Pattern functions can be unit tested independently
Composition: Combine matchers to create complex patterns
Familiar JavaScript: No special syntax to learn, just standard JS
Best Practices
Keep matcher functions pure: They should depend only on the input event type
Create reusable pattern factories: Build a library of matcher creators for your application
Compose simple matchers: Build complex patterns by combining simple ones
Test your matchers: Unit test complex matching logic independently
Consider performance: For high-frequency events, optimize your matcher functions
Last updated