Maintinance to standardize some code structure and conventions #5
@ -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>>();
|
||||
|
||||
// Track relationships where this entity is the source
|
||||
outboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
|
||||
|
||||
// Track relationships where this entity is the target
|
||||
inboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
|
||||
|
||||
// ... rest of the class
|
||||
}
|
||||
```
|
||||
Resolve a graph of relationships among entities.
|
||||
|
||||
#### `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
|
||||
};
|
||||
## Discussion of Strategy
|
||||
|
||||
type LosslessViewOne = {
|
||||
id: DomainEntityID;
|
||||
// ... existing fields ...
|
||||
relationships?: {
|
||||
outbound: RelationshipView[];
|
||||
inbound: RelationshipView[];
|
||||
};
|
||||
};
|
||||
```
|
||||
### Lossy View Composition
|
||||
|
||||
## 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,25 +94,67 @@ 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,
|
||||
state: plugin.initialize()
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,12 +167,40 @@ 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
|
||||
if (resolvedValue !== undefined) {
|
||||
entityResult.properties[propertyId] = resolvedValue;
|
||||
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 = '';
|
||||
|
||||
for (const [value, votes] of state.votes.entries()) {
|
||||
if (votes > maxVotes) {
|
||||
maxVotes = votes;
|
||||
winner = value;
|
||||
let result: PropertyTypes = '';
|
||||
|
||||
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();
|
||||
|
||||
this.relData.initializer(view);
|
||||
|
||||
// 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()
|
||||
});
|
||||
}
|
||||
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