feat: enable inter-plugin state sharing in CustomResolver
- 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.
This commit is contained in:
parent
a248f1d1a8
commit
5afd3232cb
@ -1,5 +1,5 @@
|
||||
import { jsonToAst } from '../../../src/util/json-ast';
|
||||
import { JsonNode } from '../../../src/util/json-ast/types';
|
||||
import { jsonToAst } from '../../../src/utils/json-ast';
|
||||
import { JsonNode } from '../../../src/utils/json-ast/types';
|
||||
|
||||
describe('jsonToAst', () => {
|
||||
it('should handle primitive values', () => {
|
||||
|
@ -436,6 +436,95 @@ describe('Custom Resolvers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plugin Communication', () => {
|
||||
test('plugins should be able to access each other\'s states', () => {
|
||||
// Create a plugin that depends on another property's value
|
||||
class DependentPlugin implements ResolverPlugin<{ value?: string }> {
|
||||
name = 'dependent';
|
||||
|
||||
initialize() {
|
||||
return { value: 'initial' };
|
||||
}
|
||||
|
||||
update(
|
||||
currentState: { value?: string },
|
||||
_newValue: PropertyTypes,
|
||||
_delta: CollapsedDelta,
|
||||
context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
// This plugin's value depends on the 'source' property's resolved value
|
||||
const sourceValue = context?.resolvedValues['source'];
|
||||
return {
|
||||
value: typeof sourceValue === 'string' ? `Processed: ${sourceValue}` : currentState.value
|
||||
};
|
||||
}
|
||||
|
||||
resolve(
|
||||
state: { value?: string },
|
||||
context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
): PropertyTypes | undefined {
|
||||
// In resolve, we can also check the context if needed
|
||||
const sourceValue = context?.resolvedValues['source'];
|
||||
if (typeof sourceValue === 'string' && state.value === 'initial') {
|
||||
return `Processed: ${sourceValue}`;
|
||||
}
|
||||
return state.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a resolver with both plugins
|
||||
const resolver = new CustomResolver(lossless, {
|
||||
source: new LastWriteWinsPlugin(),
|
||||
dependent: new DependentPlugin()
|
||||
});
|
||||
|
||||
// First, set up the source property
|
||||
const sourceDelta = createDelta('user1', 'host1')
|
||||
.withTimestamp(1000)
|
||||
.setProperty('entity1', 'source', 'original', 'collection')
|
||||
.buildV1();
|
||||
|
||||
lossless.ingestDelta(sourceDelta);
|
||||
|
||||
// Then set up the dependent property
|
||||
const dependentDelta = createDelta('user1', 'host1')
|
||||
.withTimestamp(2000)
|
||||
.setProperty('entity1', 'dependent', 'initial', 'collection')
|
||||
.buildV1();
|
||||
|
||||
lossless.ingestDelta(dependentDelta);
|
||||
|
||||
// Get the first result
|
||||
const result = resolver.resolve();
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// The dependent plugin should see the source value
|
||||
expect(result!['entity1'].properties).toMatchObject({
|
||||
source: 'original',
|
||||
dependent: expect.stringContaining('Processed: original')
|
||||
});
|
||||
|
||||
// Create a new delta that updates the source property
|
||||
const updateDelta = createDelta('user1', 'host1')
|
||||
.withTimestamp(3000)
|
||||
.setProperty('entity1', 'source', 'updated', 'collection')
|
||||
.buildV1();
|
||||
|
||||
// Ingest the update delta
|
||||
lossless.ingestDelta(updateDelta);
|
||||
|
||||
// Get the updated result
|
||||
const updatedResult = resolver.resolve();
|
||||
expect(updatedResult).toBeDefined();
|
||||
|
||||
// The dependent plugin should see the updated source value
|
||||
expect(updatedResult!['entity1'].properties).toMatchObject({
|
||||
source: 'updated',
|
||||
dependent: expect.stringContaining('Processed: updated')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle empty delta sets', () => {
|
||||
const resolver = new CustomResolver(lossless, {
|
||||
|
67
__tests__/unit/views/resolvers/relationship-graph.test.ts
Normal file
67
__tests__/unit/views/resolvers/relationship-graph.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { RhizomeNode, Lossless } from "../../../../src";
|
||||
import { RelationshipGraphResolver } from "../../../../src/views/resolvers/relationship-graph";
|
||||
import { createDelta } from "../../../../src/core/delta-builder";
|
||||
|
||||
// Deferring until we figure out lossy view composition
|
||||
describe.skip('RelationshipGraphResolver', () => {
|
||||
let node: RhizomeNode;
|
||||
let lossless: Lossless;
|
||||
let resolver: RelationshipGraphResolver;
|
||||
|
||||
beforeEach(() => {
|
||||
node = new RhizomeNode();
|
||||
lossless = new Lossless(node);
|
||||
resolver = new RelationshipGraphResolver(lossless);
|
||||
});
|
||||
|
||||
describe('Basic Relationship Resolution', () => {
|
||||
test('should create a basic relationship between two entities', () => {
|
||||
const delta = createDelta('user1', 'host1')
|
||||
.withId('delta1')
|
||||
.withTimestamp(1000)
|
||||
.relate('entity1', 'entity2', 'friends', { since: 1640995200000 })
|
||||
.buildV1();
|
||||
|
||||
// Ingest the delta
|
||||
lossless.ingestDelta(delta);
|
||||
|
||||
// Resolve the graph
|
||||
const graph = resolver.resolve();
|
||||
|
||||
// Verify the relationship was created
|
||||
expect(graph).toBeDefined();
|
||||
if (!graph) return;
|
||||
|
||||
console.log(JSON.stringify(graph, null, 2));
|
||||
|
||||
// Check entities exist
|
||||
expect(graph.entities.has('entity1')).toBe(true);
|
||||
expect(graph.entities.has('entity2')).toBe(true);
|
||||
|
||||
// Check relationship exists
|
||||
const relationshipId = 'delta1';
|
||||
expect(graph.relationships.has(relationshipId)).toBe(true);
|
||||
|
||||
const relationship = graph.relationships.get(relationshipId)!;
|
||||
expect(relationship).toEqual({
|
||||
id: relationshipId,
|
||||
type: 'friends',
|
||||
source: 'entity1',
|
||||
target: 'entity2',
|
||||
properties: { since: 1640995200000 },
|
||||
deltas: ['delta1']
|
||||
});
|
||||
|
||||
// Check entity relationships
|
||||
const entity1 = graph.entities.get('entity1')!;
|
||||
const entity2 = graph.entities.get('entity2')!;
|
||||
|
||||
expect(entity1.outbound.has(relationshipId)).toBe(true);
|
||||
expect(entity2.inbound.has(relationshipId)).toBe(true);
|
||||
|
||||
// Check relationship type index
|
||||
expect(graph.relationshipsByType.has('friends')).toBe(true);
|
||||
expect(graph.relationshipsByType.get('friends')?.has(relationshipId)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { jsonToAst } from '../src/util/json-ast/index';
|
||||
import { jsonToAst } from '../src/utils/json-ast/index';
|
||||
|
||||
// Example JSON data
|
||||
const exampleJson = {
|
||||
|
@ -1,222 +1,23 @@
|
||||
# Entity Relationship Graph Implementation Plan
|
||||
# Entity Relationship Graph
|
||||
|
||||
## Overview
|
||||
This document outlines the plan to implement entity relationship tracking in the rhizome-node system. The implementation treats relationships as first-class entities, each with their own identity and properties.
|
||||
## Background
|
||||
|
||||
## Core Design
|
||||
Deltas can express relationships by declaring a domain entity for the relationship itself.
|
||||
|
||||
### Relationship as First-Class Entities
|
||||
- Each relationship is a domain entity with its own unique ID
|
||||
- Relationships have standard properties: `source`, `target`, and `type`
|
||||
- Additional properties can be added to relationships
|
||||
- Relationships are created using the `relate()` method in `DeltaBuilder`
|
||||
Our initial implementation assumes there is only one structure for a relationship: a directed edge from one entity to another.
|
||||
- `source` and `target` are required properties.
|
||||
- `type` as well as arbitrary additional properties are optional.
|
||||
|
||||
### Delta Structure for Relationships
|
||||
```typescript
|
||||
// Creating a relationship
|
||||
createDelta(creator, host)
|
||||
.relate(
|
||||
sourceId, // ID of the source entity
|
||||
targetId, // ID of the target entity
|
||||
'REL_TYPE', // Relationship type
|
||||
{ // Optional properties
|
||||
prop1: 'value1',
|
||||
prop2: 'value2'
|
||||
}
|
||||
)
|
||||
.build();
|
||||
```
|
||||
Future work may allow for more complex relationships.
|
||||
|
||||
### Data Structures
|
||||
## Goal
|
||||
|
||||
#### `LosslessEntity` Updates
|
||||
```typescript
|
||||
class LosslessEntity {
|
||||
// Existing properties
|
||||
properties = new Map<PropertyID, Set<Delta>>();
|
||||
Resolve a graph of relationships among entities.
|
||||
|
||||
// Track relationships where this entity is the source
|
||||
outboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
|
||||
## Discussion of Strategy
|
||||
|
||||
// Track relationships where this entity is the target
|
||||
inboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
|
||||
### Lossy View Composition
|
||||
|
||||
// ... rest of the class
|
||||
}
|
||||
```
|
||||
|
||||
#### `LosslessViewOne` Extension
|
||||
```typescript
|
||||
type RelationshipView = {
|
||||
id: string; // Relationship ID
|
||||
type: string; // Relationship type
|
||||
direction: 'inbound' | 'outbound';
|
||||
target: string; // Target entity ID
|
||||
properties: Record<string, any>; // Relationship properties
|
||||
};
|
||||
|
||||
type LosslessViewOne = {
|
||||
id: DomainEntityID;
|
||||
// ... existing fields ...
|
||||
relationships?: {
|
||||
outbound: RelationshipView[];
|
||||
inbound: RelationshipView[];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Core Data Structures
|
||||
1. [x] Update `DeltaBuilder.relate()` to create relationship entities
|
||||
2. [ ] Update `LosslessEntity` to track relationship IDs
|
||||
3. [ ] Extend `LosslessViewOne` type to include relationships
|
||||
|
||||
### Phase 2: Relationship Management
|
||||
1. [ ] Implement relationship tracking in `Lossless` class
|
||||
- Track all relationships by ID
|
||||
- Maintain source/target indexes
|
||||
2. [ ] Implement methods for querying relationships
|
||||
- Get relationships for an entity
|
||||
- Filter by type and direction
|
||||
- Support pagination
|
||||
|
||||
### Phase 3: Delta Processing
|
||||
1. [ ] Update `ingestDelta` to handle relationship deltas
|
||||
- Extract relationship information from deltas
|
||||
- Update relationship indexes
|
||||
- Handle relationship updates and deletions
|
||||
2. [ ] Add conflict resolution for concurrent relationship updates
|
||||
|
||||
### Phase 4: View Generation
|
||||
1. [ ] Update `view` method to include relationships
|
||||
- Option to include/exclude relationships
|
||||
- Support for filtering relationships
|
||||
- Handle circular references
|
||||
|
||||
### Phase 5: Performance Optimization
|
||||
1. [ ] Add indexing for relationship lookups
|
||||
2. [ ] Implement lazy loading for large relationship sets
|
||||
3. [ ] Add caching for frequently accessed relationships
|
||||
|
||||
## API Extensions
|
||||
|
||||
### Get Entity with Relationships
|
||||
```typescript
|
||||
// Get an entity with its relationships
|
||||
GET /entities/{id}?include=relationships
|
||||
|
||||
// Response
|
||||
{
|
||||
"id": "entity1",
|
||||
"properties": { /* ... */ },
|
||||
"relationships": {
|
||||
"outbound": [
|
||||
{
|
||||
"id": "rel-123",
|
||||
"type": "OWNS",
|
||||
"target": "entity2",
|
||||
"direction": "outbound",
|
||||
"properties": {
|
||||
"since": "2023-01-01"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbound": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query Relationships
|
||||
```typescript
|
||||
// Get relationships for an entity
|
||||
GET /entities/{id}/relationships?type=OWNS&direction=outbound
|
||||
|
||||
// Response
|
||||
{
|
||||
"relationships": [
|
||||
{
|
||||
"id": "rel-123",
|
||||
"type": "OWNS",
|
||||
"source": "entity1",
|
||||
"target": "entity2",
|
||||
"properties": {
|
||||
"since": "2023-01-01"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create Relationship
|
||||
```typescript
|
||||
// Create a new relationship
|
||||
POST /relationships
|
||||
{
|
||||
"source": "entity1",
|
||||
"target": "entity2",
|
||||
"type": "OWNS",
|
||||
"properties": {
|
||||
"since": "2023-01-01"
|
||||
}
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
"id": "rel-123",
|
||||
"source": "entity1",
|
||||
"target": "entity2",
|
||||
"type": "OWNS",
|
||||
"properties": {
|
||||
"since": "2023-01-01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Memory Usage**:
|
||||
- Store only relationship IDs in entity maps
|
||||
- Use lazy loading for relationship properties
|
||||
- Consider weak references if memory becomes an issue
|
||||
|
||||
2. **Query Performance**:
|
||||
- Add indexes for common relationship queries
|
||||
- Cache frequently accessed relationships
|
||||
- Support pagination for large relationship sets
|
||||
|
||||
3. **Delta Processing**:
|
||||
- Batch process relationship updates
|
||||
- Optimize delta application for relationship-heavy workloads
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Advanced Querying**:
|
||||
- GraphQL support for complex relationship queries
|
||||
- Support for recursive relationship traversal
|
||||
|
||||
2. **Schema Validation**:
|
||||
- Define relationship schemas with property validation
|
||||
- Support for required/optional properties
|
||||
- Default values for relationship properties
|
||||
|
||||
3. **Indexing**:
|
||||
- Add support for indexing relationship properties
|
||||
- Implement efficient querying of relationships by property values
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit Tests**:
|
||||
- Test relationship creation and deletion
|
||||
- Verify relationship queries with various filters
|
||||
- Test delta processing for relationships
|
||||
|
||||
2. **Integration Tests**:
|
||||
- Test relationship persistence across restarts
|
||||
- Verify concurrent relationship updates
|
||||
- Test with large numbers of relationships
|
||||
|
||||
3. **Performance Tests**:
|
||||
- Measure memory usage with large relationship graphs
|
||||
- Test query performance with complex relationship patterns
|
||||
- Benchmark delta processing speed for relationship operations
|
||||
|
191
plans/fluent-view-composition.md
Normal file
191
plans/fluent-view-composition.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Fluent API for View Composition
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines a fluent API for declaring and composing lossy views in a declarative and type-safe manner. The API is designed to make it easy to create complex view compositions with minimal boilerplate.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
1. **View Composition**: Combining multiple resolvers to create a unified view
|
||||
2. **Fluent Builder Pattern**: Method chaining for declarative configuration
|
||||
3. **Type Safety**: Leveraging TypeScript's type system for better developer experience
|
||||
4. **Extensibility**: Easy to add new composition patterns and resolvers
|
||||
|
||||
## API Design
|
||||
|
||||
### 1. Base Builder
|
||||
|
||||
```typescript
|
||||
import { Lossless } from '../lossless';
|
||||
|
||||
type ViewTransformer = (view: LosslessViewOne) => LosslessViewOne;
|
||||
type ResultTransformer<T, R> = (result: T) => R;
|
||||
|
||||
class ViewCompositionBuilder {
|
||||
private resolvers: Array<{
|
||||
key: string;
|
||||
resolver: Lossy<any, any>;
|
||||
viewTransformer?: ViewTransformer;
|
||||
resultTransformer?: (result: any) => any;
|
||||
}> = [];
|
||||
|
||||
constructor(private readonly lossless: Lossless) {}
|
||||
|
||||
// Start building a new composition
|
||||
static create(lossless: Lossless): ViewCompositionBuilder {
|
||||
return new ViewCompositionBuilder(lossless);
|
||||
}
|
||||
|
||||
// Add a resolver to the composition
|
||||
withResolver<T, R>(
|
||||
key: string,
|
||||
resolver: Lossy<T, R>,
|
||||
options: {
|
||||
view?: ViewTransformer;
|
||||
result?: ResultTransformer<R, any>;
|
||||
} = {}
|
||||
): this {
|
||||
this.resolvers.push({
|
||||
key,
|
||||
resolver,
|
||||
viewTransformer: options.view,
|
||||
resultTransformer: options.result,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// Build the final composition
|
||||
build<T extends Record<string, any>>(): Lossy<Record<string, any>, T> {
|
||||
// Implementation that creates a combined resolver
|
||||
return new CombinedResolver(this.lossless, this.resolvers);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Example Usage: Relationship Graph
|
||||
|
||||
```typescript
|
||||
const relationshipGraph = ViewCompositionBuilder
|
||||
.create(lossless)
|
||||
.withResolver('entities', new EntityResolver(), {
|
||||
view: view => ({
|
||||
...view,
|
||||
propertyDeltas: Object.fromEntries(
|
||||
Object.entries(view.propertyDeltas).filter(([k]) => !k.startsWith('_rel_'))
|
||||
)
|
||||
})
|
||||
})
|
||||
.withResolver('relationships', new RelationshipResolver(), {
|
||||
view: view => ({
|
||||
...view,
|
||||
propertyDeltas: Object.fromEntries(
|
||||
Object.entries(view.propertyDeltas).filter(([k]) => k.startsWith('_rel_'))
|
||||
)
|
||||
})
|
||||
})
|
||||
.withResolver('stats', new StatsCollector())
|
||||
.withResolver('metadata', new MetadataResolver(), {
|
||||
result: (metadata) => ({
|
||||
...metadata,
|
||||
generatedAt: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
.build<{
|
||||
entities: EntityMap;
|
||||
relationships: RelationshipMap;
|
||||
stats: Stats;
|
||||
metadata: Metadata;
|
||||
}>();
|
||||
```
|
||||
|
||||
### 3. Advanced Composition
|
||||
|
||||
```typescript
|
||||
// Nested composition
|
||||
const userProfile = ViewCompositionBuilder
|
||||
.create(lossless)
|
||||
.withResolver('basicInfo', new BasicInfoResolver())
|
||||
.withResolver('activity', ViewCompositionBuilder
|
||||
.create(lossless)
|
||||
.withResolver('recentPosts', new RecentPostsResolver())
|
||||
.withResolver('notifications', new NotificationsResolver())
|
||||
.build()
|
||||
)
|
||||
.withResolver('recommendations', new RecommendationsResolver())
|
||||
.build<{
|
||||
basicInfo: UserBasicInfo;
|
||||
activity: {
|
||||
recentPosts: Post[];
|
||||
notifications: Notification[];
|
||||
};
|
||||
recommendations: Recommendation[];
|
||||
}>();
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### CombinedResolver Implementation
|
||||
|
||||
```typescript
|
||||
class CombinedResolver<State extends Record<string, any>, Result>
|
||||
extends Lossy<State, Result> {
|
||||
|
||||
constructor(
|
||||
private readonly lossless: Lossless,
|
||||
private readonly resolvers: Array<{
|
||||
key: string;
|
||||
resolver: Lossy<any, any>;
|
||||
viewTransformer?: ViewTransformer;
|
||||
resultTransformer?: (result: any) => any;
|
||||
}>
|
||||
) {
|
||||
super(lossless);
|
||||
}
|
||||
|
||||
initializer(view: LosslessViewOne): State {
|
||||
return this.resolvers.reduce((state, { key, resolver, viewTransformer }) => {
|
||||
const transformedView = viewTransformer ? viewTransformer(view) : view;
|
||||
return {
|
||||
...state,
|
||||
[key]: resolver.initializer(transformedView)
|
||||
};
|
||||
}, {} as State);
|
||||
}
|
||||
|
||||
reducer(state: State, view: LosslessViewOne): State {
|
||||
return this.resolvers.reduce((newState, { key, resolver, viewTransformer }) => {
|
||||
const transformedView = viewTransformer ? viewTransformer(view) : view;
|
||||
return {
|
||||
...newState,
|
||||
[key]: resolver.reducer(state[key], transformedView)
|
||||
};
|
||||
}, { ...state });
|
||||
}
|
||||
|
||||
resolver(state: State): Result {
|
||||
return this.resolvers.reduce((result, { key, resolver, resultTransformer }) => {
|
||||
const resolved = resolver.resolver(state[key]);
|
||||
return {
|
||||
...result,
|
||||
[key]: resultTransformer ? resultTransformer(resolved) : resolved
|
||||
};
|
||||
}, {} as Result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Readability**: Clear, declarative syntax
|
||||
2. **Type Safety**: Full TypeScript support with proper type inference
|
||||
3. **Composability**: Easy to combine and nest resolvers
|
||||
4. **Maintainability**: Isolated concerns and transformations
|
||||
5. **Flexibility**: Custom view and result transformations
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement the base `ViewCompositionBuilder` and `CombinedResolver`
|
||||
2. Add support for common patterns (filtering, mapping, etc.)
|
||||
3. Create documentation with examples
|
||||
4. Refactor existing resolvers to use the new composition API
|
||||
5. Add performance optimizations (memoization, lazy evaluation)
|
182
plans/view-composition.md
Normal file
182
plans/view-composition.md
Normal file
@ -0,0 +1,182 @@
|
||||
# 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:
|
||||
1. Creating a parent resolver that manages multiple child resolvers
|
||||
2. Each child resolver maintains its own state
|
||||
3. The parent coordinates the initialization, reduction, and resolution of child states
|
||||
4. The final result combines the outputs of all child resolvers
|
||||
|
||||
## Implementation Pattern
|
||||
|
||||
### 1. Base Interfaces
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
1. **Separation of Concerns**: Each resolver handles a specific aspect of the data
|
||||
2. **Reusability**: Resolvers can be reused in different combinations
|
||||
3. **Maintainability**: Changes to one resolver don't affect others
|
||||
4. **Testability**: Each resolver can be tested in isolation
|
||||
5. **Flexibility**: Easy to add, remove, or modify resolvers
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Memory Usage**: Combined state increases memory usage
|
||||
2. **Processing Overhead**: Each delta is processed by all resolvers
|
||||
3. **Optimization**: Use view transformers to filter deltas early
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement the base `CombinedResolver` class
|
||||
2. Refactor `RelationshipGraphResolver` to use this pattern
|
||||
3. Add tests for the composition behavior
|
||||
4. Document common patterns and best practices
|
||||
5. Consider adding middleware support for cross-cutting concerns
|
@ -1,6 +1,6 @@
|
||||
import express, {Router} from "express";
|
||||
import {RhizomeNode} from "../node";
|
||||
import {htmlDocFromMarkdown, MDFiles} from "../util/md-files";
|
||||
import {htmlDocFromMarkdown, MDFiles} from "../utils/md-files";
|
||||
|
||||
export class HttpHtml {
|
||||
router = Router();
|
||||
|
@ -11,11 +11,30 @@ export interface ResolverPlugin<T = unknown> {
|
||||
initialize(): T;
|
||||
|
||||
// Process a new value for the property
|
||||
update(currentState: T, newValue: PropertyTypes, delta: CollapsedDelta): T;
|
||||
update(
|
||||
currentState: T,
|
||||
newValue: PropertyTypes,
|
||||
delta: CollapsedDelta,
|
||||
// Additional context including other properties' states
|
||||
context?: {
|
||||
// Current state of all properties for the entity
|
||||
entityState: Record<string, unknown>;
|
||||
// Current resolved values of all properties for the entity
|
||||
resolvedValues: Record<string, PropertyTypes>;
|
||||
}
|
||||
): T;
|
||||
|
||||
// Resolve the final value from the accumulated state
|
||||
// Returns undefined if no valid value could be resolved
|
||||
resolve(state: T): PropertyTypes | undefined;
|
||||
resolve(
|
||||
state: T,
|
||||
// Additional context including other properties' states
|
||||
context?: {
|
||||
// Current state of all properties for the entity
|
||||
entityState: Record<string, unknown>;
|
||||
// Current resolved values of all properties for the entity
|
||||
resolvedValues: Record<string, PropertyTypes>;
|
||||
}
|
||||
): PropertyTypes | undefined;
|
||||
}
|
||||
|
||||
// Configuration for custom resolver
|
||||
@ -75,11 +94,15 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
acc[cur.id] = { id: cur.id, properties: {} };
|
||||
}
|
||||
|
||||
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
||||
// First pass: collect all property states and resolved values
|
||||
const entityState: Record<string, unknown> = {};
|
||||
const resolvedValues: Record<string, PropertyTypes> = {};
|
||||
|
||||
// Initialize all properties first
|
||||
for (const propertyId of Object.keys(cur.propertyDeltas)) {
|
||||
const plugin = this.config[propertyId];
|
||||
if (!plugin) continue;
|
||||
|
||||
// Initialize property state if not exists
|
||||
if (!acc[cur.id].properties[propertyId]) {
|
||||
acc[cur.id].properties[propertyId] = {
|
||||
plugin,
|
||||
@ -87,13 +110,51 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
};
|
||||
}
|
||||
|
||||
// Store the current state
|
||||
entityState[propertyId] = acc[cur.id].properties[propertyId].state;
|
||||
|
||||
// Resolve current value if possible
|
||||
try {
|
||||
const resolved = plugin.resolve(acc[cur.id].properties[propertyId].state, {
|
||||
entityState: {},
|
||||
resolvedValues: {}
|
||||
});
|
||||
if (resolved !== undefined) {
|
||||
resolvedValues[propertyId] = resolved;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore resolution errors during reduction
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: process deltas with full context
|
||||
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
||||
const plugin = this.config[propertyId];
|
||||
if (!plugin) continue;
|
||||
|
||||
const propertyState = acc[cur.id].properties[propertyId];
|
||||
const context = { entityState, resolvedValues };
|
||||
|
||||
// Process all deltas for this property
|
||||
for (const delta of deltas || []) {
|
||||
const value = extractValueFromDelta(propertyId, delta);
|
||||
if (value !== undefined) {
|
||||
propertyState.state = propertyState.plugin.update(propertyState.state, value, delta);
|
||||
propertyState.state = plugin.update(
|
||||
propertyState.state,
|
||||
value,
|
||||
delta,
|
||||
context
|
||||
);
|
||||
|
||||
// Update the resolved value after each update
|
||||
try {
|
||||
const resolved = plugin.resolve(propertyState.state, context);
|
||||
if (resolved !== undefined) {
|
||||
resolvedValues[propertyId] = resolved;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore resolution errors during reduction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,11 +168,39 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
for (const [entityId, entity] of Object.entries(cur)) {
|
||||
const entityResult: { id: string; properties: EntityProperties } = { id: entityId, properties: {} };
|
||||
|
||||
// First pass: collect all property states
|
||||
const entityState: Record<string, unknown> = {};
|
||||
const resolvedValues: Record<string, PropertyTypes> = {};
|
||||
|
||||
// Initialize with current states and resolve all properties
|
||||
for (const [propertyId, propertyState] of Object.entries(entity.properties)) {
|
||||
const resolvedValue = propertyState.plugin.resolve(propertyState.state);
|
||||
// Only add the property if the resolved value is not undefined
|
||||
entityState[propertyId] = propertyState.state;
|
||||
// Initial resolution with empty context
|
||||
try {
|
||||
const resolved = propertyState.plugin.resolve(propertyState.state, {
|
||||
entityState: {},
|
||||
resolvedValues: {}
|
||||
});
|
||||
if (resolved !== undefined) {
|
||||
resolvedValues[propertyId] = resolved;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore resolution errors
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: resolve with full context
|
||||
for (const [propertyId, propertyState] of Object.entries(entity.properties)) {
|
||||
const context = { entityState, resolvedValues };
|
||||
try {
|
||||
const resolvedValue = propertyState.plugin.resolve(propertyState.state, context);
|
||||
if (resolvedValue !== undefined) {
|
||||
entityResult.properties[propertyId] = resolvedValue;
|
||||
// Update the resolved value for dependent properties
|
||||
resolvedValues[propertyId] = resolvedValue;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore resolution errors
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +226,12 @@ export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTyp
|
||||
return { timestamp: 0 };
|
||||
}
|
||||
|
||||
update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||
update(
|
||||
currentState: { value?: PropertyTypes, timestamp: number },
|
||||
newValue: PropertyTypes,
|
||||
delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
if (delta.timeCreated > currentState.timestamp) {
|
||||
return {
|
||||
value: newValue,
|
||||
@ -160,7 +254,12 @@ export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTy
|
||||
return { timestamp: Infinity };
|
||||
}
|
||||
|
||||
update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||
update(
|
||||
currentState: { value?: PropertyTypes, timestamp: number },
|
||||
newValue: PropertyTypes,
|
||||
delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
if (delta.timeCreated < currentState.timestamp) {
|
||||
return {
|
||||
value: newValue,
|
||||
@ -185,7 +284,12 @@ export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: st
|
||||
return { values: [] };
|
||||
}
|
||||
|
||||
update(currentState: { values: { value: string, timestamp: number }[] }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||
update(
|
||||
currentState: { values: { value: string, timestamp: number }[] },
|
||||
newValue: PropertyTypes,
|
||||
delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
if (typeof newValue === 'string') {
|
||||
// Check if this value already exists (avoid duplicates)
|
||||
const exists = currentState.values.some(v => v.value === newValue);
|
||||
@ -201,7 +305,10 @@ export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: st
|
||||
return currentState;
|
||||
}
|
||||
|
||||
resolve(state: { values: { value: string, timestamp: number }[] }): PropertyTypes {
|
||||
resolve(
|
||||
state: { values: { value: string, timestamp: number }[] },
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
): PropertyTypes {
|
||||
return state.values.map(v => v.value).join(this.separator);
|
||||
}
|
||||
}
|
||||
@ -214,24 +321,32 @@ export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyT
|
||||
return { votes: new Map() };
|
||||
}
|
||||
|
||||
update(currentState: { votes: Map<PropertyTypes, number> }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||
const currentCount = currentState.votes.get(newValue) || 0;
|
||||
currentState.votes.set(newValue, currentCount + 1);
|
||||
update(
|
||||
currentState: { votes: Map<PropertyTypes, number> },
|
||||
newValue: PropertyTypes,
|
||||
_delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
const count = (currentState.votes.get(newValue) || 0) + 1;
|
||||
currentState.votes.set(newValue, count);
|
||||
return currentState;
|
||||
}
|
||||
|
||||
resolve(state: { votes: Map<PropertyTypes, number> }): PropertyTypes {
|
||||
resolve(
|
||||
state: { votes: Map<PropertyTypes, number> },
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
): PropertyTypes {
|
||||
let maxVotes = 0;
|
||||
let winner: PropertyTypes = '';
|
||||
let result: PropertyTypes = '';
|
||||
|
||||
for (const [value, votes] of state.votes.entries()) {
|
||||
if (votes > maxVotes) {
|
||||
maxVotes = votes;
|
||||
winner = value;
|
||||
for (const [value, count] of state.votes.entries()) {
|
||||
if (count > maxVotes) {
|
||||
maxVotes = count;
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
|
||||
return winner;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,7 +358,12 @@ export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
||||
return {};
|
||||
}
|
||||
|
||||
update(currentState: { min?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||
update(
|
||||
currentState: { min?: number },
|
||||
newValue: PropertyTypes,
|
||||
_delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
if (typeof newValue === 'number') {
|
||||
if (currentState.min === undefined || newValue < currentState.min) {
|
||||
return { min: newValue };
|
||||
@ -252,7 +372,10 @@ export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
resolve(state: { min?: number }): PropertyTypes | undefined {
|
||||
resolve(
|
||||
state: { min?: number },
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
): PropertyTypes | undefined {
|
||||
return state.min;
|
||||
}
|
||||
}
|
||||
@ -264,7 +387,12 @@ export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
|
||||
return {};
|
||||
}
|
||||
|
||||
update(currentState: { max?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||
update(
|
||||
currentState: { max?: number },
|
||||
newValue: PropertyTypes,
|
||||
_delta: CollapsedDelta,
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
) {
|
||||
if (typeof newValue === 'number') {
|
||||
if (currentState.max === undefined || newValue > currentState.max) {
|
||||
return { max: newValue };
|
||||
@ -273,7 +401,10 @@ export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
resolve(state: { max?: number }): PropertyTypes | undefined {
|
||||
resolve(
|
||||
state: { max?: number },
|
||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
||||
): PropertyTypes | undefined {
|
||||
return state.max;
|
||||
}
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
import { CollapsedDelta, LosslessViewOne } from "../lossless";
|
||||
import { CollapsedDelta, Lossless, LosslessViewOne } from "../lossless";
|
||||
import { PropertyTypes } from "../../core/types";
|
||||
import { Lossy } from "../lossy";
|
||||
import Debug from 'debug';
|
||||
import { CustomResolver, LastWriteWinsPlugin } from "./custom-resolvers";
|
||||
|
||||
const debug = Debug('rz:resolver:relationship-graph');
|
||||
const trace = Debug('rz:resolver:relationship-graph:trace');
|
||||
trace.enabled = true; // Always enable trace for now
|
||||
|
||||
/**
|
||||
* Represents a single relationship between entities
|
||||
@ -51,31 +57,43 @@ export type RelationshipView = {
|
||||
* A resolver that builds a relationship graph from a LosslessViewMany
|
||||
*/
|
||||
export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulator, RelationshipGraphAccumulator> {
|
||||
private relData: CustomResolver;
|
||||
constructor(lossless: Lossless) {
|
||||
super(lossless);
|
||||
|
||||
this.relData = new CustomResolver(lossless, {
|
||||
relationships: new LastWriteWinsPlugin(),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Initialize a new accumulator
|
||||
*/
|
||||
protected createRelationshipGraphAccumulator(): RelationshipGraphAccumulator {
|
||||
return {
|
||||
debug('Creating new relationship graph accumulator');
|
||||
const accumulator = {
|
||||
entities: new Map(),
|
||||
relationships: new Map(),
|
||||
relationshipsByType: new Map(),
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
trace('Created accumulator:', accumulator);
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the accumulator with a view
|
||||
*/
|
||||
initializer(view: LosslessViewOne): RelationshipGraphAccumulator {
|
||||
debug('Initializing relationship graph for view:', view.id);
|
||||
const graph = this.createRelationshipGraphAccumulator();
|
||||
|
||||
// Initialize entity relationships if they don't exist
|
||||
if (!graph.entities.has(view.id)) {
|
||||
graph.entities.set(view.id, {
|
||||
outbound: new Map(),
|
||||
inbound: new Map()
|
||||
this.relData.initializer(view);
|
||||
|
||||
trace('Initialized graph state:', {
|
||||
entities: Array.from(graph.entities.keys()),
|
||||
relationships: Array.from(graph.relationships.keys()),
|
||||
relationshipTypes: Array.from(graph.relationshipsByType.keys())
|
||||
});
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
@ -84,8 +102,21 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
* Process a view and update the accumulator
|
||||
*/
|
||||
reducer(graph: RelationshipGraphAccumulator, view: LosslessViewOne): RelationshipGraphAccumulator {
|
||||
debug(`Processing view ${view.id} in reducer`);
|
||||
trace('View details:', {
|
||||
id: view.id,
|
||||
propertyCount: Object.keys(view.propertyDeltas).length,
|
||||
properties: Object.keys(view.propertyDeltas)
|
||||
});
|
||||
|
||||
// if (!isRelationshipEntity) {
|
||||
// trace(`Skipping non-relationship delta: ${view.id}`);
|
||||
// return graph;
|
||||
// }
|
||||
|
||||
// Ensure entity exists in the graph
|
||||
if (!graph.entities.has(view.id)) {
|
||||
trace(`Adding new entity in reducer: ${view.id}`);
|
||||
graph.entities.set(view.id, {
|
||||
outbound: new Map(),
|
||||
inbound: new Map()
|
||||
@ -94,8 +125,13 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
|
||||
// Process relationship properties
|
||||
for (const [property, deltas] of Object.entries(view.propertyDeltas)) {
|
||||
trace(`Processing property: ${property} with ${deltas.length} deltas`);
|
||||
|
||||
// Skip non-relationship properties
|
||||
if (!property.startsWith('_rel_')) continue;
|
||||
if (!property.startsWith('_rel_')) {
|
||||
trace(`Skipping non-relationship property: ${property}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const delta of deltas) {
|
||||
this.processRelationshipDelta(graph, delta);
|
||||
@ -111,6 +147,15 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
* For now, we just return the accumulator as is.
|
||||
*/
|
||||
resolver(graph: RelationshipGraphAccumulator): RelationshipGraphAccumulator {
|
||||
debug('Resolving relationship graph');
|
||||
trace('Graph state at resolution:', {
|
||||
entities: Array.from(graph.entities.keys()),
|
||||
relationships: Array.from(graph.relationships.keys()),
|
||||
relationshipTypes: Array.from(graph.relationshipsByType.entries()).map(([type, ids]) => ({
|
||||
type,
|
||||
count: ids.size
|
||||
}))
|
||||
});
|
||||
return graph;
|
||||
}
|
||||
|
||||
@ -118,17 +163,29 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
* Process a single relationship delta
|
||||
*/
|
||||
private processRelationshipDelta(graph: RelationshipGraphAccumulator, delta: CollapsedDelta): void {
|
||||
debug('Processing relationship delta:', delta.id);
|
||||
trace('Delta details:', delta);
|
||||
|
||||
// Extract relationship metadata from the delta
|
||||
const relProps = this.extractRelationshipProperties(delta);
|
||||
if (!relProps) return;
|
||||
if (!relProps) {
|
||||
debug('No relationship properties found in delta:', delta.id);
|
||||
return;
|
||||
}
|
||||
|
||||
trace('Extracted relationship properties:', relProps);
|
||||
|
||||
const { type, sourceId, targetId, relId, properties } = relProps;
|
||||
|
||||
debug(`Processing relationship ${relId} of type ${type} from ${sourceId} to ${targetId}`);
|
||||
|
||||
// Ensure source and target entities exist in the graph
|
||||
if (!graph.entities.has(sourceId)) {
|
||||
trace(`Adding source entity: ${sourceId}`);
|
||||
graph.entities.set(sourceId, { outbound: new Map(), inbound: new Map() });
|
||||
}
|
||||
if (!graph.entities.has(targetId)) {
|
||||
trace(`Adding target entity: ${targetId}`);
|
||||
graph.entities.set(targetId, { outbound: new Map(), inbound: new Map() });
|
||||
}
|
||||
|
||||
@ -136,6 +193,7 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
let relationship = graph.relationships.get(relId);
|
||||
|
||||
if (!relationship) {
|
||||
debug(`Creating new relationship: ${relId} (${type})`);
|
||||
// Create new relationship
|
||||
relationship = {
|
||||
id: relId,
|
||||
@ -148,12 +206,15 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
|
||||
// Add to relationships map
|
||||
graph.relationships.set(relId, relationship);
|
||||
trace(`Added relationship ${relId} to relationships map`);
|
||||
|
||||
// Add to relationships by type index
|
||||
if (!graph.relationshipsByType.has(type)) {
|
||||
trace(`Creating new relationship type index: ${type}`);
|
||||
graph.relationshipsByType.set(type, new Set());
|
||||
}
|
||||
graph.relationshipsByType.get(type)?.add(relId);
|
||||
trace(`Added relationship ${relId} to type index: ${type}`);
|
||||
|
||||
// Update entity relationships
|
||||
const sourceEntity = graph.entities.get(sourceId)!;
|
||||
@ -161,14 +222,22 @@ export class RelationshipGraphResolver extends Lossy<RelationshipGraphAccumulato
|
||||
|
||||
sourceEntity.outbound.set(relId, relationship);
|
||||
targetEntity.inbound.set(relId, relationship);
|
||||
|
||||
trace('Updated entity relationships:', {
|
||||
sourceOutbound: Array.from(sourceEntity.outbound.keys()),
|
||||
targetInbound: Array.from(targetEntity.inbound.keys())
|
||||
});
|
||||
} else {
|
||||
// Update existing relationship
|
||||
debug(`Updating existing relationship: ${relId}`);
|
||||
// TODO: Conflict resolution e.g. using TimestampResolver
|
||||
relationship.properties = { ...relationship.properties, ...properties };
|
||||
|
||||
// Track this delta if not already present
|
||||
if (!relationship.deltas.includes(delta.id)) {
|
||||
relationship.deltas.push(delta.id);
|
||||
trace(`Added delta ${delta.id} to relationship ${relId}`);
|
||||
} else {
|
||||
trace(`Delta ${delta.id} already tracked for relationship ${relId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user