- 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
9.9 KiB
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
-
ResolverPlugin Interface
- Defines the contract for all resolver implementations
- Key methods:
initialize()
: Creates initial stateupdate()
: Processes new values with timestampsresolve()
: Produces final value from accumulated state
-
CustomResolver Class
- Manages resolution of entity properties using configured plugins
- Implements the core resolution logic:
initializer
: Creates initial state structurereducer
: Processes deltas and updates state using pluginsresolver
: Produces final resolved values
-
Built-in Plugins
LastWriteWinsPlugin
: Keeps the most recent valueFirstWriteWinsPlugin
: Keeps the first value seenConcatenationPlugin
: Combines string values with a separatorMajorityVotePlugin
: Selects the most common valueMinPlugin
/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:
- Deterministic Execution: Plugins are processed after their dependencies
- Dependency Validation: Circular dependencies are detected and prevented
- Optimized Processing: Only necessary states are processed in the correct order
Declaring Dependencies
Each plugin can declare its dependencies using the dependencies
property:
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
- Topological Sorting: Plugins are processed in an order that respects their dependencies
- Cycle Detection: Circular dependencies are detected during initialization
- Lazy Resolution: Dependencies are only resolved when needed
Example: Chained Dependencies
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
- Minimal Dependencies: Only declare dependencies that are actually needed
- Avoid Cycles: Design your plugins to avoid circular dependencies
- Document Dependencies: Clearly document what each plugin depends on
- 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:
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
-
Return Type of
resolve
:- The
resolve
method must return a value of typePropertyTypes
orundefined
- This is different from the
update
method which returns the updated state of typeT
- The returned value will be used as the resolved property value
- The
-
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
- Dependencies are declared using the
-
State Access:
- The
allStates
parameter inupdate
andresolve
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
- The
-
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:
- Collect all plugin states before processing updates
- 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:
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
-
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
-
Performance Considerations:
- Access only the states you need in the
allStates
object - Consider caching resolved values if the same calculation is performed multiple times
- Access only the states you need in the
-
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:
- Updated
ResolverPlugin
interface withallStates
parameter - Enhanced
CustomResolver
class for state sharing between plugins - Updated all built-in plugins for compatibility
- Comprehensive test coverage including:
- Basic functionality of all built-in plugins
- Inter-plugin dependency scenarios
- Edge cases and error conditions
- Complete documentation with examples
Usage Examples
Basic Usage
const resolver = new CustomResolver(losslessView, {
title: new LastWriteWinsPlugin(),
price: new LastWriteWinsPlugin(),
discount: new LastWriteWinsPlugin()
});
With Dependent Plugins
const resolver = new CustomResolver(losslessView, {
basePrice: new LastWriteWinsPlugin(),
discount: new LastWriteWinsPlugin(),
finalPrice: new DiscountedPricePlugin() // Depends on discount
});
Complex Example
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
- Plugin Dependencies: Explicitly declare dependencies between plugins
- Caching: Cache resolved values for better performance
- Validation: Add validation to prevent circular dependencies
- Debugging: Add logging for plugin execution order and state access
- Optimization: Lazy-load plugin states to improve performance with many properties
Example Configurations
Basic Usage
const resolver = new CustomResolver(losslessView, {
title: new LastWriteWinsPlugin(),
price: new LastWriteWinsPlugin(),
discount: new LastWriteWinsPlugin()
});
With Dependent Plugins
const resolver = new CustomResolver(losslessView, {
basePrice: new LastWriteWinsPlugin(),
discount: new LastWriteWinsPlugin(),
finalPrice: new DiscountedPricePlugin()
});
Future Enhancements
- Plugin Dependencies: Explicitly declare dependencies between plugins
- Caching: Cache resolved values for better performance
- Validation: Add validation to prevent circular dependencies
- Debugging: Add logging for plugin execution order and state access