rhizome-node/plans/fluent-view-composition.md
Lentil Hoffman 5afd3232cb
feat: enable inter-plugin state sharing in CustomResolver
- Update ResolverPlugin interface to include context in update and resolve methods
- Modify CustomResolver to pass entity state and resolved values to plugins
- Update built-in plugins to accept and use the new context parameter
- Add comprehensive test for inter-plugin communication
- Add documentation for the new view composition patterns

This change enables plugins to access each other's states during both update and resolve phases, allowing for more powerful and flexible resolver compositions.
2025-06-22 20:42:05 -05:00

5.4 KiB

Fluent API for View Composition

Overview

This document outlines a fluent API for declaring and composing lossy views in a declarative and type-safe manner. The API is designed to make it easy to create complex view compositions with minimal boilerplate.

Core Concepts

  1. View Composition: Combining multiple resolvers to create a unified view
  2. Fluent Builder Pattern: Method chaining for declarative configuration
  3. Type Safety: Leveraging TypeScript's type system for better developer experience
  4. Extensibility: Easy to add new composition patterns and resolvers

API Design

1. Base Builder

import { Lossless } from '../lossless';

type ViewTransformer = (view: LosslessViewOne) => LosslessViewOne;
type ResultTransformer<T, R> = (result: T) => R;

class ViewCompositionBuilder {
  private resolvers: Array<{
    key: string;
    resolver: Lossy<any, any>;
    viewTransformer?: ViewTransformer;
    resultTransformer?: (result: any) => any;
  }> = [];

  constructor(private readonly lossless: Lossless) {}

  // Start building a new composition
  static create(lossless: Lossless): ViewCompositionBuilder {
    return new ViewCompositionBuilder(lossless);
  }

  // Add a resolver to the composition
  withResolver<T, R>(
    key: string,
    resolver: Lossy<T, R>,
    options: {
      view?: ViewTransformer;
      result?: ResultTransformer<R, any>;
    } = {}
  ): this {
    this.resolvers.push({
      key,
      resolver,
      viewTransformer: options.view,
      resultTransformer: options.result,
    });
    return this;
  }

  // Build the final composition
  build<T extends Record<string, any>>(): Lossy<Record<string, any>, T> {
    // Implementation that creates a combined resolver
    return new CombinedResolver(this.lossless, this.resolvers);
  }
}

2. Example Usage: Relationship Graph

const relationshipGraph = ViewCompositionBuilder
  .create(lossless)
  .withResolver('entities', new EntityResolver(), {
    view: view => ({
      ...view,
      propertyDeltas: Object.fromEntries(
        Object.entries(view.propertyDeltas).filter(([k]) => !k.startsWith('_rel_'))
      )
    })
  })
  .withResolver('relationships', new RelationshipResolver(), {
    view: view => ({
      ...view,
      propertyDeltas: Object.fromEntries(
        Object.entries(view.propertyDeltas).filter(([k]) => k.startsWith('_rel_'))
      )
    })
  })
  .withResolver('stats', new StatsCollector())
  .withResolver('metadata', new MetadataResolver(), {
    result: (metadata) => ({
      ...metadata,
      generatedAt: new Date().toISOString()
    })
  })
  .build<{
    entities: EntityMap;
    relationships: RelationshipMap;
    stats: Stats;
    metadata: Metadata;
  }>();

3. Advanced Composition

// Nested composition
const userProfile = ViewCompositionBuilder
  .create(lossless)
  .withResolver('basicInfo', new BasicInfoResolver())
  .withResolver('activity', ViewCompositionBuilder
    .create(lossless)
    .withResolver('recentPosts', new RecentPostsResolver())
    .withResolver('notifications', new NotificationsResolver())
    .build()
  )
  .withResolver('recommendations', new RecommendationsResolver())
  .build<{
    basicInfo: UserBasicInfo;
    activity: {
      recentPosts: Post[];
      notifications: Notification[];
    };
    recommendations: Recommendation[];
  }>();

Implementation Details

CombinedResolver Implementation

class CombinedResolver<State extends Record<string, any>, Result> 
  extends Lossy<State, Result> {
  
  constructor(
    private readonly lossless: Lossless,
    private readonly resolvers: Array<{
      key: string;
      resolver: Lossy<any, any>;
      viewTransformer?: ViewTransformer;
      resultTransformer?: (result: any) => any;
    }>
  ) {
    super(lossless);
  }

  initializer(view: LosslessViewOne): State {
    return this.resolvers.reduce((state, { key, resolver, viewTransformer }) => {
      const transformedView = viewTransformer ? viewTransformer(view) : view;
      return {
        ...state,
        [key]: resolver.initializer(transformedView)
      };
    }, {} as State);
  }

  reducer(state: State, view: LosslessViewOne): State {
    return this.resolvers.reduce((newState, { key, resolver, viewTransformer }) => {
      const transformedView = viewTransformer ? viewTransformer(view) : view;
      return {
        ...newState,
        [key]: resolver.reducer(state[key], transformedView)
      };
    }, { ...state });
  }

  resolver(state: State): Result {
    return this.resolvers.reduce((result, { key, resolver, resultTransformer }) => {
      const resolved = resolver.resolver(state[key]);
      return {
        ...result,
        [key]: resultTransformer ? resultTransformer(resolved) : resolved
      };
    }, {} as Result);
  }
}

Benefits

  1. Readability: Clear, declarative syntax
  2. Type Safety: Full TypeScript support with proper type inference
  3. Composability: Easy to combine and nest resolvers
  4. Maintainability: Isolated concerns and transformations
  5. Flexibility: Custom view and result transformations

Next Steps

  1. Implement the base ViewCompositionBuilder and CombinedResolver
  2. Add support for common patterns (filtering, mapping, etc.)
  3. Create documentation with examples
  4. Refactor existing resolvers to use the new composition API
  5. Add performance optimizations (memoization, lazy evaluation)