rhizome-node/docs/resolvers.md
Lentil Hoffman 50ac2b7b35
refactor: rename Lossless to Hyperview and Lossy to View
- Renamed Lossless class to Hyperview for clarity
- Renamed Lossy class to View for simplicity
- Updated all imports and references throughout the codebase
- Updated documentation to reflect new terminology
- Fixed test files to use new class names
- Added proper exports in index files
2025-07-09 14:28:52 -05:00

3.9 KiB

Views and Resolvers

Core Concepts

Views

A View (previously known as Lossy) is a derived, computed representation of your data that provides efficient access to resolved values. It's built on top of the Hyperview (previously Hyperview) storage layer, which maintains the complete history of all deltas.

// Basic View implementation
export abstract class View<Accumulator, Result = Accumulator> {
  // Core methods
  abstract reducer(acc: Accumulator, cur: HyperviewOne): Accumulator;
  resolver?(acc: Accumulator, entityIds: DomainEntityID[]): Result;
  
  // Built-in functionality
  resolve(entityIds?: DomainEntityID[]): Result | undefined;
}

Hyperview

Hyperview (previously Hyperview) maintains the complete, immutable history of all deltas and provides the foundation for building derived views.

// Basic Hyperview usage
const hyperview = new Hyperview(rhizomeNode);
const view = new MyCustomView(hyperview);
const result = view.resolve(['entity1', 'entity2']);

Creating Custom Resolvers

Basic Resolver Pattern

  1. Define your accumulator type: This holds the state of your view
  2. Implement the reducer: Pure function that updates the accumulator
  3. Optionally implement resolver: Transforms the accumulator into the final result
class CountResolver extends View<number> {
  reducer(acc: number, view: HyperviewOne): number {
    return acc + Object.keys(view.propertyDeltas).length;
  }
  
  initializer() { return 0; }
}

Custom Resolver with Dependencies

Resolvers can depend on other resolvers using the CustomResolver system:

// Define resolver plugins
const resolver = new CustomResolver(hyperview, {
  totalItems: new CountPlugin(),
  average: new AveragePlugin()
});

// Get resolved values
const result = resolver.resolve(['entity1', 'entity2']);

Built-in Resolvers

Last Write Wins

Keeps the most recent value based on timestamp:

const resolver = new CustomResolver(hyperview, {
  status: new LastWriteWinsPlugin()
});

First Write Wins

Keeps the first value that was written:

const resolver = new CustomResolver(hyperview, {
  initialStatus: new FirstWriteWinsPlugin()
});

Aggregation Resolvers

Built-in aggregators for common operations:

  • SumPlugin: Sum of numeric values
  • AveragePlugin: Average of numeric values
  • CountPlugin: Count of values
  • MinPlugin: Minimum value
  • MaxPlugin: Maximum value

Advanced Topics

Performance Considerations

  1. Efficient Reducers: Keep reducers pure and fast
  2. Selective Updates: Only process changed entities
  3. Memoization: Cache expensive computations

Common Patterns

  1. Derived Properties: Calculate values based on other properties
  2. State Machines: Track state transitions over time
  3. Validation: Enforce data consistency rules

Best Practices

  1. Keep Reducers Pure: Avoid side effects in reducers
  2. Use Strong Typing: Leverage TypeScript for type safety
  3. Handle Edge Cases: Consider empty states and error conditions
  4. Profile Performance: Monitor and optimize hot paths

Examples

Simple Counter

class CounterView extends View<number> {
  reducer(acc: number, view: HyperviewOne): number {
    return acc + view.propertyDeltas['increment']?.length || 0;
  }
  
  initializer() { return 0; }
}

Running Average

class RunningAverageView extends View<{sum: number, count: number}, number> {
  reducer(acc: {sum: number, count: number}, view: HyperviewOne): {sum: number, count: number} {
    const value = // extract value from view
    return {
      sum: acc.sum + value,
      count: acc.count + 1
    };
  }
  
  resolver(acc: {sum: number, count: number}): number {
    return acc.count > 0 ? acc.sum / acc.count : 0;
  }
  
  initializer() { return {sum: 0, count: 0}; }
}