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:

  1. Unlimited Flexibility: Any matching logic can be implemented

  2. Zero Parse-Time Overhead: Patterns are just functions, no parsing needed

  3. Type Safety: TypeScript can fully type your pattern functions

  4. Testability: Pattern functions can be unit tested independently

  5. Composition: Combine matchers to create complex patterns

  6. 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