The Event Continuum

In Happen, we've distilled event processing to its purest essence: a continuous flow of functions that process events and determine what happens next.

The Pure Functional Flow Model

At its core, the Event Continuum views event handling as a single entry point that can branch into a chain of functions through return values:

// Register a single entry point
orderNode.on("create-order", validateOrder);

// The flow is controlled by function returns
function validateOrder(event, context) {
  if (!isValidOrder(event.payload)) {
    return { success: false, reason: "Invalid order" };
  }
  
  // Return the next function to execute
  return processOrder;
}

function processOrder(event, context) {
  // Process the order
  const orderId = createOrderInDatabase(event.payload);
  
  // Store in context
  context.orderId = orderId;
  
  // Return the next function
  return notifyCustomer;
}

function notifyCustomer(event, context) {
  // Send notification using context data
  sendOrderConfirmation(context.orderId, event.payload.customer);
  
  // Return final result
  return { success: true, orderId: context.orderId };
}

This creates an elegant flow system with just one fundamental mechanism: functions that return either the next function or a final value.

How the Continuum Works

When an event arrives at a node, the system:

  1. Calls the registered handler function

  2. Examines the return value:

    • If it's a function: Execute that function next

    • If it's any other value: Complete the flow with that value as the result

This simple mechanism creates a remarkably powerful and flexible flow system.

Flow Control Through Return Values

The Event Continuum achieves its power through a pure functional approach:

Continuing to the Next Step

When a handler returns another function, the flow continues with that function:

function checkInventory(event, context) {
  // Check inventory
  const inventoryResult = validateStock(event.payload.items);
  
  if (!inventoryResult.available) {
    return handleInventoryShortage;
  }
  
  // Continue to payment processing
  return processPayment;
}

Completing the Flow with a Result

When a handler returns any non-function value, the flow completes with that value as the result:

function processPayment(event, context) {
  // Process payment
  const paymentResult = chargeCustomer(event.payload.payment);
  
  if (!paymentResult.success) {
    // Complete with failure result
    return {
      success: false,
      reason: "payment-failed",
      details: paymentResult.error
    };
  }
  
  // Continue to shipping
  return createShipment;
}

Dynamic Flow Selection

Functions can return different next steps based on any condition:

function determinePaymentMethod(event, context) {
  // Choose next step based on payment method
  const method = event.payload.paymentMethod;
  
  // Return different functions based on conditions
  if (method === "credit-card") {
    return processCreditCard;
  } else if (method === "paypal") {
    return processPaypal;
  } else {
    return processAlternativePayment;
  }
}

Shared Context

A shared context object flows through the function chain, allowing communication between steps:

function validateOrder(event, context) {
  // Store validation result in context
  context.validation = validateOrderData(event.payload);
  
  if (!context.validation.valid) {
    return { success: false, errors: context.validation.errors };
  }
  
  return processInventory;
}

function processInventory(event, context) {
  // Access context from previous step
  const validatedItems = context.validation.items;
  
  // Add more to context
  context.inventory = checkInventoryForItems(validatedItems);
  
  return processPayment;
}

This provides a clean way for functions to build up state as the flow progresses.

Complex Flow Patterns

The pure functional model supports sophisticated flow patterns:

Conditional Branching

function processOrder(event, context) {
  // Branch based on order type
  if (event.payload.isRush) {
    return processRushOrder;
  } else if (event.payload.isInternational) {
    return processInternationalOrder;
  } else {
    return processStandardOrder;
  }
}

Loop Patterns

You can create loops by returning a function that's already been executed:

function processItems(event, context) {
  // Initialize if first time
  if (!context.itemIndex) {
    context.itemIndex = 0;
    context.results = [];
  }
  
  // Get current item
  const items = event.payload.items;
  const currentItem = items[context.itemIndex];
  
  // Process current item
  const result = processItem(currentItem);
  context.results.push(result);
  
  // Increment index
  context.itemIndex++;
  
  // Loop if more items
  if (context.itemIndex < items.length) {
    return processItems; // Loop back to this function
  }
  
  // Otherwise, continue to next handler
  return summarizeResults;
}

Error Handling Patterns

Error handling becomes part of the natural flow:

function processPayment(event, context) {
  try {
    // Attempt to process payment
    const result = chargeCustomer(event.payload.payment);
    context.payment = result;
    
    // Continue to shipping
    return createShipment;
  } catch (error) {
    // Handle error
    context.error = error;
    
    // Branch to error handler
    return handlePaymentError;
  }
}

function handlePaymentError(event, context) {
  // Log error
  logPaymentFailure(context.error);
  
  // Return error result
  return {
    success: false,
    reason: "payment-failed",
    error: context.error.message
  };
}

Building Complete Workflows

The Event Continuum naturally supports building complex workflows:

// Entry point
orderNode.on("process-order", validateOrder);

// Define workflow steps as functions
function validateOrder(event, context) {
  const validation = performValidation(event.payload);
  
  if (!validation.valid) {
    return { success: false, errors: validation.errors };
  }
  
  // Store in context and continue
  context.validation = validation;
  return checkInventory;
}

function checkInventory(event, context) {
  const inventoryResult = checkStockLevels(event.payload.items);
  
  if (!inventoryResult.available) {
    return handleInventoryShortage;
  }
  
  // Store in context and continue
  context.inventory = inventoryResult;
  return processPayment;
}

function processPayment(event, context) {
  const paymentResult = processTransaction(event.payload.payment);
  
  if (!paymentResult.success) {
    return handlePaymentFailure;
  }
  
  // Store in context and continue
  context.payment = paymentResult;
  return createShipment;
}

