Dependency Management
In line with Happen's philosophy of simplicity, our approach to dependencies emphasizes direct runtime access with minimal framework abstractions.
Core Principles
Happen's dependency management follows three key principles:
Runtime Transparency: Your code has direct access to the runtime environment
Minimal System Dependencies: Only essential framework dependencies (primarily NATS) are injected at initialization
Event-Based Communication: Nodes interact through events, not traditional dependency injection
System-Level Dependencies
System dependencies are configured once during framework initialization:
// Initialize Happen with system-level dependencies
const happen = initializeHappen({
// NATS configuration as the primary dependency
nats: {
// Direct NATS client for server environments
server: {
servers: ['nats://localhost:4222'],
jetstream: true
},
// WebSocket client for browser environments
browser: {
servers: ['wss://localhost:8443'],
jetstream: true
}
},
// Optional: Override crypto implementation
crypto: cryptoImplementation
});
// The initialized framework provides the node creation function
const { createNode } = happen;
These system dependencies represent the minimal set required for Happen to function across different runtime environments. By injecting them at initialization, we maintain runtime agnosticism while ensuring consistent behavior.
Direct Runtime Access
Rather than exposing a subset of features through abstraction layers, Happen encourages direct access to the runtime:
// Import runtime capabilities directly
import { WebSocketServer } from 'ws';
import * as fs from 'fs';
const notificationNode = createNode('notification-service');
// Set up WebSocket server directly using runtime capabilities
const wss = new WebSocketServer({ port: 8080 });
const clients = new Map();
// Use runtime WebSocket events directly
wss.on('connection', (ws, req) => {
const userId = getUserIdFromRequest(req);
clients.set(userId, ws);
ws.on('close', () => {
clients.delete(userId);
});
});
// Process events using the Event Continuum
notificationNode.on('notify-user', function notifyUser(event, context) {
const { userId, message } = event.payload;
// Direct runtime access - no framework abstraction
const clientSocket = clients.get(userId);
if (!clientSocket || clientSocket.readyState !== WebSocket.OPEN) {
return {
success: false,
reason: "user-not-connected"
};
}
// Send notification
clientSocket.send(JSON.stringify({
type: 'notification',
message
}));
return {
success: true,
delivered: true
};
});
This approach provides several benefits:
Zero overhead: No performance penalty for accessing runtime features
Full capabilities: Access to everything the runtime offers, not just what the framework exposes
Runtime evolution: Immediate access to new runtime features without framework updates
Ecosystem compatibility: Works seamlessly with the broader ecosystem
Framework Agnosticism for Third-Party Libraries
When you need external libraries, import and use them directly:
// Import NATS client directly - no framework-specific imports
import { connect } from 'nats';
const databaseNode = createNode('database-service');
// Use the NATS client directly
const nc = await connect({ servers: 'nats://localhost:4222' });
const js = nc.jetstream();
databaseNode.on('query-users', function queryUsers(event, context) {
const { searchTerm } = event.payload;
// Direct use of NATS client with KV store
const kv = js.keyValue("users");
const values = [];
for await (const k of kv.keys()) {
if (k.includes(searchTerm)) {
const entry = await kv.get(k);
values.push(JSON.parse(entry.string()));
}
}
return {
success: true,
results: values
};
});
Dependency Injection Through Closures
Use closures to create handlers with access to dependencies:
// Initialize node with dependencies
function initializeDataNode() {
// Create NATS client and JetStream KV store
const nc = await connect({ servers: 'nats://localhost:4222' });
const js = nc.jetstream();
const kv = js.keyValue("data");
// Create node
const dataNode = createNode('data-service');
// Register handler using closure to capture dependencies
dataNode.on('query-data', (event, context) => {
// Use kv from closure scope
return processQuery(kv, event, context);
});
return dataNode;
}
// Function that uses dependency passed via parameters
function processQuery(kv, event, context) {
const { query, params } = event.payload;
// Use dependency from parameters
return new Promise((resolve, reject) => {
// Use NATS KV store for data access
try {
const results = [];
for (const key of query.keys) {
const entry = await kv.get(key);
if (entry) {
results.push(JSON.parse(entry.string()));
}
}
resolve({
success: true,
results
});
} catch (err) {
reject(err);
}
});
}
// Create node
const dataNode = initializeDataNode();
Flow State Through Closures
Although Happen provides a common object to share data across each flow you can also use closures to manage both dependencies and flow state, with explicit control over the flow through function returns:
function initializeOrderNode() {
// Create dependencies including NATS client
const nc = await connect({ servers: 'nats://localhost:4222' });
const js = nc.jetstream();
const orderKV = js.keyValue("orders");
const emailService = createEmailService();
// Create node
const orderNode = createNode('order-service');
// Register initial handler with dependencies captured in closure
orderNode.on('process-order', (event, context) => {
// Create initial flow state
const flowState = {
orderId: generateOrderId(),
items: event.payload.items,
customer: event.payload.customer
};
// Return the first step function with dependencies and state in closure
// This function will be executed next in the flow
return validateOrder(orderKV, emailService, flowState);
});
return orderNode;
}
// Each flow function creates and returns the next handler in the chain
function validateOrder(orderKV, emailService, state) {
// This function is returned by the initial handler and executed by the framework
return (event, context) => {
// Validate order
const validationResult = validateOrderData(state.items);
if (!validationResult.valid) {
// Return a value (not a function) to complete the flow with error
return {
success: false,
errors: validationResult.errors
};
}
// Update state (immutably)
const updatedState = {
...state,
validated: true,
validatedAt: Date.now()
};
// Return the next function to be executed in the flow
return processPayment(orderKV, emailService, updatedState);
};
}
function processPayment(orderKV, emailService, state) {
// Return a function that will be executed as the next step
return (event, context) => {
// Process payment asynchronously
const paymentResult = processCustomerPayment(state.customer, calculateTotal(state.items));
if (!paymentResult.success) {
// Return a value (not a function) to complete the flow with error
return {
success: false,
reason: "payment-failed"
};
}
// Update state (immutably)
const updatedState = {
...state,
paymentId: paymentResult.transactionId,
paymentMethod: paymentResult.method,
paidAt: Date.now()
};
// Return the next function to be executed in the flow
return finalizeOrder(orderKV, emailService, updatedState);
};
}
function finalizeOrder(orderKV, emailService, state) {
// Return a function that will be executed as the next step
return async (event, context) => {
// Create order in KV store
await orderKV.put(state.orderId, JSON.stringify({
id: state.orderId,
customer: state.customer,
items: state.items,
payment: {
id: state.paymentId,
method: state.paymentMethod
}
}));
// Send confirmation email
emailService.sendOrderConfirmation(
state.customer.email,
state.orderId,
state.items
);
// Return a value (not a function) to complete the flow with success
return {
success: true,
orderId: state.orderId,
transactionId: state.paymentId
};
};
}
How Flow Control Works With Closures
In this pattern, the flow is controlled through a clear mechanism:
Each flow step returns a function: When a handler wants to continue the flow, it returns a function that becomes the next handler.
Flow continues while functions are returned: The framework executes each returned function in sequence, as long as functions are being returned.
Flow ends when a non-function is returned: When a handler returns anything other than a function (like an object), the flow completes with that value as the result.
Dependencies and state flow through function parameters: Each step receives dependencies and state through its parameters, not through context.
Each step creates the next step with updated state: Functions create and return the next function in the chain, passing updated state and dependencies.
Benefits of Closure-Based Dependency Management
This closure-based approach offers several advantages:
Pure Functions: Flow handlers become pure functions with explicit dependencies
Clear Data Flow: Dependencies and state are explicitly passed between functions
Testability: Each flow step can be easily tested in isolation with mocked dependencies
Immutability: Encourages immutable state updates through function parameters
Separation of Concerns: Clearly separates framework concerns from application logic
NATS as the Primary Dependency
In the Happen framework, NATS serves as the primary dependency, providing core messaging, persistence, and coordination capabilities. This focused approach means:
Single Core Dependency: NATS is the primary external dependency you need to understand
Mature Ecosystem: NATS has clients for all major languages and platforms
Unified Capability Set: One system provides messaging, persistence, and coordination
Direct NATS Access: Your code can access NATS capabilities directly when needed
Dependency Management Philosophy
Happen's approach to dependencies reflects its broader philosophy: provide just enough framework to enable powerful capabilities, while getting out of the way and letting your code work directly with the runtime.
By limiting injected dependencies to only essential system needs (primarily NATS) and embracing direct runtime access and closure-based dependency management, Happen reduces complexity while maximizing flexibility.
There's no complex dependency injection system because, in most cases, you simply use JavaScript's natural closure mechanism to capture and pass dependencies where needed.
Last updated