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:

  1. Runtime Transparency: Your code has direct access to the runtime environment

  2. Minimal System Dependencies: Only essential framework dependencies (primarily NATS) are injected at initialization

  3. 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:

  1. Each flow step returns a function: When a handler wants to continue the flow, it returns a function that becomes the next handler.

  2. Flow continues while functions are returned: The framework executes each returned function in sequence, as long as functions are being returned.

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

  4. Dependencies and state flow through function parameters: Each step receives dependencies and state through its parameters, not through context.

  5. 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:

  1. Pure Functions: Flow handlers become pure functions with explicit dependencies

  2. Clear Data Flow: Dependencies and state are explicitly passed between functions

  3. Testability: Each flow step can be easily tested in isolation with mocked dependencies

  4. Immutability: Encourages immutable state updates through function parameters

  5. 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:

  1. Single Core Dependency: NATS is the primary external dependency you need to understand

  2. Mature Ecosystem: NATS has clients for all major languages and platforms

  3. Unified Capability Set: One system provides messaging, persistence, and coordination

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