function createShipment(event, context) {
  // Create shipment
  const shipment = generateShipment(event.payload.address, event.payload.items);
  context.shipment = shipment;
  
  return finalizeOrder;
}

function finalizeOrder(event, context) {
  // Finalize the order
  const finalOrder = {
    orderId: generateOrderId(),
    customer: event.payload.customer,
    payment: context.payment,
    shipment: context.shipment,
    status: "completed"
  };
  
  // Save order
  saveOrderToDatabase(finalOrder);
  
  // Return final result
  return {
    success: true,
    order: finalOrder
  };
}

// Error handling functions
function handleInventoryShortage(event, context) {
  notifyInventoryTeam(event.payload.items);
  
  return {
    success: false,
    reason: "inventory-shortage",
    availableOn: estimateRestockDate(event.payload.items)
  };
}

function handlePaymentFailure(event, context) {
  // Log failure
  logPaymentFailure(event.payload.payment);
  
  return {
    success: false,
    reason: "payment-failed",
    paymentError: context.payment.error
  };
}

The Power of Pure Functional Flows

This pure functional approach offers several benefits:

  1. Ultimate Simplicity: One consistent pattern for all event handling

  2. Maximum Flexibility: Functions can compose and branch in unlimited ways

  3. Transparent Logic: Flow control is explicit in function returns

  4. Perfect Testability: Each function can be tested independently

  5. Minimal API Surface: Just .on() with a single handler

This approach embodies Happen's philosophy in its purest form - a single registration method and function returns create a system of unlimited expressiveness.

Examples of the Event Continuum in Action

Simple Event Handling

// Simple handler that completes immediately
orderNode.on("order-created", (event) => {
  console.log("Order created:", event.payload.id);
  return { acknowledged: true };
});

Validation Pattern

// Entry point with validation
orderNode.on("create-user", (event) => {
  const validation = validateUserData(event.payload);
  
  if (!validation.valid) {
    return { success: false, errors: validation.errors };
  }
  
  // Continue to user creation
  return createUserRecord;
});

function createUserRecord(event, context) {
  // Create user in database
  const userId = createUser(event.payload);
  
  // Return success
  return { success: true, userId };
}

Request-Response with Direct Return

// Direct communication with explicit return
dataNode.on("get-user-data", (event, context) => {
  const { userId, fields } = event.payload;
  
  // Fetch user data
  const userData = fetchUserData(userId, fields);
  
  // Return directly to requester - this value will be sent back
  return userData;
});

Ending Flow with Bare Return

A bare return statement will end the flow without returning any value:

// End flow with a bare return statement
dataNode.on("log-activity", (event, context) => {
  // Log the activity
  logUserActivity(event.payload.userId, event.payload.action);
  
  // End the flow with a bare return
  return;
});

This is equivalent to returning undefined but is more explicit about the intention to end the flow without a value.

Building Reusable Flow Patterns

The functional nature of the Event Continuum encourages building reusable patterns:

// Create a validation wrapper
function withValidation(validator, nextStep) {
  return function(event, context) {
    const validation = validator(event.payload);
    
    if (!validation.valid) {
      return { success: false, errors: validation.errors };
    }
    
    // Store validation result
    context.validation = validation;
    
    // Continue to next step
    return nextStep;
  };
}

// Create a retry wrapper
function withRetry(handler, maxRetries = 3) {
  return function retryHandler(event, context) {
    // Initialize retry count
    context.retryCount = context.retryCount || 0;
    
    try {
      // Attempt to execute handler
      return handler(event, context);
    } catch (error) {
      // Increment retry count
      context.retryCount++;
      
      // Check if we can retry
      if (context.retryCount <= maxRetries) {
        console.log(`Retrying (${context.retryCount}/${maxRetries})...`);
        return retryHandler; // Recurse to retry
      }
      
      // Too many retries
      return {
        success: false,
        error: error.message,
        retries: context.retryCount
      };
    }
  };
}

// Usage
orderNode.on("create-order", 
  withValidation(validateOrder, 
    withRetry(processOrder)
  )
);

Parallel Processing

For more advanced scenarios, you can implement parallel processing:

// Parallel execution helper
function parallel(functions) {
  return async function(event, context) {
    // Execute all functions in parallel
    const results = await Promise.all(
      functions.map(fn => fn(event, context))
    );
    
    // Store results
    context.parallelResults = results;
    
    // Return the continuation function
    return context.continuation;
  };
}

// Usage
orderNode.on("process-order", (event, context) => {
  // Set up the continuation
  context.continuation = finalizeOrder;
  
  // Execute these functions in parallel
  return parallel([
    validateInventory,
    processPayment,
    prepareShipping
  ]);
});

function finalizeOrder(event, context) {
  // Access results from parallel execution
  const [inventoryResult, paymentResult, shippingResult] = context.parallelResults;
  
  // Proceed based on all results
  if (inventoryResult.success && paymentResult.success && shippingResult.success) {
    return { success: true, order: combineResults(context.parallelResults) };
  } else {
    return { success: false, failures: findFailures(context.parallelResults) };
  }
}

By treating event handling as a pure functional flow where each function determines what happens next, Happen enables a system of unlimited expressiveness that can handle everything from simple events to complex workflows with the same consistent pattern.

Ready to explore more? Continue to the Communication Patterns section to see how the Event Continuum integrates with other aspects of the Happen framework.

Last updated