- Implement RelationshipGraphResolver extending Lossy - Add type-safe accumulator for relationship graph state - Update tests and documentation
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
import {
|
|
RhizomeNode,
|
|
Lossless,
|
|
TimestampResolver,
|
|
CreatorIdTimestampResolver,
|
|
DeltaIdTimestampResolver,
|
|
HostIdTimestampResolver,
|
|
LexicographicTimestampResolver
|
|
} from "../../../../src";
|
|
import { createDelta } from "../../../../src/core/delta-builder";
|
|
|
|
describe('Timestamp Resolvers', () => {
|
|
let node: RhizomeNode;
|
|
let lossless: Lossless;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
lossless = new Lossless(node);
|
|
});
|
|
|
|
describe('Basic Timestamp Resolution', () => {
|
|
test('should resolve by most recent timestamp', () => {
|
|
// Add older delta
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 10)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add newer delta
|
|
lossless.ingestDelta(createDelta('user2', 'host2')
|
|
.withId('delta2')
|
|
.withTimestamp(2000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 20)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new TimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.score).toBe(20); // More recent value wins
|
|
});
|
|
|
|
test('should handle multiple entities with different timestamps', () => {
|
|
// Entity1 - older value
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'value')
|
|
.addPointer('value', 100)
|
|
.buildV1()
|
|
);
|
|
|
|
// Entity2 - newer value
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withTimestamp(2000)
|
|
.addPointer('collection', 'entity2', 'value')
|
|
.addPointer('value', 200)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new TimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(100);
|
|
expect(result!['entity2'].properties.value).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Tie-Breaking Strategies', () => {
|
|
test('should break ties using creator-id strategy', () => {
|
|
// Two deltas with same timestamp, different creators
|
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 10)
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
|
.withId('delta2')
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 20)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new CreatorIdTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// user_z comes later lexicographically, so should win
|
|
expect(result!['entity1'].properties.score).toBe(10);
|
|
});
|
|
|
|
test('should break ties using delta-id strategy', () => {
|
|
// Two deltas with same timestamp, different delta IDs
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta_a') // Lexicographically earlier
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 10)
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta_z') // Lexicographically later
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 20)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new DeltaIdTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// delta_z comes later lexicographically, so should win
|
|
expect(result!['entity1'].properties.score).toBe(20);
|
|
});
|
|
|
|
test('should break ties using host-id strategy', () => {
|
|
// Two deltas with same timestamp, different hosts
|
|
lossless.ingestDelta(createDelta('user1', 'host_z') // Lexicographically later
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 10)
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user1', 'host_a') // Lexicographically earlier
|
|
.withId('delta2')
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 20)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new HostIdTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// host_z comes later lexicographically, so should win
|
|
expect(result!['entity1'].properties.score).toBe(10);
|
|
});
|
|
|
|
test('should break ties using lexicographic strategy with string values', () => {
|
|
// Two deltas with same timestamp, different string values
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'name')
|
|
.addPointer('name', 'alice')
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta2')
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'name')
|
|
.addPointer('name', 'bob')
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new LexicographicTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// 'bob' comes later lexicographically than 'alice', so should win
|
|
expect(result!['entity1'].properties.name).toBe('bob');
|
|
});
|
|
|
|
test('should break ties using lexicographic strategy with numeric values (falls back to delta ID)', () => {
|
|
// Two deltas with same timestamp, numeric values (should fall back to delta ID comparison)
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta_a') // Lexicographically earlier
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 100)
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta_z') // Lexicographically later
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 200)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new LexicographicTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// Should fall back to delta ID comparison: delta_z > delta_a
|
|
expect(result!['entity1'].properties.score).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Complex Tie-Breaking Scenarios', () => {
|
|
test('should handle multiple properties with different tie-breaking outcomes', () => {
|
|
// Add deltas for multiple properties with same timestamp
|
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
|
.withId('delta_z')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'name')
|
|
.addPointer('name', 'alice')
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
|
.withId('delta_a')
|
|
.withTimestamp(1000) // Same timestamp
|
|
.addPointer('collection', 'entity1', 'name')
|
|
.addPointer('name', 'bob')
|
|
.buildV1()
|
|
);
|
|
|
|
const creatorResolver = new CreatorIdTimestampResolver(lossless);
|
|
const deltaResolver = new DeltaIdTimestampResolver(lossless);
|
|
|
|
const creatorResult = creatorResolver.resolve();
|
|
const deltaResult = deltaResolver.resolve();
|
|
|
|
expect(creatorResult).toBeDefined();
|
|
expect(deltaResult).toBeDefined();
|
|
|
|
// Creator strategy: user_z > user_a, so 'bob' wins
|
|
expect(creatorResult!['entity1'].properties.name).toBe('bob');
|
|
|
|
// Delta ID strategy: delta_z > delta_a, so 'alice' wins
|
|
expect(deltaResult!['entity1'].properties.name).toBe('alice');
|
|
});
|
|
|
|
test('should work consistently with timestamp priority over tie-breaking', () => {
|
|
// Add older delta with "better" tie-breaking attributes
|
|
lossless.ingestDelta(createDelta('user_z', 'host1')
|
|
.withId('delta_z') // Would win in delta ID tie-breaking
|
|
.withTimestamp(1000) // Older timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 10)
|
|
.buildV1()
|
|
);
|
|
|
|
// Add newer delta with "worse" tie-breaking attributes
|
|
lossless.ingestDelta(createDelta('user_a', 'host1')
|
|
.withId('delta_a') // Would lose in delta ID tie-breaking
|
|
.withTimestamp(2000) // Newer timestamp
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 20)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new CreatorIdTimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
// Timestamp should take priority over tie-breaking, so newer value (20) wins
|
|
expect(result!['entity1'].properties.score).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle single delta correctly', () => {
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'value')
|
|
.addPointer('value', 42)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new TimestampResolver(lossless, 'creator-id');
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.value).toBe(42);
|
|
});
|
|
|
|
test('should handle mixed value types correctly', () => {
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta1')
|
|
.withTimestamp(1000)
|
|
.addPointer('collection', 'entity1', 'name')
|
|
.addPointer('name', 'test')
|
|
.buildV1()
|
|
);
|
|
|
|
lossless.ingestDelta(createDelta('user1', 'host1')
|
|
.withId('delta2')
|
|
.withTimestamp(1001)
|
|
.addPointer('collection', 'entity1', 'score')
|
|
.addPointer('score', 100)
|
|
.buildV1()
|
|
);
|
|
|
|
const resolver = new TimestampResolver(lossless);
|
|
const result = resolver.resolve();
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!['entity1'].properties.name).toBe('test');
|
|
expect(result!['entity1'].properties.score).toBe(100);
|
|
});
|
|
});
|
|
}); |