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:
Durable Streams: JetStream stores sequences of messages with configurable retention
Key-Value Store: Built on streams, preserves state changes as versioned entries
Message Headers: Carry metadata about events, including causal relationships
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:
Durable History: State history persists even across system restarts
Efficient Storage: JetStream optimizes storage with minimal overhead
Causal Tracking: Event relationships are preserved throughout history
Tunable Retention: Customize retention based on your specific needs
Cross-Node Consistency: Historical state is consistent across node instances
Performance: JetStream provides high-performance access to historical data
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