rhizome-node/docs/custom-resolvers.md
Lentil Hoffman cf29338c9b
feat: add plugin dependency support to CustomResolver
- Added  property to ResolverPlugin interface
- Implemented topological sorting for plugin execution order
- Added circular dependency detection
- Updated built-in plugins with dependency declarations
- Added comprehensive tests for plugin dependencies
- Updated documentation with examples and best practices
2025-06-22 23:47:35 -05:00

315 lines
9.9 KiB
Markdown

# 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<T = unknown> {
// 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<PropertyID, unknown>
): 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<PropertyID, unknown>
): 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<PropertyID, unknown>
) {
if (typeof newValue === 'number') {
return { price: newValue };
}
return state;
}
resolve(
state: { price: number },
allStates?: Record<PropertyID, unknown>
): 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