Skip to content

Cursors & State Management

The Thermostat Analogy

Your home thermostat works two ways:

  1. Events: When temperature changes, it triggers heating/cooling
  2. 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:

typescript
// 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:

typescript
// Automatic state tracking
const userCursor = bus.cursor('user:login', null);

// Later, always get the latest
console.log(userCursor.value); // Always up-to-date

The 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:

typescript
// Component mounted at t=25
// How do I know who's logged in NOW?

Traditional solutions are brittle:

  1. Manual variables: Forget to update → stale state
  2. Global stores: Redux/Zustand → separate system to learn
  3. Event replay: Complex, memory-intensive

The Solution: Data Cursors

Cursors provide synchronous access to the latest event value:

typescript
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:

typescript
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

typescript
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

typescript
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?

typescript
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?

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

ApproachRead SpeedWrite OverheadLearning CurveSize
Manual VariablesO(1)LowLow0 KB
ReduxO(1)High (actions/reducers)High50+ KB
ZustandO(1)MediumMedium3 KB
Nexus CursorsO(1)ZeroLow0 KB (built-in)

Nexus advantage: No separate state management library needed.

Integration with React

typescript
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

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

typescript
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

typescript
// 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:

typescript
// GOOD: Emit new state
bus.emit('cart', {
  ...cursor.value,
  items: [...cursor.value.items, newItem]
});

❌ Don't: Over-Emit

typescript
// 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:

typescript
// GOOD: Emit once after processing
let result = processData();
bus.emit('counter', result);

Next Steps

Now that you understand state management, explore resilience:

Quick Reference

Create cursor:

typescript
const cursor = bus.cursor<Type>('event:name', initialValue);

Read value:

typescript
const current = cursor.value; // Always synchronous

Update value:

typescript
bus.emit('event:name', newValue); // Cursor auto-updates

TypeScript typing:

typescript
interface MyState { count: number; }
const cursor = bus.cursor<MyState>('state', { count: 0 });
cursor.value.count; // Type-safe!

Released under the MIT License.