import { RhizomeNode, Lossless, Delta, LastWriteWins, TimestampResolver, SumResolver, CustomResolver, LastWriteWinsPlugin, MajorityVotePlugin } from "../src"; describe('Concurrent Write Scenarios', () => { let node: RhizomeNode; let lossless: Lossless; beforeEach(() => { node = new RhizomeNode(); lossless = new Lossless(node); }); describe('Simultaneous Writes with Same Timestamp', () => { test('should handle simultaneous writes using last-write-wins resolver', () => { const timestamp = 1000; // Simulate two writers updating the same property at the exact same time lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', id: 'delta-a', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 100 }] })); lossless.ingestDelta(new Delta({ creator: 'writer2', host: 'host2', id: 'delta-b', timeCreated: timestamp, // Same timestamp pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 200 }] })); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); expect(result).toBeDefined(); // Should resolve deterministically (likely based on delta processing order) expect(typeof result!['entity1'].properties.score).toBe('number'); expect([100, 200]).toContain(result!['entity1'].properties.score); }); test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => { const timestamp = 1000; lossless.ingestDelta(new Delta({ creator: 'writer_z', // Lexicographically later host: 'host1', id: 'delta-a', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 100 }] })); lossless.ingestDelta(new Delta({ creator: 'writer_a', // Lexicographically earlier host: 'host2', id: 'delta-b', timeCreated: timestamp, // Same timestamp pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 200 }] })); const resolver = new TimestampResolver(lossless, 'creator-id'); const result = resolver.resolve(); expect(result).toBeDefined(); // writer_z should win due to lexicographic ordering expect(result!['entity1'].properties.score).toBe(100); }); test('should handle multiple writers with aggregation resolver', () => { const timestamp = 1000; // Multiple writers add values simultaneously lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "points" }, { localContext: "points", target: 10 }] })); lossless.ingestDelta(new Delta({ creator: 'writer2', host: 'host2', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "points" }, { localContext: "points", target: 20 }] })); lossless.ingestDelta(new Delta({ creator: 'writer3', host: 'host3', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "points" }, { localContext: "points", target: 30 }] })); const resolver = new SumResolver(lossless, ['points']); const result = resolver.resolve(); expect(result).toBeDefined(); // All values should be summed regardless of timing expect(result!['entity1'].properties.points).toBe(60); // 10 + 20 + 30 }); }); describe('Out-of-Order Write Arrival', () => { test('should handle writes arriving out of chronological order', () => { // Newer delta arrives first lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: 2000, // Later timestamp pointers: [{ localContext: "collection", target: "entity1", targetContext: "value" }, { localContext: "value", target: 'newer' }] })); // Older delta arrives later lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: 1000, // Earlier timestamp pointers: [{ localContext: "collection", target: "entity1", targetContext: "value" }, { localContext: "value", target: 'older' }] })); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); expect(result).toBeDefined(); // Should still resolve to the chronologically newer value expect(result!['entity1'].properties.value).toBe('newer'); }); test('should maintain correct aggregation despite out-of-order arrival', () => { // Add deltas in reverse chronological order lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: 3000, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 30 }] })); lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: 1000, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 10 }] })); lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: 2000, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 20 }] })); const resolver = new SumResolver(lossless, ['score']); const result = resolver.resolve(); expect(result).toBeDefined(); // Sum should be correct regardless of arrival order expect(result!['entity1'].properties.score).toBe(60); // 10 + 20 + 30 }); }); describe('High-Frequency Concurrent Updates', () => { test('should handle rapid concurrent updates to the same entity', () => { const baseTimestamp = 1000; const numWriters = 10; const writesPerWriter = 5; // Simulate multiple writers making rapid updates for (let writer = 0; writer < numWriters; writer++) { for (let write = 0; write < writesPerWriter; write++) { lossless.ingestDelta(new Delta({ creator: `writer${writer}`, host: `host${writer}`, timeCreated: baseTimestamp + write, // Small time increments pointers: [{ localContext: "collection", target: "entity1", targetContext: "counter" }, { localContext: "counter", target: 1 // Each update adds 1 }] })); } } const resolver = new SumResolver(lossless, ['counter']); const result = resolver.resolve(); expect(result).toBeDefined(); // Should count all updates expect(result!['entity1'].properties.counter).toBe(numWriters * writesPerWriter); }); test('should handle concurrent updates to multiple properties', () => { const timestamp = 1000; // Writer 1 updates name and score lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "name" }, { localContext: "name", target: 'alice' }] })); lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: timestamp + 1, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 100 }] })); // Writer 2 updates name and score concurrently lossless.ingestDelta(new Delta({ creator: 'writer2', host: 'host2', timeCreated: timestamp + 2, pointers: [{ localContext: "collection", target: "entity1", targetContext: "name" }, { localContext: "name", target: 'bob' }] })); lossless.ingestDelta(new Delta({ creator: 'writer2', host: 'host2', timeCreated: timestamp + 3, pointers: [{ localContext: "collection", target: "entity1", targetContext: "score" }, { localContext: "score", target: 200 }] })); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin(), score: new LastWriteWinsPlugin() }); const result = resolver.resolve(); expect(result).toBeDefined(); expect(result!['entity1'].properties.name).toBe('bob'); // Later timestamp expect(result!['entity1'].properties.score).toBe(200); // Later timestamp }); }); describe('Cross-Entity Concurrent Writes', () => { test('should handle concurrent writes to different entities', () => { const timestamp = 1000; // Multiple writers updating different entities simultaneously for (let i = 0; i < 5; i++) { lossless.ingestDelta(new Delta({ creator: `writer${i}`, host: `host${i}`, timeCreated: timestamp, pointers: [{ localContext: "collection", target: `entity${i}`, targetContext: "value" }, { localContext: "value", target: (i + 1) * 10 // Start from 10 to avoid 0 values }] })); } const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); expect(result).toBeDefined(); expect(Object.keys(result!)).toHaveLength(5); for (let i = 0; i < 5; i++) { expect(result![`entity${i}`].properties.value).toBe((i + 1) * 10); } }); test('should handle mixed entity and property conflicts', () => { const timestamp = 1000; // Entity1: Multiple writers competing for same property lossless.ingestDelta(new Delta({ creator: 'writer1', host: 'host1', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "votes" }, { localContext: "votes", target: 'option_a' }] })); lossless.ingestDelta(new Delta({ creator: 'writer2', host: 'host2', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "votes" }, { localContext: "votes", target: 'option_a' }] })); lossless.ingestDelta(new Delta({ creator: 'writer3', host: 'host3', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity1", targetContext: "votes" }, { localContext: "votes", target: 'option_b' }] })); // Entity2: Single writer, no conflict lossless.ingestDelta(new Delta({ creator: 'writer4', host: 'host4', timeCreated: timestamp, pointers: [{ localContext: "collection", target: "entity2", targetContext: "status" }, { localContext: "status", target: 'active' }] })); const resolver = new CustomResolver(lossless, { votes: new MajorityVotePlugin(), status: new LastWriteWinsPlugin() }); const result = resolver.resolve(); expect(result).toBeDefined(); expect(result!['entity1'].properties.votes).toBe('option_a'); // 2 votes vs 1 expect(result!['entity2'].properties.status).toBe('active'); }); }); describe('Stress Testing', () => { test('should handle large number of concurrent writes efficiently', () => { const numEntities = 100; const numWritersPerEntity = 10; const baseTimestamp = 1000; // Generate a large number of concurrent writes for (let entity = 0; entity < numEntities; entity++) { for (let writer = 0; writer < numWritersPerEntity; writer++) { lossless.ingestDelta(new Delta({ creator: `writer${writer}`, host: `host${writer}`, timeCreated: baseTimestamp + Math.floor(Math.random() * 1000), // Random timestamps pointers: [{ localContext: "collection", target: `entity${entity}`, targetContext: "score" }, { localContext: "score", target: Math.floor(Math.random() * 100) // Random scores }] })); } } const resolver = new SumResolver(lossless, ['score']); const result = resolver.resolve(); expect(result).toBeDefined(); expect(Object.keys(result!)).toHaveLength(numEntities); // Each entity should have a score (sum of all writer contributions) for (let entity = 0; entity < numEntities; entity++) { expect(result![`entity${entity}`]).toBeDefined(); expect(typeof result![`entity${entity}`].properties.score).toBe('number'); expect(result![`entity${entity}`].properties.score).toBeGreaterThan(0); } }); test('should maintain consistency under rapid updates and resolution calls', () => { const entityId = 'stress-test-entity'; let updateCount = 0; // Add initial deltas for (let i = 0; i < 50; i++) { lossless.ingestDelta(new Delta({ creator: `writer${i % 5}`, host: `host${i % 3}`, timeCreated: 1000 + i, pointers: [{ localContext: "collection", target: entityId, targetContext: "counter" }, { localContext: "counter", target: 1 }] })); updateCount++; } // Verify initial state let resolver = new SumResolver(lossless, ['counter']); let result = resolver.resolve(); expect(result).toBeDefined(); expect(result![entityId].properties.counter).toBe(updateCount); // Add more deltas and verify consistency for (let i = 0; i < 25; i++) { lossless.ingestDelta(new Delta({ creator: 'late-writer', host: 'late-host', timeCreated: 2000 + i, pointers: [{ localContext: "collection", target: entityId, targetContext: "counter" }, { localContext: "counter", target: 2 }] })); updateCount += 2; // Create a fresh resolver to avoid accumulator caching issues resolver = new SumResolver(lossless, ['counter']); result = resolver.resolve(); expect(result![entityId].properties.counter).toBe(updateCount); } }); }); });