- Update ResolverPlugin interface to include context in update and resolve methods - Modify CustomResolver to pass entity state and resolved values to plugins - Update built-in plugins to accept and use the new context parameter - Add comprehensive test for inter-plugin communication - Add documentation for the new view composition patterns This change enables plugins to access each other's states during both update and resolve phases, allowing for more powerful and flexible resolver compositions.
4.8 KiB
4.8 KiB
View Composition with Combined State
Overview
This document outlines the design for composing multiple lossy views using a combined state approach. This pattern allows different resolvers to work on their own parts of the state while sharing the same underlying delta stream.
Core Concept
The combined state approach involves:
- Creating a parent resolver that manages multiple child resolvers
- Each child resolver maintains its own state
- The parent coordinates the initialization, reduction, and resolution of child states
- The final result combines the outputs of all child resolvers
Implementation Pattern
1. Base Interfaces
interface CombinedState {
[resolverKey: string]: unknown;
}
interface CombinedResult {
[resolverKey: string]: unknown;
}
class CombinedResolver<State extends CombinedState, Result extends CombinedResult>
extends Lossy<State, Result> {
private resolvers: {
[key: string]: {
instance: Lossy<any, any>;
initializer: (view: LosslessViewOne) => any;
reducer: (state: any, view: LosslessViewOne) => any;
resolver: (state: any) => any;
};
} = {};
}
2. Registering Resolvers
registerResolver<T, U>(
key: string,
resolver: Lossy<T, U>,
options?: {
// Optional: Transform the view before passing to the child resolver
viewTransformer?: (view: LosslessViewOne) => LosslessViewOne;
// Optional: Transform the result after resolution
resultTransformer?: (result: U) => unknown;
}
) {
this.resolvers[key] = {
instance: resolver,
initializer: (view) => {
const transformedView = options?.viewTransformer?.(view) ?? view;
return resolver.initializer(transformedView);
},
reducer: (state, view) => {
const transformedView = options?.viewTransformer?.(view) ?? view;
return resolver.reducer(state, transformedView);
},
resolver: (state) => {
const result = resolver.resolver(state);
return options?.resultTransformer?.(result) ?? result;
}
};
}
3. Core Methods
initializer(view: LosslessViewOne): State {
const state = {} as State;
for (const [key, { initializer }] of Object.entries(this.resolvers)) {
state[key] = initializer(view);
}
return state;
}
reducer(state: State, view: LosslessViewOne): State {
const newState = { ...state };
for (const [key, { reducer }] of Object.entries(this.resolvers)) {
newState[key] = reducer(state[key], view);
}
return newState;
}
resolver(state: State): Result {
const result = {} as Result;
for (const [key, { resolver }] of Object.entries(this.resolvers)) {
result[key] = resolver(state[key]);
}
return result;
}
Example: Relationship Graph with Multiple Resolvers
class RelationshipGraphResolver extends CombinedResolver<
{
entities: EntityState;
relationships: RelationshipState;
metadata: MetadataState;
},
{
graph: GraphResult;
stats: StatsResult;
metadata: MetadataResult;
}
> {
constructor(lossless: Lossless) {
super(lossless);
// Register entity resolver
this.registerResolver('entities', new EntityResolver(lossless));
// Register relationship resolver
this.registerResolver(
'relationships',
new RelationshipResolver(lossless),
{
// Only process relationship deltas
viewTransformer: view => ({
...view,
propertyDeltas: Object.fromEntries(
Object.entries(view.propertyDeltas)
.filter(([key]) => key.startsWith('_rel_'))
)
})
}
);
// Register metadata resolver
this.registerResolver('metadata', new MetadataResolver(lossless));
}
// Override resolver to combine results
resolver(state: any) {
const results = super.resolver(state);
return {
nodes: results.entities,
edges: results.relationships,
stats: results.stats,
metadata: results.metadata
};
}
}
Benefits
- Separation of Concerns: Each resolver handles a specific aspect of the data
- Reusability: Resolvers can be reused in different combinations
- Maintainability: Changes to one resolver don't affect others
- Testability: Each resolver can be tested in isolation
- Flexibility: Easy to add, remove, or modify resolvers
Performance Considerations
- Memory Usage: Combined state increases memory usage
- Processing Overhead: Each delta is processed by all resolvers
- Optimization: Use view transformers to filter deltas early
Next Steps
- Implement the base
CombinedResolver
class - Refactor
RelationshipGraphResolver
to use this pattern - Add tests for the composition behavior
- Document common patterns and best practices
- Consider adding middleware support for cross-cutting concerns