diff --git a/README.md b/README.md index b38dc9e..d395aed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ See [spec.md](spec.md) for additional specification details about this project. +## Documentation + +- [Custom Resolvers](./docs/custom-resolvers/overview.md) - Flexible property resolution system with plugin support + + # Development / Demo ## Setup diff --git a/docs/custom-resolvers.md b/docs/custom-resolvers.md deleted file mode 100644 index 3173619..0000000 --- a/docs/custom-resolvers.md +++ /dev/null @@ -1,314 +0,0 @@ -# Custom Resolvers - -## Overview - -The `CustomResolver` class provides a flexible system for resolving property conflicts in a distributed system. This document covers the implementation details, including the support for inter-plugin dependencies. - -## Current Implementation - -### Core Components - -1. **ResolverPlugin Interface** - - Defines the contract for all resolver implementations - - Key methods: - - `initialize()`: Creates initial state - - `update()`: Processes new values with timestamps - - `resolve()`: Produces final value from accumulated state - -2. **CustomResolver Class** - - Manages resolution of entity properties using configured plugins - - Implements the core resolution logic: - - `initializer`: Creates initial state structure - - `reducer`: Processes deltas and updates state using plugins - - `resolver`: Produces final resolved values - -3. **Built-in Plugins** - - `LastWriteWinsPlugin`: Keeps the most recent value - - `FirstWriteWinsPlugin`: Keeps the first value seen - - `ConcatenationPlugin`: Combines string values with a separator - - `MajorityVotePlugin`: Selects the most common value - - `MinPlugin`/`MaxPlugin`: Tracks minimum/maximum numeric values - -## Plugin Dependencies - -### Overview - -The system supports explicit declaration of dependencies between plugins, ensuring they are processed in the correct order. This enables: - -1. **Deterministic Execution**: Plugins are processed after their dependencies -2. **Dependency Validation**: Circular dependencies are detected and prevented -3. **Optimized Processing**: Only necessary states are processed in the correct order - -### Declaring Dependencies - -Each plugin can declare its dependencies using the `dependencies` property: - -```typescript -class MyPlugin implements ResolverPlugin { - name = 'my-plugin'; - - // List of property IDs this plugin depends on - dependencies: PropertyID[] = ['other-property']; - - // ... rest of the implementation -} -``` - -### Dependency Resolution - -1. **Topological Sorting**: Plugins are processed in an order that respects their dependencies -2. **Cycle Detection**: Circular dependencies are detected during initialization -3. **Lazy Resolution**: Dependencies are only resolved when needed - -### Example: Chained Dependencies - -```typescript -const resolver = new CustomResolver(losslessView, { - // This will be processed first (no dependencies) - basePrice: new LastWriteWinsPlugin(), - - // This depends on basePrice - tax: new TaxCalculatorPlugin(), - - // This depends on both basePrice and tax - total: new TotalCalculatorPlugin() -}); -``` - -### Best Practices - -1. **Minimal Dependencies**: Only declare dependencies that are actually needed -2. **Avoid Cycles**: Design your plugins to avoid circular dependencies -3. **Document Dependencies**: Clearly document what each plugin depends on -4. **Test Dependencies**: Include tests that verify the dependency behavior - -### Implementation Details - -#### ResolverPlugin Interface - -The `ResolverPlugin` interface defines the contract that all resolver plugins must implement. It has been updated to support inter-plugin dependencies and state sharing: - -```typescript -interface ResolverPlugin { - // Unique name of the plugin - name: string; - - // List of property IDs this plugin depends on - // Plugins will be processed after their dependencies - dependencies?: PropertyID[]; - - // Initialize the state for a property - initialize(): T; - - // Process a new value for the property - update( - currentState: T, - newValue: PropertyTypes, - delta: CollapsedDelta, - allStates?: Record - ): T; - - // Resolve the final value from the current state - // Must return a value of type PropertyTypes or undefined - // This is the value that will be used as the resolved property value - resolve( - state: T, - allStates?: Record - ): PropertyTypes | undefined; -} -``` - -#### Important Notes - -1. **Return Type of `resolve`**: - - The `resolve` method must return a value of type `PropertyTypes` or `undefined` - - This is different from the `update` method which returns the updated state of type `T` - - The returned value will be used as the resolved property value - -2. **Plugin Dependencies**: - - Dependencies are declared using the `dependencies` property - - Dependencies should be an array of property IDs that this plugin depends on - - The resolver ensures that all dependencies are processed before the plugin that depends on them - -3. **State Access**: - - The `allStates` parameter in `update` and `resolve` provides access to the current state of all other properties - - This allows plugins to make decisions based on the state of other properties - - The states are keyed by property ID and contain the raw state objects for each plugin - -4. **Error Handling**: - - If a plugin throws an error during initialization, update, or resolve, the error will be logged and the plugin will be skipped - - It's recommended to handle errors within the plugin methods when possible - -#### CustomResolver Class - -The `CustomResolver` class has been enhanced to: -1. Collect all plugin states before processing updates -2. Pass the complete state to each plugin during updates and resolution - -### Complex Example: Discounted Price with Dependencies - -This example demonstrates a more complex scenario with multiple interdependent plugins: - -Here's a practical example of a plugin that calculates a discounted price based on another property: - -```typescript -class DiscountedPricePlugin implements ResolverPlugin<{ price: number }> { - name = 'discounted-price'; - - initialize() { - return { price: 0 }; - } - - update( - state: { price: number }, - newValue: PropertyTypes, - _delta: CollapsedDelta, - _allStates?: Record - ) { - if (typeof newValue === 'number') { - return { price: newValue }; - } - return state; - } - - resolve( - state: { price: number }, - allStates?: Record - ): number | undefined { - // Access the discount value from another plugin's state - const discountState = allStates?.['discount'] as { value: number } | undefined; - if (discountState) { - // Apply discount if available - return state.price * (1 - (discountState.value / 100)); - } - return state.price; - } -} - -// Usage with a discount plugin -const resolver = new CustomResolver(losslessView, { - price: new DiscountedPricePlugin(), - discount: new LastWriteWinsPlugin() -}); -``` - -### Best Practices - -1. **Dependency Management**: - - Clearly document which properties your plugin depends on - - Handle cases where dependencies might be undefined - - Consider using TypeScript type guards for safer property access - -2. **Performance Considerations**: - - Access only the states you need in the `allStates` object - - Consider caching resolved values if the same calculation is performed multiple times - -3. **Testing**: - - Test plugins with and without their dependencies - - Verify behavior when dependencies are updated in different orders - - Test edge cases like missing or invalid dependencies - -### Built-in Plugins - -All built-in plugins have been updated to be compatible with the new interface: - -- `LastWriteWinsPlugin` -- `FirstWriteWinsPlugin` -- `ConcatenationPlugin` -- `MajorityVotePlugin` -- `MinPlugin` -- `MaxPlugin` - -These plugins maintain backward compatibility while supporting the new functionality. - -## Implementation Status - -The inter-plugin dependency feature has been implemented and includes: - -1. Updated `ResolverPlugin` interface with `allStates` parameter -2. Enhanced `CustomResolver` class for state sharing between plugins -3. Updated all built-in plugins for compatibility -4. Comprehensive test coverage including: - - Basic functionality of all built-in plugins - - Inter-plugin dependency scenarios - - Edge cases and error conditions -5. Complete documentation with examples - -## Usage Examples - -### Basic Usage - -```typescript -const resolver = new CustomResolver(losslessView, { - title: new LastWriteWinsPlugin(), - price: new LastWriteWinsPlugin(), - discount: new LastWriteWinsPlugin() -}); -``` - -### With Dependent Plugins - -```typescript -const resolver = new CustomResolver(losslessView, { - basePrice: new LastWriteWinsPlugin(), - discount: new LastWriteWinsPlugin(), - finalPrice: new DiscountedPricePlugin() // Depends on discount -}); -``` - -### Complex Example - -```typescript -const resolver = new CustomResolver(losslessView, { - // Basic properties - name: new LastWriteWinsPlugin(), - description: new ConcatenationPlugin(' '), - - // Pricing - basePrice: new LastWriteWinsPlugin(), - taxRate: new LastWriteWinsPlugin(), - discount: new LastWriteWinsPlugin(), - - // Calculated fields - subtotal: new SubtotalCalculator(), // Uses basePrice and quantity - tax: new TaxCalculator(), // Uses subtotal and taxRate - total: new TotalCalculator() // Uses subtotal, tax, and discount -}); -``` - -## Future Enhancements - -1. **Plugin Dependencies**: Explicitly declare dependencies between plugins -2. **Caching**: Cache resolved values for better performance -3. **Validation**: Add validation to prevent circular dependencies -4. **Debugging**: Add logging for plugin execution order and state access -5. **Optimization**: Lazy-load plugin states to improve performance with many properties - -## Example Configurations - -### Basic Usage - -```typescript -const resolver = new CustomResolver(losslessView, { - title: new LastWriteWinsPlugin(), - price: new LastWriteWinsPlugin(), - discount: new LastWriteWinsPlugin() -}); -``` - -### With Dependent Plugins - -```typescript -const resolver = new CustomResolver(losslessView, { - basePrice: new LastWriteWinsPlugin(), - discount: new LastWriteWinsPlugin(), - finalPrice: new DiscountedPricePlugin() -}); -``` - -## Future Enhancements - -1. **Plugin Dependencies**: Explicitly declare dependencies between plugins -2. **Caching**: Cache resolved values for better performance -3. **Validation**: Add validation to prevent circular dependencies -4. **Debugging**: Add logging for plugin execution order and state access diff --git a/docs/custom-resolvers/api/custom-resolver-class.md b/docs/custom-resolvers/api/custom-resolver-class.md new file mode 100644 index 0000000..c3b3c65 --- /dev/null +++ b/docs/custom-resolvers/api/custom-resolver-class.md @@ -0,0 +1,205 @@ +# CustomResolver Class + +## Overview + +The `CustomResolver` class is the main entry point for the Custom Resolver system. It manages the resolution of entity properties using the configured plugins and handles dependency resolution between them. + +## Class Definition + +```typescript +class CustomResolver { + /** + * Creates a new CustomResolver instance + * @param view The lossless view to resolve + * @param config Plugin configuration + */ + constructor( + private readonly view: LosslessView, + private readonly config: ResolverConfig + ); + + /** + * Processes all entities in the view and returns the resolved values + */ + resolve(): CustomResolverResult; + + /** + * Processes a single entity and returns its resolved values + * @param entityId ID of the entity to resolve + */ + resolveEntity(entityId: string): EntityResult | undefined; + + /** + * Gets the current processing order of properties + */ + getProcessingOrder(): string[]; + + /** + * Gets the dependency graph + */ + getDependencyGraph(): Map>; +} +``` + +## Constructor + +### `new CustomResolver(view, config)` + +Creates a new instance of the CustomResolver. + +**Parameters:** +- `view: LosslessView` - The lossless view containing the data to resolve +- `config: ResolverConfig` - Configuration object mapping property IDs to their resolver plugins + +**Example:** +```typescript +const resolver = new CustomResolver(view, { + price: new LastWriteWinsPlugin(), + discount: new LastWriteWinsPlugin(), + total: new TotalPricePlugin() +}); +``` + +## Methods + +### `resolve(): CustomResolverResult` + +Processes all entities in the view and returns the resolved values. + +**Returns:** `CustomResolverResult` - Object mapping entity IDs to their resolved properties + +**Example:** +```typescript +const results = resolver.resolve(); +console.log(results); +// { +// 'entity1': { +// id: 'entity1', +// properties: { price: 100, discount: 10, total: 90 } +// }, +// // ... +// } +``` + +### `resolveEntity(entityId: string): EntityResult | undefined` + +Processes a single entity and returns its resolved values. + +**Parameters:** +- `entityId: string` - ID of the entity to resolve + +**Returns:** `EntityResult | undefined` - Resolved entity or undefined if not found + +**Example:** +```typescript +const result = resolver.resolveEntity('order-123'); +if (result) { + console.log(`Order total: ${result.properties.total}`); +} +``` + +### `getProcessingOrder(): string[]` + +Gets the current processing order of properties based on their dependencies. + +**Returns:** `string[]` - Array of property IDs in processing order + +**Example:** +```typescript +const order = resolver.getProcessingOrder(); +console.log('Processing order:', order); +// ['price', 'discount', 'total'] +``` + +### `getDependencyGraph(): Map>` + +Gets the dependency graph used for resolution. + +**Returns:** `Map>` - Map where keys are property IDs and values are sets of their dependencies + +**Example:** +```typescript +const graph = resolver.getDependencyGraph(); +for (const [prop, deps] of graph.entries()) { + console.log(`${prop} depends on:`, [...deps]); +} +``` + +## Configuration + +The resolver is configured with an object mapping property IDs to their resolver plugins: + +```typescript +interface ResolverConfig { + [propertyId: string]: ResolverPlugin; +} +``` + +## Error Handling + +The resolver may throw the following errors: + +- `Error` - For invalid configurations or dependency cycles +- `TypeError` - For type mismatches or invalid plugin implementations + +## Example: Complete Usage + +```typescript +import { CustomResolver, LastWriteWinsPlugin } from './resolver'; +import { LosslessView } from '../lossless-view'; + +// Create a lossless view with some data +const view = new LosslessView(); +// ... add data to the view ... + +// Configure the resolver +const resolver = new CustomResolver(view, { + // Simple plugins + productId: new LastWriteWinsPlugin(), + quantity: new LastWriteWinsPlugin(), + + // Plugin with dependencies + unitPrice: new LastWriteWinsPlugin(), + discount: new LastWriteWinsPlugin(), + taxRate: new LastWriteWinsPlugin(), + + // Complex plugin with multiple dependencies + subtotal: new class implements ResolverPlugin { + readonly name = 'subtotal' as const; + readonly dependencies = ['unitPrice', 'quantity'] as const; + + initialize() { return { value: 0 }; } + + update(_state, _value, _delta, deps) { + const unitPrice = deps.unitPrice as number; + const quantity = deps.quantity as number; + return { value: unitPrice * quantity }; + } + + resolve(state) { return state.value; } + }(), + + // More plugins... +}); + +// Resolve all entities +const results = resolver.resolve(); +console.log(results); + +// Or resolve a single entity +const order = resolver.resolveEntity('order-123'); +console.log(order); +``` + +## Best Practices + +1. **Reuse Instances**: Create a single resolver instance and reuse it +2. **Minimize Dependencies**: Keep the dependency graph simple and acyclic +3. **Error Handling**: Always handle potential errors in plugin implementations +4. **Performance**: Be mindful of plugin complexity in hot code paths + +## Related + +- [ResolverPlugin Interface](./resolver-plugin-interface.md) +- [Creating Custom Plugins](../plugins/creating-plugins.md) +- [Dependency Resolution](../dependencies/dependency-resolution.md) diff --git a/docs/custom-resolvers/api/resolver-plugin-interface.md b/docs/custom-resolvers/api/resolver-plugin-interface.md new file mode 100644 index 0000000..0286444 --- /dev/null +++ b/docs/custom-resolvers/api/resolver-plugin-interface.md @@ -0,0 +1,176 @@ +# ResolverPlugin Interface + +## Overview + +The `ResolverPlugin` interface defines the contract that all resolver plugins must implement. It provides type-safe access to plugin state and dependencies. + +## Interface Definition + +```typescript +interface ResolverPlugin { + /** + * Unique identifier for the plugin + */ + readonly name: string; + + /** + * Array of property IDs this plugin depends on + * @default [] + */ + readonly dependencies?: readonly D[]; + + /** + * Initializes the plugin's state + * @returns Initial state object + */ + initialize(): T; + + /** + * Processes a new value and updates the plugin's state + * @param currentState Current plugin state + * @param newValue New value to process + * @param delta Delta information + * @param dependencies Resolved states of all declared dependencies + * @returns Updated plugin state + */ + update( + currentState: T, + newValue: PropertyTypes, + delta: CollapsedDelta, + dependencies: DependencyStates + ): T; + + /** + * Resolves the final value from the current state + * @param state Current plugin state + * @param dependencies Resolved states of all declared dependencies + * @returns Resolved value or undefined if no value should be set + */ + resolve( + state: T, + dependencies: DependencyStates + ): PropertyTypes | undefined; +} +``` + +## Type Parameters + +| Parameter | Description | +|-----------|-------------| +| `T` | Type of the plugin's internal state | +| `D` | Union type of dependency names (must extend `string`) | + +## Methods + +### `initialize()` + +Initializes the plugin's internal state. Called once when the resolver is created. + +**Returns:** `T` - The initial state object + +### `update(currentState, newValue, delta, dependencies)` + +Processes a new value and updates the plugin's state. + +**Parameters:** +- `currentState: T` - Current plugin state +- `newValue: PropertyTypes` - New value to process +- `delta: CollapsedDelta` - Delta information +- `dependencies: DependencyStates` - Resolved states of all declared dependencies + +**Returns:** `T` - Updated plugin state + +### `resolve(state, dependencies)` + +Resolves the final value from the current state. + +**Parameters:** +- `state: T` - Current plugin state +- `dependencies: DependencyStates` - Resolved states of all declared dependencies + +**Returns:** `PropertyTypes | undefined` - Resolved value or undefined if no value should be set + +## Example Implementation + +```typescript +class CounterPlugin implements ResolverPlugin { + readonly name = 'counter' as const; + + initialize(): CounterState { + return { count: 0 }; + } + + update( + state: CounterState, + _newValue: unknown, + _delta: CollapsedDelta, + _deps: {} + ): CounterState { + return { count: state.count + 1 }; + } + + resolve(state: CounterState): number { + return state.count; + } +} +``` + +## Best Practices + +1. **Immutability**: Always return new state objects instead of mutating +2. **Purity**: Keep methods pure and side-effect free +3. **Error Handling**: Handle unexpected input gracefully +4. **Documentation**: Document expected types and behavior + +## Common Patterns + +### Accessing Dependencies + +```typescript +class PriceCalculator implements ResolverPlugin { + readonly name = 'price-calculator' as const; + readonly dependencies = ['basePrice', 'taxRate'] as const; + + update( + _state: PriceState, + _newValue: unknown, + _delta: CollapsedDelta, + deps: DependencyStates<'basePrice' | 'taxRate'>, + ): PriceState { + const basePrice = deps.basePrice as number; + const taxRate = deps.taxRate as number; + return { total: basePrice * (1 + taxRate) }; + } + + // ... +} +``` + +### Optional Dependencies + +```typescript +class OptionalDepPlugin implements ResolverPlugin { + readonly name = 'optional-dep' as const; + readonly dependencies = ['required', 'optional?'] as const; + + update( + state: State, + _newValue: unknown, + _delta: CollapsedDelta, + deps: any, + ): State { + const required = deps.required as number; // Always present + const optional = deps['optional?'] as number | undefined; // Might be undefined + + // ... + } + + // ... +} +``` + +## Related + +- [Creating Custom Plugins](../plugins/creating-plugins.md) +- [Type-Safe Dependencies](../dependencies/type-safety.md) +- [CustomResolver Class](./custom-resolver-class.md) diff --git a/docs/custom-resolvers/api/types.md b/docs/custom-resolvers/api/types.md new file mode 100644 index 0000000..22fe816 --- /dev/null +++ b/docs/custom-resolvers/api/types.md @@ -0,0 +1,278 @@ +# Common Types and Interfaces + +This document describes the common types and interfaces used throughout the Custom Resolver system. + +## Core Types + +### `PropertyID` + +```typescript +type PropertyID = string; +``` + +Unique identifier for a property. + +### `PropertyTypes` + +```typescript +type PropertyTypes = + | string + | number + | boolean + | null + | undefined + | PropertyTypes[] + | { [key: string]: PropertyTypes }; +``` + +All possible property value types that can be handled by the resolver. + +### `DependencyStates` + +```typescript +type DependencyStates = { + [K in D]: unknown; +}; +``` + +Maps dependency names to their resolved values. The actual type of each value depends on the plugin that produced it. + +### `CollapsedDelta` + +```typescript +interface CollapsedDelta { + timestamp: number; + source?: string; + // ... other delta metadata +} +``` + +Represents a collapsed delta with metadata about the change. + +## Plugin Types + +### `ResolverPlugin` + +```typescript +interface ResolverPlugin { + readonly name: string; + readonly dependencies?: readonly D[]; + + initialize(): T; + update( + currentState: T, + newValue: PropertyTypes, + delta: CollapsedDelta, + dependencies: DependencyStates + ): T; + + resolve( + state: T, + dependencies: DependencyStates + ): PropertyTypes | undefined; +} +``` + +The main plugin interface. See [ResolverPlugin Interface](./resolver-plugin-interface.md) for details. + +## Result Types + +### `EntityResult` + +```typescript +interface EntityResult { + id: string; + properties: { + [propertyId: string]: PropertyTypes; + }; +} +``` + +Represents the resolved properties for a single entity. + +### `CustomResolverResult` + +```typescript +interface CustomResolverResult { + [entityId: string]: EntityResult; +} +``` + +Maps entity IDs to their resolved properties. + +## Configuration Types + +### `ResolverConfig` + +```typescript +interface ResolverConfig { + [propertyId: string]: ResolverPlugin; +} +``` + +Configuration object mapping property IDs to their resolver plugins. + +## Built-in Plugin Types + +### `LastWriteWinsPlugin` + +```typescript +class LastWriteWinsPlugin implements ResolverPlugin { + readonly name = 'last-write-wins'; + // ... +} + +interface LastWriteWinsState { + value?: PropertyTypes; + timestamp: number; +} +``` + +### `FirstWriteWinsPlugin` + +```typescript +class FirstWriteWinsPlugin implements ResolverPlugin { + readonly name = 'first-write-wins'; + // ... +} + +interface FirstWriteWinsState { + value?: PropertyTypes; + isSet: boolean; +} +``` + +### `ConcatenationPlugin` + +```typescript +interface ConcatenationOptions { + separator?: string; + sort?: boolean; +} + +class ConcatenationPlugin implements ResolverPlugin { + readonly name = 'concatenation'; + + constructor(private options: ConcatenationOptions = {}) { + this.options = { + separator: options.separator || ', ', + sort: options.sort || false + }; + } + // ... +} + +interface ConcatenationState { + values: Set; + separator: string; + sort: boolean; +} +``` + +### `MajorityVotePlugin` + +```typescript +interface MajorityVoteOptions { + minVotes?: number; +} + +class MajorityVotePlugin implements ResolverPlugin { + readonly name = 'majority-vote'; + + constructor(private options: MajorityVoteOptions = {}) { + this.options = { + minVotes: options.minVotes || 1 + }; + } + // ... +} + +interface MajorityVoteState { + votes: Map; + minVotes: number; +} +``` + +## Type Utilities + +### `DependencyKeys` + +Extracts the dependency keys from a plugin type. + +```typescript +type DependencyKeys = + T extends ResolverPlugin ? D : never; +``` + +### `PluginState` + +Extracts the state type from a plugin type. + +```typescript +type PluginState = + T extends ResolverPlugin ? S : never; +``` + +## Example: Working with Types + +### Type-Safe Plugin Implementation + +```typescript +// Define state and dependency types +interface CounterState { + count: number; +} + +type CounterDeps = 'incrementBy' | 'resetThreshold'; + +// Implement plugin with type safety +class CounterPlugin implements ResolverPlugin { + readonly name = 'counter' as const; + readonly dependencies = ['incrementBy', 'resetThreshold'] as const; + + initialize(): CounterState { + return { count: 0 }; + } + + update( + state: CounterState, + _newValue: unknown, + _delta: CollapsedDelta, + deps: DependencyStates + ): CounterState { + const increment = deps.incrementBy as number; + const threshold = deps.resetThreshold as number; + + const newCount = state.count + increment; + return { + count: newCount >= threshold ? 0 : newCount + }; + } + + resolve(state: CounterState): number { + return state.count; + } +} +``` + +## Type Assertions + +When working with dependency values, you'll often need to use type assertions since they're typed as `unknown`: + +```typescript +// Type assertion +const value = deps.someDependency as SomeType; + +// Type guard +if (typeof deps.someDependency === 'number') { + // deps.someDependency is now typed as number +} +``` + +## Best Practices + +1. **Use `as const`** for string literals to get the most specific type +2. **Narrow types** when accessing dependency values +3. **Document expected types** in JSDoc comments +4. **Handle undefined** values when dependencies might be missing +5. **Use type guards** for runtime type safety when needed diff --git a/docs/custom-resolvers/dependencies/dependency-resolution.md b/docs/custom-resolvers/dependencies/dependency-resolution.md new file mode 100644 index 0000000..8212e78 --- /dev/null +++ b/docs/custom-resolvers/dependencies/dependency-resolution.md @@ -0,0 +1,104 @@ +# Dependency Resolution + +## Overview + +The Custom Resolver system includes a robust dependency resolution mechanism that ensures plugins are processed in the correct order based on their dependencies. This document explains how the resolution process works and how to work with complex dependency graphs. + +## How Dependency Resolution Works + +1. **Graph Construction**: The system builds a directed graph of plugin dependencies during initialization. +2. **Cycle Detection**: The graph is checked for cycles to prevent infinite loops. +3. **Topological Sort**: Plugins are ordered such that all dependencies are processed before the plugins that depend on them. +4. **State Resolution**: During processing, each plugin receives the resolved states of its dependencies. + +## Example: Resolving Dependencies + +Consider the following plugin configuration: + +```typescript +const resolver = new CustomResolver(view, { + basePrice: new LastWriteWinsPlugin(), + discount: new LastWriteWinsPlugin(), + taxRate: new LastWriteWinsPlugin(), + tax: new TaxCalculatorPlugin(), // Depends on basePrice and taxRate + total: new TotalPricePlugin() // Depends on basePrice, discount, and tax +}); +``` + +The system will automatically determine the correct processing order: + +1. `basePrice`, `discount`, `taxRate` (no dependencies) +2. `tax` (depends on basePrice and taxRate) +3. `total` (depends on basePrice, discount, and tax) + +## Handling Circular Dependencies + +Circular dependencies are detected during initialization and will result in an error. For example: + +```typescript +// This will throw an error during initialization +const resolver = new CustomResolver(view, { + a: new SomePlugin({ deps: ['b'] }), + b: new SomePlugin({ deps: ['a'] }) // Circular dependency! +}); +``` + +## Advanced Dependency Patterns + +### Optional Dependencies + +To make a dependency optional, mark it with a `?` suffix: + +```typescript +class MyPlugin implements ResolverPlugin { + readonly name = 'my-plugin' as const; + readonly dependencies = ['required', 'optional?'] as const; + + // ... +} +``` + +### Dynamic Dependencies + +For plugins that need to determine dependencies at runtime, you can implement a custom resolver: + +```typescript +class DynamicDepsPlugin implements ResolverPlugin { + readonly name = 'dynamic' as const; + + getDependencies(config: any): string[] { + // Determine dependencies based on config + return ['always']; + } + + // ... +} +``` + +## Debugging Dependency Issues + +### Common Issues + +1. **Missing Dependencies**: Ensure all required dependencies are declared +2. **Type Mismatches**: Verify that dependency types match expected types +3. **Circular Dependencies**: Break circular references in your dependency graph + +### Debugging Tools + +1. **Dependency Graph**: Use `resolver.getDependencyGraph()` to inspect the dependency graph +2. **Processing Order**: Check `resolver.getProcessingOrder()` to see the resolved execution order +3. **Logging**: Enable debug logging to track dependency resolution + +## Performance Considerations + +1. **Dependency Depth**: Deep dependency chains can impact performance +2. **Graph Complexity**: Complex graphs take longer to process +3. **Caching**: The system caches resolved dependencies for performance + +## Best Practices + +1. **Minimize Dependencies**: Only depend on what you need +2. **Keep Graphs Shallow**: Prefer wide, shallow dependency graphs over deep ones +3. **Document Dependencies**: Clearly document all plugin dependencies +4. **Test Edge Cases**: Test with missing or invalid dependencies +5. **Monitor Performance**: Keep an eye on resolution time for large graphs diff --git a/docs/custom-resolvers/dependencies/index.md b/docs/custom-resolvers/dependencies/index.md new file mode 100644 index 0000000..ac1ea5c --- /dev/null +++ b/docs/custom-resolvers/dependencies/index.md @@ -0,0 +1,59 @@ +# Plugin Dependencies + +## Overview + +The Custom Resolver system provides a powerful dependency management system that allows plugins to depend on the resolved values of other properties. This enables complex resolution strategies where the value of one property can influence how another property is resolved. + +## Key Concepts + +1. **Dependency Declaration**: Plugins declare their dependencies using the `dependencies` property +2. **Type Safety**: Dependencies are type-checked at compile time +3. **Automatic Resolution**: The system resolves dependencies in the correct order +4. **Cycle Detection**: Circular dependencies are detected and reported + +## How It Works + +1. **Dependency Graph**: The system builds a directed acyclic graph (DAG) of plugin dependencies +2. **Topological Sort**: Plugins are processed in an order that respects their dependencies +3. **Dependency Injection**: Required dependencies are automatically injected into plugin methods +4. **Lazy Resolution**: Dependencies are only resolved when needed + +## Example + +```typescript +class TotalPricePlugin implements ResolverPlugin { + readonly name = 'total' as const; + readonly dependencies = ['price', 'tax'] as const; + + initialize(): TotalState { + return { total: 0 }; + } + + update( + state: TotalState, + _newValue: unknown, + _delta: CollapsedDelta, + deps: DependencyStates<'price' | 'tax'> + ): TotalState { + const price = deps.price as number; + const tax = deps.tax as number; + return { total: price + tax }; + } + + resolve(state: TotalState): number { + return state.total; + } +} +``` + +## Best Practices + +1. **Minimal Dependencies**: Only declare dependencies that are actually needed +2. **Acyclic Dependencies**: Keep the dependency graph acyclic +3. **Document Dependencies**: Clearly document what each dependency is used for +4. **Handle Missing Dependencies**: Gracefully handle cases where dependencies might be undefined + +## Next Steps + +- Learn about [Type-Safe Dependencies](./type-safety.md) +- Understand [Dependency Resolution](./dependency-resolution.md) diff --git a/docs/custom-resolvers/dependencies/type-safety.md b/docs/custom-resolvers/dependencies/type-safety.md new file mode 100644 index 0000000..3a079ca --- /dev/null +++ b/docs/custom-resolvers/dependencies/type-safety.md @@ -0,0 +1,149 @@ +# Type-Safe Dependencies + +## Overview + +The Custom Resolver system leverages TypeScript's type system to provide compile-time safety for plugin dependencies. This ensures that: + +1. Only declared dependencies can be accessed +2. Dependencies are properly typed +3. Refactoring is safer with compiler support +4. Common errors are caught during development + +## How It Works + +The `ResolverPlugin` interface uses TypeScript generics to enforce type safety: + +```typescript +interface ResolverPlugin { + readonly name: string; + readonly dependencies?: readonly D[]; + + // ... methods receive properly typed dependencies + update( + currentState: T, + newValue: PropertyTypes, + delta: CollapsedDelta, + dependencies: DependencyStates + ): T; +} + +type DependencyStates = { + [K in D]: unknown; +}; +``` + +## Declaring Dependencies + +Dependencies are declared as a readonly array of string literals: + +```typescript +class MyPlugin implements ResolverPlugin { + readonly name = 'my-plugin' as const; + readonly dependencies = ['dep1', 'dep2'] as const; + + // ... implementation +} +``` + +### Type Inference + +Using `as const` with the dependencies array ensures TypeScript infers the most specific type possible: + +```typescript +// Type is readonly ["dep1", "dep2"] +readonly dependencies = ['dep1', 'dep2'] as const; +``` + +## Accessing Dependencies + +Dependencies are accessed through the `dependencies` parameter in plugin methods: + +```typescript +update( + state: MyState, + _newValue: unknown, + _delta: CollapsedDelta, + deps: DependencyStates<'price' | 'tax'> +): MyState { + // TypeScript knows price and tax are available + const price = deps.price as number; + const tax = deps.tax as number; + + // This would be a TypeScript error: + // const unknown = deps.unknown; // Error: Property 'unknown' does not exist + + return { /* ... */ }; +} +``` + +## Type Assertions + +Since dependency values are typed as `unknown`, you'll need to use type assertions or type guards: + +```typescript +// Using type assertion +const price = deps.price as number; + +// Using type guard +if (typeof deps.price === 'number') { + // deps.price is now typed as number +} +``` + +## Best Practices + +1. **Use `as const`** for dependency arrays to get the most specific type +2. **Narrow types** when accessing dependency values +3. **Document expected types** of dependencies in JSDoc +4. **Handle undefined** values when dependencies might be missing + +## Common Patterns + +### Optional Dependencies + +```typescript +class MyPlugin implements ResolverPlugin { + readonly name = 'my-plugin' as const; + readonly dependencies = ['required', 'optional?'] as const; + + update(_state: MyState, _value: unknown, _delta: CollapsedDelta, deps: any) { + const required = deps.required as number; // Always present + const optional = deps['optional?'] as number | undefined; // Might be undefined + + // ... + } +} +``` + +### Multiple Dependencies with Same Type + +```typescript +type PriceDependencies = 'price1' | 'price2' | 'price3'; + +class PriceAggregator implements ResolverPlugin { + readonly name = 'price-aggregator' as const; + readonly dependencies: readonly PriceDependencies[] = ['price1', 'price2', 'price3'] as const; + + update(_state: PriceState, _value: unknown, _delta: CollapsedDelta, deps: any) { + const prices = this.dependencies + .map(dep => deps[dep] as number | undefined) + .filter((p): p is number => p !== undefined); + + // ... + } +} +``` + +## Troubleshooting + +### Type Errors + +- **Missing Dependencies**: Ensure all dependencies are properly declared in the type parameter +- **Incorrect Types**: Verify type assertions match the expected types +- **Readonly Arrays**: Use `readonly` and `as const` for dependency arrays + +### Runtime Errors + +- **Undefined Dependencies**: Check if a dependency exists before using it +- **Type Mismatches**: Validate types at runtime when necessary +- **Circular Dependencies**: Ensure your dependency graph is acyclic diff --git a/docs/custom-resolvers/overview.md b/docs/custom-resolvers/overview.md new file mode 100644 index 0000000..aa827ef --- /dev/null +++ b/docs/custom-resolvers/overview.md @@ -0,0 +1,47 @@ +# Custom Resolvers + +## Overview + +The `CustomResolver` system provides a flexible framework for resolving property conflicts in distributed systems. It enables you to define custom resolution strategies through plugins, complete with type-safe dependencies between resolvers. + +## Key Features + +- **Plugin-based Architecture**: Extend functionality with custom resolvers +- **Type-Safe Dependencies**: Compile-time checking of plugin dependencies +- **Built-in Resolvers**: Common resolution strategies included +- **Efficient Processing**: Only processes changed deltas +- **Deterministic Results**: Same input always produces the same output + +## Core Concepts + +1. **Resolver Plugins**: Implement resolution logic for properties +2. **Dependency Management**: Declare and manage dependencies between plugins +3. **State Management**: Maintain and update state based on incoming deltas +4. **Resolution Pipeline**: Process updates and resolve final values + +## Getting Started + +```typescript +import { CustomResolver, LastWriteWinsPlugin } from './resolver'; +import { LosslessView } from '../lossless-view'; + +// Create a lossless view +const view = new LosslessView(); + +// Create a resolver with a last-write-wins strategy +const resolver = new CustomResolver(view, { + myProperty: new LastWriteWinsPlugin() +}); + +// Process updates +// ... + +// Get resolved values +const result = resolver.resolve(); +``` + +## Next Steps + +- Learn about [Built-in Plugins](./plugins/builtin-plugins.md) +- Understand [Type-Safe Dependencies](./dependencies/type-safety.md) +- Explore [Creating Custom Plugins](./plugins/creating-plugins.md) diff --git a/docs/custom-resolvers/plugins/builtin-plugins.md b/docs/custom-resolvers/plugins/builtin-plugins.md new file mode 100644 index 0000000..0aba6bc --- /dev/null +++ b/docs/custom-resolvers/plugins/builtin-plugins.md @@ -0,0 +1,88 @@ +# Built-in Plugins + +## Overview + +The Custom Resolver system includes several built-in plugins that implement common resolution strategies. These can be used directly or as examples for creating custom plugins. + +## Available Plugins + +### Last Write Wins + +Keeps the most recent value based on timestamp. + +```typescript +const resolver = new CustomResolver(view, { + myProperty: new LastWriteWinsPlugin() +}); +``` + +### First Write Wins + +Keeps the first non-null value seen. + +```typescript +const resolver = new CustomResolver(view, { + myProperty: new FirstWriteWinsPlugin() +}); +``` + +### Concatenation + +Combines string values with a separator. + +```typescript +const resolver = new CustomResolver(view, { + fullName: new ConcatenationPlugin({ + separator: ' ', + sort: true // Sort values before concatenation + }) +}); +``` + +### Majority Vote + +Selects the most common value. + +```typescript +const resolver = new CustomResolver(view, { + status: new MajorityVotePlugin({ + minVotes: 2 // Minimum votes required to select a winner + }) +}); +``` + +### Minimum Value + +Tracks the minimum numeric value. + +```typescript +const resolver = new CustomResolver(view, { + minPrice: new MinPlugin() +}); +``` + +### Maximum Value + +Tracks the maximum numeric value. + +```typescript +const resolver = new CustomResolver(view, { + maxScore: new MaxPlugin() +}); +``` + +## Plugin Options + +| Plugin | Options | Default | Description | +|--------|---------|---------|-------------| +| `ConcatenationPlugin` | `separator: string`
`sort: boolean` | `', '`
`false` | Separator between values and whether to sort | +| `MajorityVotePlugin` | `minVotes: number` | `1` | Minimum votes needed to select a winner | +| `LastWriteWins`
`FirstWriteWins`
`MinPlugin`
`MaxPlugin` | None | N/A | No configuration options | + +## Choosing the Right Plugin + +- Use `LastWriteWins` for simple timestamp-based resolution +- Use `FirstWriteWins` to preserve the initial value +- Use `ConcatenationPlugin` for combining string values +- Use `MajorityVote` for consensus-based resolution +- Use `MinPlugin`/`MaxPlugin` for numeric ranges diff --git a/docs/custom-resolvers/plugins/creating-plugins.md b/docs/custom-resolvers/plugins/creating-plugins.md new file mode 100644 index 0000000..0d6ce97 --- /dev/null +++ b/docs/custom-resolvers/plugins/creating-plugins.md @@ -0,0 +1,122 @@ +# Creating Custom Plugins + +## Overview + +Custom plugins allow you to implement custom resolution logic for your specific use case. This guide walks through creating a new plugin from scratch. + +## Basic Plugin Structure + +A minimal plugin must implement the `ResolverPlugin` interface: + +```typescript +import { ResolverPlugin } from '../resolver'; + +class MyPlugin implements ResolverPlugin { + readonly name = 'my-plugin' as const; + + initialize(): MyState { + // Return initial state + return { /* ... */ }; + } + + update( + currentState: MyState, + newValue: unknown, + delta: CollapsedDelta, + dependencies: {} + ): MyState { + // Update state based on new value + return { /* updated state */ }; + } + + resolve(state: MyState): unknown { + // Return the resolved value + return /* resolved value */; + } +} +``` + +## Adding Dependencies + +To depend on other properties, specify the dependency types: + +```typescript +class DiscountedPricePlugin implements ResolverPlugin { + readonly name = 'discounted-price' as const; + readonly dependencies = ['basePrice', 'discount'] as const; + + initialize(): DiscountState { + return { finalPrice: 0 }; + } + + update( + state: DiscountState, + _newValue: unknown, + _delta: CollapsedDelta, + deps: DependencyStates<'basePrice' | 'discount'> + ): DiscountState { + const basePrice = deps.basePrice as number; + const discount = deps.discount as number; + return { finalPrice: basePrice * (1 - discount) }; + } + + resolve(state: DiscountState): number { + return state.finalPrice; + } +} +``` + +## Best Practices + +1. **Immutable State**: Always return new state objects instead of mutating +2. **Pure Functions**: Keep update and resolve methods pure and side-effect free +3. **Error Handling**: Handle unexpected input gracefully +4. **Type Safety**: Use TypeScript types to catch errors early +5. **Documentation**: Document your plugin's behavior and requirements + +## Testing Your Plugin + +Create tests to verify your plugin's behavior: + +```typescript +describe('DiscountedPricePlugin', () => { + let view: LosslessView; + let resolver: CustomResolver; + + beforeEach(() => { + view = new LosslessView(); + resolver = new CustomResolver(view, { + basePrice: new LastWriteWinsPlugin(), + discount: new LastWriteWinsPlugin(), + finalPrice: new DiscountedPricePlugin() + }); + }); + + it('applies discount to base price', () => { + // Test your plugin's behavior + }); +}); +``` + +## Advanced Topics + +### Handling Complex Dependencies + +For plugins with complex dependency requirements, you can use the `dependencies` array to declare all required properties and access them in a type-safe way through the `dependencies` parameter. + +### Performance Considerations + +- Keep state updates minimal and efficient +- Avoid expensive computations in the update method +- Consider memoization for expensive resolve operations + +### Debugging + +Add logging to track state changes and resolution: + +```typescript +update(currentState: MyState, newValue: unknown): MyState { + console.log('Updating with:', { currentState, newValue }); + // ... +} +``` diff --git a/docs/custom-resolvers/plugins/index.md b/docs/custom-resolvers/plugins/index.md new file mode 100644 index 0000000..ffaa415 --- /dev/null +++ b/docs/custom-resolvers/plugins/index.md @@ -0,0 +1,30 @@ +# Resolver Plugins + +## Overview + +Resolver plugins implement the core resolution logic for properties in the Custom Resolver system. Each plugin is responsible for: + +1. Initializing its internal state +2. Processing updates to the property +3. Resolving the final value from the current state + +## Plugin Lifecycle + +1. **Initialization**: When a resolver is created, each plugin's `initialize()` method is called to set up its initial state. +2. **Update Processing**: For each new delta, the `update()` method is called with the current state, new value, and any dependency states. +3. **Resolution**: The `resolve()` method is called to produce the final value from the current state. + +## Built-in Plugins + +The system includes several common resolution strategies: + +- [Last Write Wins](./builtin-plugins.md#last-write-wins) +- [First Write Wins](./builtin-plugins.md#first-write-wins) +- [Concatenation](./builtin-plugins.md#concatenation) +- [Majority Vote](./builtin-plugins.md#majority-vote) +- [Minimum Value](./builtin-plugins.md#min) +- [Maximum Value](./builtin-plugins.md#max) + +## Creating Custom Plugins + +See the [Creating Custom Plugins](./creating-plugins.md) guide for detailed information on implementing your own resolver plugins. diff --git a/plans/plugin-dep-vis.md b/plans/plugin-dep-vis.md deleted file mode 100644 index 0c6c369..0000000 --- a/plans/plugin-dep-vis.md +++ /dev/null @@ -1,152 +0,0 @@ -# Plugin Dependency Visibility Implementation Plan - -## Overview -This document outlines the implementation plan for enforcing restricted state visibility in the CustomResolver system. The goal is to ensure that each plugin can only access the states of properties it has explicitly declared as dependencies. - -## Current Behavior -- All plugins currently receive the complete `allStates` object containing all property states -- There's no enforcement of which states a plugin can access -- Dependencies are declared but not used for access control - -## Proposed Changes - -### 1. Update ResolverPlugin Interface -```typescript -interface ResolverPlugin { - name: string; - dependencies?: PropertyID[]; // Explicitly declare which properties this plugin depends on - // ... rest of the interface -} -``` - -### 2. Modify CustomResolver Implementation - -#### 2.1 Update Reducer Method -Modify the `reducer` method to filter states before passing to plugins: - -```typescript -public reducer( - acc: CustomResolverAccumulator, - cur: LosslessViewOne -): CustomResolverAccumulator { - // ... existing setup code ... - - // Process each property in execution order - for (const propertyId of executionOrder) { - // ... existing delta processing ... - - // Create filtered states object with only declared dependencies - const visibleStates: Record = {}; - - // Add states for declared dependencies - if (plugin.dependencies) { - for (const dep of plugin.dependencies) { - if (allStates[dep] !== undefined) { - visibleStates[dep] = allStates[dep]; - } - } - } - - // Pass only visible states to the plugin - propertyState.state = propertyState.plugin.update( - propertyState.state, - value, - delta, - visibleStates - ); - - // ... rest of the method ... - } -} -``` - -#### 2.2 Update Resolver Method -Similarly update the `resolver` method: - -```typescript -resolver(cur: CustomResolverAccumulator): CustomResolverResult { - const res: CustomResolverResult = {}; - - for (const [entityId, entity] of Object.entries(cur)) { - const entityResult = { id: entityId, properties: {} }; - const allStates: Record = {}; - - // First pass: collect all states - for (const [propId, propState] of Object.entries(entity.properties)) { - allStates[propId] = propState.state; - } - - // Second pass: resolve each property with filtered states - for (const [propId, propState] of Object.entries(entity.properties)) { - const plugin = propState.plugin; - const visibleStates: Record = {}; - - // Only include declared dependencies - if (plugin.dependencies) { - for (const dep of plugin.dependencies) { - if (allStates[dep] !== undefined) { - visibleStates[dep] = allStates[dep]; - } - } - } - - const resolvedValue = plugin.resolve(propState.state, visibleStates); - if (resolvedValue !== undefined) { - entityResult.properties[propId] = resolvedValue; - } - } - - if (Object.keys(entityResult.properties).length > 0) { - res[entityId] = entityResult; - } - } - - return res; -} -``` - -### 3. Add Validation -Add validation to ensure dependencies exist: - -```typescript -private validateDependencies(): void { - // Existing cycle detection... - - // Add validation that all dependencies exist - for (const [pluginId, plugin] of Object.entries(this.config)) { - for (const dep of plugin.dependencies || []) { - if (!this.config[dep]) { - throw new Error(`Plugin '${pluginId}' depends on unknown property: ${dep}`); - } - } - } -} -``` - -### 4. Update Tests -1. Add tests for state visibility: - - Test that plugins only receive their declared dependencies - - Test that plugins can't access undeclared dependencies - - Test that dependency validation works - - Test that existing functionality remains unchanged - -2. Update existing tests to account for the new behavior - -## Migration Strategy -1. This is a breaking change for any plugins that were accessing undeclared dependencies -2. Add warnings in the next minor version -3. Make the behavior opt-in initially with a flag -4. In the next major version, make it the default - -## Implementation Steps -1. [ ] Add the state filtering to `reducer` -2. [ ] Add the state filtering to `resolver` -3. [ ] Update dependency validation -4. [ ] Add comprehensive tests -5. [ ] Update documentation -6. [ ] Add deprecation warnings for plugins accessing undeclared dependencies - -## Future Considerations -1. Add a debug mode that logs when plugins access undeclared dependencies -2. Consider adding granular permissions (read/write) for dependencies -3. Add support for wildcard dependencies for plugins that need access to many properties \ No newline at end of file