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
This commit is contained in:
parent
8de512cd5b
commit
cf29338c9b
@ -326,7 +326,223 @@ describe('Custom Resolvers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inter-Plugin Dependencies', () => {
|
||||
describe('Plugin Dependencies', () => {
|
||||
test('should detect circular dependencies', () => {
|
||||
class PluginA implements ResolverPlugin {
|
||||
name = 'a';
|
||||
dependencies = ['b'];
|
||||
initialize() { return {}; }
|
||||
update() { return {}; }
|
||||
resolve() { return 'a'; }
|
||||
}
|
||||
|
||||
class PluginB implements ResolverPlugin {
|
||||
name = 'b';
|
||||
dependencies = ['a'];
|
||||
initialize() { return {}; }
|
||||
update() { return {}; }
|
||||
resolve() { return 'b'; }
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
new CustomResolver(lossless, {
|
||||
'a': new PluginA(),
|
||||
'b': new PluginB()
|
||||
});
|
||||
}).toThrow('Circular dependency detected');
|
||||
});
|
||||
|
||||
test('should process plugins in dependency order', () => {
|
||||
// Enable debug logging for this test
|
||||
process.env.DEBUG = 'rz:*';
|
||||
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
// Create test plugins with dependency tracking
|
||||
const pluginTracker = {
|
||||
first: { updated: false, resolved: false },
|
||||
second: { updated: false, resolved: false }
|
||||
};
|
||||
|
||||
interface PluginState {
|
||||
value: string;
|
||||
updated: boolean;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
class FirstPlugin implements ResolverPlugin<PluginState> {
|
||||
name = 'first';
|
||||
dependencies: string[] = [];
|
||||
|
||||
initialize(): PluginState {
|
||||
console.log('First plugin initialized');
|
||||
executionOrder.push('first-init');
|
||||
return { value: '', updated: false, resolved: false };
|
||||
}
|
||||
|
||||
update(
|
||||
state: PluginState,
|
||||
value: unknown,
|
||||
_delta?: unknown,
|
||||
_allStates?: Record<string, unknown>
|
||||
): PluginState {
|
||||
console.log('First plugin updated with value:', value);
|
||||
executionOrder.push('first-update');
|
||||
pluginTracker.first.updated = true;
|
||||
return {
|
||||
...state,
|
||||
value: String(value),
|
||||
updated: true
|
||||
};
|
||||
}
|
||||
|
||||
resolve(state: PluginState, _allStates?: Record<string, unknown>): string {
|
||||
console.log('First plugin resolved with value:', state.value);
|
||||
executionOrder.push('first-resolve');
|
||||
pluginTracker.first.resolved = true;
|
||||
return state.value;
|
||||
}
|
||||
}
|
||||
|
||||
class SecondPlugin implements ResolverPlugin<PluginState> {
|
||||
name = 'second';
|
||||
dependencies: string[] = ['first'];
|
||||
|
||||
initialize(): PluginState {
|
||||
console.log('Second plugin initialized');
|
||||
executionOrder.push('second-init');
|
||||
return { value: '', updated: false, resolved: false };
|
||||
}
|
||||
|
||||
update(
|
||||
state: PluginState,
|
||||
value: unknown,
|
||||
_delta?: unknown,
|
||||
allStates?: Record<string, unknown>
|
||||
): PluginState {
|
||||
console.log('Second plugin updated with value:', value);
|
||||
executionOrder.push('second-update');
|
||||
pluginTracker.second.updated = true;
|
||||
|
||||
// Check if we have access to first plugin's state
|
||||
const firstState = allStates?.first as PluginState | undefined;
|
||||
if (firstState) {
|
||||
executionOrder.push('second-has-first-state');
|
||||
console.log('Second plugin has access to first plugin state:', firstState);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
value: `${value}-${firstState?.value || 'unknown'}`,
|
||||
updated: true
|
||||
};
|
||||
}
|
||||
|
||||
resolve(state: PluginState, _allStates?: Record<string, unknown>): string {
|
||||
console.log('Second plugin resolved with value:', state.value);
|
||||
executionOrder.push('second-resolve');
|
||||
pluginTracker.second.resolved = true;
|
||||
return state.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Create resolver with dependency order: first -> second
|
||||
console.log('Creating resolver with plugins');
|
||||
|
||||
// Create resolver with test plugins first
|
||||
const firstPlugin = new FirstPlugin();
|
||||
const secondPlugin = new SecondPlugin();
|
||||
|
||||
const testResolver = new CustomResolver(lossless, {
|
||||
first: firstPlugin,
|
||||
second: secondPlugin
|
||||
});
|
||||
|
||||
// Verify plugins are not yet initialized
|
||||
expect(pluginTracker.first.updated).toBe(false);
|
||||
expect(pluginTracker.second.updated).toBe(false);
|
||||
|
||||
// Verify the execution order array is empty before processing
|
||||
expect(executionOrder).not.toContain('first-init');
|
||||
expect(executionOrder).not.toContain('second-init');
|
||||
expect(executionOrder).toHaveLength(0);
|
||||
|
||||
// Create and ingest test data
|
||||
const delta = createDelta('test1', 'host1')
|
||||
.withTimestamp(1000)
|
||||
.setProperty('test1', 'first', 'first', 'prop1')
|
||||
.setProperty('test1', 'second', 'second', 'prop2')
|
||||
.buildV1();
|
||||
|
||||
lossless.ingestDelta(delta);
|
||||
|
||||
// Resolve the view
|
||||
const result = testResolver.resolve();
|
||||
|
||||
// Verify the result
|
||||
expect(result).toBeDefined();
|
||||
if (!result) return;
|
||||
|
||||
const testEntity = result['test1'];
|
||||
expect(testEntity).toBeDefined();
|
||||
if (!testEntity) return;
|
||||
|
||||
// Check if properties exist
|
||||
expect(testEntity.properties).toBeDefined();
|
||||
|
||||
// Check if plugins were resolved
|
||||
expect(pluginTracker.first.resolved).toBe(true);
|
||||
expect(pluginTracker.second.resolved).toBe(true);
|
||||
|
||||
// Check if second plugin has access to first plugin's state
|
||||
expect(executionOrder).toContain('second-has-first-state');
|
||||
|
||||
// Check if first plugin was processed before second
|
||||
const firstUpdateIndex = executionOrder.indexOf('first-update');
|
||||
const secondUpdateIndex = executionOrder.indexOf('second-update');
|
||||
expect(firstUpdateIndex).not.toBe(-1);
|
||||
expect(secondUpdateIndex).not.toBe(-1);
|
||||
expect(firstUpdateIndex).toBeLessThan(secondUpdateIndex);
|
||||
|
||||
// Verify initialization order (first should be initialized before second)
|
||||
const firstInitIndex = executionOrder.indexOf('first-init');
|
||||
const secondInitIndex = executionOrder.indexOf('second-init');
|
||||
expect(firstInitIndex).not.toBe(-1);
|
||||
expect(secondInitIndex).not.toBe(-1);
|
||||
expect(firstInitIndex).toBeLessThan(secondInitIndex);
|
||||
|
||||
// Check if resolve was called in the right order
|
||||
const firstResolveIndex = executionOrder.indexOf('first-resolve');
|
||||
const secondResolveIndex = executionOrder.indexOf('second-resolve');
|
||||
expect(firstResolveIndex).not.toBe(-1);
|
||||
expect(secondResolveIndex).not.toBe(-1);
|
||||
expect(firstResolveIndex).toBeLessThan(secondResolveIndex);
|
||||
expect(firstInitIndex).toBeLessThan(secondInitIndex);
|
||||
|
||||
// Verify update order (first should be updated before second)
|
||||
expect(firstUpdateIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(secondUpdateIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(firstUpdateIndex).toBeLessThan(secondUpdateIndex);
|
||||
|
||||
// Verify resolve order (first should be resolved before second)
|
||||
expect(firstResolveIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(secondResolveIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(firstResolveIndex).toBeLessThan(secondResolveIndex);
|
||||
|
||||
// Check if second plugin could access first plugin's state
|
||||
expect(executionOrder).toContain('second-has-first-state');
|
||||
|
||||
// Check resolved values if they exist
|
||||
if (testEntity.properties.first) {
|
||||
expect(testEntity.properties.first).toBe('first');
|
||||
}
|
||||
|
||||
if (testEntity.properties.second) {
|
||||
// Second plugin's value is 'second-<first plugin's value>'
|
||||
expect(testEntity.properties.second).toBe('second-first');
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow plugins to depend on other plugin states', () => {
|
||||
// A plugin that applies a discount to a price
|
||||
class DiscountedPricePlugin implements ResolverPlugin<{ price: number }> {
|
||||
|
@ -29,22 +29,74 @@ The `CustomResolver` class provides a flexible system for resolving property con
|
||||
- `MajorityVotePlugin`: Selects the most common value
|
||||
- `MinPlugin`/`MaxPlugin`: Tracks minimum/maximum numeric values
|
||||
|
||||
## Inter-Plugin Dependencies
|
||||
## Plugin Dependencies
|
||||
|
||||
### Overview
|
||||
|
||||
The system now supports inter-plugin dependencies, allowing plugins to access the state of other plugins during both the update and resolve phases. This enables the creation of more sophisticated resolution strategies that can depend on multiple properties.
|
||||
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 has been updated to include an optional `allStates` parameter in both the `update` and `resolve` methods:
|
||||
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;
|
||||
|
||||
@ -53,25 +105,49 @@ interface ResolverPlugin<T = unknown> {
|
||||
currentState: T,
|
||||
newValue: PropertyTypes,
|
||||
delta: CollapsedDelta,
|
||||
allStates?: Record<PropertyID, unknown> // Access to other plugin states
|
||||
allStates?: Record<PropertyID, unknown>
|
||||
): T;
|
||||
|
||||
// Resolve the final value from the accumulated state
|
||||
// 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> // Access to other plugin states
|
||||
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
|
||||
3. Maintain backward compatibility with existing plugins
|
||||
|
||||
### Example: Discounted Price Plugin
|
||||
### 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:
|
||||
|
||||
|
@ -7,6 +7,12 @@ import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../core/
|
||||
export interface ResolverPlugin<T = unknown> {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Array of property IDs that this plugin depends on.
|
||||
* These properties will be processed before this plugin.
|
||||
*/
|
||||
dependencies?: PropertyID[];
|
||||
|
||||
// Initialize the state for a property
|
||||
initialize(): T;
|
||||
|
||||
@ -19,7 +25,6 @@ export interface ResolverPlugin<T = unknown> {
|
||||
): T;
|
||||
|
||||
// Resolve the final value from the accumulated state
|
||||
// Returns undefined if no valid value could be resolved
|
||||
resolve(
|
||||
state: T,
|
||||
allStates?: Record<PropertyID, unknown>
|
||||
@ -65,11 +70,17 @@ function extractValueFromDelta(propertyId: PropertyID, delta: CollapsedDelta): P
|
||||
}
|
||||
|
||||
export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResolverResult> {
|
||||
private executionOrder: PropertyID[];
|
||||
private readonly config: CustomResolverConfig;
|
||||
|
||||
constructor(
|
||||
lossless: Lossless,
|
||||
private config: CustomResolverConfig
|
||||
config: CustomResolverConfig
|
||||
) {
|
||||
super(lossless);
|
||||
this.config = config;
|
||||
this.validateDependencies();
|
||||
this.executionOrder = this.getExecutionOrder();
|
||||
}
|
||||
|
||||
initializer(view: LosslessViewOne): CustomResolverAccumulator {
|
||||
@ -78,36 +89,119 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
};
|
||||
}
|
||||
|
||||
reducer(acc: CustomResolverAccumulator, cur: LosslessViewOne): CustomResolverAccumulator {
|
||||
/**
|
||||
* Validates that there are no circular dependencies between plugins
|
||||
* @throws Error if circular dependencies are detected
|
||||
*/
|
||||
private validateDependencies(): void {
|
||||
const visited = new Set<PropertyID>();
|
||||
const visiting = new Set<PropertyID>();
|
||||
const plugins = Object.entries(this.config);
|
||||
|
||||
const visit = (pluginId: PropertyID): void => {
|
||||
if (visiting.has(pluginId)) {
|
||||
throw new Error(`Circular dependency detected involving property: ${pluginId}`);
|
||||
}
|
||||
|
||||
if (visited.has(pluginId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(pluginId);
|
||||
const plugin = this.config[pluginId];
|
||||
|
||||
// Visit all dependencies first
|
||||
for (const dep of plugin?.dependencies || []) {
|
||||
if (this.config[dep]) {
|
||||
visit(dep);
|
||||
} else {
|
||||
throw new Error(`Plugin '${pluginId}' depends on unknown property: ${dep}`);
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(pluginId);
|
||||
visited.add(pluginId);
|
||||
};
|
||||
|
||||
// Check each plugin for circular dependencies
|
||||
for (const [id] of plugins) {
|
||||
if (!visited.has(id)) {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the execution order of properties based on their dependencies
|
||||
* @returns Array of property IDs in execution order
|
||||
*/
|
||||
private getExecutionOrder(): PropertyID[] {
|
||||
const visited = new Set<PropertyID>();
|
||||
const order: PropertyID[] = [];
|
||||
|
||||
const visit = (pluginId: PropertyID): void => {
|
||||
if (visited.has(pluginId)) return;
|
||||
|
||||
const plugin = this.config[pluginId];
|
||||
if (!plugin) return;
|
||||
|
||||
// Visit dependencies first
|
||||
for (const dep of plugin.dependencies || []) {
|
||||
visit(dep);
|
||||
}
|
||||
|
||||
// Then add this plugin
|
||||
if (!visited.has(pluginId)) {
|
||||
visited.add(pluginId);
|
||||
order.push(pluginId);
|
||||
}
|
||||
};
|
||||
|
||||
// Visit each plugin
|
||||
for (const id of Object.keys(this.config)) {
|
||||
visit(id);
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
public reducer(
|
||||
acc: CustomResolverAccumulator,
|
||||
cur: LosslessViewOne
|
||||
): CustomResolverAccumulator {
|
||||
if (!acc[cur.id]) {
|
||||
acc[cur.id] = { id: cur.id, properties: {} };
|
||||
}
|
||||
|
||||
// First pass: collect all property states for this entity
|
||||
|
||||
// Get the execution order based on dependencies
|
||||
const executionOrder = this.getExecutionOrder();
|
||||
|
||||
// First pass: collect all current states for this entity
|
||||
const allStates: Record<PropertyID, unknown> = {};
|
||||
for (const [propertyId, propertyState] of Object.entries(acc[cur.id].properties)) {
|
||||
allStates[propertyId] = propertyState.state;
|
||||
}
|
||||
|
||||
// Second pass: update each property with access to all states
|
||||
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
||||
// Process each property in dependency order
|
||||
for (const propertyId of executionOrder) {
|
||||
const deltas = cur.propertyDeltas[propertyId];
|
||||
if (!deltas) continue;
|
||||
|
||||
const plugin = this.config[propertyId];
|
||||
if (!plugin) continue;
|
||||
|
||||
// Initialize property state if not exists
|
||||
// Initialize property state if it doesn't exist
|
||||
if (!acc[cur.id].properties[propertyId]) {
|
||||
acc[cur.id].properties[propertyId] = {
|
||||
plugin,
|
||||
state: plugin.initialize()
|
||||
};
|
||||
// Update allStates with the new state
|
||||
allStates[propertyId] = acc[cur.id].properties[propertyId].state;
|
||||
}
|
||||
|
||||
// Process each delta for this property
|
||||
const propertyState = acc[cur.id].properties[propertyId];
|
||||
|
||||
// Process all deltas for this property
|
||||
for (const delta of deltas || []) {
|
||||
for (const delta of deltas) {
|
||||
const value = extractValueFromDelta(propertyId, delta);
|
||||
if (value !== undefined) {
|
||||
propertyState.state = propertyState.plugin.update(
|
||||
@ -116,11 +210,18 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
delta,
|
||||
allStates
|
||||
);
|
||||
// Update allStates with the new state
|
||||
// Update the state in our tracking object
|
||||
allStates[propertyId] = propertyState.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any properties not in the execution order (shouldn't normally happen)
|
||||
for (const [propertyId, _deltas] of Object.entries(cur.propertyDeltas)) {
|
||||
if (!executionOrder.includes(propertyId) && this.config[propertyId]) {
|
||||
console.warn(`Property '${propertyId}' not in execution order but has deltas`);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
@ -166,6 +267,7 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
// Last Write Wins plugin
|
||||
export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
|
||||
name = 'last-write-wins';
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
initialize() {
|
||||
return { timestamp: 0 };
|
||||
@ -197,6 +299,7 @@ export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTyp
|
||||
// First Write Wins plugin
|
||||
export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
|
||||
name = 'first-write-wins';
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
initialize() {
|
||||
return { timestamp: Infinity };
|
||||
@ -228,7 +331,8 @@ export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTy
|
||||
// Concatenation plugin (for string values)
|
||||
export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: string, timestamp: number }[] }> {
|
||||
name = 'concatenation';
|
||||
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
constructor(private separator: string = ' ') { }
|
||||
|
||||
initialize() {
|
||||
@ -267,6 +371,7 @@ export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: st
|
||||
// Majority vote plugin
|
||||
export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyTypes, number> }> {
|
||||
name = 'majority-vote';
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
initialize() {
|
||||
return { votes: new Map() };
|
||||
@ -301,9 +406,10 @@ export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyT
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric min/max plugins
|
||||
// Numeric min plugin
|
||||
export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
||||
name = 'min';
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
initialize() {
|
||||
return {};
|
||||
@ -331,8 +437,10 @@ export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric max plugin
|
||||
export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
|
||||
name = 'max';
|
||||
dependencies: PropertyID[] = [];
|
||||
|
||||
initialize() {
|
||||
return {};
|
||||
|
Loading…
x
Reference in New Issue
Block a user