Updated and organized documentation

This commit is contained in:
Lentil Hoffman 2025-06-23 07:21:19 -05:00
parent ceb9d9a0cf
commit 1329a75a8f
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
13 changed files with 1263 additions and 466 deletions

View File

@ -1,5 +1,10 @@
See [spec.md](spec.md) for additional specification details about this project. 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 # Development / Demo
## Setup ## Setup

View File

@ -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<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

View File

@ -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<string, Set<string>>;
}
```
## 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<string, Set<string>>`
Gets the dependency graph used for resolution.
**Returns:** `Map<string, Set<string>>` - 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<any, string>;
}
```
## 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<SubtotalState, 'unitPrice' | 'quantity'> {
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)

View File

@ -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<T = unknown, D extends string = never> {
/**
* 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<D>
): 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<D>
): 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<D>` - 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<D>` - 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<CounterState> {
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<PriceState, 'basePrice' | 'taxRate'> {
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<State, 'required' | 'optional?'> {
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)

View File

@ -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<D>`
```typescript
type DependencyStates<D extends string> = {
[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<T, D>`
```typescript
interface ResolverPlugin<T = unknown, D extends string = never> {
readonly name: string;
readonly dependencies?: readonly D[];
initialize(): T;
update(
currentState: T,
newValue: PropertyTypes,
delta: CollapsedDelta,
dependencies: DependencyStates<D>
): T;
resolve(
state: T,
dependencies: DependencyStates<D>
): 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<any, string>;
}
```
Configuration object mapping property IDs to their resolver plugins.
## Built-in Plugin Types
### `LastWriteWinsPlugin`
```typescript
class LastWriteWinsPlugin implements ResolverPlugin<LastWriteWinsState> {
readonly name = 'last-write-wins';
// ...
}
interface LastWriteWinsState {
value?: PropertyTypes;
timestamp: number;
}
```
### `FirstWriteWinsPlugin`
```typescript
class FirstWriteWinsPlugin implements ResolverPlugin<FirstWriteWinsState> {
readonly name = 'first-write-wins';
// ...
}
interface FirstWriteWinsState {
value?: PropertyTypes;
isSet: boolean;
}
```
### `ConcatenationPlugin`
```typescript
interface ConcatenationOptions {
separator?: string;
sort?: boolean;
}
class ConcatenationPlugin implements ResolverPlugin<ConcatenationState> {
readonly name = 'concatenation';
constructor(private options: ConcatenationOptions = {}) {
this.options = {
separator: options.separator || ', ',
sort: options.sort || false
};
}
// ...
}
interface ConcatenationState {
values: Set<string>;
separator: string;
sort: boolean;
}
```
### `MajorityVotePlugin`
```typescript
interface MajorityVoteOptions {
minVotes?: number;
}
class MajorityVotePlugin implements ResolverPlugin<MajorityVoteState> {
readonly name = 'majority-vote';
constructor(private options: MajorityVoteOptions = {}) {
this.options = {
minVotes: options.minVotes || 1
};
}
// ...
}
interface MajorityVoteState {
votes: Map<string, number>;
minVotes: number;
}
```
## Type Utilities
### `DependencyKeys<T>`
Extracts the dependency keys from a plugin type.
```typescript
type DependencyKeys<T> =
T extends ResolverPlugin<any, infer D> ? D : never;
```
### `PluginState<T>`
Extracts the state type from a plugin type.
```typescript
type PluginState<T> =
T extends ResolverPlugin<infer S, any> ? 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<CounterState, CounterDeps> {
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<CounterDeps>
): 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

View File

@ -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<MyState, 'required' | 'optional?'> {
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<DynamicState> {
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

View File

@ -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<TotalState, 'price' | 'tax'> {
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)

View File

@ -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<T = unknown, D extends string = never> {
readonly name: string;
readonly dependencies?: readonly D[];
// ... methods receive properly typed dependencies
update(
currentState: T,
newValue: PropertyTypes,
delta: CollapsedDelta,
dependencies: DependencyStates<D>
): T;
}
type DependencyStates<D extends string> = {
[K in D]: unknown;
};
```
## Declaring Dependencies
Dependencies are declared as a readonly array of string literals:
```typescript
class MyPlugin implements ResolverPlugin<MyState, 'dep1' | 'dep2'> {
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<MyState, 'required' | 'optional?'> {
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<PriceState, PriceDependencies> {
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

View File

@ -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)

View File

@ -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`<br>`sort: boolean` | `', '`<br>`false` | Separator between values and whether to sort |
| `MajorityVotePlugin` | `minVotes: number` | `1` | Minimum votes needed to select a winner |
| `LastWriteWins`<br>`FirstWriteWins`<br>`MinPlugin`<br>`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

View File

@ -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<MyState> {
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<DiscountState, 'basePrice' | 'discount'> {
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 });
// ...
}
```

View File

@ -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.

View File

@ -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<T = unknown> {
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<PropertyID, unknown> = {};
// 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<PropertyID, unknown> = {};
// 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<PropertyID, unknown> = {};
// 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