Temporal State

In Happen, the journey is just as important as the destination. Temporal State leverages NATS JetStream's powerful capabilities to provide access to your state across time, enabling powerful historical analysis and recovery with minimal complexity.

Temporal State gives nodes the ability to access and work with historical state—allowing you to see not just what your state is now, but what it was at important points in your application's history.

How Temporal State Works with JetStream

At its core, Temporal State in Happen is built on NATS JetStream's key features:

  1. Durable Streams: JetStream stores sequences of messages with configurable retention

  2. Key-Value Store: Built on streams, preserves state changes as versioned entries

  3. Message Headers: Carry metadata about events, including causal relationships

  4. Event Sourcing Pattern: Natural event sourcing capabilities through message ordering

When an event flows through a node and modifies state, both the event and the resulting state change are recorded in JetStream:

// Conceptually, a temporal state snapshot includes:
{
  state: { /* state at this point in time */ },
  context: { // Essential event context
    id: 'evt-456', // The event that created this state
    causationId: 'evt-123', // What caused this event
    correlationId: 'order-789', // Transaction this event belongs to
    sender: 'payment-service', // Node that sent the event
    timestamp: 1621452789000, // When it happened
    eventType: 'payment-processed' // Type of event
  }
}

This combination of event and state history provides a complete record of how your system evolved over time.

JetStream Key-Value Store for Historical State

Happen uses JetStream's Key-Value store capabilities to implement Temporal State:

// Initialize with Temporal State configuration
const happen = initializeHappen({
  nats: {
    capabilities: {
      persistence: {
        enabled: true,
        keyValue: {
          enabled: true,
          buckets: {
            state: "happen-state",
            temporal: "happen-temporal"
          }
        },
        // Temporal state configuration
        temporal: {
          enabled: true,
          history: 100,  // Keep 100 versions per key
          maxAge: "30d"  // Keep history for 30 days
        }
      }
    }
  }
});

This configuration creates a specialized Key-Value bucket that preserves historical versions of state entries.

Accessing Temporal State

Happen provides a clean and intuitive way to access historical state through the .when() function:

// Access state after a specific event
orderNode.state.when('evt-123', (snapshots) => {
  // Work with the complete historical snapshot
  const { state, context } = snapshots[0];
  
  console.log(`Order status was: ${state.orders["order-123"].status}`);
  console.log(`Event type: ${context.eventType}`);
  console.log(`Caused by: ${context.causationId}`);
  
  return processHistoricalState(state, context);
});

The when function follows Happen's pattern-matching approach, accepting either:

  • A string event ID: 'evt-123'

  • A function matcher: eventId => eventId.startsWith('payment-')

Traversing Causal Chains

Since each snapshot includes context information, you can easily traverse causal chains using pattern matching:

// Find all states caused by a specific event
orderNode.state.when(
  event => event.causationId === 'evt-123',
  (snapshots) => {
    // Process all snapshots directly caused by evt-123
    const eventTypes = snapshots.map(snap => snap.context.eventType);
    
    console.log(`Event evt-123 caused these events: ${eventTypes.join(', ')}`);
    
    return analyzeEffects(snapshots);
  }
);

// Or use correlation IDs to get entire transaction flows
orderNode.state.when(
  event => event.correlationId === 'order-789',
  (snapshots) => {
    // Process all snapshots in the transaction
    // Arrange by timestamp to see the sequence
    const eventSequence = [...snapshots]
      .sort((a, b) => a.context.timestamp - b.context.timestamp)
      .map(snap => snap.context.eventType);
    
    console.log(`Transaction order-789 flow: ${eventSequence.join(' → ')}`);
    
    return createAuditTrail(snapshots);
  }
);

Efficient Implementation through JetStream

Rather than storing complete copies of state for every event, Happen leverages JetStream's efficient storage capabilities:

  • JetStream automatically compresses and deduplicates data

  • Key revisions are tracked with minimal overhead

  • The system intelligently manages resource usage

  • Historical data is pruned based on configurable policies

This approach balances efficient storage with powerful historical access capabilities.

Customizing Retention Policies

Happen allows you to customize which historical states are retained through JetStream's retention policies:

// Node with custom retention policy
const orderNode = createNode('order-service', {
  persistence: {
    temporal: {
      history: 50,         // Keep up to 50 versions
      maxAge: "7d",        // Keep history for 7 days
      subject: "order.*"   // Only track states for order events
    }
  }
});

This gives you control over:

  • How many historical versions to keep

  • How long to keep historical versions

  • Which events should create temporal snapshots

Event Sourcing

Temporal State makes implementing the Event Sourcing pattern remarkably straightforward:

// Rebuild state at a specific point in time
orderNode.state.when('evt-123', (snapshot) => {
  // Replace current state with historical state
  orderNode.state.set(() => snapshot.state);
  
  // Let the system know we rebuilt state
  orderNode.broadcast({
    type: 'state-rebuilt',
    payload: {
      fromEvent: snapshot.context.id,
      timestamp: snapshot.context.timestamp
    }
  });
  
  return { rebuilt: true };
});

Recovery and Resilience

Temporal State provides natural resilience capabilities:

// After node restart, recover from latest known state
function recoverLatestState() {
  // Find the latest event we processed before crashing
  const latestEventId = loadLatestEventIdFromDisk();
  
  if (latestEventId) {
    // Recover state from that point
    orderNode.state.when(latestEventId, (snapshot) => {
      if (snapshot) {
        // Restore state
        orderNode.state.set(() => snapshot.state);
        console.log('State recovered successfully');
      }
    });
  }
}

Benefits of JetStream-Powered Temporal State

Happen's JetStream-based approach to Temporal State offers several key advantages:

  1. Durable History: State history persists even across system restarts

  2. Efficient Storage: JetStream optimizes storage with minimal overhead

  3. Causal Tracking: Event relationships are preserved throughout history

  4. Tunable Retention: Customize retention based on your specific needs

  5. Cross-Node Consistency: Historical state is consistent across node instances

  6. Performance: JetStream provides high-performance access to historical data

  7. Scalability: Works from single-process to globally distributed systems

By leveraging NATS JetStream's capabilities, Happen provides powerful temporal state features without the complexity typically associated with time-travel debugging and historical analysis.

Adding the dimension of time to your application's data model, Temporal State opens up powerful capabilities with minimal added complexity. Since it builds on NATS JetStream's existing features, it provides powerful capabilities that feel natural and integrated.

With Temporal State, your applications gain new powers for auditing, debugging, analysis, recovery, and understanding—all while maintaining Happen's commitment to simplicity and power through minimal primitives.

Last updated