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:
Calls the registered handler function
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:
Ultimate Simplicity: One consistent pattern for all event handling
Maximum Flexibility: Functions can compose and branch in unlimited ways
Transparent Logic: Flow control is explicit in function returns
Perfect Testability: Each function can be tested independently
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