diff --git a/.windsurf/workflows/delta-builder.md b/.windsurf/workflows/delta-builder.md index 64cc47c..deca0b2 100644 --- a/.windsurf/workflows/delta-builder.md +++ b/.windsurf/workflows/delta-builder.md @@ -2,5 +2,6 @@ description: Update the current file to use delta builder --- -Replace each deltav2 instantiation with a fluent call to createDelta from delta builder, using the following process: - - pass creator and host as arguments to createDelta \ No newline at end of file +Replace each delta instantiation with a fluent call to createDelta from delta builder + - pass creator and host as arguments to createDelta + - use setProperty where appropriate \ No newline at end of file diff --git a/__tests__/aggregation-resolvers.ts b/__tests__/aggregation-resolvers.ts index 2038de2..46bff08 100644 --- a/__tests__/aggregation-resolvers.ts +++ b/__tests__/aggregation-resolvers.ts @@ -1,7 +1,6 @@ import { RhizomeNode, Lossless, - Delta, AggregationResolver, MinResolver, MaxResolver, @@ -10,6 +9,8 @@ import { CountResolver, AggregationType } from "../src"; +import { createDelta } from "../src/core/delta-builder"; +import { Delta } from "../src/core/delta"; describe('Aggregation Resolvers', () => { let node: RhizomeNode; @@ -23,46 +24,22 @@ describe('Aggregation Resolvers', () => { describe('Basic Aggregation', () => { test('should aggregate numbers using min resolver', () => { // Add first entity with score 10 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 10, 'collection') + .buildV1() + ); // Add second entity with score 5 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "score" - }, { - localContext: "score", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'score', 5, 'collection') + .buildV1() + ); // Add third entity with score 15 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity3", - targetContext: "score" - }, { - localContext: "score", - target: 15 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity3', 'score', 15, 'collection') + .buildV1() + ); const minResolver = new MinResolver(lossless, ['score']); const result = minResolver.resolve(); @@ -76,44 +53,20 @@ describe('Aggregation Resolvers', () => { test('should aggregate numbers using max resolver', () => { // Add deltas for entities - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "score" - }, { - localContext: "score", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'score', 5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity3", - targetContext: "score" - }, { - localContext: "score", - target: 15 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity3', 'score', 15, 'collection') + .buildV1() + ); const maxResolver = new MaxResolver(lossless, ['score']); const result = maxResolver.resolve(); @@ -126,46 +79,22 @@ describe('Aggregation Resolvers', () => { test('should aggregate numbers using sum resolver', () => { // Add first value for entity1 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); // Add second value for entity1 (should sum) - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 20, 'collection') + .buildV1() + ); // Add value for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "value" - }, { - localContext: "value", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'value', 5, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['value']); const result = sumResolver.resolve(); @@ -176,46 +105,22 @@ describe('Aggregation Resolvers', () => { }); test('should aggregate numbers using average resolver', () => { - // Add multiple values for entity1 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + // Add multiple scores for entity1 + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 20, 'collection') + .buildV1() + ); // Single value for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "score" - }, { - localContext: "score", - target: 30 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'score', 30, 'collection') + .buildV1() + ); const avgResolver = new AverageResolver(lossless, ['score']); const result = avgResolver.resolve(); @@ -227,45 +132,21 @@ describe('Aggregation Resolvers', () => { test('should count values using count resolver', () => { // Add multiple visit deltas for entity1 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "visits" - }, { - localContext: "visits", - target: 1 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'visits', 1, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "visits" - }, { - localContext: "visits", - target: 1 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'visits', 1, 'collection') + .buildV1() + ); // Single visit for entity2 - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "visits" - }, { - localContext: "visits", - target: 1 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity2', 'visits', 1, 'collection') + .buildV1() + ); const countResolver = new CountResolver(lossless, ['visits']); const result = countResolver.resolve(); @@ -279,84 +160,36 @@ describe('Aggregation Resolvers', () => { describe('Custom Aggregation Configuration', () => { test('should handle mixed aggregation types', () => { // Add first set of values - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "min_val" - }, { - localContext: "min_val", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'min_val', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "max_val" - }, { - localContext: "max_val", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'max_val', 5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "sum_val" - }, { - localContext: "sum_val", - target: 3 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'sum_val', 3, 'collection') + .buildV1() + ); // Add second set of values - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "min_val" - }, { - localContext: "min_val", - target: 5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'min_val', 5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "max_val" - }, { - localContext: "max_val", - target: 15 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'max_val', 15, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "sum_val" - }, { - localContext: "sum_val", - target: 7 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'sum_val', 7, 'collection') + .buildV1() + ); const resolver = new AggregationResolver(lossless, { min_val: 'min' as AggregationType, @@ -375,46 +208,22 @@ describe('Aggregation Resolvers', () => { test('should ignore non-numeric values', () => { // Add numeric value - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 10, 'collection') + .buildV1() + ); // Add non-numeric value (string) - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); // Add another numeric value - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'score', 20, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['score', 'name']); const result = sumResolver.resolve(); @@ -427,18 +236,10 @@ describe('Aggregation Resolvers', () => { test('should handle empty value arrays', () => { // Create entity with non-aggregated property - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['score']); const result = sumResolver.resolve(); @@ -451,18 +252,10 @@ describe('Aggregation Resolvers', () => { describe('Edge Cases', () => { test('should handle single value aggregations', () => { - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 42 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 42, 'collection') + .buildV1() + ); const avgResolver = new AverageResolver(lossless, ['value']); const result = avgResolver.resolve(); @@ -472,31 +265,15 @@ describe('Aggregation Resolvers', () => { }); test('should handle zero values', () => { - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 0 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 0, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); const sumResolver = new SumResolver(lossless, ['value']); const result = sumResolver.resolve(); @@ -506,31 +283,15 @@ describe('Aggregation Resolvers', () => { }); test('should handle negative values', () => { - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: -5 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', -5, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'test', - host: 'host1', - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('test', 'host1') + .setProperty('entity1', 'value', 10, 'collection') + .buildV1() + ); const minResolver = new MinResolver(lossless, ['value']); const result = minResolver.resolve(); diff --git a/__tests__/compose-decompose.ts b/__tests__/compose-decompose.ts index d3b64fd..160b630 100644 --- a/__tests__/compose-decompose.ts +++ b/__tests__/compose-decompose.ts @@ -5,7 +5,7 @@ */ import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; describe('Lossless View Compose/Decompose', () => { let node: RhizomeNode; @@ -18,22 +18,14 @@ describe('Lossless View Compose/Decompose', () => { it('should compose and decompose simple entity deltas correctly', () => { // Create simple entity deltas const nameDeltas = [ - new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice Smith' } - ] - }), - new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'email' }, - { localContext: 'email', target: 'alice@example.com' } - ] - }) + createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice Smith') + .buildV1(), + createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'email') + .addPointer('email', 'alice@example.com') + .buildV1() ]; // Ingest the deltas @@ -73,17 +65,13 @@ describe('Lossless View Compose/Decompose', () => { it('should handle multi-pointer relationship deltas correctly', () => { // Create a complex relationship delta - const relationshipDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'relationships' }, - { localContext: 'partner', target: 'bob' }, - { localContext: 'type', target: 'friendship' }, - { localContext: 'since', target: '2020-01-15' }, - { localContext: 'intensity', target: 8 } - ] - }); + const relationshipDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'relationships') + .addPointer('partner', 'bob') + .addPointer('type', 'friendship') + .addPointer('since', '2020-01-15') + .addPointer('intensity', 8) + .buildV1(); node.lossless.ingestDelta(relationshipDelta); @@ -115,33 +103,21 @@ describe('Lossless View Compose/Decompose', () => { it('should handle reference relationships correctly', () => { // Create entities first - const aliceDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice' } - ] - }); + const aliceDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice') + .buildV1(); - const bobDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'bob', targetContext: 'name' }, - { localContext: 'name', target: 'Bob' } - ] - }); + const bobDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'bob', 'name') + .addPointer('name', 'Bob') + .buildV1(); // Create friendship relationship - const friendshipDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friend', target: 'bob', targetContext: 'friends' } - ] - }); + const friendshipDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'friends') + .addPointer('friend', 'bob', 'friends') + .buildV1(); [aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d)); @@ -171,14 +147,10 @@ describe('Lossless View Compose/Decompose', () => { }); it('should preserve delta metadata correctly', () => { - const originalDelta = new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice' } - ] - }); + const originalDelta = createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice') + .buildV1(); node.lossless.ingestDelta(originalDelta); @@ -198,30 +170,18 @@ describe('Lossless View Compose/Decompose', () => { it('should handle multiple deltas for the same property', () => { // Create multiple name changes for alice const nameDeltas = [ - new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice' } - ] - }), - new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice Smith' } - ] - }), - new Delta({ - creator: 'test-creator', - host: 'test-host', - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'name' }, - { localContext: 'name', target: 'Alice Johnson' } - ] - }) + createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice') + .buildV1(), + createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice Smith') + .buildV1(), + createDelta('test-creator', 'test-host') + .addPointer('users', 'alice', 'name') + .addPointer('name', 'Alice Johnson') + .buildV1() ]; nameDeltas.forEach(d => node.lossless.ingestDelta(d)); diff --git a/__tests__/concurrent-writes.ts b/__tests__/concurrent-writes.ts index f16bb5b..97c0c55 100644 --- a/__tests__/concurrent-writes.ts +++ b/__tests__/concurrent-writes.ts @@ -1,3 +1,4 @@ +import { createDelta } from '../src/core/delta-builder'; import { RhizomeNode, Lossless, @@ -24,35 +25,19 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer1', 'host1') + .withId('delta-a') + .withTimestamp(timestamp) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - 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 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withId('delta-b') + .withTimestamp(timestamp) // Same timestamp + .setProperty('entity1', 'score', 200, 'collection') + .buildV1() + ); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); @@ -66,35 +51,19 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer_z', 'host1') // Lexicographically later + .withId('delta-a') + .withTimestamp(timestamp) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - 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 - }] - })); + lossless.ingestDelta(createDelta('writer_a', 'host2') // Lexicographically earlier + .withId('delta-b') + .withTimestamp(timestamp) // Same timestamp + .setProperty('entity1', 'score', 200, 'collection') + .buildV1() + ); const resolver = new TimestampResolver(lossless, 'creator-id'); const result = resolver.resolve(); @@ -108,47 +77,24 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'points', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer2', - host: 'host2', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "points" - }, { - localContext: "points", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withTimestamp(1000) // Same timestamp + .setProperty('entity1', 'points', 20, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer3', - host: 'host3', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "points" - }, { - localContext: "points", - target: 30 - }] - })); + // Third writer adds another value + lossless.ingestDelta(createDelta('writer3', 'host3') + .withTimestamp(1000) // Same timestamp + .setProperty('entity1', 'points', 30, 'collection') + .buildV1() + ); const resolver = new SumResolver(lossless, ['points']); const result = resolver.resolve(); @@ -162,34 +108,20 @@ describe('Concurrent Write Scenarios', () => { 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' - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 'newer') + .buildV1() + ); // 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' - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 'older') + .buildV1() + ); const resolver = new LastWriteWins(lossless); const result = resolver.resolve(); @@ -201,47 +133,26 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer1', 'host1') + .withTimestamp(3000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 30) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 20) + .buildV1() + ); const resolver = new SumResolver(lossless, ['score']); const result = resolver.resolve(); @@ -261,19 +172,12 @@ describe('Concurrent Write Scenarios', () => { // 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 - }] - })); + lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`) + .withTimestamp(baseTimestamp + write) + .addPointer('collection', 'entity1', 'counter') + .addPointer('counter', 1) + .buildV1() + ); } } @@ -289,62 +193,34 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer1', 'host1') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer1', - host: 'host1', - timeCreated: timestamp + 1, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('writer1', 'host1') + .withTimestamp(timestamp + 1) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 100) + .buildV1() + ); // 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(createDelta('writer2', 'host2') + .withTimestamp(timestamp + 2) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'bob') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer2', - host: 'host2', - timeCreated: timestamp + 3, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 200 - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withTimestamp(timestamp + 3) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 200) + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin(), @@ -365,19 +241,12 @@ describe('Concurrent Write Scenarios', () => { // 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 - }] - })); + lossless.ingestDelta(createDelta(`writer${i}`, `host${i}`) + .withTimestamp(timestamp) + .addPointer('collection', `entity${i}`, 'value') + .addPointer('value', (i + 1) * 10) + .buildV1() + ); } const resolver = new LastWriteWins(lossless); @@ -395,62 +264,34 @@ describe('Concurrent Write Scenarios', () => { 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(createDelta('writer1', 'host1') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'votes') + .addPointer('votes', 'option_a') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer2', - host: 'host2', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "votes" - }, { - localContext: "votes", - target: 'option_a' - }] - })); + lossless.ingestDelta(createDelta('writer2', 'host2') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'votes') + .addPointer('votes', 'option_a') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'writer3', - host: 'host3', - timeCreated: timestamp, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "votes" - }, { - localContext: "votes", - target: 'option_b' - }] - })); + lossless.ingestDelta(createDelta('writer3', 'host3') + .withTimestamp(timestamp) + .addPointer('collection', 'entity1', 'votes') + .addPointer('votes', 'option_b') + .buildV1() + ); // 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' - }] - })); + lossless.ingestDelta(createDelta('writer4', 'host4') + .withTimestamp(timestamp) + .addPointer('collection', 'entity2', 'status') + .addPointer('status', 'active') + .buildV1() + ); const resolver = new CustomResolver(lossless, { votes: new MajorityVotePlugin(), @@ -474,19 +315,12 @@ describe('Concurrent Write Scenarios', () => { // 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 - }] - })); + lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`) + .withTimestamp(baseTimestamp + Math.floor(Math.random() * 1000)) + .addPointer('collection', `entity${entity}`, 'score') + .addPointer('score', Math.floor(Math.random() * 100)) + .buildV1() + ); } } @@ -510,19 +344,15 @@ describe('Concurrent Write Scenarios', () => { // 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 - }] - })); + lossless.ingestDelta(createDelta( + `writer${i % 5}`, + `host${i % 3}` + ) + .withTimestamp(1000 + i) + .addPointer('collection', entityId, 'counter') + .addPointer('counter', 1) + .buildV1() + ); updateCount++; } @@ -534,19 +364,12 @@ describe('Concurrent Write Scenarios', () => { // 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 - }] - })); + lossless.ingestDelta(createDelta('late-writer', 'late-host') + .withTimestamp(2000 + i) + .addPointer('collection', entityId, 'counter') + .addPointer('counter', 2) + .buildV1() + ); updateCount += 2; // Create a fresh resolver to avoid accumulator caching issues diff --git a/__tests__/custom-resolvers.ts b/__tests__/custom-resolvers.ts index b6d3a70..6c3b7c1 100644 --- a/__tests__/custom-resolvers.ts +++ b/__tests__/custom-resolvers.ts @@ -11,7 +11,8 @@ import { MinPlugin, MaxPlugin, PropertyTypes, - CollapsedDelta + CollapsedDelta, + createDelta } from "../src"; describe('Custom Resolvers', () => { @@ -25,33 +26,21 @@ describe('Custom Resolvers', () => { describe('Built-in Plugins', () => { test('LastWriteWinsPlugin should resolve to most recent value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'first' - }] - })); + // First delta with earlier timestamp + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'first', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'second' - }] - })); + // Second delta with later timestamp (should win) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'name', 'second', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin() @@ -63,33 +52,21 @@ describe('Custom Resolvers', () => { }); test('FirstWriteWinsPlugin should resolve to earliest value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'second' - }] - })); + // Later delta (should be ignored by FirstWriteWins) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'name', 'second', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'first' - }] - })); + // Earlier delta (should win with FirstWriteWins) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'first', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new FirstWriteWinsPlugin() @@ -101,47 +78,29 @@ describe('Custom Resolvers', () => { }); test('ConcatenationPlugin should join string values chronologically', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' - }] - })); + // First tag + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'tags', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'blue' - }] - })); + // Second tag (with later timestamp) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'tags', 'blue', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'green' - }] - })); + // Third tag (with timestamp between first and second) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'tags', 'green', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { tags: new ConcatenationPlugin(' ') @@ -153,33 +112,21 @@ describe('Custom Resolvers', () => { }); test('ConcatenationPlugin should handle duplicates', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' - }] - })); + // First tag + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'tags', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "tags" - }, { - localContext: "tags", - target: 'red' // duplicate - }] - })); + // Duplicate tag with later timestamp + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'tags', 'red', 'collection') // duplicate + .buildV1() + ); const resolver = new CustomResolver(lossless, { tags: new ConcatenationPlugin(',') @@ -192,76 +139,41 @@ describe('Custom Resolvers', () => { test('MajorityVotePlugin should resolve to most voted value', () => { // Add 3 votes for 'red' - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'red' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host1', - timeCreated: 1001, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'red' - }] - })); + lossless.ingestDelta( + createDelta('user2', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'red', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user3', - host: 'host1', - timeCreated: 1002, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'red' - }] - })); + lossless.ingestDelta( + createDelta('user3', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'red', 'collection') + .buildV1() + ); // Add 2 votes for 'blue' - lossless.ingestDelta(new Delta({ - creator: 'user4', - host: 'host1', - timeCreated: 1003, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'blue' - }] - })); + lossless.ingestDelta( + createDelta('user4', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'blue', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user5', - host: 'host1', - timeCreated: 1004, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "color" - }, { - localContext: "color", - target: 'blue' - }] - })); + lossless.ingestDelta( + createDelta('user5', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'color', 'blue', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { color: new MajorityVotePlugin() @@ -273,47 +185,29 @@ describe('Custom Resolvers', () => { }); test('MinPlugin should resolve to minimum numeric value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + // First score (100) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 50 - }] - })); + // Second score (50) - this is the minimum + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'score', 50, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 75 - }] - })); + // Third score (75) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 75, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MinPlugin() @@ -325,47 +219,29 @@ describe('Custom Resolvers', () => { }); test('MaxPlugin should resolve to maximum numeric value', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + // First score (100) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 150 - }] - })); + // Second score (150) - this is the maximum + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'score', 150, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 75 - }] - })); + // Third score (75) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 75, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MaxPlugin() @@ -380,62 +256,36 @@ describe('Custom Resolvers', () => { describe('Mixed Plugin Configurations', () => { test('should handle different plugins for different properties', () => { // Add name with different timestamps - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'old_name' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'old_name', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'new_name' - }] - })); + // Update name with newer timestamp + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'name', 'new_name', 'collection') + .buildV1() + ); // Add scores - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 100, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 50 - }] - })); + // Add another score (MinPlugin will pick the smaller one) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'score', 50, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin(), // Should resolve to 'new_name' @@ -450,34 +300,20 @@ describe('Custom Resolvers', () => { test('should only include entities with configured properties', () => { // Entity1 has configured property - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'name', 'test', 'collection') + .buildV1() + ); // Entity2 has non-configured property - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "other" - }, { - localContext: "other", - target: 'value' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity2', 'other_prop', 'value', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { name: new LastWriteWinsPlugin() @@ -510,47 +346,29 @@ describe('Custom Resolvers', () => { } } - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'first' - }] - })); + // First update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'updates', 'first', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'second' - }] - })); + // Second update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'updates', 'second', 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "updates" - }, { - localContext: "updates", - target: 'third' - }] - })); + // Third update + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'updates', 'third', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { updates: new CountPlugin() @@ -585,47 +403,29 @@ describe('Custom Resolvers', () => { } } - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + // First score (10) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 10, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + // Second score (20) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(2000) + .setProperty('entity1', 'score', 20, 'collection') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 3000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 30 - }] - })); + // Third score (30) + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(3000) + .setProperty('entity1', 'score', 30, 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new RunningAveragePlugin() @@ -650,19 +450,12 @@ describe('Custom Resolvers', () => { test('should handle non-matching property types gracefully', () => { // Add string value to numeric plugin - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 'not_a_number' - }] - })); + lossless.ingestDelta( + createDelta('user1', 'host1') + .withTimestamp(1000) + .setProperty('entity1', 'score', 'not_a_number', 'collection') + .buildV1() + ); const resolver = new CustomResolver(lossless, { score: new MinPlugin() // Expects numeric values diff --git a/__tests__/delta.ts b/__tests__/delta.ts index 2af876f..95da3cf 100644 --- a/__tests__/delta.ts +++ b/__tests__/delta.ts @@ -1,19 +1,12 @@ +import { createDelta } from '../src/core/delta-builder'; import {DeltaV1, DeltaV2} from "../src"; describe("Delta", () => { it("can convert DeltaV1 to DeltaV2", () => { - const deltaV1 = new DeltaV1({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: 'color', - target: 'red' - }, { - localContext: 'furniture', - target: 'chair-1', - targetContext: 'color' - }] - }); + const deltaV1 = createDelta('a', 'h') + .addPointer('color', 'red') + .addPointer('furniture', 'chair-1', 'color') + .buildV1(); const deltaV2 = DeltaV2.fromV1(deltaV1); @@ -27,14 +20,10 @@ describe("Delta", () => { }); it("can convert DeltaV2 to DeltaV1", () => { - const deltaV2 = new DeltaV2({ - creator: 'a', - host: 'h', - pointers: { - color: 'red', - furniture: {'chair-1': 'color'} - } - }); + const deltaV2 = createDelta('a', 'h') + .addPointer('color', 'red') + .addPointer('furniture', 'chair-1', 'color') + .buildV2(); const deltaV1 = deltaV2.toV1(); diff --git a/__tests__/jest-setup.ts b/__tests__/jest-setup.ts index 0f33cc9..df58a9c 100644 --- a/__tests__/jest-setup.ts +++ b/__tests__/jest-setup.ts @@ -1,5 +1,11 @@ // Set up environment variables for tests -process.env.DEBUG = 'rz:*'; +// DEBUG handling examples: +// npm test // will set DEBUG=rz:* by default +// NO_DEBUG=true npm test // will not set DEBUG +// DEBUG=other npm test // will set DEBUG=other +if (!process.env.DEBUG && !process.env.NO_DEBUG) { + process.env.DEBUG = 'rz:*'; +} // Extend the global Jest namespace declare global { diff --git a/__tests__/last-write-wins.ts b/__tests__/last-write-wins.ts index 7303fdf..38a4ddd 100644 --- a/__tests__/last-write-wins.ts +++ b/__tests__/last-write-wins.ts @@ -1,4 +1,5 @@ import Debug from "debug"; +import { createDelta } from '../src/core/delta-builder'; import {Delta, LastWriteWins, Lossless, RhizomeNode} from "../src"; const debug = Debug('test:last-write-wins'); @@ -11,31 +12,15 @@ describe('Last write wins', () => { const lossy = new LastWriteWins(lossless); beforeAll(() => { - lossless.ingestDelta(new Delta({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: "vegetable", - target: "broccoli", - targetContext: "want" - }, { - localContext: "desire", - target: 95, - }] - })); + lossless.ingestDelta(createDelta('a', 'h') + .setProperty('broccoli', 'want', 95, 'vegetable') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: "vegetable", - target: "broccoli", - targetContext: "want" - }, { - localContext: "want", - target: 90, - }] - })); + lossless.ingestDelta(createDelta('a', 'h') + .setProperty('broccoli', 'want', 90, 'vegetable') + .buildV1() + ); }); it('our resolver should return the most recently written value', () => { diff --git a/__tests__/lossy.ts b/__tests__/lossy.ts index e730389..5c2ad8c 100644 --- a/__tests__/lossy.ts +++ b/__tests__/lossy.ts @@ -1,6 +1,5 @@ import Debug from 'debug'; import { - Delta, PointerTarget, lastValueFromDeltas, valueFromCollapsedDelta, @@ -9,6 +8,7 @@ import { Lossy, RhizomeNode } from "../src"; +import { createDelta } from "../src/core/delta-builder"; const debug = Debug('test:lossy'); type Role = { @@ -62,29 +62,14 @@ describe('Lossy', () => { const lossy = new Summarizer(lossless); beforeAll(() => { - lossless.ingestDelta(new Delta({ - creator: 'a', - host: 'h', - pointers: [{ - localContext: "actor", - target: "keanu", - targetContext: "roles" - }, { - localContext: "role", - target: "neo", - targetContext: "actor" - }, { - localContext: "film", - target: "the_matrix", - targetContext: "cast" - }, { - localContext: "base_salary", - target: 1000000 - }, { - localContext: "salary_currency", - target: "usd" - }] - })); + lossless.ingestDelta(createDelta('a', 'h') + .addPointer('actor', 'keanu', 'roles') + .addPointer('role', 'neo', 'actor') + .addPointer('film', 'the_matrix', 'cast') + .addPointer('base_salary', 1000000) + .addPointer('salary_currency', 'usd') + .buildV1() + ); }); it('example summary', () => { diff --git a/__tests__/multi-pointer-resolution.ts b/__tests__/multi-pointer-resolution.ts index 8b45ba7..7934c47 100644 --- a/__tests__/multi-pointer-resolution.ts +++ b/__tests__/multi-pointer-resolution.ts @@ -5,7 +5,7 @@ */ import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema'; import { TypedCollectionImpl } from '../src/collections'; @@ -76,17 +76,13 @@ describe('Multi-Pointer Delta Resolution', () => { await roleCollection.put('neo', { name: 'Neo' }); // Create a complex casting delta with multiple entity references and scalar values - const castingDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'actors', target: 'keanu', targetContext: 'filmography' }, - { localContext: 'movies', target: 'matrix', targetContext: 'cast' }, - { localContext: 'roles', target: 'neo', targetContext: 'portrayals' }, - { localContext: 'salary', target: 15000000 }, - { localContext: 'contract_date', target: '1999-03-31' } - ] - }); + const castingDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('actors', 'keanu', 'filmography') + .addPointer('movies', 'matrix', 'cast') + .addPointer('roles', 'neo', 'portrayals') + .addPointer('salary', 15000000) + .addPointer('contract_date', '1999-03-31') + .buildV1(); node.lossless.ingestDelta(castingDelta); // Test from Keanu's perspective @@ -164,17 +160,13 @@ describe('Multi-Pointer Delta Resolution', () => { await personCollection.put('bob', { name: 'Bob' }); // Create a relationship delta with one entity reference and multiple scalars - const relationshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'people', target: 'alice', targetContext: 'relationships' }, - { localContext: 'partner', target: 'bob' }, // Entity reference - { localContext: 'type', target: 'friendship' }, // Scalar - { localContext: 'since', target: '2020-01-15' }, // Scalar - { localContext: 'intensity', target: 8 } // Scalar number - ] - }); + const relationshipDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('people', 'alice', 'relationships') + .addPointer('partner', 'bob') + .addPointer('type', 'friendship') + .addPointer('since', '2020-01-15') + .addPointer('intensity', 8) + .buildV1(); node.lossless.ingestDelta(relationshipDelta); // Test from Alice's perspective @@ -243,17 +235,13 @@ describe('Multi-Pointer Delta Resolution', () => { await designerCollection.put('bob', { name: 'Bob Designer' }); // Create a collaboration delta with multiple entity references - const collaborationDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'projects', target: 'website', targetContext: 'collaborations' }, - { localContext: 'developer', target: 'alice' }, // Entity reference - { localContext: 'designer', target: 'bob' }, // Entity reference - { localContext: 'budget', target: 50000 }, // Scalar - { localContext: 'deadline', target: '2024-06-01' } // Scalar - ] - }); + const collaborationDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('projects', 'website', 'collaborations') + .addPointer('developer', 'alice') + .addPointer('designer', 'bob') + .addPointer('budget', 50000) + .addPointer('deadline', '2024-06-01') + .buildV1(); node.lossless.ingestDelta(collaborationDelta); // Test from project's perspective diff --git a/__tests__/negation.ts b/__tests__/negation.ts index 857f97a..28ee328 100644 --- a/__tests__/negation.ts +++ b/__tests__/negation.ts @@ -1,4 +1,5 @@ import Debug from 'debug'; +import { createDelta } from '../src/core/delta-builder'; import { Delta } from '../src/core'; import { NegationHelper } from '../src/features'; import { RhizomeNode } from '../src/node'; @@ -17,14 +18,9 @@ describe('Negation System', () => { describe('Negation Helper', () => { it('should create negation deltas correctly', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'entity1', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Alice') + .buildV1(); const negationDelta = NegationHelper.createNegation( originalDelta.id, @@ -44,11 +40,9 @@ describe('Negation System', () => { }); it('should identify negation deltas', () => { - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const regularDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); const negationDelta = NegationHelper.createNegation( 'delta-to-negate', @@ -71,27 +65,21 @@ describe('Negation System', () => { const extractedId = NegationHelper.getNegatedDeltaId(negationDelta); expect(extractedId).toBe(targetDeltaId); - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const regularDelta = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull(); }); it('should find negations for specific deltas', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1'); @@ -110,17 +98,13 @@ describe('Negation System', () => { }); it('should check if deltas are negated', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const allDeltas = [delta1, delta2, negation1]; @@ -130,23 +114,17 @@ describe('Negation System', () => { }); it('should filter out negated deltas', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); - const delta3 = new Delta({ - creator: 'user3', - host: 'host1', - pointers: [{ localContext: 'email', target: 'entity1', targetContext: 'email' }] - }); + const delta3 = createDelta('user3', 'host1') + .setProperty('entity1', 'email', 'entity1@example.com') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1'); @@ -160,17 +138,13 @@ describe('Negation System', () => { }); it('should provide negation statistics', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity1', 'name', 'Entity 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity1', 'age', 25) + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const allDeltas = [delta1, delta2, negation1]; @@ -189,22 +163,18 @@ describe('Negation System', () => { const baseTime = Date.now(); // Create deltas with specific timestamps - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime, - pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }] - }); + const delta1 = createDelta('user1', 'host1') + .withTimestamp(baseTime) + .setProperty('entity1', 'status', 'active') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); negation1.timeCreated = baseTime + 1000; // 1 second later - const delta2 = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime + 2000, // 2 seconds later - pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }] - }); + const delta2 = createDelta('user1', 'host1') + .withTimestamp(baseTime + 2000) + .setProperty('entity1', 'status', 'inactive') + .buildV1(); const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1'); negation2.timeCreated = baseTime + 3000; // 3 seconds later @@ -220,14 +190,9 @@ describe('Negation System', () => { describe('Lossless View Integration', () => { it('should filter negated deltas in lossless views', () => { // Create original delta - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'user123', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('user123', 'name', 'Alice') + .buildV1(); // Create negation delta const negationDelta = NegationHelper.createNegation( @@ -238,14 +203,9 @@ describe('Negation System', () => { // Create another non-negated delta - const nonNegatedDelta = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'age', target: 'user123', targetContext: 'age' }, - { localContext: 'value', target: 25 } - ] - }); + const nonNegatedDelta = createDelta('user2', 'host1') + .setProperty('user123', 'age', 25) + .buildV1(); // Ingest all deltas lossless.ingestDelta(originalDelta); @@ -263,14 +223,9 @@ describe('Negation System', () => { }); it('should handle multiple negations of the same delta', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Original content' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('post1', 'content', 'Original content') + .buildV1(); const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1'); @@ -286,23 +241,13 @@ describe('Negation System', () => { }); it('should provide negation statistics for entities', () => { - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'title', target: 'article1', targetContext: 'title' }, - { localContext: 'value', target: 'Original Title' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('article1', 'title', 'Original Title') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'article1', targetContext: 'content' }, - { localContext: 'value', target: 'Article content' } - ] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('article1', 'content', 'Article content') + .buildV1(); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); @@ -321,14 +266,9 @@ describe('Negation System', () => { }); it('should retrieve negation deltas for entities', () => { - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'status', target: 'task1', targetContext: 'status' }, - { localContext: 'value', target: 'pending' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('task1', 'status', 'pending') + .buildV1(); const negationDelta = NegationHelper.createNegation( originalDelta.id, @@ -349,25 +289,16 @@ describe('Negation System', () => { const transactionId = 'tx-negation'; // Create transaction declaration - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(transactionId, 2) + .buildV1() + ); // Create original delta in transaction - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'comment', target: 'post1', targetContext: 'comments' }, - { localContext: 'text', target: 'Inappropriate comment' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .declareTransaction(transactionId, 2) + .setProperty('post1', 'comments', 'Inappropriate comment') + .buildV1(); // Create negation delta in same transaction const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1'); @@ -389,30 +320,20 @@ describe('Negation System', () => { const baseTime = Date.now(); // User posts content - const postDelta = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime, - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Original post' } - ] - }); + const postDelta = createDelta('user1', 'host1') + .withTimestamp(baseTime) + .setProperty('post1', 'content', 'Original post') + .buildV1(); // Moderator negates it const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1'); negationDelta.timeCreated = baseTime + 1000; // User edits content (after negation) - const editDelta = new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: baseTime + 2000, - pointers: [ - { localContext: 'content', target: 'post1', targetContext: 'content' }, - { localContext: 'value', target: 'Edited post' } - ] - }); + const editDelta = createDelta('user1', 'host1') + .withTimestamp(baseTime + 2000) + .setProperty('post1', 'content', 'Edited post') + .buildV1(); lossless.ingestDelta(postDelta); lossless.ingestDelta(negationDelta); @@ -447,14 +368,10 @@ describe('Negation System', () => { it('should handle self-referential entities in negations', () => { // Create a delta that references itself - const selfRefDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'parent', target: 'node1', targetContext: 'parent' }, - { localContext: 'child', target: 'node1' } // Self-reference - ] - }); + const selfRefDelta = createDelta('user1', 'host1') + .setProperty('node1', 'parent', 'node1') + .setProperty('node1', 'child', 'node1') // Self-reference + .buildV1(); const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1'); @@ -470,14 +387,9 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create the original delta - const originalDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'title', target: 'entity2', targetContext: 'title' }, - { localContext: 'status', target: 'Draft' } - ] - }); + const originalDelta = createDelta('user1', 'host1') + .setProperty('entity2', 'title', 'Draft') + .buildV1(); // Create two negations of the same delta const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1'); @@ -506,14 +418,9 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create the original delta - const deltaA = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'content', target: 'entity3', targetContext: 'content' }, - { localContext: 'text', target: 'Hello World' } - ] - }); + const deltaA = createDelta('user1', 'host1') + .setProperty('entity3', 'content', 'Hello World') + .buildV1(); // Create a chain of negations: B negates A, C negates B, D negates C const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1'); @@ -584,23 +491,13 @@ describe('Negation System', () => { const testLossless = new Lossless(testNode); // Create two independent deltas - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'item', target: 'entity4', targetContext: 'item' }, - { localContext: 'name', target: 'Item 1' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .setProperty('entity4', 'item', 'Item 1') + .buildV1(); - const delta2 = new Delta({ - creator: 'user2', - host: 'host1', - pointers: [ - { localContext: 'item', target: 'entity4', targetContext: 'item' }, - { localContext: 'name', target: 'Item 2' } - ] - }); + const delta2 = createDelta('user2', 'host1') + .setProperty('entity4', 'item', 'Item 2') + .buildV1(); // Create negations for both deltas const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1'); diff --git a/__tests__/nested-resolution-performance.ts b/__tests__/nested-resolution-performance.ts index 35b7e41..e3904c2 100644 --- a/__tests__/nested-resolution-performance.ts +++ b/__tests__/nested-resolution-performance.ts @@ -12,7 +12,7 @@ import Debug from 'debug'; import { RhizomeNode } from '../src/node'; const debug = Debug('rz:test:nested-resolution-performance'); -import { Delta } from '../src/core'; +import { Delta, createDelta } from '../src/core'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema'; import { TypedCollectionImpl } from '../src/collections'; @@ -80,14 +80,9 @@ describe('Nested Object Resolution Performance', () => { const friendIndex = Math.floor(Math.random() * userCount); if (friendIndex !== i) { const friendId = userIds[friendIndex]; - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'friends' }, - { localContext: 'friends', target: friendId } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'friends', friendId, 'users') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); } } @@ -98,14 +93,9 @@ describe('Nested Object Resolution Performance', () => { const followerIndex = Math.floor(Math.random() * userCount); if (followerIndex !== i) { const followerId = userIds[followerIndex]; - const followDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'followers' }, - { localContext: 'followers', target: followerId } - ] - }); + const followDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'followers', followerId, 'users') + .buildV1(); node.lossless.ingestDelta(followDelta); } } @@ -114,14 +104,9 @@ describe('Nested Object Resolution Performance', () => { if (i > 0) { const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure const mentorId = userIds[mentorIndex]; - const mentorshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: userId, targetContext: 'mentor' }, - { localContext: 'mentor', target: mentorId } - ] - }); + const mentorshipDelta = createDelta(node.config.creator, node.config.peerId) + .setProperty(userId, 'mentor', mentorId, 'users') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta); } } diff --git a/__tests__/nested-resolution.ts b/__tests__/nested-resolution.ts index 830fcc9..d6ea7be 100644 --- a/__tests__/nested-resolution.ts +++ b/__tests__/nested-resolution.ts @@ -10,11 +10,11 @@ */ import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema'; import { CommonSchemas } from '../util/schemas'; import { TypedCollectionImpl } from '../src/collections'; +import { createDelta } from '../src/core/delta-builder'; describe('Nested Object Resolution', () => { let node: RhizomeNode; @@ -55,14 +55,10 @@ describe('Nested Object Resolution', () => { }); // Create friendship relationship - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); // Get Alice's lossless view @@ -107,14 +103,10 @@ describe('Nested Object Resolution', () => { // Create user with reference to non-existent friend await userCollection.put('alice', { name: 'Alice' }); - const friendshipDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'nonexistent' } - ] - }); + const friendshipDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'nonexistent') + .buildV1(); node.lossless.ingestDelta(friendshipDelta); const aliceViews = node.lossless.view(['alice']); @@ -162,25 +154,17 @@ describe('Nested Object Resolution', () => { await userCollection.put('charlie', { name: 'Charlie' }); // Alice's mentor is Bob - const mentorshipDelta1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'deep-users', target: 'alice', targetContext: 'mentor' }, - { localContext: 'mentor', target: 'bob' } - ] - }); + const mentorshipDelta1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('deep-users', 'alice', 'mentor') + .addPointer('mentor', 'bob') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta1); // Bob's mentor is Charlie - const mentorshipDelta2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'deep-users', target: 'bob', targetContext: 'mentor' }, - { localContext: 'mentor', target: 'charlie' } - ] - }); + const mentorshipDelta2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('deep-users', 'bob', 'mentor') + .addPointer('mentor', 'charlie') + .buildV1(); node.lossless.ingestDelta(mentorshipDelta2); const aliceViews = node.lossless.view(['alice']); @@ -246,24 +230,16 @@ describe('Nested Object Resolution', () => { await userCollection.put('bob', { name: 'Bob' }); // Create circular friendship: Alice -> Bob -> Alice - const friendship1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship1); - const friendship2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'bob', targetContext: 'friends' }, - { localContext: 'friends', target: 'alice' } - ] - }); + const friendship2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'bob', 'friends') + .addPointer('friends', 'alice') + .buildV1(); node.lossless.ingestDelta(friendship2); const aliceViews = node.lossless.view(['alice']); @@ -295,14 +271,10 @@ describe('Nested Object Resolution', () => { await userCollection.put('alice', { name: 'Alice' }); // Alice is friends with herself - const selfFriendship = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'alice' } - ] - }); + const selfFriendship = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'alice') + .buildV1(); node.lossless.ingestDelta(selfFriendship); const aliceViews = node.lossless.view(['alice']); @@ -335,24 +307,16 @@ describe('Nested Object Resolution', () => { await userCollection.put('charlie', { name: 'Charlie' }); // Alice has multiple friends - const friendship1 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship1 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship1); - const friendship2 = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'charlie' } - ] - }); + const friendship2 = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'charlie') + .buildV1(); node.lossless.ingestDelta(friendship2); const aliceViews = node.lossless.view(['alice']); @@ -405,14 +369,10 @@ describe('Nested Object Resolution', () => { }); // Create friendship - const friendship = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'alice', targetContext: 'friends' }, - { localContext: 'friends', target: 'bob' } - ] - }); + const friendship = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'alice', 'friends') + .addPointer('friends', 'bob') + .buildV1(); node.lossless.ingestDelta(friendship); const aliceViews = node.lossless.view(['alice']); diff --git a/__tests__/query.ts b/__tests__/query.ts index 28a42fc..940ed96 100644 --- a/__tests__/query.ts +++ b/__tests__/query.ts @@ -3,7 +3,7 @@ import { Lossless } from '../src/views'; import { DefaultSchemaRegistry } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas } from '../src/schema'; import { CommonSchemas } from '../util/schemas'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { RhizomeNode } from '../src/node'; describe('Query Engine', () => { @@ -48,100 +48,65 @@ describe('Query Engine', () => { async function createUser(id: string, name: string, age?: number, email?: string) { // Create user entity with name - const nameDelta = new Delta({ - id: `delta-${id}-name-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'user', target: id, targetContext: 'name' }, - { localContext: 'value', target: name } - ] - }); + const nameDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-name-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'name', name, 'user') + .buildV1(); lossless.ingestDelta(nameDelta); // Add age if provided if (age !== undefined) { - const ageDelta = new Delta({ - id: `delta-${id}-age-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'user', target: id, targetContext: 'age' }, - { localContext: 'value', target: age } - ] - }); + const ageDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-age-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'age', age, 'user') + .buildV1(); lossless.ingestDelta(ageDelta); } // Add email if provided if (email) { - const emailDelta = new Delta({ - id: `delta-${id}-email-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'user', target: id, targetContext: 'email' }, - { localContext: 'value', target: email } - ] - }); + const emailDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-email-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'email', email, 'user') + .buildV1(); lossless.ingestDelta(emailDelta); } } async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) { // Title delta - const titleDelta = new Delta({ - id: `delta-${id}-title-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'post', target: id, targetContext: 'title' }, - { localContext: 'value', target: title } - ] - }); + const titleDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-title-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'title', title, 'post') + .buildV1(); lossless.ingestDelta(titleDelta); // Author delta - const authorDelta = new Delta({ - id: `delta-${id}-author-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'post', target: id, targetContext: 'author' }, - { localContext: 'value', target: author } - ] - }); + const authorDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-author-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'author', author, 'post') + .buildV1(); lossless.ingestDelta(authorDelta); // Published delta - const publishedDelta = new Delta({ - id: `delta-${id}-published-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'post', target: id, targetContext: 'published' }, - { localContext: 'value', target: published } - ] - }); + const publishedDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-published-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'published', published, 'post') + .buildV1(); lossless.ingestDelta(publishedDelta); // Views delta - const viewsDelta = new Delta({ - id: `delta-${id}-views-${Date.now()}`, - creator: 'test', - host: 'test-host', - timeCreated: Date.now(), - pointers: [ - { localContext: 'post', target: id, targetContext: 'views' }, - { localContext: 'value', target: views } - ] - }); + const viewsDelta = createDelta('test', 'test-host') + .withId(`delta-${id}-views-${Date.now()}`) + .withTimestamp(Date.now()) + .setProperty(id, 'views', views, 'post') + .buildV1(); lossless.ingestDelta(viewsDelta); } diff --git a/__tests__/run/001-single-node-orchestrated.ts b/__tests__/run/001-single-node-orchestrated.ts index b75c3e1..a33d6d6 100644 --- a/__tests__/run/001-single-node-orchestrated.ts +++ b/__tests__/run/001-single-node-orchestrated.ts @@ -9,22 +9,12 @@ describe('Run (Orchestrated)', () => { let apiUrl: string; beforeAll(async () => { - console.time('Test setup'); - console.time('Create config'); // Configure and start the node const config: NodeConfig = { id: 'app-001', }; - console.timeEnd('Create config'); - - console.time('Start node'); nodeHandle = await orchestrator.startNode(config); - console.timeEnd('Start node'); - - console.time('Get API URL'); apiUrl = nodeHandle.getApiUrl(); - console.timeEnd('Get API URL'); - console.timeEnd('Test setup'); }, 60000); // Increase timeout to 60s for this hook afterAll(async () => { diff --git a/__tests__/run/002-two-nodes-orchestrated.ts b/__tests__/run/002-two-nodes-orchestrated.ts index 829fa9f..7a00652 100644 --- a/__tests__/run/002-two-nodes-orchestrated.ts +++ b/__tests__/run/002-two-nodes-orchestrated.ts @@ -19,41 +19,26 @@ describe('Run (Two Nodes Orchestrated)', () => { const nodeIds = ['app-002-A', 'app-002-B']; beforeAll(async () => { - console.time('Test setup'); // Start first node - console.time('Create node1 config'); const node1Config: NodeConfig = { id: nodeIds[0], }; - console.timeEnd('Create node1 config'); - - console.time('Start node1'); const node1 = (await orchestrator.startNode(node1Config)) as FullNodeHandle; - console.timeEnd('Start node1'); // Start second node with first node as bootstrap peer - console.time('Create node2 config'); const node2Config: NodeConfig = { id: nodeIds[1], network: { bootstrapPeers: [`localhost:${node1.getRequestPort()}`], }, }; - console.timeEnd('Create node2 config'); - - console.time('Start node2'); const node2 = (await orchestrator.startNode(node2Config)) as FullNodeHandle; - console.timeEnd('Start node2'); nodes.push(node1, node2); // Connect the nodes - console.time('Connect nodes'); await orchestrator.connectNodes(node1, node2); - console.timeEnd('Connect nodes'); - - console.timeEnd('Test setup'); }, 120000); // Increase timeout to 120s for this hook afterAll(async () => { diff --git a/__tests__/run/005-docker-orchestrator.ts b/__tests__/run/005-docker-orchestrator.ts index 6a94ff4..c2efab4 100644 --- a/__tests__/run/005-docker-orchestrator.ts +++ b/__tests__/run/005-docker-orchestrator.ts @@ -30,7 +30,7 @@ interface ExtendedNodeStatus extends Omit { // Set default timeout for all tests to 5 minutes jest.setTimeout(300000); -describe('Docker Orchestrator V2', () => { +describe('Docker Orchestrator', () => { let docker: Docker; let orchestrator: DockerOrchestrator; let node: NodeHandle | null = null; @@ -43,27 +43,31 @@ describe('Docker Orchestrator V2', () => { beforeAll(async () => { debug('Setting up Docker client and orchestrator...'); - // Initialize Docker client - docker = new Docker(); + // Initialize Docker client with increased timeout + docker = new Docker({ + timeout: 60000, // 60 second timeout for Docker operations + }); // Verify Docker is running try { await docker.ping(); debug('Docker daemon is responding'); } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); debug('Docker daemon is not responding: %o', error); - throw error; + throw new Error(`Docker daemon is not running or not accessible: ${errorMessage}`); } // Initialize the orchestrator with the Docker client and test image orchestrator = createOrchestrator('docker') as DockerOrchestrator; debug('Docker orchestrator initialized'); - // Create a basic node config for testing + // Create a basic node config for testing with unique network ID + const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`; nodePort = 3000 + Math.floor(Math.random() * 1000); nodeConfig = { - id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, - networkId: 'test-network', + id: `node-${testRunId}`, + networkId: `test-network-${testRunId}`, port: nodePort, resources: { memory: 256, // 256MB @@ -79,85 +83,8 @@ describe('Docker Orchestrator V2', () => { afterAll(async () => { debug('Starting test cleanup...'); - const cleanupPromises: Promise[] = []; - - // Helper function to clean up a node with retries - const cleanupNode = async (nodeToClean: NodeHandle | null, nodeName: string) => { - if (!nodeToClean) return; - - debug(`[${nodeName}] Starting cleanup for node ${nodeToClean.id}...`); - try { - // First try the normal stop - await orchestrator.stopNode(nodeToClean).catch(error => { - debug(`[${nodeName}] Warning stopping node normally: %s`, error.message); - throw error; // Will be caught by outer catch - }); - debug(`[${nodeName}] Node ${nodeToClean.id} stopped gracefully`); - } catch (error) { - debug(`[${nodeName}] Error stopping node ${nodeToClean.id}: %o`, error); - - // If normal stop fails, try force cleanup - try { - debug(`[${nodeName}] Attempting force cleanup for node ${nodeToClean.id}...`); - const container = orchestrator.docker.getContainer(`rhizome-${nodeToClean.id}`); - await container.stop({ t: 1 }).catch(() => { - debug(`[${nodeName}] Container stop timed out, forcing removal...`); - }); - await container.remove({ force: true }); - debug(`[${nodeName}] Node ${nodeToClean.id} force-removed`); - } catch (forceError) { - debug(`[${nodeName}] Force cleanup failed for node ${nodeToClean.id}: %o`, forceError); - } - } - }; - - // Clean up all created nodes - if (node) { - cleanupPromises.push(cleanupNode(node, 'node1')); - } - - if (node2) { - cleanupPromises.push(cleanupNode(node2, 'node2')); - } - // Wait for all node cleanups to complete before cleaning up networks - if (cleanupPromises.length > 0) { - debug('Waiting for node cleanups to complete...'); - await Promise.race([ - Promise.all(cleanupPromises), - new Promise(resolve => setTimeout(() => { - debug('Node cleanup timed out, proceeding with network cleanup...'); - resolve(null); - }, 30000)) // 30s timeout for node cleanup - ]); - } - - // Clean up any dangling networks using NetworkManager - try { - debug('Cleaning up networks...'); - // Get the network manager from the orchestrator - const networkManager = (orchestrator as any).networkManager; - if (!networkManager) { - debug('Network manager not available for cleanup'); - return; - } - - // Get all networks managed by this test - const networks = Array.from((orchestrator as any).networks.entries() || []); - - const cleanupResults = await networkManager.cleanupNetworks((orchestrator as any).networks); - - // Log any cleanup errors - cleanupResults.forEach(({ resource, error }: { resource: string; error: Error }) => { - if (error) { - debug(`Failed to clean up network ${resource || 'unknown'}: %s`, error.message); - } else { - debug(`Successfully cleaned up network ${resource || 'unknown'}`); - } - }); - } catch (error) { - debug('Error during network cleanup: %o', error); - } + await orchestrator.cleanup(); debug('All test cleanups completed'); }, 120000); // 2 minute timeout for afterAll @@ -166,12 +93,19 @@ describe('Docker Orchestrator V2', () => { debug('Starting test: should start and stop a node'); // Create a new config with a unique ID for this test + const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`; const testNodeConfig = { ...nodeConfig, - id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, + id: `node-${testRunId}`, + networkId: `test-network-${testRunId}`, network: { ...nodeConfig.network, enableHttpApi: true + }, + // Add retry logic for Docker operations + docker: { + maxRetries: 3, + retryDelay: 1000 } }; @@ -209,7 +143,7 @@ describe('Docker Orchestrator V2', () => { debug('Error during node cleanup: %o', e); } } - }, 30000); // 30 second timeout for this test + }); it('should enforce resource limits', async () => { debug('Starting test: should enforce resource limits'); diff --git a/__tests__/schema.ts b/__tests__/schema.ts index 5ebc2ff..df18cbf 100644 --- a/__tests__/schema.ts +++ b/__tests__/schema.ts @@ -10,7 +10,7 @@ import { DefaultSchemaRegistry } from '../src/schema'; import { CommonSchemas } from '../util/schemas'; import { TypedCollectionImpl, SchemaValidationError } from '../src/collections'; import { RhizomeNode } from '../src/node'; -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; describe('Schema System', () => { let schemaRegistry: DefaultSchemaRegistry; @@ -20,8 +20,10 @@ describe('Schema System', () => { schemaRegistry = new DefaultSchemaRegistry(); node = new RhizomeNode(); }); + describe('Schema Builder', () => { + it('should create a basic schema', () => { const schema = SchemaBuilder .create('user') @@ -308,14 +310,10 @@ describe('Schema System', () => { await collection.put('user2', { name: 'Bob' }); // Manually create an invalid entity by bypassing validation - const invalidDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'user3', targetContext: 'email' }, - { localContext: 'email', target: 'invalid@test.com' } - ] - }); + const invalidDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'user3', 'email') + .addPointer('email', 'invalid@test.com') + .buildV1(); node.lossless.ingestDelta(invalidDelta); const stats = collection.getValidationStats(); @@ -337,14 +335,10 @@ describe('Schema System', () => { await collection.put('user2', { name: 'Bob' }); // Create invalid entity manually - const invalidDelta = new Delta({ - creator: node.config.creator, - host: node.config.peerId, - pointers: [ - { localContext: 'users', target: 'user3', targetContext: 'age' }, - { localContext: 'age', target: 'not-a-number' } - ] - }); + const invalidDelta = createDelta(node.config.creator, node.config.peerId) + .addPointer('users', 'user3', 'age') + .addPointer('age', 'not-a-number') + .buildV1(); node.lossless.ingestDelta(invalidDelta); const validIds = collection.getValidEntities(); diff --git a/__tests__/storage.ts b/__tests__/storage.ts index f10e4df..bf58f85 100644 --- a/__tests__/storage.ts +++ b/__tests__/storage.ts @@ -1,39 +1,25 @@ import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage'; +import { createDelta } from '../src/core/delta-builder'; import { Delta } from '../src/core'; import { DeltaQueryStorage } from '../src/storage/interface'; describe('Delta Storage', () => { const testDeltas = [ - new Delta({ - id: 'delta1', - creator: 'alice', - host: 'host1', - timeCreated: Date.now() - 1000, - pointers: [ - { localContext: 'user', target: 'user1', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }), - new Delta({ - id: 'delta2', - creator: 'bob', - host: 'host1', - timeCreated: Date.now() - 500, - pointers: [ - { localContext: 'user', target: 'user1', targetContext: 'age' }, - { localContext: 'value', target: 25 } - ] - }), - new Delta({ - id: 'delta3', - creator: 'alice', - host: 'host2', - timeCreated: Date.now(), - pointers: [ - { localContext: 'user', target: 'user2', targetContext: 'name' }, - { localContext: 'value', target: 'Bob' } - ] - }) + createDelta('alice', 'host1') + .withId('delta1') + .withTimestamp(Date.now() - 1000) + .setProperty('user1', 'name', 'Alice', 'user') + .buildV1(), + createDelta('bob', 'host1') + .withId('delta2') + .withTimestamp(Date.now() - 500) + .setProperty('user1', 'age', 25, 'user') + .buildV1(), + createDelta('alice', 'host2') + .withId('delta3') + .withTimestamp(Date.now()) + .setProperty('user2', 'name', 'Bob', 'user') + .buildV1() ]; describe('Memory Storage', () => { diff --git a/__tests__/timestamp-resolvers.ts b/__tests__/timestamp-resolvers.ts index 9541600..33c6160 100644 --- a/__tests__/timestamp-resolvers.ts +++ b/__tests__/timestamp-resolvers.ts @@ -1,13 +1,13 @@ import { RhizomeNode, Lossless, - Delta, TimestampResolver, CreatorIdTimestampResolver, DeltaIdTimestampResolver, HostIdTimestampResolver, LexicographicTimestampResolver } from "../src"; +import { createDelta } from "../src/core/delta-builder"; describe('Timestamp Resolvers', () => { let node: RhizomeNode; @@ -21,36 +21,22 @@ describe('Timestamp Resolvers', () => { describe('Basic Timestamp Resolution', () => { test('should resolve by most recent timestamp', () => { // Add older delta - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); // Add newer delta - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host2', - id: 'delta2', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + 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(); @@ -61,34 +47,20 @@ describe('Timestamp Resolvers', () => { test('should handle multiple entities with different timestamps', () => { // Entity1 - older value - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'value') + .addPointer('value', 100) + .buildV1() + ); // Entity2 - newer value - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - timeCreated: 2000, - pointers: [{ - localContext: "collection", - target: "entity2", - targetContext: "value" - }, { - localContext: "value", - target: 200 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withTimestamp(2000) + .addPointer('collection', 'entity2', 'value') + .addPointer('value', 200) + .buildV1() + ); const resolver = new TimestampResolver(lossless); const result = resolver.resolve(); @@ -102,35 +74,21 @@ describe('Timestamp Resolvers', () => { describe('Tie-Breaking Strategies', () => { test('should break ties using creator-id strategy', () => { // Two deltas with same timestamp, different creators - lossless.ingestDelta(new Delta({ - creator: 'user_z', // Lexicographically later - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('user_z', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user_a', // Lexicographically earlier - host: 'host1', - id: 'delta2', - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + 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(); @@ -142,35 +100,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using delta-id strategy', () => { // Two deltas with same timestamp, different delta IDs - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta_a', // Lexicographically earlier - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta_a') // Lexicographically earlier + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta_z', // Lexicographically later - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + 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(); @@ -182,35 +126,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using host-id strategy', () => { // Two deltas with same timestamp, different hosts - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host_z', // Lexicographically later - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host_z') // Lexicographically later + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 10) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host_a', // Lexicographically earlier - id: 'delta2', - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + 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(); @@ -222,35 +152,21 @@ describe('Timestamp Resolvers', () => { test('should break ties using lexicographic strategy with string values', () => { // Two deltas with same timestamp, different string values - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'alice' - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta2', - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'bob' - }] - })); + 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(); @@ -262,35 +178,21 @@ describe('Timestamp Resolvers', () => { 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(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta_a', // Lexicographically earlier - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta_a') // Lexicographically earlier + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'score') + .addPointer('score', 100) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta_z', // Lexicographically later - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 200 - }] - })); + 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(); @@ -304,35 +206,21 @@ describe('Timestamp Resolvers', () => { 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(new Delta({ - creator: 'user_a', - host: 'host1', - id: 'delta_z', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'alice' - }] - })); + lossless.ingestDelta(createDelta('user_a', 'host1') + .withId('delta_z') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'alice') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user_z', - host: 'host1', - id: 'delta_a', - timeCreated: 1000, // Same timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'bob' - }] - })); + 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); @@ -352,36 +240,22 @@ describe('Timestamp Resolvers', () => { test('should work consistently with timestamp priority over tie-breaking', () => { // Add older delta with "better" tie-breaking attributes - lossless.ingestDelta(new Delta({ - creator: 'user_z', // Would win in creator tie-breaking - host: 'host1', - id: 'delta_z', // Would win in delta ID tie-breaking - timeCreated: 1000, // Older timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 10 - }] - })); + 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(new Delta({ - creator: 'user_a', // Would lose in creator tie-breaking - host: 'host1', - id: 'delta_a', // Would lose in delta ID tie-breaking - timeCreated: 2000, // Newer timestamp - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 20 - }] - })); + 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(); @@ -394,20 +268,13 @@ describe('Timestamp Resolvers', () => { describe('Edge Cases', () => { test('should handle single delta correctly', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "value" - }, { - localContext: "value", - target: 42 - }] - })); + 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(); @@ -417,35 +284,21 @@ describe('Timestamp Resolvers', () => { }); test('should handle mixed value types correctly', () => { - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta1', - timeCreated: 1000, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "name" - }, { - localContext: "name", - target: 'test' - }] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .withId('delta1') + .withTimestamp(1000) + .addPointer('collection', 'entity1', 'name') + .addPointer('name', 'test') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - id: 'delta2', - timeCreated: 1001, - pointers: [{ - localContext: "collection", - target: "entity1", - targetContext: "score" - }, { - localContext: "score", - target: 100 - }] - })); + 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(); diff --git a/__tests__/transactions.ts b/__tests__/transactions.ts index 46c4db3..67ca445 100644 --- a/__tests__/transactions.ts +++ b/__tests__/transactions.ts @@ -1,4 +1,4 @@ -import { Delta } from '../src/core'; +import { createDelta } from '../src/core/delta-builder'; import { Lossless } from '../src/views'; import { RhizomeNode } from '../src/node'; import { DeltaFilter } from '../src/core'; @@ -17,36 +17,23 @@ describe('Transactions', () => { const transactionId = 'tx-123'; // Create a delta that declares a transaction with size 3 - const txDeclaration = new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 3 } - ] - }); + const txDeclaration = createDelta('system', 'host1') + .declareTransaction(transactionId, 3) + .buildV1(); // Create first delta in transaction - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'name', target: 'user123', targetContext: 'name' }, - { localContext: 'value', target: 'Alice' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('name', 'user123', 'name') + .addPointer('value', 'Alice') + .buildV1(); // Create second delta in transaction - const delta2 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'age', target: 'user123', targetContext: 'age' }, - { localContext: 'value', target: 25 } - ] - }); + const delta2 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('age', 'user123', 'age') + .addPointer('value', 25) + .buildV1(); // Ingest transaction declaration and first two deltas lossless.ingestDelta(txDeclaration); @@ -58,15 +45,11 @@ describe('Transactions', () => { expect(view.user123).toBeUndefined(); // Add the third delta to complete the transaction - const delta3 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'email', target: 'user123', targetContext: 'email' }, - { localContext: 'value', target: 'alice@example.com' } - ] - }); + const delta3 = createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('email', 'user123', 'email') + .addPointer('value', 'alice@example.com') + .buildV1(); lossless.ingestDelta(delta3); @@ -83,44 +66,30 @@ describe('Transactions', () => { const tx2 = 'tx-002'; // Declare two transactions - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: tx1, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(tx1, 2) + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: tx2, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(tx2, 2) + .buildV1() + ); // Add deltas for both transactions - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: tx1, targetContext: 'deltas' }, - { localContext: 'status', target: 'order1', targetContext: 'status' }, - { localContext: 'value', target: 'pending' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .inTransaction(tx1) + .addPointer('status', 'order1', 'status') + .addPointer('value', 'pending') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host2', - pointers: [ - { localContext: '_transaction', target: tx2, targetContext: 'deltas' }, - { localContext: 'status', target: 'order2', targetContext: 'status' }, - { localContext: 'value', target: 'shipped' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(tx2) + .addPointer('status', 'order2', 'status') + .addPointer('value', 'shipped') + .buildV1() + ); // Neither transaction is complete let view = lossless.view(['order1', 'order2']); @@ -128,15 +97,12 @@ describe('Transactions', () => { expect(view.order2).toBeUndefined(); // Complete tx1 - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: tx1, targetContext: 'deltas' }, - { localContext: 'total', target: 'order1', targetContext: 'total' }, - { localContext: 'value', target: 100 } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .inTransaction(tx1) + .addPointer('total', 'order1', 'total') + .addPointer('value', 100) + .buildV1() + ); // tx1 is complete, tx2 is not view = lossless.view(['order1', 'order2']); @@ -146,15 +112,12 @@ describe('Transactions', () => { expect(view.order2).toBeUndefined(); // Complete tx2 - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host2', - pointers: [ - { localContext: '_transaction', target: tx2, targetContext: 'deltas' }, - { localContext: 'tracking', target: 'order2', targetContext: 'tracking' }, - { localContext: 'value', target: 'TRACK123' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(tx2) + .addPointer('tracking', 'order2', 'tracking') + .addPointer('value', 'TRACK123') + .buildV1() + ); // Both transactions complete view = lossless.view(['order1', 'order2']); @@ -168,35 +131,25 @@ describe('Transactions', () => { const transactionId = 'tx-filter-test'; // Create transaction with 2 deltas - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .declareTransaction(transactionId, 2) + .buildV1() + ); // Add both deltas - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'type', target: 'doc1', targetContext: 'type' }, - { localContext: 'value', target: 'report' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .inTransaction(transactionId) + .addPointer('type', 'doc1', 'type') + .addPointer('value', 'report') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user2', - host: 'host2', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'author', target: 'doc1', targetContext: 'author' }, - { localContext: 'value', target: 'Bob' } - ] - })); + lossless.ingestDelta(createDelta('user2', 'host2') + .inTransaction(transactionId) + .addPointer('author', 'doc1', 'author') + .addPointer('value', 'Bob') + .buildV1() + ); // Create a filter that only accepts deltas from user1 const userFilter: DeltaFilter = (delta) => delta.creator === 'user1'; @@ -215,37 +168,28 @@ describe('Transactions', () => { const transactionId = 'tx-multi-entity'; // Transaction that updates multiple entities atomically - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 3 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 3) + .buildV1() + ); // Transfer money from account1 to account2 - lossless.ingestDelta(new Delta({ - creator: 'bank', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'balance', target: 'account1', targetContext: 'balance' }, - { localContext: 'value', target: 900 }, - { localContext: 'operation', target: 'debit' } - ] - })); + lossless.ingestDelta(createDelta('bank', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('balance', 'account1', 'balance') + .addPointer('value', 900) + .addPointer('operation', 'debit') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'bank', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'balance', target: 'account2', targetContext: 'balance' }, - { localContext: 'value', target: 1100 }, - { localContext: 'operation', target: 'credit' } - ] - })); + lossless.ingestDelta(createDelta('bank', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('balance', 'account2', 'balance') + .addPointer('value', 1100) + .addPointer('operation', 'credit') + .buildV1() + ); // Transaction incomplete - no entities should show updates let view = lossless.view(['account1', 'account2']); @@ -253,17 +197,14 @@ describe('Transactions', () => { expect(view.account2).toBeUndefined(); // Complete transaction with audit log - lossless.ingestDelta(new Delta({ - creator: 'bank', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'transfer', target: 'transfer123', targetContext: 'details' }, - { localContext: 'from', target: 'account1' }, - { localContext: 'to', target: 'account2' }, - { localContext: 'amount', target: 100 } - ] - })); + lossless.ingestDelta(createDelta('bank', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('transfer', 'transfer123', 'details') + .addPointer('from', 'account1') + .addPointer('to', 'account2') + .addPointer('amount', 100) + .buildV1() + ); // All entities should now be visible view = lossless.view(['account1', 'account2', 'transfer123']); @@ -285,40 +226,29 @@ describe('Transactions', () => { }); // Create transaction - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add first delta - const delta1 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'field1', target: 'entity1', targetContext: 'field1' }, - { localContext: 'value', target: 'value1' } - ] - }); + const delta1 = createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('field1', 'entity1', 'field1') + .addPointer('value', 'value1') + .buildV1(); lossless.ingestDelta(delta1); // No events should be emitted yet expect(updateEvents).toHaveLength(0); // Add second delta to complete transaction - const delta2 = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'field2', target: 'entity1', targetContext: 'field2' }, - { localContext: 'value', target: 'value2' } - ] - }); + const delta2 = createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('field2', 'entity1', 'field2') + .addPointer('value', 'value2') + .buildV1(); lossless.ingestDelta(delta2); // Wait for async event processing @@ -339,25 +269,19 @@ describe('Transactions', () => { const transactionId = 'tx-wait'; // Create transaction - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add first delta - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'status', target: 'job1', targetContext: 'status' }, - { localContext: 'value', target: 'processing' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('status', 'job1', 'status') + .addPointer('value', 'processing') + .buildV1() + ); // Start waiting for transaction const waitPromise = lossless.transactions.waitFor(transactionId); @@ -369,15 +293,12 @@ describe('Transactions', () => { expect(isResolved).toBe(false); // Complete transaction - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'status', target: 'job1', targetContext: 'status' }, - { localContext: 'value', target: 'completed' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('status', 'job1', 'status') + .addPointer('value', 'completed') + .buildV1() + ); // Wait should now resolve await waitPromise; @@ -391,14 +312,10 @@ describe('Transactions', () => { it('should handle non-transactional deltas normally', () => { // Regular delta without transaction - const regularDelta = new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: 'name', target: 'user456', targetContext: 'name' }, - { localContext: 'value', target: 'Charlie' } - ] - }); + const regularDelta = createDelta('user1', 'host1') + .addPointer('name', 'user456', 'name') + .addPointer('value', 'Charlie') + .buildV1(); const updateEvents: string[] = []; lossless.eventStream.on('updated', (entityId) => { @@ -422,33 +339,24 @@ describe('Transactions', () => { const transactionId = 'tx-resize'; // Initially declare transaction with size 2 - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 2 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 2) + .buildV1() + ); // Add 2 deltas - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'item1', target: 'cart1', targetContext: 'items' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('item1', 'cart1', 'items') + .buildV1() + ); - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'item2', target: 'cart1', targetContext: 'items' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('item2', 'cart1', 'items') + .buildV1() + ); // Transaction should be complete expect(lossless.transactions.isComplete(transactionId)).toBe(true); @@ -462,15 +370,12 @@ describe('Transactions', () => { const transactionId = 'tx-no-size'; // Add delta with transaction reference but no size declaration - lossless.ingestDelta(new Delta({ - creator: 'user1', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, - { localContext: 'data', target: 'entity1', targetContext: 'data' }, - { localContext: 'value', target: 'test' } - ] - })); + lossless.ingestDelta(createDelta('user1', 'host1') + .addPointer('_transaction', transactionId, 'deltas') + .addPointer('data', 'entity1', 'data') + .addPointer('value', 'test') + .buildV1() + ); // Transaction should not be complete (no size) expect(lossless.transactions.isComplete(transactionId)).toBe(false); @@ -480,14 +385,11 @@ describe('Transactions', () => { expect(view.entity1).toBeUndefined(); // Declare size after the fact - lossless.ingestDelta(new Delta({ - creator: 'system', - host: 'host1', - pointers: [ - { localContext: '_transaction', target: transactionId, targetContext: 'size' }, - { localContext: 'size', target: 1 } - ] - })); + lossless.ingestDelta(createDelta('system', 'host1') + .addPointer('_transaction', transactionId, 'size') + .addPointer('size', 1) + .buildV1() + ); // Now transaction should be complete expect(lossless.transactions.isComplete(transactionId)).toBe(true); diff --git a/package.json b/package.json index 0c23c35..421026c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "test": "jest", "coverage": "./scripts/coverage.sh", "coverage-report": "npm run test -- --coverage --coverageDirectory=coverage", - "example-app": "node dist/examples/app.js" + "example-app": "node dist/examples/app.js", + "stop-all": "docker ps -a --filter \"name=^/rhizome-node-\" --format {{.Names}} | xargs -r docker stop", + "build-test-image": "docker build -t rhizome-node-test -f Dockerfile.test ." }, "jest": { "testEnvironment": "node", @@ -35,11 +37,10 @@ "forceExit": true, "verbose": true, "testEnvironmentOptions": { - "NODE_ENV": "test", - "DEBUG": "rz:*" + "NODE_ENV": "test" } }, - "author": "Taliesin (Ladd) ", + "author": "Lentil ", "license": "Unlicense", "dependencies": { "@types/dockerode": "^3.3.40", diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index f08f493..d21dc54 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -134,6 +134,13 @@ export class DeltaBuilder { buildV1(): DeltaV1 { return this.buildV2().toV1(); } + + /** + * Default to V1 for now + */ + build(): DeltaV1 { + return this.buildV1(); + } } /** diff --git a/src/core/index.ts b/src/core/index.ts index 5d09e0f..f8a0681 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,5 @@ export * from './delta'; +export * from './delta-builder'; export * from './types'; export * from './context'; export { Entity } from './entity'; \ No newline at end of file diff --git a/src/orchestration/docker-orchestrator/managers/image-manager.ts b/src/orchestration/docker-orchestrator/managers/image-manager.ts index 5d59802..e1e63ce 100644 --- a/src/orchestration/docker-orchestrator/managers/image-manager.ts +++ b/src/orchestration/docker-orchestrator/managers/image-manager.ts @@ -63,16 +63,11 @@ export class ImageManager implements IImageManager { debug('Created build context tar stream'); testImageBuildPromise = new Promise((resolve, reject) => { - const logMessages: string[] = []; - const log = (...args: any[]) => { - const timestamp = new Date().toISOString(); const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) ).join(' '); - const logMessage = `[${timestamp}] ${message}\n`; - process.stdout.write(logMessage); - logMessages.push(logMessage); + debug(message); }; this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => { @@ -143,17 +138,4 @@ export class ImageManager implements IImageManager { }); }); } - - /** - * Check if an image exists locally - */ - async imageExists(imageName: string): Promise { - try { - const image = this.docker.getImage(imageName); - await image.inspect(); - return true; - } catch (error) { - return false; - } - } } diff --git a/src/query/query-engine.ts b/src/query/query-engine.ts index 2fa249b..36a7d58 100644 --- a/src/query/query-engine.ts +++ b/src/query/query-engine.ts @@ -258,7 +258,7 @@ export class QueryEngine { // Convert each schema property from lossless view deltas for (const [propertyId, propertySchema] of Object.entries(schema.properties)) { const deltas = view.propertyDeltas[propertyId] || []; - + if (deltas.length === 0) { obj[propertyId] = null; continue; @@ -268,9 +268,13 @@ export class QueryEngine { switch (propertySchema.type) { case 'primitive': { // Use last-write-wins for primitives - const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0]; - const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId); - obj[propertyId] = primitiveValue; + const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated); + for (let delta of deltasSorted) { + const primitiveValue = this.extractPrimitiveValue(delta, propertyId); + if (primitiveValue !== null) { + obj[propertyId] = primitiveValue; + } + } break; } @@ -304,12 +308,12 @@ export class QueryEngine { /** * Extract primitive value from a delta for a given property */ - private extractPrimitiveValue(delta: CollapsedDelta, _propertyId: string): unknown { + private extractPrimitiveValue(delta: CollapsedDelta, propertyId: string): unknown { // Look for the value in collapsed pointers // CollapsedPointer is {[key: PropertyID]: PropertyTypes} for (const pointer of delta.pointers) { - if (pointer.value !== undefined) { - return pointer.value; + if (pointer[propertyId] !== undefined) { + return pointer[propertyId]; } } return null; diff --git a/src/query/storage-query-engine.ts b/src/query/storage-query-engine.ts index 16f92be..3559ce4 100644 --- a/src/query/storage-query-engine.ts +++ b/src/query/storage-query-engine.ts @@ -263,6 +263,8 @@ export class StorageQueryEngine { default: properties[propertyId] = propDeltas.length; } + + debug(`Resolved property ${propertyId}:`, properties[propertyId]); } return properties; @@ -271,9 +273,9 @@ export class StorageQueryEngine { /** * Extract primitive value from a delta for a given property */ - private extractPrimitiveValue(delta: Delta, _propertyId: string): unknown { + private extractPrimitiveValue(delta: Delta, propertyId: string): unknown { for (const pointer of delta.pointers) { - if (pointer.localContext === 'value') { + if (pointer.localContext === propertyId) { return pointer.target; } } @@ -281,11 +283,11 @@ export class StorageQueryEngine { } /** - * Extract reference value (target ID) from a delta for a given property + * Extract reference value from a delta for a given property */ - private extractReferenceValue(delta: Delta, _propertyId: string): string | null { + private extractReferenceValue(delta: Delta, propertyId: string): string | null { for (const pointer of delta.pointers) { - if (pointer.localContext === 'value' && typeof pointer.target === 'string') { + if (pointer.localContext === propertyId && typeof pointer.target === 'string') { return pointer.target; } }