Cursors & State Management
The Thermostat Analogy
Your home thermostat works two ways:
- Events: When temperature changes, it triggers heating/cooling
- State: You can walk up and read the current temperature anytime
Most event buses only do #1 (events). You can't ask "what's the current value?"
Traditional approach:
// Manual state tracking (the pain)
let currentUser = null;
bus.on('user:login', (user) => {
currentUser = user; // Must manually sync
});
// Later, in another component
if (currentUser) {
console.log(currentUser.name); // Might be stale!
}Nexus cursors:
// Automatic state tracking
const userCursor = bus.cursor('user:login', null);
// Later, always get the latest
console.log(userCursor.value); // Always up-to-dateThe Problem: Events Are Temporal, State Is Spatial
Events happen in time:
t=0 → user:login { name: 'Alice' }
t=10 → user:logout
t=20 → user:login { name: 'Bob' }But components often need current state, not history:
// Component mounted at t=25
// How do I know who's logged in NOW?Traditional solutions are brittle:
- Manual variables: Forget to update → stale state
- Global stores: Redux/Zustand → separate system to learn
- Event replay: Complex, memory-intensive
The Solution: Data Cursors
Cursors provide synchronous access to the latest event value:
const authCursor = bus.cursor('auth:state', { loggedIn: false });
// Read anytime, always current
console.log(authCursor.value); // { loggedIn: false }
// Emitting automatically updates the cursor
bus.emit('auth:state', { loggedIn: true, user: 'Alice' });
console.log(authCursor.value); // { loggedIn: true, user: 'Alice' }Key insight: Cursors are live references, not snapshots.
How Cursors Work
Behind the scenes:
class Nexus {
private cursorValues = new Map<string, any>();
cursor<T>(event: string, initialValue: T) {
// Set initial if not present
if (!this.cursorValues.has(event)) {
this.cursorValues.set(event, initialValue);
}
// Return live reference
return {
get value() {
return this.cursorValues.get(event);
}
};
}
emit(event: string, payload: any) {
// Update cursor on every emit
this.cursorValues.set(event, payload);
// ... rest of emit logic
}
}Performance: Reading cursor.value is O(1) Map lookup - extremely fast.
Basic Example: Authentication State
import { Nexus } from '@caeligo/nexus-orchestrator';
interface AuthState {
loggedIn: boolean;
user: string | null;
token: string | null;
}
const bus = new Nexus();
// Create cursor with initial state
const authCursor = bus.cursor<AuthState>('auth:state', {
loggedIn: false,
user: null,
token: null
});
// Login function
async function login(username: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (response.ok) {
const { token } = await response.json();
// Emit updates the cursor automatically
bus.emit('auth:state', {
loggedIn: true,
user: username,
token
});
}
}
// Any component can read current state
function UserProfile() {
const auth = authCursor.value;
if (!auth.loggedIn) {
return '<div>Please log in</div>';
}
return `<div>Welcome, ${auth.user}!</div>`;
}
// Reactive rendering
bus.on('auth:state', () => {
document.getElementById('app').innerHTML = UserProfile();
});Real-World Example: Shopping Cart
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
const bus = new Nexus();
// Cursor for cart state
const cartCursor = bus.cursor<CartState>('cart:state', {
items: [],
total: 0
});
// Helper to calculate total
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
// Add to cart
function addToCart(product: { id: number; name: string; price: number }) {
const currentCart = cartCursor.value;
const existingItem = currentCart.items.find(i => i.productId === product.id);
let newItems: CartItem[];
if (existingItem) {
// Increment quantity
newItems = currentCart.items.map(item =>
item.productId === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// Add new item
newItems = [
...currentCart.items,
{
productId: product.id,
name: product.name,
price: product.price,
quantity: 1
}
];
}
// Update state
bus.emit('cart:state', {
items: newItems,
total: calculateTotal(newItems)
});
}
// Remove from cart
function removeFromCart(productId: number) {
const currentCart = cartCursor.value;
const newItems = currentCart.items.filter(i => i.productId !== productId);
bus.emit('cart:state', {
items: newItems,
total: calculateTotal(newItems)
});
}
// UI Components can access state directly
function CartBadge() {
const itemCount = cartCursor.value.items.reduce(
(sum, item) => sum + item.quantity,
0
);
return `<span class="badge">${itemCount}</span>`;
}
function CartTotal() {
const total = cartCursor.value.total;
return `<div class="total">$${total.toFixed(2)}</div>`;
}
// React to changes
bus.on('cart:state', () => {
document.getElementById('cart-badge')!.innerHTML = CartBadge();
document.getElementById('cart-total')!.innerHTML = CartTotal();
});Edge Case: Reading Before First Emit
What if you read a cursor before any event is emitted?
const cursor = bus.cursor('user:data', { name: 'Guest' });
// Never emitted 'user:data' yet
console.log(cursor.value); // { name: 'Guest' } (initial value)
// First emit
bus.emit('user:data', { name: 'Alice' });
console.log(cursor.value); // { name: 'Alice' }Cursors always return something - either the latest value or the initial value.
Edge Case: Multiple Cursors on Same Event
Can you create multiple cursors for the same event?
const cursor1 = bus.cursor('count', 0);
const cursor2 = bus.cursor('count', 999); // Different initial value!
console.log(cursor1.value); // 0 (first cursor set the initial)
console.log(cursor2.value); // 0 (same underlying storage)
bus.emit('count', 42);
console.log(cursor1.value); // 42
console.log(cursor2.value); // 42 (both cursors see the same value)Key insight: Cursors are views into shared storage. Multiple cursors point to the same data.
Edge Case: Cursor + Replay
Cursors work seamlessly with replay memory:
const bus = new Nexus({ replayMemory: 10 });
// Emit early
bus.emit('config', { theme: 'dark' });
// Create cursor later
const configCursor = bus.cursor('config', { theme: 'light' });
console.log(configCursor.value); // { theme: 'dark' } (latest value)
// Late subscriber with replay
bus.on('config', (config) => {
console.log('Late listener got:', config);
}, { replay: true });
// Output: { theme: 'dark' } (replayed from history)Cursors always reflect the latest, while replay gives you history.
Pattern: Cursor-Driven UI
Use cursors to build reactive UIs without frameworks:
interface AppState {
route: string;
loading: boolean;
error: string | null;
}
const stateCursor = bus.cursor<AppState>('app:state', {
route: '/',
loading: false,
error: null
});
function render() {
const state = stateCursor.value;
return `
<div class="${state.loading ? 'loading' : ''}">
${state.error ? `<div class="error">${state.error}</div>` : ''}
${renderRoute(state.route)}
</div>
`;
}
// Re-render on state change
bus.on('app:state', () => {
document.getElementById('app')!.innerHTML = render();
});
// Navigation updates state
function navigate(route: string) {
bus.emit('app:state', {
...stateCursor.value,
route,
loading: true
});
// Simulate async load
setTimeout(() => {
bus.emit('app:state', {
...stateCursor.value,
loading: false
});
}, 500);
}Pattern: Derived State
Compute values from cursor data:
const userCursor = bus.cursor('user', null);
const cartCursor = bus.cursor('cart', { items: [] });
// Derived state (computed on-demand)
function isCheckoutEnabled(): boolean {
const user = userCursor.value;
const cart = cartCursor.value;
return user !== null && cart.items.length > 0;
}
// Use in UI
document.getElementById('checkout').disabled = !isCheckoutEnabled();
// Update when either cursor changes
bus.on('user', () => updateCheckoutButton());
bus.on('cart', () => updateCheckoutButton());
function updateCheckoutButton() {
document.getElementById('checkout').disabled = !isCheckoutEnabled();
}Pattern: State Persistence
Save cursor state to localStorage:
const STORAGE_KEY = 'app:cart';
// Load initial state from storage
const savedCart = localStorage.getItem(STORAGE_KEY);
const initialCart = savedCart ? JSON.parse(savedCart) : { items: [] };
const cartCursor = bus.cursor('cart', initialCart);
// Persist on every change
bus.on('cart', (cart) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(cart));
});
// Now cart survives page refresh!Comparison: Cursors vs. Other State Solutions
| Approach | Read Speed | Write Overhead | Learning Curve | Size |
|---|---|---|---|---|
| Manual Variables | O(1) | Low | Low | 0 KB |
| Redux | O(1) | High (actions/reducers) | High | 50+ KB |
| Zustand | O(1) | Medium | Medium | 3 KB |
| Nexus Cursors | O(1) | Zero | Low | 0 KB (built-in) |
Nexus advantage: No separate state management library needed.
Integration with React
import { Nexus } from '@caeligo/nexus-orchestrator';
import { useState, useEffect } from 'react';
const bus = new Nexus();
const authCursor = bus.cursor('auth', { loggedIn: false });
function useNexusCursor<T>(cursor: { value: T }, event: string): T {
const [value, setValue] = useState<T>(cursor.value);
useEffect(() => {
const sub = bus.on(event, (newValue) => {
setValue(newValue);
});
return () => sub.unsubscribe();
}, [event]);
return value;
}
// Usage in component
function App() {
const auth = useNexusCursor(authCursor, 'auth');
return (
<div>
{auth.loggedIn ? (
<h1>Welcome!</h1>
) : (
<button onClick={() => bus.emit('auth', { loggedIn: true })}>
Log In
</button>
)}
</div>
);
}Integration with Vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { bus, authCursor } from './bus';
const auth = ref(authCursor.value);
let subscription;
onMounted(() => {
subscription = bus.on('auth', (newAuth) => {
auth.value = newAuth;
});
});
onUnmounted(() => {
subscription.unsubscribe();
});
</script>
<template>
<div>
<div v-if="auth.loggedIn">Welcome, {{ auth.user }}!</div>
<button v-else @click="login">Log In</button>
</div>
</template>Performance Considerations
Memory: Each cursor is a lightweight getter - only ~100 bytes.
Garbage Collection: Cursors don't prevent GC. They're just getters that read from a Map.
Throughput: 10M+ reads/second on modern hardware.
Testing with Cursors
import { describe, it, expect } from 'vitest';
import { Nexus } from '@caeligo/nexus-orchestrator';
describe('Cart Cursor', () => {
it('should update on add', () => {
const bus = new Nexus();
const cartCursor = bus.cursor('cart', { items: [], total: 0 });
// Initial state
expect(cartCursor.value.items).toHaveLength(0);
// Add item
bus.emit('cart', {
items: [{ id: 1, name: 'Widget', price: 10, quantity: 1 }],
total: 10
});
// Cursor reflects change
expect(cartCursor.value.items).toHaveLength(1);
expect(cartCursor.value.total).toBe(10);
});
it('should maintain reference across emits', () => {
const bus = new Nexus();
const cursor1 = bus.cursor('counter', 0);
const cursor2 = bus.cursor('counter', 999);
// Both point to same storage
expect(cursor1.value).toBe(cursor2.value);
bus.emit('counter', 42);
// Both see the update
expect(cursor1.value).toBe(42);
expect(cursor2.value).toBe(42);
});
});Anti-Patterns to Avoid
❌ Don't: Mutate Cursor Values
// BAD: Mutation doesn't trigger updates
const cursor = bus.cursor('cart', { items: [] });
cursor.value.items.push(newItem); // ❌ Won't work!Problem: Cursors are getters, not setters. Mutations don't emit events.
Fix: Always emit new values:
// GOOD: Emit new state
bus.emit('cart', {
...cursor.value,
items: [...cursor.value.items, newItem]
});❌ Don't: Over-Emit
// BAD: Emitting in a loop
for (let i = 0; i < 1000; i++) {
bus.emit('counter', i); // 1000 events!
}Problem: Every emit triggers listeners and updates cursors.
Fix: Batch updates:
// GOOD: Emit once after processing
let result = processData();
bus.emit('counter', result);Next Steps
Now that you understand state management, explore resilience:
- Resilience - Add retries and circuit breakers
- Chaos Monkey - Test failure scenarios
- AI Prediction - Learn from cursor state changes
Quick Reference
Create cursor:
const cursor = bus.cursor<Type>('event:name', initialValue);Read value:
const current = cursor.value; // Always synchronousUpdate value:
bus.emit('event:name', newValue); // Cursor auto-updatesTypeScript typing:
interface MyState { count: number; }
const cursor = bus.cursor<MyState>('state', { count: 0 });
cursor.value.count; // Type-safe!