refactor: update test files to use DeltaBuilder fluent API

- Refactored delta creation in test files to use createDelta() pattern
- Replaced direct Delta instantiations with fluent builder API
- Updated relationship deltas to use setProperty with proper entity context
- Ensured all tests pass with the new delta creation approach

This is part of the ongoing effort to standardize on the DeltaBuilder
API across the codebase for better consistency and maintainability.
This commit is contained in:
Lentil Hoffman 2025-06-20 21:40:51 -05:00
parent 5c1c8a23b8
commit 60ad920b30
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
27 changed files with 1165 additions and 2426 deletions

View File

@ -2,5 +2,6 @@
description: Update the current file to use delta builder 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: Replace each delta instantiation with a fluent call to createDelta from delta builder
- pass creator and host as arguments to createDelta - pass creator and host as arguments to createDelta
- use setProperty where appropriate

View File

@ -1,7 +1,6 @@
import { import {
RhizomeNode, RhizomeNode,
Lossless, Lossless,
Delta,
AggregationResolver, AggregationResolver,
MinResolver, MinResolver,
MaxResolver, MaxResolver,
@ -10,6 +9,8 @@ import {
CountResolver, CountResolver,
AggregationType AggregationType
} from "../src"; } from "../src";
import { createDelta } from "../src/core/delta-builder";
import { Delta } from "../src/core/delta";
describe('Aggregation Resolvers', () => { describe('Aggregation Resolvers', () => {
let node: RhizomeNode; let node: RhizomeNode;
@ -23,46 +24,22 @@ describe('Aggregation Resolvers', () => {
describe('Basic Aggregation', () => { describe('Basic Aggregation', () => {
test('should aggregate numbers using min resolver', () => { test('should aggregate numbers using min resolver', () => {
// Add first entity with score 10 // Add first entity with score 10
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
// Add second entity with score 5 // Add second entity with score 5
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity2', 'score', 5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity2",
targetContext: "score"
}, {
localContext: "score",
target: 5
}]
}));
// Add third entity with score 15 // Add third entity with score 15
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity3', 'score', 15, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity3",
targetContext: "score"
}, {
localContext: "score",
target: 15
}]
}));
const minResolver = new MinResolver(lossless, ['score']); const minResolver = new MinResolver(lossless, ['score']);
const result = minResolver.resolve(); const result = minResolver.resolve();
@ -76,44 +53,20 @@ describe('Aggregation Resolvers', () => {
test('should aggregate numbers using max resolver', () => { test('should aggregate numbers using max resolver', () => {
// Add deltas for entities // Add deltas for entities
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity2', 'score', 5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity2",
targetContext: "score"
}, {
localContext: "score",
target: 5
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity3', 'score', 15, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity3",
targetContext: "score"
}, {
localContext: "score",
target: 15
}]
}));
const maxResolver = new MaxResolver(lossless, ['score']); const maxResolver = new MaxResolver(lossless, ['score']);
const result = maxResolver.resolve(); const result = maxResolver.resolve();
@ -126,46 +79,22 @@ describe('Aggregation Resolvers', () => {
test('should aggregate numbers using sum resolver', () => { test('should aggregate numbers using sum resolver', () => {
// Add first value for entity1 // Add first value for entity1
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 10
}]
}));
// Add second value for entity1 (should sum) // Add second value for entity1 (should sum)
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 20, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 20
}]
}));
// Add value for entity2 // Add value for entity2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity2', 'value', 5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity2",
targetContext: "value"
}, {
localContext: "value",
target: 5
}]
}));
const sumResolver = new SumResolver(lossless, ['value']); const sumResolver = new SumResolver(lossless, ['value']);
const result = sumResolver.resolve(); const result = sumResolver.resolve();
@ -176,46 +105,22 @@ describe('Aggregation Resolvers', () => {
}); });
test('should aggregate numbers using average resolver', () => { test('should aggregate numbers using average resolver', () => {
// Add multiple values for entity1 // Add multiple scores for entity1
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 20, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
// Single value for entity2 // Single value for entity2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity2', 'score', 30, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity2",
targetContext: "score"
}, {
localContext: "score",
target: 30
}]
}));
const avgResolver = new AverageResolver(lossless, ['score']); const avgResolver = new AverageResolver(lossless, ['score']);
const result = avgResolver.resolve(); const result = avgResolver.resolve();
@ -227,45 +132,21 @@ describe('Aggregation Resolvers', () => {
test('should count values using count resolver', () => { test('should count values using count resolver', () => {
// Add multiple visit deltas for entity1 // Add multiple visit deltas for entity1
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'visits', 1, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "visits"
}, {
localContext: "visits",
target: 1
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'visits', 1, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "visits"
}, {
localContext: "visits",
target: 1
}]
}));
// Single visit for entity2 // Single visit for entity2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity2', 'visits', 1, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity2",
targetContext: "visits"
}, {
localContext: "visits",
target: 1
}]
}));
const countResolver = new CountResolver(lossless, ['visits']); const countResolver = new CountResolver(lossless, ['visits']);
const result = countResolver.resolve(); const result = countResolver.resolve();
@ -279,84 +160,36 @@ describe('Aggregation Resolvers', () => {
describe('Custom Aggregation Configuration', () => { describe('Custom Aggregation Configuration', () => {
test('should handle mixed aggregation types', () => { test('should handle mixed aggregation types', () => {
// Add first set of values // Add first set of values
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'min_val', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "min_val"
}, {
localContext: "min_val",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'max_val', 5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "max_val"
}, {
localContext: "max_val",
target: 5
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'sum_val', 3, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "sum_val"
}, {
localContext: "sum_val",
target: 3
}]
}));
// Add second set of values // Add second set of values
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'min_val', 5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "min_val"
}, {
localContext: "min_val",
target: 5
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'max_val', 15, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "max_val"
}, {
localContext: "max_val",
target: 15
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'sum_val', 7, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "sum_val"
}, {
localContext: "sum_val",
target: 7
}]
}));
const resolver = new AggregationResolver(lossless, { const resolver = new AggregationResolver(lossless, {
min_val: 'min' as AggregationType, min_val: 'min' as AggregationType,
@ -375,46 +208,22 @@ describe('Aggregation Resolvers', () => {
test('should ignore non-numeric values', () => { test('should ignore non-numeric values', () => {
// Add numeric value // Add numeric value
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
// Add non-numeric value (string) // Add non-numeric value (string)
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'name', 'test', 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'test'
}]
}));
// Add another numeric value // Add another numeric value
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'score', 20, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const sumResolver = new SumResolver(lossless, ['score', 'name']); const sumResolver = new SumResolver(lossless, ['score', 'name']);
const result = sumResolver.resolve(); const result = sumResolver.resolve();
@ -427,18 +236,10 @@ describe('Aggregation Resolvers', () => {
test('should handle empty value arrays', () => { test('should handle empty value arrays', () => {
// Create entity with non-aggregated property // Create entity with non-aggregated property
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'name', 'test', 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'test'
}]
}));
const sumResolver = new SumResolver(lossless, ['score']); const sumResolver = new SumResolver(lossless, ['score']);
const result = sumResolver.resolve(); const result = sumResolver.resolve();
@ -451,18 +252,10 @@ describe('Aggregation Resolvers', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
test('should handle single value aggregations', () => { test('should handle single value aggregations', () => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 42, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 42
}]
}));
const avgResolver = new AverageResolver(lossless, ['value']); const avgResolver = new AverageResolver(lossless, ['value']);
const result = avgResolver.resolve(); const result = avgResolver.resolve();
@ -472,31 +265,15 @@ describe('Aggregation Resolvers', () => {
}); });
test('should handle zero values', () => { test('should handle zero values', () => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 0, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 0
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 10
}]
}));
const sumResolver = new SumResolver(lossless, ['value']); const sumResolver = new SumResolver(lossless, ['value']);
const result = sumResolver.resolve(); const result = sumResolver.resolve();
@ -506,31 +283,15 @@ describe('Aggregation Resolvers', () => {
}); });
test('should handle negative values', () => { test('should handle negative values', () => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', -5, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: -5
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('test', 'host1')
creator: 'test', .setProperty('entity1', 'value', 10, 'collection')
host: 'host1', .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 10
}]
}));
const minResolver = new MinResolver(lossless, ['value']); const minResolver = new MinResolver(lossless, ['value']);
const result = minResolver.resolve(); const result = minResolver.resolve();

View File

@ -5,7 +5,7 @@
*/ */
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
describe('Lossless View Compose/Decompose', () => { describe('Lossless View Compose/Decompose', () => {
let node: RhizomeNode; let node: RhizomeNode;
@ -18,22 +18,14 @@ describe('Lossless View Compose/Decompose', () => {
it('should compose and decompose simple entity deltas correctly', () => { it('should compose and decompose simple entity deltas correctly', () => {
// Create simple entity deltas // Create simple entity deltas
const nameDeltas = [ const nameDeltas = [
new Delta({ createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'name')
host: 'test-host', .addPointer('name', 'Alice Smith')
pointers: [ .buildV1(),
{ localContext: 'users', target: 'alice', targetContext: 'name' }, createDelta('test-creator', 'test-host')
{ localContext: 'name', target: 'Alice Smith' } .addPointer('users', 'alice', 'email')
] .addPointer('email', 'alice@example.com')
}), .buildV1()
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'email' },
{ localContext: 'email', target: 'alice@example.com' }
]
})
]; ];
// Ingest the deltas // Ingest the deltas
@ -73,17 +65,13 @@ describe('Lossless View Compose/Decompose', () => {
it('should handle multi-pointer relationship deltas correctly', () => { it('should handle multi-pointer relationship deltas correctly', () => {
// Create a complex relationship delta // Create a complex relationship delta
const relationshipDelta = new Delta({ const relationshipDelta = createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'relationships')
host: 'test-host', .addPointer('partner', 'bob')
pointers: [ .addPointer('type', 'friendship')
{ localContext: 'users', target: 'alice', targetContext: 'relationships' }, .addPointer('since', '2020-01-15')
{ localContext: 'partner', target: 'bob' }, .addPointer('intensity', 8)
{ localContext: 'type', target: 'friendship' }, .buildV1();
{ localContext: 'since', target: '2020-01-15' },
{ localContext: 'intensity', target: 8 }
]
});
node.lossless.ingestDelta(relationshipDelta); node.lossless.ingestDelta(relationshipDelta);
@ -115,33 +103,21 @@ describe('Lossless View Compose/Decompose', () => {
it('should handle reference relationships correctly', () => { it('should handle reference relationships correctly', () => {
// Create entities first // Create entities first
const aliceDelta = new Delta({ const aliceDelta = createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'name')
host: 'test-host', .addPointer('name', 'Alice')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice' }
]
});
const bobDelta = new Delta({ const bobDelta = createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'bob', 'name')
host: 'test-host', .addPointer('name', 'Bob')
pointers: [ .buildV1();
{ localContext: 'users', target: 'bob', targetContext: 'name' },
{ localContext: 'name', target: 'Bob' }
]
});
// Create friendship relationship // Create friendship relationship
const friendshipDelta = new Delta({ const friendshipDelta = createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'friends')
host: 'test-host', .addPointer('friend', 'bob', 'friends')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friend', target: 'bob', targetContext: 'friends' }
]
});
[aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d)); [aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d));
@ -171,14 +147,10 @@ describe('Lossless View Compose/Decompose', () => {
}); });
it('should preserve delta metadata correctly', () => { it('should preserve delta metadata correctly', () => {
const originalDelta = new Delta({ const originalDelta = createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'name')
host: 'test-host', .addPointer('name', 'Alice')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice' }
]
});
node.lossless.ingestDelta(originalDelta); node.lossless.ingestDelta(originalDelta);
@ -198,30 +170,18 @@ describe('Lossless View Compose/Decompose', () => {
it('should handle multiple deltas for the same property', () => { it('should handle multiple deltas for the same property', () => {
// Create multiple name changes for alice // Create multiple name changes for alice
const nameDeltas = [ const nameDeltas = [
new Delta({ createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'name')
host: 'test-host', .addPointer('name', 'Alice')
pointers: [ .buildV1(),
{ localContext: 'users', target: 'alice', targetContext: 'name' }, createDelta('test-creator', 'test-host')
{ localContext: 'name', target: 'Alice' } .addPointer('users', 'alice', 'name')
] .addPointer('name', 'Alice Smith')
}), .buildV1(),
new Delta({ createDelta('test-creator', 'test-host')
creator: 'test-creator', .addPointer('users', 'alice', 'name')
host: 'test-host', .addPointer('name', 'Alice Johnson')
pointers: [ .buildV1()
{ 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' }
]
})
]; ];
nameDeltas.forEach(d => node.lossless.ingestDelta(d)); nameDeltas.forEach(d => node.lossless.ingestDelta(d));

View File

@ -1,3 +1,4 @@
import { createDelta } from '../src/core/delta-builder';
import { import {
RhizomeNode, RhizomeNode,
Lossless, Lossless,
@ -24,35 +25,19 @@ describe('Concurrent Write Scenarios', () => {
const timestamp = 1000; const timestamp = 1000;
// Simulate two writers updating the same property at the exact same time // Simulate two writers updating the same property at the exact same time
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withId('delta-a')
host: 'host1', .withTimestamp(timestamp)
id: 'delta-a', .setProperty('entity1', 'score', 100, 'collection')
timeCreated: timestamp, .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer2', 'host2')
creator: 'writer2', .withId('delta-b')
host: 'host2', .withTimestamp(timestamp) // Same timestamp
id: 'delta-b', .setProperty('entity1', 'score', 200, 'collection')
timeCreated: timestamp, // Same timestamp .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 200
}]
}));
const resolver = new LastWriteWins(lossless); const resolver = new LastWriteWins(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -66,35 +51,19 @@ describe('Concurrent Write Scenarios', () => {
test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => { test('should handle simultaneous writes using timestamp resolver with tie-breaking', () => {
const timestamp = 1000; const timestamp = 1000;
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer_z', 'host1') // Lexicographically later
creator: 'writer_z', // Lexicographically later .withId('delta-a')
host: 'host1', .withTimestamp(timestamp)
id: 'delta-a', .setProperty('entity1', 'score', 100, 'collection')
timeCreated: timestamp, .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer_a', 'host2') // Lexicographically earlier
creator: 'writer_a', // Lexicographically earlier .withId('delta-b')
host: 'host2', .withTimestamp(timestamp) // Same timestamp
id: 'delta-b', .setProperty('entity1', 'score', 200, 'collection')
timeCreated: timestamp, // Same timestamp .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 200
}]
}));
const resolver = new TimestampResolver(lossless, 'creator-id'); const resolver = new TimestampResolver(lossless, 'creator-id');
const result = resolver.resolve(); const result = resolver.resolve();
@ -108,47 +77,24 @@ describe('Concurrent Write Scenarios', () => {
const timestamp = 1000; const timestamp = 1000;
// Multiple writers add values simultaneously // Multiple writers add values simultaneously
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(1000)
host: 'host1', .setProperty('entity1', 'points', 10, 'collection')
timeCreated: timestamp, .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "points"
}, {
localContext: "points",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer2', 'host2')
creator: 'writer2', .withTimestamp(1000) // Same timestamp
host: 'host2', .setProperty('entity1', 'points', 20, 'collection')
timeCreated: timestamp, .buildV1()
pointers: [{ );
localContext: "collection",
target: "entity1",
targetContext: "points"
}, {
localContext: "points",
target: 20
}]
}));
lossless.ingestDelta(new Delta({ // Third writer adds another value
creator: 'writer3', lossless.ingestDelta(createDelta('writer3', 'host3')
host: 'host3', .withTimestamp(1000) // Same timestamp
timeCreated: timestamp, .setProperty('entity1', 'points', 30, 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "points"
}, {
localContext: "points",
target: 30
}]
}));
const resolver = new SumResolver(lossless, ['points']); const resolver = new SumResolver(lossless, ['points']);
const result = resolver.resolve(); const result = resolver.resolve();
@ -162,34 +108,20 @@ describe('Concurrent Write Scenarios', () => {
describe('Out-of-Order Write Arrival', () => { describe('Out-of-Order Write Arrival', () => {
test('should handle writes arriving out of chronological order', () => { test('should handle writes arriving out of chronological order', () => {
// Newer delta arrives first // Newer delta arrives first
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(2000)
host: 'host1', .addPointer('collection', 'entity1', 'value')
timeCreated: 2000, // Later timestamp .addPointer('value', 'newer')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 'newer'
}]
}));
// Older delta arrives later // Older delta arrives later
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(1000)
host: 'host1', .addPointer('collection', 'entity1', 'value')
timeCreated: 1000, // Earlier timestamp .addPointer('value', 'older')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 'older'
}]
}));
const resolver = new LastWriteWins(lossless); const resolver = new LastWriteWins(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -201,47 +133,26 @@ describe('Concurrent Write Scenarios', () => {
test('should maintain correct aggregation despite out-of-order arrival', () => { test('should maintain correct aggregation despite out-of-order arrival', () => {
// Add deltas in reverse chronological order // Add deltas in reverse chronological order
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(3000)
host: 'host1', .addPointer('collection', 'entity1', 'score')
timeCreated: 3000, .addPointer('score', 30)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 30
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(1000)
host: 'host1', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(2000)
host: 'host1', .addPointer('collection', 'entity1', 'score')
timeCreated: 2000, .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new SumResolver(lossless, ['score']); const resolver = new SumResolver(lossless, ['score']);
const result = resolver.resolve(); const result = resolver.resolve();
@ -261,19 +172,12 @@ describe('Concurrent Write Scenarios', () => {
// Simulate multiple writers making rapid updates // Simulate multiple writers making rapid updates
for (let writer = 0; writer < numWriters; writer++) { for (let writer = 0; writer < numWriters; writer++) {
for (let write = 0; write < writesPerWriter; write++) { for (let write = 0; write < writesPerWriter; write++) {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`)
creator: `writer${writer}`, .withTimestamp(baseTimestamp + write)
host: `host${writer}`, .addPointer('collection', 'entity1', 'counter')
timeCreated: baseTimestamp + write, // Small time increments .addPointer('counter', 1)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "counter"
}, {
localContext: "counter",
target: 1 // Each update adds 1
}]
}));
} }
} }
@ -289,62 +193,34 @@ describe('Concurrent Write Scenarios', () => {
const timestamp = 1000; const timestamp = 1000;
// Writer 1 updates name and score // Writer 1 updates name and score
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(timestamp)
host: 'host1', .addPointer('collection', 'entity1', 'name')
timeCreated: timestamp, .addPointer('name', 'alice')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'alice'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(timestamp + 1)
host: 'host1', .addPointer('collection', 'entity1', 'score')
timeCreated: timestamp + 1, .addPointer('score', 100)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
// Writer 2 updates name and score concurrently // Writer 2 updates name and score concurrently
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer2', 'host2')
creator: 'writer2', .withTimestamp(timestamp + 2)
host: 'host2', .addPointer('collection', 'entity1', 'name')
timeCreated: timestamp + 2, .addPointer('name', 'bob')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'bob'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer2', 'host2')
creator: 'writer2', .withTimestamp(timestamp + 3)
host: 'host2', .addPointer('collection', 'entity1', 'score')
timeCreated: timestamp + 3, .addPointer('score', 200)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 200
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
name: new LastWriteWinsPlugin(), name: new LastWriteWinsPlugin(),
@ -365,19 +241,12 @@ describe('Concurrent Write Scenarios', () => {
// Multiple writers updating different entities simultaneously // Multiple writers updating different entities simultaneously
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta(`writer${i}`, `host${i}`)
creator: `writer${i}`, .withTimestamp(timestamp)
host: `host${i}`, .addPointer('collection', `entity${i}`, 'value')
timeCreated: timestamp, .addPointer('value', (i + 1) * 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: `entity${i}`,
targetContext: "value"
}, {
localContext: "value",
target: (i + 1) * 10 // Start from 10 to avoid 0 values
}]
}));
} }
const resolver = new LastWriteWins(lossless); const resolver = new LastWriteWins(lossless);
@ -395,62 +264,34 @@ describe('Concurrent Write Scenarios', () => {
const timestamp = 1000; const timestamp = 1000;
// Entity1: Multiple writers competing for same property // Entity1: Multiple writers competing for same property
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer1', 'host1')
creator: 'writer1', .withTimestamp(timestamp)
host: 'host1', .addPointer('collection', 'entity1', 'votes')
timeCreated: timestamp, .addPointer('votes', 'option_a')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "votes"
}, {
localContext: "votes",
target: 'option_a'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer2', 'host2')
creator: 'writer2', .withTimestamp(timestamp)
host: 'host2', .addPointer('collection', 'entity1', 'votes')
timeCreated: timestamp, .addPointer('votes', 'option_a')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "votes"
}, {
localContext: "votes",
target: 'option_a'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer3', 'host3')
creator: 'writer3', .withTimestamp(timestamp)
host: 'host3', .addPointer('collection', 'entity1', 'votes')
timeCreated: timestamp, .addPointer('votes', 'option_b')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "votes"
}, {
localContext: "votes",
target: 'option_b'
}]
}));
// Entity2: Single writer, no conflict // Entity2: Single writer, no conflict
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('writer4', 'host4')
creator: 'writer4', .withTimestamp(timestamp)
host: 'host4', .addPointer('collection', 'entity2', 'status')
timeCreated: timestamp, .addPointer('status', 'active')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity2",
targetContext: "status"
}, {
localContext: "status",
target: 'active'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
votes: new MajorityVotePlugin(), votes: new MajorityVotePlugin(),
@ -474,19 +315,12 @@ describe('Concurrent Write Scenarios', () => {
// Generate a large number of concurrent writes // Generate a large number of concurrent writes
for (let entity = 0; entity < numEntities; entity++) { for (let entity = 0; entity < numEntities; entity++) {
for (let writer = 0; writer < numWritersPerEntity; writer++) { for (let writer = 0; writer < numWritersPerEntity; writer++) {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta(`writer${writer}`, `host${writer}`)
creator: `writer${writer}`, .withTimestamp(baseTimestamp + Math.floor(Math.random() * 1000))
host: `host${writer}`, .addPointer('collection', `entity${entity}`, 'score')
timeCreated: baseTimestamp + Math.floor(Math.random() * 1000), // Random timestamps .addPointer('score', Math.floor(Math.random() * 100))
pointers: [{ .buildV1()
localContext: "collection", );
target: `entity${entity}`,
targetContext: "score"
}, {
localContext: "score",
target: Math.floor(Math.random() * 100) // Random scores
}]
}));
} }
} }
@ -510,19 +344,15 @@ describe('Concurrent Write Scenarios', () => {
// Add initial deltas // Add initial deltas
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta(
creator: `writer${i % 5}`, `writer${i % 5}`,
host: `host${i % 3}`, `host${i % 3}`
timeCreated: 1000 + i, )
pointers: [{ .withTimestamp(1000 + i)
localContext: "collection", .addPointer('collection', entityId, 'counter')
target: entityId, .addPointer('counter', 1)
targetContext: "counter" .buildV1()
}, { );
localContext: "counter",
target: 1
}]
}));
updateCount++; updateCount++;
} }
@ -534,19 +364,12 @@ describe('Concurrent Write Scenarios', () => {
// Add more deltas and verify consistency // Add more deltas and verify consistency
for (let i = 0; i < 25; i++) { for (let i = 0; i < 25; i++) {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('late-writer', 'late-host')
creator: 'late-writer', .withTimestamp(2000 + i)
host: 'late-host', .addPointer('collection', entityId, 'counter')
timeCreated: 2000 + i, .addPointer('counter', 2)
pointers: [{ .buildV1()
localContext: "collection", );
target: entityId,
targetContext: "counter"
}, {
localContext: "counter",
target: 2
}]
}));
updateCount += 2; updateCount += 2;
// Create a fresh resolver to avoid accumulator caching issues // Create a fresh resolver to avoid accumulator caching issues

View File

@ -11,7 +11,8 @@ import {
MinPlugin, MinPlugin,
MaxPlugin, MaxPlugin,
PropertyTypes, PropertyTypes,
CollapsedDelta CollapsedDelta,
createDelta
} from "../src"; } from "../src";
describe('Custom Resolvers', () => { describe('Custom Resolvers', () => {
@ -25,33 +26,21 @@ describe('Custom Resolvers', () => {
describe('Built-in Plugins', () => { describe('Built-in Plugins', () => {
test('LastWriteWinsPlugin should resolve to most recent value', () => { test('LastWriteWinsPlugin should resolve to most recent value', () => {
lossless.ingestDelta(new Delta({ // First delta with earlier timestamp
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'name', 'first', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "name"
}, {
localContext: "name",
target: 'first'
}]
}));
lossless.ingestDelta(new Delta({ // Second delta with later timestamp (should win)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'name', 'second', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "name"
}, {
localContext: "name",
target: 'second'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
name: new LastWriteWinsPlugin() name: new LastWriteWinsPlugin()
@ -63,33 +52,21 @@ describe('Custom Resolvers', () => {
}); });
test('FirstWriteWinsPlugin should resolve to earliest value', () => { test('FirstWriteWinsPlugin should resolve to earliest value', () => {
lossless.ingestDelta(new Delta({ // Later delta (should be ignored by FirstWriteWins)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'name', 'second', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "name"
}, {
localContext: "name",
target: 'second'
}]
}));
lossless.ingestDelta(new Delta({ // Earlier delta (should win with FirstWriteWins)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'name', 'first', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "name"
}, {
localContext: "name",
target: 'first'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
name: new FirstWriteWinsPlugin() name: new FirstWriteWinsPlugin()
@ -101,47 +78,29 @@ describe('Custom Resolvers', () => {
}); });
test('ConcatenationPlugin should join string values chronologically', () => { test('ConcatenationPlugin should join string values chronologically', () => {
lossless.ingestDelta(new Delta({ // First tag
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'tags', 'red', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "tags"
}, {
localContext: "tags",
target: 'red'
}]
}));
lossless.ingestDelta(new Delta({ // Second tag (with later timestamp)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 3000, .withTimestamp(3000)
pointers: [{ .setProperty('entity1', 'tags', 'blue', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "tags"
}, {
localContext: "tags",
target: 'blue'
}]
}));
lossless.ingestDelta(new Delta({ // Third tag (with timestamp between first and second)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'tags', 'green', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "tags"
}, {
localContext: "tags",
target: 'green'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
tags: new ConcatenationPlugin(' ') tags: new ConcatenationPlugin(' ')
@ -153,33 +112,21 @@ describe('Custom Resolvers', () => {
}); });
test('ConcatenationPlugin should handle duplicates', () => { test('ConcatenationPlugin should handle duplicates', () => {
lossless.ingestDelta(new Delta({ // First tag
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'tags', 'red', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "tags"
}, {
localContext: "tags",
target: 'red'
}]
}));
lossless.ingestDelta(new Delta({ // Duplicate tag with later timestamp
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'tags', 'red', 'collection') // duplicate
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "tags"
}, {
localContext: "tags",
target: 'red' // duplicate
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
tags: new ConcatenationPlugin(',') tags: new ConcatenationPlugin(',')
@ -192,76 +139,41 @@ describe('Custom Resolvers', () => {
test('MajorityVotePlugin should resolve to most voted value', () => { test('MajorityVotePlugin should resolve to most voted value', () => {
// Add 3 votes for 'red' // Add 3 votes for 'red'
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity1', 'color', 'red', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "color"
}, {
localContext: "color",
target: 'red'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user2', createDelta('user2', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1001, .setProperty('entity1', 'color', 'red', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "color"
}, {
localContext: "color",
target: 'red'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user3', createDelta('user3', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1002, .setProperty('entity1', 'color', 'red', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "color"
}, {
localContext: "color",
target: 'red'
}]
}));
// Add 2 votes for 'blue' // Add 2 votes for 'blue'
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user4', createDelta('user4', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1003, .setProperty('entity1', 'color', 'blue', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "color"
}, {
localContext: "color",
target: 'blue'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user5', createDelta('user5', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1004, .setProperty('entity1', 'color', 'blue', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "color"
}, {
localContext: "color",
target: 'blue'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
color: new MajorityVotePlugin() color: new MajorityVotePlugin()
@ -273,47 +185,29 @@ describe('Custom Resolvers', () => {
}); });
test('MinPlugin should resolve to minimum numeric value', () => { test('MinPlugin should resolve to minimum numeric value', () => {
lossless.ingestDelta(new Delta({ // First score (100)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'score', 100, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ // Second score (50) - this is the minimum
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'score', 50, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 50
}]
}));
lossless.ingestDelta(new Delta({ // Third score (75)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 3000, .withTimestamp(3000)
pointers: [{ .setProperty('entity1', 'score', 75, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 75
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
score: new MinPlugin() score: new MinPlugin()
@ -325,47 +219,29 @@ describe('Custom Resolvers', () => {
}); });
test('MaxPlugin should resolve to maximum numeric value', () => { test('MaxPlugin should resolve to maximum numeric value', () => {
lossless.ingestDelta(new Delta({ // First score (100)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'score', 100, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ // Second score (150) - this is the maximum
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'score', 150, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 150
}]
}));
lossless.ingestDelta(new Delta({ // Third score (75)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 3000, .withTimestamp(3000)
pointers: [{ .setProperty('entity1', 'score', 75, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 75
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
score: new MaxPlugin() score: new MaxPlugin()
@ -380,62 +256,36 @@ describe('Custom Resolvers', () => {
describe('Mixed Plugin Configurations', () => { describe('Mixed Plugin Configurations', () => {
test('should handle different plugins for different properties', () => { test('should handle different plugins for different properties', () => {
// Add name with different timestamps // Add name with different timestamps
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity1', 'name', 'old_name', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'old_name'
}]
}));
lossless.ingestDelta(new Delta({ // Update name with newer timestamp
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'name', 'new_name', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "name"
}, {
localContext: "name",
target: 'new_name'
}]
}));
// Add scores // Add scores
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity1', 'score', 100, 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ // Add another score (MinPlugin will pick the smaller one)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'score', 50, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 50
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
name: new LastWriteWinsPlugin(), // Should resolve to 'new_name' name: new LastWriteWinsPlugin(), // Should resolve to 'new_name'
@ -450,34 +300,20 @@ describe('Custom Resolvers', () => {
test('should only include entities with configured properties', () => { test('should only include entities with configured properties', () => {
// Entity1 has configured property // Entity1 has configured property
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity1', 'name', 'test', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'test'
}]
}));
// Entity2 has non-configured property // Entity2 has non-configured property
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity2', 'other_prop', 'value', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity2",
targetContext: "other"
}, {
localContext: "other",
target: 'value'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
name: new LastWriteWinsPlugin() name: new LastWriteWinsPlugin()
@ -510,47 +346,29 @@ describe('Custom Resolvers', () => {
} }
} }
lossless.ingestDelta(new Delta({ // First update
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'updates', 'first', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "updates"
}, {
localContext: "updates",
target: 'first'
}]
}));
lossless.ingestDelta(new Delta({ // Second update
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'updates', 'second', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "updates"
}, {
localContext: "updates",
target: 'second'
}]
}));
lossless.ingestDelta(new Delta({ // Third update
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 3000, .withTimestamp(3000)
pointers: [{ .setProperty('entity1', 'updates', 'third', 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "updates"
}, {
localContext: "updates",
target: 'third'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
updates: new CountPlugin() updates: new CountPlugin()
@ -585,47 +403,29 @@ describe('Custom Resolvers', () => {
} }
} }
lossless.ingestDelta(new Delta({ // First score (10)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 1000, .withTimestamp(1000)
pointers: [{ .setProperty('entity1', 'score', 10, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ // Second score (20)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 2000, .withTimestamp(2000)
pointers: [{ .setProperty('entity1', 'score', 20, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
lossless.ingestDelta(new Delta({ // Third score (30)
creator: 'user1', lossless.ingestDelta(
host: 'host1', createDelta('user1', 'host1')
timeCreated: 3000, .withTimestamp(3000)
pointers: [{ .setProperty('entity1', 'score', 30, 'collection')
localContext: "collection", .buildV1()
target: "entity1", );
targetContext: "score"
}, {
localContext: "score",
target: 30
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
score: new RunningAveragePlugin() score: new RunningAveragePlugin()
@ -650,19 +450,12 @@ describe('Custom Resolvers', () => {
test('should handle non-matching property types gracefully', () => { test('should handle non-matching property types gracefully', () => {
// Add string value to numeric plugin // Add string value to numeric plugin
lossless.ingestDelta(new Delta({ lossless.ingestDelta(
creator: 'user1', createDelta('user1', 'host1')
host: 'host1', .withTimestamp(1000)
timeCreated: 1000, .setProperty('entity1', 'score', 'not_a_number', 'collection')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 'not_a_number'
}]
}));
const resolver = new CustomResolver(lossless, { const resolver = new CustomResolver(lossless, {
score: new MinPlugin() // Expects numeric values score: new MinPlugin() // Expects numeric values

View File

@ -1,19 +1,12 @@
import { createDelta } from '../src/core/delta-builder';
import {DeltaV1, DeltaV2} from "../src"; import {DeltaV1, DeltaV2} from "../src";
describe("Delta", () => { describe("Delta", () => {
it("can convert DeltaV1 to DeltaV2", () => { it("can convert DeltaV1 to DeltaV2", () => {
const deltaV1 = new DeltaV1({ const deltaV1 = createDelta('a', 'h')
creator: 'a', .addPointer('color', 'red')
host: 'h', .addPointer('furniture', 'chair-1', 'color')
pointers: [{ .buildV1();
localContext: 'color',
target: 'red'
}, {
localContext: 'furniture',
target: 'chair-1',
targetContext: 'color'
}]
});
const deltaV2 = DeltaV2.fromV1(deltaV1); const deltaV2 = DeltaV2.fromV1(deltaV1);
@ -27,14 +20,10 @@ describe("Delta", () => {
}); });
it("can convert DeltaV2 to DeltaV1", () => { it("can convert DeltaV2 to DeltaV1", () => {
const deltaV2 = new DeltaV2({ const deltaV2 = createDelta('a', 'h')
creator: 'a', .addPointer('color', 'red')
host: 'h', .addPointer('furniture', 'chair-1', 'color')
pointers: { .buildV2();
color: 'red',
furniture: {'chair-1': 'color'}
}
});
const deltaV1 = deltaV2.toV1(); const deltaV1 = deltaV2.toV1();

View File

@ -1,5 +1,11 @@
// Set up environment variables for tests // 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 // Extend the global Jest namespace
declare global { declare global {

View File

@ -1,4 +1,5 @@
import Debug from "debug"; import Debug from "debug";
import { createDelta } from '../src/core/delta-builder';
import {Delta, LastWriteWins, Lossless, RhizomeNode} from "../src"; import {Delta, LastWriteWins, Lossless, RhizomeNode} from "../src";
const debug = Debug('test:last-write-wins'); const debug = Debug('test:last-write-wins');
@ -11,31 +12,15 @@ describe('Last write wins', () => {
const lossy = new LastWriteWins(lossless); const lossy = new LastWriteWins(lossless);
beforeAll(() => { beforeAll(() => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('a', 'h')
creator: 'a', .setProperty('broccoli', 'want', 95, 'vegetable')
host: 'h', .buildV1()
pointers: [{ );
localContext: "vegetable",
target: "broccoli",
targetContext: "want"
}, {
localContext: "desire",
target: 95,
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('a', 'h')
creator: 'a', .setProperty('broccoli', 'want', 90, 'vegetable')
host: 'h', .buildV1()
pointers: [{ );
localContext: "vegetable",
target: "broccoli",
targetContext: "want"
}, {
localContext: "want",
target: 90,
}]
}));
}); });
it('our resolver should return the most recently written value', () => { it('our resolver should return the most recently written value', () => {

View File

@ -1,6 +1,5 @@
import Debug from 'debug'; import Debug from 'debug';
import { import {
Delta,
PointerTarget, PointerTarget,
lastValueFromDeltas, lastValueFromDeltas,
valueFromCollapsedDelta, valueFromCollapsedDelta,
@ -9,6 +8,7 @@ import {
Lossy, Lossy,
RhizomeNode RhizomeNode
} from "../src"; } from "../src";
import { createDelta } from "../src/core/delta-builder";
const debug = Debug('test:lossy'); const debug = Debug('test:lossy');
type Role = { type Role = {
@ -62,29 +62,14 @@ describe('Lossy', () => {
const lossy = new Summarizer(lossless); const lossy = new Summarizer(lossless);
beforeAll(() => { beforeAll(() => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('a', 'h')
creator: 'a', .addPointer('actor', 'keanu', 'roles')
host: 'h', .addPointer('role', 'neo', 'actor')
pointers: [{ .addPointer('film', 'the_matrix', 'cast')
localContext: "actor", .addPointer('base_salary', 1000000)
target: "keanu", .addPointer('salary_currency', 'usd')
targetContext: "roles" .buildV1()
}, { );
localContext: "role",
target: "neo",
targetContext: "actor"
}, {
localContext: "film",
target: "the_matrix",
targetContext: "cast"
}, {
localContext: "base_salary",
target: 1000000
}, {
localContext: "salary_currency",
target: "usd"
}]
}));
}); });
it('example summary', () => { it('example summary', () => {

View File

@ -5,7 +5,7 @@
*/ */
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
import { DefaultSchemaRegistry } from '../src/schema'; import { DefaultSchemaRegistry } from '../src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema';
import { TypedCollectionImpl } from '../src/collections'; import { TypedCollectionImpl } from '../src/collections';
@ -76,17 +76,13 @@ describe('Multi-Pointer Delta Resolution', () => {
await roleCollection.put('neo', { name: 'Neo' }); await roleCollection.put('neo', { name: 'Neo' });
// Create a complex casting delta with multiple entity references and scalar values // Create a complex casting delta with multiple entity references and scalar values
const castingDelta = new Delta({ const castingDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('actors', 'keanu', 'filmography')
host: node.config.peerId, .addPointer('movies', 'matrix', 'cast')
pointers: [ .addPointer('roles', 'neo', 'portrayals')
{ localContext: 'actors', target: 'keanu', targetContext: 'filmography' }, .addPointer('salary', 15000000)
{ localContext: 'movies', target: 'matrix', targetContext: 'cast' }, .addPointer('contract_date', '1999-03-31')
{ localContext: 'roles', target: 'neo', targetContext: 'portrayals' }, .buildV1();
{ localContext: 'salary', target: 15000000 },
{ localContext: 'contract_date', target: '1999-03-31' }
]
});
node.lossless.ingestDelta(castingDelta); node.lossless.ingestDelta(castingDelta);
// Test from Keanu's perspective // Test from Keanu's perspective
@ -164,17 +160,13 @@ describe('Multi-Pointer Delta Resolution', () => {
await personCollection.put('bob', { name: 'Bob' }); await personCollection.put('bob', { name: 'Bob' });
// Create a relationship delta with one entity reference and multiple scalars // Create a relationship delta with one entity reference and multiple scalars
const relationshipDelta = new Delta({ const relationshipDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('people', 'alice', 'relationships')
host: node.config.peerId, .addPointer('partner', 'bob')
pointers: [ .addPointer('type', 'friendship')
{ localContext: 'people', target: 'alice', targetContext: 'relationships' }, .addPointer('since', '2020-01-15')
{ localContext: 'partner', target: 'bob' }, // Entity reference .addPointer('intensity', 8)
{ localContext: 'type', target: 'friendship' }, // Scalar .buildV1();
{ localContext: 'since', target: '2020-01-15' }, // Scalar
{ localContext: 'intensity', target: 8 } // Scalar number
]
});
node.lossless.ingestDelta(relationshipDelta); node.lossless.ingestDelta(relationshipDelta);
// Test from Alice's perspective // Test from Alice's perspective
@ -243,17 +235,13 @@ describe('Multi-Pointer Delta Resolution', () => {
await designerCollection.put('bob', { name: 'Bob Designer' }); await designerCollection.put('bob', { name: 'Bob Designer' });
// Create a collaboration delta with multiple entity references // Create a collaboration delta with multiple entity references
const collaborationDelta = new Delta({ const collaborationDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('projects', 'website', 'collaborations')
host: node.config.peerId, .addPointer('developer', 'alice')
pointers: [ .addPointer('designer', 'bob')
{ localContext: 'projects', target: 'website', targetContext: 'collaborations' }, .addPointer('budget', 50000)
{ localContext: 'developer', target: 'alice' }, // Entity reference .addPointer('deadline', '2024-06-01')
{ localContext: 'designer', target: 'bob' }, // Entity reference .buildV1();
{ localContext: 'budget', target: 50000 }, // Scalar
{ localContext: 'deadline', target: '2024-06-01' } // Scalar
]
});
node.lossless.ingestDelta(collaborationDelta); node.lossless.ingestDelta(collaborationDelta);
// Test from project's perspective // Test from project's perspective

View File

@ -1,4 +1,5 @@
import Debug from 'debug'; import Debug from 'debug';
import { createDelta } from '../src/core/delta-builder';
import { Delta } from '../src/core'; import { Delta } from '../src/core';
import { NegationHelper } from '../src/features'; import { NegationHelper } from '../src/features';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
@ -17,14 +18,9 @@ describe('Negation System', () => {
describe('Negation Helper', () => { describe('Negation Helper', () => {
it('should create negation deltas correctly', () => { it('should create negation deltas correctly', () => {
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Alice')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'name', target: 'entity1', targetContext: 'name' },
{ localContext: 'value', target: 'Alice' }
]
});
const negationDelta = NegationHelper.createNegation( const negationDelta = NegationHelper.createNegation(
originalDelta.id, originalDelta.id,
@ -44,11 +40,9 @@ describe('Negation System', () => {
}); });
it('should identify negation deltas', () => { it('should identify negation deltas', () => {
const regularDelta = new Delta({ const regularDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
const negationDelta = NegationHelper.createNegation( const negationDelta = NegationHelper.createNegation(
'delta-to-negate', 'delta-to-negate',
@ -71,27 +65,21 @@ describe('Negation System', () => {
const extractedId = NegationHelper.getNegatedDeltaId(negationDelta); const extractedId = NegationHelper.getNegatedDeltaId(negationDelta);
expect(extractedId).toBe(targetDeltaId); expect(extractedId).toBe(targetDeltaId);
const regularDelta = new Delta({ const regularDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull(); expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull();
}); });
it('should find negations for specific deltas', () => { it('should find negations for specific deltas', () => {
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('entity1', 'age', 25)
host: 'host1', .buildV1();
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1'); const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1');
@ -110,17 +98,13 @@ describe('Negation System', () => {
}); });
it('should check if deltas are negated', () => { it('should check if deltas are negated', () => {
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('entity1', 'age', 25)
host: 'host1', .buildV1();
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const allDeltas = [delta1, delta2, negation1]; const allDeltas = [delta1, delta2, negation1];
@ -130,23 +114,17 @@ describe('Negation System', () => {
}); });
it('should filter out negated deltas', () => { it('should filter out negated deltas', () => {
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('entity1', 'age', 25)
host: 'host1', .buildV1();
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
});
const delta3 = new Delta({ const delta3 = createDelta('user3', 'host1')
creator: 'user3', .setProperty('entity1', 'email', 'entity1@example.com')
host: 'host1', .buildV1();
pointers: [{ localContext: 'email', target: 'entity1', targetContext: 'email' }]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1'); const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1');
@ -160,17 +138,13 @@ describe('Negation System', () => {
}); });
it('should provide negation statistics', () => { it('should provide negation statistics', () => {
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity1', 'name', 'Entity 1')
host: 'host1', .buildV1();
pointers: [{ localContext: 'name', target: 'entity1', targetContext: 'name' }]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('entity1', 'age', 25)
host: 'host1', .buildV1();
pointers: [{ localContext: 'age', target: 'entity1', targetContext: 'age' }]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const allDeltas = [delta1, delta2, negation1]; const allDeltas = [delta1, delta2, negation1];
@ -189,22 +163,18 @@ describe('Negation System', () => {
const baseTime = Date.now(); const baseTime = Date.now();
// Create deltas with specific timestamps // Create deltas with specific timestamps
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(baseTime)
host: 'host1', .setProperty('entity1', 'status', 'active')
timeCreated: baseTime, .buildV1();
pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
negation1.timeCreated = baseTime + 1000; // 1 second later negation1.timeCreated = baseTime + 1000; // 1 second later
const delta2 = new Delta({ const delta2 = createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(baseTime + 2000)
host: 'host1', .setProperty('entity1', 'status', 'inactive')
timeCreated: baseTime + 2000, // 2 seconds later .buildV1();
pointers: [{ localContext: 'status', target: 'doc1', targetContext: 'status' }]
});
const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1'); const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1');
negation2.timeCreated = baseTime + 3000; // 3 seconds later negation2.timeCreated = baseTime + 3000; // 3 seconds later
@ -220,14 +190,9 @@ describe('Negation System', () => {
describe('Lossless View Integration', () => { describe('Lossless View Integration', () => {
it('should filter negated deltas in lossless views', () => { it('should filter negated deltas in lossless views', () => {
// Create original delta // Create original delta
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('user123', 'name', 'Alice')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'name', target: 'user123', targetContext: 'name' },
{ localContext: 'value', target: 'Alice' }
]
});
// Create negation delta // Create negation delta
const negationDelta = NegationHelper.createNegation( const negationDelta = NegationHelper.createNegation(
@ -238,14 +203,9 @@ describe('Negation System', () => {
// Create another non-negated delta // Create another non-negated delta
const nonNegatedDelta = new Delta({ const nonNegatedDelta = createDelta('user2', 'host1')
creator: 'user2', .setProperty('user123', 'age', 25)
host: 'host1', .buildV1();
pointers: [
{ localContext: 'age', target: 'user123', targetContext: 'age' },
{ localContext: 'value', target: 25 }
]
});
// Ingest all deltas // Ingest all deltas
lossless.ingestDelta(originalDelta); lossless.ingestDelta(originalDelta);
@ -263,14 +223,9 @@ describe('Negation System', () => {
}); });
it('should handle multiple negations of the same delta', () => { it('should handle multiple negations of the same delta', () => {
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('post1', 'content', 'Original content')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'content', target: 'post1', targetContext: 'content' },
{ localContext: 'value', target: 'Original content' }
]
});
const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1'); const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1');
@ -286,23 +241,13 @@ describe('Negation System', () => {
}); });
it('should provide negation statistics for entities', () => { it('should provide negation statistics for entities', () => {
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('article1', 'title', 'Original Title')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'title', target: 'article1', targetContext: 'title' },
{ localContext: 'value', target: 'Original Title' }
]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('article1', 'content', 'Article content')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'content', target: 'article1', targetContext: 'content' },
{ localContext: 'value', target: 'Article content' }
]
});
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
@ -321,14 +266,9 @@ describe('Negation System', () => {
}); });
it('should retrieve negation deltas for entities', () => { it('should retrieve negation deltas for entities', () => {
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('task1', 'status', 'pending')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'status', target: 'task1', targetContext: 'status' },
{ localContext: 'value', target: 'pending' }
]
});
const negationDelta = NegationHelper.createNegation( const negationDelta = NegationHelper.createNegation(
originalDelta.id, originalDelta.id,
@ -349,25 +289,16 @@ describe('Negation System', () => {
const transactionId = 'tx-negation'; const transactionId = 'tx-negation';
// Create transaction declaration // Create transaction declaration
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .declareTransaction(transactionId, 2)
host: 'host1', .buildV1()
pointers: [ );
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
{ localContext: 'size', target: 2 }
]
}));
// Create original delta in transaction // Create original delta in transaction
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .declareTransaction(transactionId, 2)
host: 'host1', .setProperty('post1', 'comments', 'Inappropriate comment')
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ localContext: 'comment', target: 'post1', targetContext: 'comments' },
{ localContext: 'text', target: 'Inappropriate comment' }
]
});
// Create negation delta in same transaction // Create negation delta in same transaction
const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1'); const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1');
@ -389,30 +320,20 @@ describe('Negation System', () => {
const baseTime = Date.now(); const baseTime = Date.now();
// User posts content // User posts content
const postDelta = new Delta({ const postDelta = createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(baseTime)
host: 'host1', .setProperty('post1', 'content', 'Original post')
timeCreated: baseTime, .buildV1();
pointers: [
{ localContext: 'content', target: 'post1', targetContext: 'content' },
{ localContext: 'value', target: 'Original post' }
]
});
// Moderator negates it // Moderator negates it
const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1'); const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1');
negationDelta.timeCreated = baseTime + 1000; negationDelta.timeCreated = baseTime + 1000;
// User edits content (after negation) // User edits content (after negation)
const editDelta = new Delta({ const editDelta = createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(baseTime + 2000)
host: 'host1', .setProperty('post1', 'content', 'Edited post')
timeCreated: baseTime + 2000, .buildV1();
pointers: [
{ localContext: 'content', target: 'post1', targetContext: 'content' },
{ localContext: 'value', target: 'Edited post' }
]
});
lossless.ingestDelta(postDelta); lossless.ingestDelta(postDelta);
lossless.ingestDelta(negationDelta); lossless.ingestDelta(negationDelta);
@ -447,14 +368,10 @@ describe('Negation System', () => {
it('should handle self-referential entities in negations', () => { it('should handle self-referential entities in negations', () => {
// Create a delta that references itself // Create a delta that references itself
const selfRefDelta = new Delta({ const selfRefDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('node1', 'parent', 'node1')
host: 'host1', .setProperty('node1', 'child', 'node1') // Self-reference
pointers: [ .buildV1();
{ localContext: 'parent', target: 'node1', targetContext: 'parent' },
{ localContext: 'child', target: 'node1' } // Self-reference
]
});
const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1'); const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1');
@ -470,14 +387,9 @@ describe('Negation System', () => {
const testLossless = new Lossless(testNode); const testLossless = new Lossless(testNode);
// Create the original delta // Create the original delta
const originalDelta = new Delta({ const originalDelta = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity2', 'title', 'Draft')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'title', target: 'entity2', targetContext: 'title' },
{ localContext: 'status', target: 'Draft' }
]
});
// Create two negations of the same delta // Create two negations of the same delta
const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1'); const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1');
@ -506,14 +418,9 @@ describe('Negation System', () => {
const testLossless = new Lossless(testNode); const testLossless = new Lossless(testNode);
// Create the original delta // Create the original delta
const deltaA = new Delta({ const deltaA = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity3', 'content', 'Hello World')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'content', target: 'entity3', targetContext: 'content' },
{ localContext: 'text', target: 'Hello World' }
]
});
// Create a chain of negations: B negates A, C negates B, D negates C // Create a chain of negations: B negates A, C negates B, D negates C
const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1'); const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1');
@ -584,23 +491,13 @@ describe('Negation System', () => {
const testLossless = new Lossless(testNode); const testLossless = new Lossless(testNode);
// Create two independent deltas // Create two independent deltas
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .setProperty('entity4', 'item', 'Item 1')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
{ localContext: 'name', target: 'Item 1' }
]
});
const delta2 = new Delta({ const delta2 = createDelta('user2', 'host1')
creator: 'user2', .setProperty('entity4', 'item', 'Item 2')
host: 'host1', .buildV1();
pointers: [
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
{ localContext: 'name', target: 'Item 2' }
]
});
// Create negations for both deltas // Create negations for both deltas
const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1'); const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1');

View File

@ -12,7 +12,7 @@ import Debug from 'debug';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
const debug = Debug('rz:test:nested-resolution-performance'); 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 { DefaultSchemaRegistry } from '../src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema';
import { TypedCollectionImpl } from '../src/collections'; import { TypedCollectionImpl } from '../src/collections';
@ -80,14 +80,9 @@ describe('Nested Object Resolution Performance', () => {
const friendIndex = Math.floor(Math.random() * userCount); const friendIndex = Math.floor(Math.random() * userCount);
if (friendIndex !== i) { if (friendIndex !== i) {
const friendId = userIds[friendIndex]; const friendId = userIds[friendIndex];
const friendshipDelta = new Delta({ const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .setProperty(userId, 'friends', friendId, 'users')
host: node.config.peerId, .buildV1();
pointers: [
{ localContext: 'users', target: userId, targetContext: 'friends' },
{ localContext: 'friends', target: friendId }
]
});
node.lossless.ingestDelta(friendshipDelta); node.lossless.ingestDelta(friendshipDelta);
} }
} }
@ -98,14 +93,9 @@ describe('Nested Object Resolution Performance', () => {
const followerIndex = Math.floor(Math.random() * userCount); const followerIndex = Math.floor(Math.random() * userCount);
if (followerIndex !== i) { if (followerIndex !== i) {
const followerId = userIds[followerIndex]; const followerId = userIds[followerIndex];
const followDelta = new Delta({ const followDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .setProperty(userId, 'followers', followerId, 'users')
host: node.config.peerId, .buildV1();
pointers: [
{ localContext: 'users', target: userId, targetContext: 'followers' },
{ localContext: 'followers', target: followerId }
]
});
node.lossless.ingestDelta(followDelta); node.lossless.ingestDelta(followDelta);
} }
} }
@ -114,14 +104,9 @@ describe('Nested Object Resolution Performance', () => {
if (i > 0) { if (i > 0) {
const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure
const mentorId = userIds[mentorIndex]; const mentorId = userIds[mentorIndex];
const mentorshipDelta = new Delta({ const mentorshipDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .setProperty(userId, 'mentor', mentorId, 'users')
host: node.config.peerId, .buildV1();
pointers: [
{ localContext: 'users', target: userId, targetContext: 'mentor' },
{ localContext: 'mentor', target: mentorId }
]
});
node.lossless.ingestDelta(mentorshipDelta); node.lossless.ingestDelta(mentorshipDelta);
} }
} }

View File

@ -10,11 +10,11 @@
*/ */
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { Delta } from '../src/core';
import { DefaultSchemaRegistry } from '../src/schema'; import { DefaultSchemaRegistry } from '../src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema';
import { CommonSchemas } from '../util/schemas'; import { CommonSchemas } from '../util/schemas';
import { TypedCollectionImpl } from '../src/collections'; import { TypedCollectionImpl } from '../src/collections';
import { createDelta } from '../src/core/delta-builder';
describe('Nested Object Resolution', () => { describe('Nested Object Resolution', () => {
let node: RhizomeNode; let node: RhizomeNode;
@ -55,14 +55,10 @@ describe('Nested Object Resolution', () => {
}); });
// Create friendship relationship // Create friendship relationship
const friendshipDelta = new Delta({ const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'bob')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
node.lossless.ingestDelta(friendshipDelta); node.lossless.ingestDelta(friendshipDelta);
// Get Alice's lossless view // Get Alice's lossless view
@ -107,14 +103,10 @@ describe('Nested Object Resolution', () => {
// Create user with reference to non-existent friend // Create user with reference to non-existent friend
await userCollection.put('alice', { name: 'Alice' }); await userCollection.put('alice', { name: 'Alice' });
const friendshipDelta = new Delta({ const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'nonexistent')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'nonexistent' }
]
});
node.lossless.ingestDelta(friendshipDelta); node.lossless.ingestDelta(friendshipDelta);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);
@ -162,25 +154,17 @@ describe('Nested Object Resolution', () => {
await userCollection.put('charlie', { name: 'Charlie' }); await userCollection.put('charlie', { name: 'Charlie' });
// Alice's mentor is Bob // Alice's mentor is Bob
const mentorshipDelta1 = new Delta({ const mentorshipDelta1 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('deep-users', 'alice', 'mentor')
host: node.config.peerId, .addPointer('mentor', 'bob')
pointers: [ .buildV1();
{ localContext: 'deep-users', target: 'alice', targetContext: 'mentor' },
{ localContext: 'mentor', target: 'bob' }
]
});
node.lossless.ingestDelta(mentorshipDelta1); node.lossless.ingestDelta(mentorshipDelta1);
// Bob's mentor is Charlie // Bob's mentor is Charlie
const mentorshipDelta2 = new Delta({ const mentorshipDelta2 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('deep-users', 'bob', 'mentor')
host: node.config.peerId, .addPointer('mentor', 'charlie')
pointers: [ .buildV1();
{ localContext: 'deep-users', target: 'bob', targetContext: 'mentor' },
{ localContext: 'mentor', target: 'charlie' }
]
});
node.lossless.ingestDelta(mentorshipDelta2); node.lossless.ingestDelta(mentorshipDelta2);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);
@ -246,24 +230,16 @@ describe('Nested Object Resolution', () => {
await userCollection.put('bob', { name: 'Bob' }); await userCollection.put('bob', { name: 'Bob' });
// Create circular friendship: Alice -> Bob -> Alice // Create circular friendship: Alice -> Bob -> Alice
const friendship1 = new Delta({ const friendship1 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'bob')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
node.lossless.ingestDelta(friendship1); node.lossless.ingestDelta(friendship1);
const friendship2 = new Delta({ const friendship2 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'bob', 'friends')
host: node.config.peerId, .addPointer('friends', 'alice')
pointers: [ .buildV1();
{ localContext: 'users', target: 'bob', targetContext: 'friends' },
{ localContext: 'friends', target: 'alice' }
]
});
node.lossless.ingestDelta(friendship2); node.lossless.ingestDelta(friendship2);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);
@ -295,14 +271,10 @@ describe('Nested Object Resolution', () => {
await userCollection.put('alice', { name: 'Alice' }); await userCollection.put('alice', { name: 'Alice' });
// Alice is friends with herself // Alice is friends with herself
const selfFriendship = new Delta({ const selfFriendship = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'alice')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'alice' }
]
});
node.lossless.ingestDelta(selfFriendship); node.lossless.ingestDelta(selfFriendship);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);
@ -335,24 +307,16 @@ describe('Nested Object Resolution', () => {
await userCollection.put('charlie', { name: 'Charlie' }); await userCollection.put('charlie', { name: 'Charlie' });
// Alice has multiple friends // Alice has multiple friends
const friendship1 = new Delta({ const friendship1 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'bob')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
node.lossless.ingestDelta(friendship1); node.lossless.ingestDelta(friendship1);
const friendship2 = new Delta({ const friendship2 = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'charlie')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'charlie' }
]
});
node.lossless.ingestDelta(friendship2); node.lossless.ingestDelta(friendship2);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);
@ -405,14 +369,10 @@ describe('Nested Object Resolution', () => {
}); });
// Create friendship // Create friendship
const friendship = new Delta({ const friendship = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'alice', 'friends')
host: node.config.peerId, .addPointer('friends', 'bob')
pointers: [ .buildV1();
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
node.lossless.ingestDelta(friendship); node.lossless.ingestDelta(friendship);
const aliceViews = node.lossless.view(['alice']); const aliceViews = node.lossless.view(['alice']);

View File

@ -3,7 +3,7 @@ import { Lossless } from '../src/views';
import { DefaultSchemaRegistry } from '../src/schema'; import { DefaultSchemaRegistry } from '../src/schema';
import { SchemaBuilder, PrimitiveSchemas } from '../src/schema'; import { SchemaBuilder, PrimitiveSchemas } from '../src/schema';
import { CommonSchemas } from '../util/schemas'; import { CommonSchemas } from '../util/schemas';
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
describe('Query Engine', () => { describe('Query Engine', () => {
@ -48,100 +48,65 @@ describe('Query Engine', () => {
async function createUser(id: string, name: string, age?: number, email?: string) { async function createUser(id: string, name: string, age?: number, email?: string) {
// Create user entity with name // Create user entity with name
const nameDelta = new Delta({ const nameDelta = createDelta('test', 'test-host')
id: `delta-${id}-name-${Date.now()}`, .withId(`delta-${id}-name-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'name', name, 'user')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'user', target: id, targetContext: 'name' },
{ localContext: 'value', target: name }
]
});
lossless.ingestDelta(nameDelta); lossless.ingestDelta(nameDelta);
// Add age if provided // Add age if provided
if (age !== undefined) { if (age !== undefined) {
const ageDelta = new Delta({ const ageDelta = createDelta('test', 'test-host')
id: `delta-${id}-age-${Date.now()}`, .withId(`delta-${id}-age-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'age', age, 'user')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'user', target: id, targetContext: 'age' },
{ localContext: 'value', target: age }
]
});
lossless.ingestDelta(ageDelta); lossless.ingestDelta(ageDelta);
} }
// Add email if provided // Add email if provided
if (email) { if (email) {
const emailDelta = new Delta({ const emailDelta = createDelta('test', 'test-host')
id: `delta-${id}-email-${Date.now()}`, .withId(`delta-${id}-email-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'email', email, 'user')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'user', target: id, targetContext: 'email' },
{ localContext: 'value', target: email }
]
});
lossless.ingestDelta(emailDelta); lossless.ingestDelta(emailDelta);
} }
} }
async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) { async function createBlogPost(id: string, title: string, author: string, published = false, views = 0) {
// Title delta // Title delta
const titleDelta = new Delta({ const titleDelta = createDelta('test', 'test-host')
id: `delta-${id}-title-${Date.now()}`, .withId(`delta-${id}-title-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'title', title, 'post')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'post', target: id, targetContext: 'title' },
{ localContext: 'value', target: title }
]
});
lossless.ingestDelta(titleDelta); lossless.ingestDelta(titleDelta);
// Author delta // Author delta
const authorDelta = new Delta({ const authorDelta = createDelta('test', 'test-host')
id: `delta-${id}-author-${Date.now()}`, .withId(`delta-${id}-author-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'author', author, 'post')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'post', target: id, targetContext: 'author' },
{ localContext: 'value', target: author }
]
});
lossless.ingestDelta(authorDelta); lossless.ingestDelta(authorDelta);
// Published delta // Published delta
const publishedDelta = new Delta({ const publishedDelta = createDelta('test', 'test-host')
id: `delta-${id}-published-${Date.now()}`, .withId(`delta-${id}-published-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'published', published, 'post')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'post', target: id, targetContext: 'published' },
{ localContext: 'value', target: published }
]
});
lossless.ingestDelta(publishedDelta); lossless.ingestDelta(publishedDelta);
// Views delta // Views delta
const viewsDelta = new Delta({ const viewsDelta = createDelta('test', 'test-host')
id: `delta-${id}-views-${Date.now()}`, .withId(`delta-${id}-views-${Date.now()}`)
creator: 'test', .withTimestamp(Date.now())
host: 'test-host', .setProperty(id, 'views', views, 'post')
timeCreated: Date.now(), .buildV1();
pointers: [
{ localContext: 'post', target: id, targetContext: 'views' },
{ localContext: 'value', target: views }
]
});
lossless.ingestDelta(viewsDelta); lossless.ingestDelta(viewsDelta);
} }

View File

@ -9,22 +9,12 @@ describe('Run (Orchestrated)', () => {
let apiUrl: string; let apiUrl: string;
beforeAll(async () => { beforeAll(async () => {
console.time('Test setup');
console.time('Create config');
// Configure and start the node // Configure and start the node
const config: NodeConfig = { const config: NodeConfig = {
id: 'app-001', id: 'app-001',
}; };
console.timeEnd('Create config');
console.time('Start node');
nodeHandle = await orchestrator.startNode(config); nodeHandle = await orchestrator.startNode(config);
console.timeEnd('Start node');
console.time('Get API URL');
apiUrl = nodeHandle.getApiUrl(); apiUrl = nodeHandle.getApiUrl();
console.timeEnd('Get API URL');
console.timeEnd('Test setup');
}, 60000); // Increase timeout to 60s for this hook }, 60000); // Increase timeout to 60s for this hook
afterAll(async () => { afterAll(async () => {

View File

@ -19,41 +19,26 @@ describe('Run (Two Nodes Orchestrated)', () => {
const nodeIds = ['app-002-A', 'app-002-B']; const nodeIds = ['app-002-A', 'app-002-B'];
beforeAll(async () => { beforeAll(async () => {
console.time('Test setup');
// Start first node // Start first node
console.time('Create node1 config');
const node1Config: NodeConfig = { const node1Config: NodeConfig = {
id: nodeIds[0], id: nodeIds[0],
}; };
console.timeEnd('Create node1 config');
console.time('Start node1');
const node1 = (await orchestrator.startNode(node1Config)) as FullNodeHandle; const node1 = (await orchestrator.startNode(node1Config)) as FullNodeHandle;
console.timeEnd('Start node1');
// Start second node with first node as bootstrap peer // Start second node with first node as bootstrap peer
console.time('Create node2 config');
const node2Config: NodeConfig = { const node2Config: NodeConfig = {
id: nodeIds[1], id: nodeIds[1],
network: { network: {
bootstrapPeers: [`localhost:${node1.getRequestPort()}`], bootstrapPeers: [`localhost:${node1.getRequestPort()}`],
}, },
}; };
console.timeEnd('Create node2 config');
console.time('Start node2');
const node2 = (await orchestrator.startNode(node2Config)) as FullNodeHandle; const node2 = (await orchestrator.startNode(node2Config)) as FullNodeHandle;
console.timeEnd('Start node2');
nodes.push(node1, node2); nodes.push(node1, node2);
// Connect the nodes // Connect the nodes
console.time('Connect nodes');
await orchestrator.connectNodes(node1, node2); await orchestrator.connectNodes(node1, node2);
console.timeEnd('Connect nodes');
console.timeEnd('Test setup');
}, 120000); // Increase timeout to 120s for this hook }, 120000); // Increase timeout to 120s for this hook
afterAll(async () => { afterAll(async () => {

View File

@ -30,7 +30,7 @@ interface ExtendedNodeStatus extends Omit<NodeStatus, 'network'> {
// Set default timeout for all tests to 5 minutes // Set default timeout for all tests to 5 minutes
jest.setTimeout(300000); jest.setTimeout(300000);
describe('Docker Orchestrator V2', () => { describe('Docker Orchestrator', () => {
let docker: Docker; let docker: Docker;
let orchestrator: DockerOrchestrator; let orchestrator: DockerOrchestrator;
let node: NodeHandle | null = null; let node: NodeHandle | null = null;
@ -43,27 +43,31 @@ describe('Docker Orchestrator V2', () => {
beforeAll(async () => { beforeAll(async () => {
debug('Setting up Docker client and orchestrator...'); debug('Setting up Docker client and orchestrator...');
// Initialize Docker client // Initialize Docker client with increased timeout
docker = new Docker(); docker = new Docker({
timeout: 60000, // 60 second timeout for Docker operations
});
// Verify Docker is running // Verify Docker is running
try { try {
await docker.ping(); await docker.ping();
debug('Docker daemon is responding'); debug('Docker daemon is responding');
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
debug('Docker daemon is not responding: %o', 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 // Initialize the orchestrator with the Docker client and test image
orchestrator = createOrchestrator('docker') as DockerOrchestrator; orchestrator = createOrchestrator('docker') as DockerOrchestrator;
debug('Docker orchestrator initialized'); 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); nodePort = 3000 + Math.floor(Math.random() * 1000);
nodeConfig = { nodeConfig = {
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, id: `node-${testRunId}`,
networkId: 'test-network', networkId: `test-network-${testRunId}`,
port: nodePort, port: nodePort,
resources: { resources: {
memory: 256, // 256MB memory: 256, // 256MB
@ -79,85 +83,8 @@ describe('Docker Orchestrator V2', () => {
afterAll(async () => { afterAll(async () => {
debug('Starting test cleanup...'); debug('Starting test cleanup...');
const cleanupPromises: Promise<unknown>[] = [];
// Helper function to clean up a node with retries await orchestrator.cleanup();
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);
}
debug('All test cleanups completed'); debug('All test cleanups completed');
}, 120000); // 2 minute timeout for afterAll }, 120000); // 2 minute timeout for afterAll
@ -166,12 +93,19 @@ describe('Docker Orchestrator V2', () => {
debug('Starting test: should start and stop a node'); debug('Starting test: should start and stop a node');
// Create a new config with a unique ID for this test // Create a new config with a unique ID for this test
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const testNodeConfig = { const testNodeConfig = {
...nodeConfig, ...nodeConfig,
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`, id: `node-${testRunId}`,
networkId: `test-network-${testRunId}`,
network: { network: {
...nodeConfig.network, ...nodeConfig.network,
enableHttpApi: true 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); debug('Error during node cleanup: %o', e);
} }
} }
}, 30000); // 30 second timeout for this test });
it('should enforce resource limits', async () => { it('should enforce resource limits', async () => {
debug('Starting test: should enforce resource limits'); debug('Starting test: should enforce resource limits');

View File

@ -10,7 +10,7 @@ import { DefaultSchemaRegistry } from '../src/schema';
import { CommonSchemas } from '../util/schemas'; import { CommonSchemas } from '../util/schemas';
import { TypedCollectionImpl, SchemaValidationError } from '../src/collections'; import { TypedCollectionImpl, SchemaValidationError } from '../src/collections';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
describe('Schema System', () => { describe('Schema System', () => {
let schemaRegistry: DefaultSchemaRegistry; let schemaRegistry: DefaultSchemaRegistry;
@ -21,7 +21,9 @@ describe('Schema System', () => {
node = new RhizomeNode(); node = new RhizomeNode();
}); });
describe('Schema Builder', () => { describe('Schema Builder', () => {
it('should create a basic schema', () => { it('should create a basic schema', () => {
const schema = SchemaBuilder const schema = SchemaBuilder
.create('user') .create('user')
@ -308,14 +310,10 @@ describe('Schema System', () => {
await collection.put('user2', { name: 'Bob' }); await collection.put('user2', { name: 'Bob' });
// Manually create an invalid entity by bypassing validation // Manually create an invalid entity by bypassing validation
const invalidDelta = new Delta({ const invalidDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'user3', 'email')
host: node.config.peerId, .addPointer('email', 'invalid@test.com')
pointers: [ .buildV1();
{ localContext: 'users', target: 'user3', targetContext: 'email' },
{ localContext: 'email', target: 'invalid@test.com' }
]
});
node.lossless.ingestDelta(invalidDelta); node.lossless.ingestDelta(invalidDelta);
const stats = collection.getValidationStats(); const stats = collection.getValidationStats();
@ -337,14 +335,10 @@ describe('Schema System', () => {
await collection.put('user2', { name: 'Bob' }); await collection.put('user2', { name: 'Bob' });
// Create invalid entity manually // Create invalid entity manually
const invalidDelta = new Delta({ const invalidDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', 'user3', 'age')
host: node.config.peerId, .addPointer('age', 'not-a-number')
pointers: [ .buildV1();
{ localContext: 'users', target: 'user3', targetContext: 'age' },
{ localContext: 'age', target: 'not-a-number' }
]
});
node.lossless.ingestDelta(invalidDelta); node.lossless.ingestDelta(invalidDelta);
const validIds = collection.getValidEntities(); const validIds = collection.getValidEntities();

View File

@ -1,39 +1,25 @@
import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage'; import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage';
import { createDelta } from '../src/core/delta-builder';
import { Delta } from '../src/core'; import { Delta } from '../src/core';
import { DeltaQueryStorage } from '../src/storage/interface'; import { DeltaQueryStorage } from '../src/storage/interface';
describe('Delta Storage', () => { describe('Delta Storage', () => {
const testDeltas = [ const testDeltas = [
new Delta({ createDelta('alice', 'host1')
id: 'delta1', .withId('delta1')
creator: 'alice', .withTimestamp(Date.now() - 1000)
host: 'host1', .setProperty('user1', 'name', 'Alice', 'user')
timeCreated: Date.now() - 1000, .buildV1(),
pointers: [ createDelta('bob', 'host1')
{ localContext: 'user', target: 'user1', targetContext: 'name' }, .withId('delta2')
{ localContext: 'value', target: 'Alice' } .withTimestamp(Date.now() - 500)
] .setProperty('user1', 'age', 25, 'user')
}), .buildV1(),
new Delta({ createDelta('alice', 'host2')
id: 'delta2', .withId('delta3')
creator: 'bob', .withTimestamp(Date.now())
host: 'host1', .setProperty('user2', 'name', 'Bob', 'user')
timeCreated: Date.now() - 500, .buildV1()
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' }
]
})
]; ];
describe('Memory Storage', () => { describe('Memory Storage', () => {

View File

@ -1,13 +1,13 @@
import { import {
RhizomeNode, RhizomeNode,
Lossless, Lossless,
Delta,
TimestampResolver, TimestampResolver,
CreatorIdTimestampResolver, CreatorIdTimestampResolver,
DeltaIdTimestampResolver, DeltaIdTimestampResolver,
HostIdTimestampResolver, HostIdTimestampResolver,
LexicographicTimestampResolver LexicographicTimestampResolver
} from "../src"; } from "../src";
import { createDelta } from "../src/core/delta-builder";
describe('Timestamp Resolvers', () => { describe('Timestamp Resolvers', () => {
let node: RhizomeNode; let node: RhizomeNode;
@ -21,36 +21,22 @@ describe('Timestamp Resolvers', () => {
describe('Basic Timestamp Resolution', () => { describe('Basic Timestamp Resolution', () => {
test('should resolve by most recent timestamp', () => { test('should resolve by most recent timestamp', () => {
// Add older delta // Add older delta
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta1')
host: 'host1', .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
// Add newer delta // Add newer delta
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user2', 'host2')
creator: 'user2', .withId('delta2')
host: 'host2', .withTimestamp(2000)
id: 'delta2', .addPointer('collection', 'entity1', 'score')
timeCreated: 2000, .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new TimestampResolver(lossless); const resolver = new TimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -61,34 +47,20 @@ describe('Timestamp Resolvers', () => {
test('should handle multiple entities with different timestamps', () => { test('should handle multiple entities with different timestamps', () => {
// Entity1 - older value // Entity1 - older value
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(1000)
host: 'host1', .addPointer('collection', 'entity1', 'value')
timeCreated: 1000, .addPointer('value', 100)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 100
}]
}));
// Entity2 - newer value // Entity2 - newer value
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withTimestamp(2000)
host: 'host1', .addPointer('collection', 'entity2', 'value')
timeCreated: 2000, .addPointer('value', 200)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity2",
targetContext: "value"
}, {
localContext: "value",
target: 200
}]
}));
const resolver = new TimestampResolver(lossless); const resolver = new TimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -102,35 +74,21 @@ describe('Timestamp Resolvers', () => {
describe('Tie-Breaking Strategies', () => { describe('Tie-Breaking Strategies', () => {
test('should break ties using creator-id strategy', () => { test('should break ties using creator-id strategy', () => {
// Two deltas with same timestamp, different creators // Two deltas with same timestamp, different creators
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_z', 'host1')
creator: 'user_z', // Lexicographically later .withId('delta1')
host: 'host1', .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_a', 'host1')
creator: 'user_a', // Lexicographically earlier .withId('delta2')
host: 'host1', .withTimestamp(1000) // Same timestamp
id: 'delta2', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, // Same timestamp .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new CreatorIdTimestampResolver(lossless); const resolver = new CreatorIdTimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -142,35 +100,21 @@ describe('Timestamp Resolvers', () => {
test('should break ties using delta-id strategy', () => { test('should break ties using delta-id strategy', () => {
// Two deltas with same timestamp, different delta IDs // Two deltas with same timestamp, different delta IDs
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta_a') // Lexicographically earlier
host: 'host1', .withTimestamp(1000)
id: 'delta_a', // Lexicographically earlier .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta_z') // Lexicographically later
host: 'host1', .withTimestamp(1000) // Same timestamp
id: 'delta_z', // Lexicographically later .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, // Same timestamp .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new DeltaIdTimestampResolver(lossless); const resolver = new DeltaIdTimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -182,35 +126,21 @@ describe('Timestamp Resolvers', () => {
test('should break ties using host-id strategy', () => { test('should break ties using host-id strategy', () => {
// Two deltas with same timestamp, different hosts // Two deltas with same timestamp, different hosts
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host_z') // Lexicographically later
creator: 'user1', .withId('delta1')
host: 'host_z', // Lexicographically later .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host_a') // Lexicographically earlier
creator: 'user1', .withId('delta2')
host: 'host_a', // Lexicographically earlier .withTimestamp(1000) // Same timestamp
id: 'delta2', .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, // Same timestamp .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new HostIdTimestampResolver(lossless); const resolver = new HostIdTimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -222,35 +152,21 @@ describe('Timestamp Resolvers', () => {
test('should break ties using lexicographic strategy with string values', () => { test('should break ties using lexicographic strategy with string values', () => {
// Two deltas with same timestamp, different string values // Two deltas with same timestamp, different string values
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta1')
host: 'host1', .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'name')
timeCreated: 1000, .addPointer('name', 'alice')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'alice'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta2')
host: 'host1', .withTimestamp(1000) // Same timestamp
id: 'delta2', .addPointer('collection', 'entity1', 'name')
timeCreated: 1000, // Same timestamp .addPointer('name', 'bob')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'bob'
}]
}));
const resolver = new LexicographicTimestampResolver(lossless); const resolver = new LexicographicTimestampResolver(lossless);
const result = resolver.resolve(); 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)', () => { 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) // Two deltas with same timestamp, numeric values (should fall back to delta ID comparison)
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta_a') // Lexicographically earlier
host: 'host1', .withTimestamp(1000)
id: 'delta_a', // Lexicographically earlier .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, .addPointer('score', 100)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta_z') // Lexicographically later
host: 'host1', .withTimestamp(1000) // Same timestamp
id: 'delta_z', // Lexicographically later .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, // Same timestamp .addPointer('score', 200)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 200
}]
}));
const resolver = new LexicographicTimestampResolver(lossless); const resolver = new LexicographicTimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -304,35 +206,21 @@ describe('Timestamp Resolvers', () => {
describe('Complex Tie-Breaking Scenarios', () => { describe('Complex Tie-Breaking Scenarios', () => {
test('should handle multiple properties with different tie-breaking outcomes', () => { test('should handle multiple properties with different tie-breaking outcomes', () => {
// Add deltas for multiple properties with same timestamp // Add deltas for multiple properties with same timestamp
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_a', 'host1')
creator: 'user_a', .withId('delta_z')
host: 'host1', .withTimestamp(1000)
id: 'delta_z', .addPointer('collection', 'entity1', 'name')
timeCreated: 1000, .addPointer('name', 'alice')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'alice'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_z', 'host1')
creator: 'user_z', .withId('delta_a')
host: 'host1', .withTimestamp(1000) // Same timestamp
id: 'delta_a', .addPointer('collection', 'entity1', 'name')
timeCreated: 1000, // Same timestamp .addPointer('name', 'bob')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'bob'
}]
}));
const creatorResolver = new CreatorIdTimestampResolver(lossless); const creatorResolver = new CreatorIdTimestampResolver(lossless);
const deltaResolver = new DeltaIdTimestampResolver(lossless); const deltaResolver = new DeltaIdTimestampResolver(lossless);
@ -352,36 +240,22 @@ describe('Timestamp Resolvers', () => {
test('should work consistently with timestamp priority over tie-breaking', () => { test('should work consistently with timestamp priority over tie-breaking', () => {
// Add older delta with "better" tie-breaking attributes // Add older delta with "better" tie-breaking attributes
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_z', 'host1')
creator: 'user_z', // Would win in creator tie-breaking .withId('delta_z') // Would win in delta ID tie-breaking
host: 'host1', .withTimestamp(1000) // Older timestamp
id: 'delta_z', // Would win in delta ID tie-breaking .addPointer('collection', 'entity1', 'score')
timeCreated: 1000, // Older timestamp .addPointer('score', 10)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 10
}]
}));
// Add newer delta with "worse" tie-breaking attributes // Add newer delta with "worse" tie-breaking attributes
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user_a', 'host1')
creator: 'user_a', // Would lose in creator tie-breaking .withId('delta_a') // Would lose in delta ID tie-breaking
host: 'host1', .withTimestamp(2000) // Newer timestamp
id: 'delta_a', // Would lose in delta ID tie-breaking .addPointer('collection', 'entity1', 'score')
timeCreated: 2000, // Newer timestamp .addPointer('score', 20)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 20
}]
}));
const resolver = new CreatorIdTimestampResolver(lossless); const resolver = new CreatorIdTimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();
@ -394,20 +268,13 @@ describe('Timestamp Resolvers', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
test('should handle single delta correctly', () => { test('should handle single delta correctly', () => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta1')
host: 'host1', .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'value')
timeCreated: 1000, .addPointer('value', 42)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "value"
}, {
localContext: "value",
target: 42
}]
}));
const resolver = new TimestampResolver(lossless, 'creator-id'); const resolver = new TimestampResolver(lossless, 'creator-id');
const result = resolver.resolve(); const result = resolver.resolve();
@ -417,35 +284,21 @@ describe('Timestamp Resolvers', () => {
}); });
test('should handle mixed value types correctly', () => { test('should handle mixed value types correctly', () => {
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta1')
host: 'host1', .withTimestamp(1000)
id: 'delta1', .addPointer('collection', 'entity1', 'name')
timeCreated: 1000, .addPointer('name', 'test')
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "name"
}, {
localContext: "name",
target: 'test'
}]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .withId('delta2')
host: 'host1', .withTimestamp(1001)
id: 'delta2', .addPointer('collection', 'entity1', 'score')
timeCreated: 1001, .addPointer('score', 100)
pointers: [{ .buildV1()
localContext: "collection", );
target: "entity1",
targetContext: "score"
}, {
localContext: "score",
target: 100
}]
}));
const resolver = new TimestampResolver(lossless); const resolver = new TimestampResolver(lossless);
const result = resolver.resolve(); const result = resolver.resolve();

View File

@ -1,4 +1,4 @@
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
import { Lossless } from '../src/views'; import { Lossless } from '../src/views';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { DeltaFilter } from '../src/core'; import { DeltaFilter } from '../src/core';
@ -17,36 +17,23 @@ describe('Transactions', () => {
const transactionId = 'tx-123'; const transactionId = 'tx-123';
// Create a delta that declares a transaction with size 3 // Create a delta that declares a transaction with size 3
const txDeclaration = new Delta({ const txDeclaration = createDelta('system', 'host1')
creator: 'system', .declareTransaction(transactionId, 3)
host: 'host1', .buildV1();
pointers: [
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
{ localContext: 'size', target: 3 }
]
});
// Create first delta in transaction // Create first delta in transaction
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .inTransaction(transactionId)
host: 'host1', .addPointer('name', 'user123', 'name')
pointers: [ .addPointer('value', 'Alice')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1();
{ localContext: 'name', target: 'user123', targetContext: 'name' },
{ localContext: 'value', target: 'Alice' }
]
});
// Create second delta in transaction // Create second delta in transaction
const delta2 = new Delta({ const delta2 = createDelta('user1', 'host1')
creator: 'user1', .inTransaction(transactionId)
host: 'host1', .addPointer('age', 'user123', 'age')
pointers: [ .addPointer('value', 25)
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1();
{ localContext: 'age', target: 'user123', targetContext: 'age' },
{ localContext: 'value', target: 25 }
]
});
// Ingest transaction declaration and first two deltas // Ingest transaction declaration and first two deltas
lossless.ingestDelta(txDeclaration); lossless.ingestDelta(txDeclaration);
@ -58,15 +45,11 @@ describe('Transactions', () => {
expect(view.user123).toBeUndefined(); expect(view.user123).toBeUndefined();
// Add the third delta to complete the transaction // Add the third delta to complete the transaction
const delta3 = new Delta({ const delta3 = createDelta('user1', 'host1')
creator: 'user1', .inTransaction(transactionId)
host: 'host1', .addPointer('email', 'user123', 'email')
pointers: [ .addPointer('value', 'alice@example.com')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1();
{ localContext: 'email', target: 'user123', targetContext: 'email' },
{ localContext: 'value', target: 'alice@example.com' }
]
});
lossless.ingestDelta(delta3); lossless.ingestDelta(delta3);
@ -83,44 +66,30 @@ describe('Transactions', () => {
const tx2 = 'tx-002'; const tx2 = 'tx-002';
// Declare two transactions // Declare two transactions
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .declareTransaction(tx1, 2)
host: 'host1', .buildV1()
pointers: [ );
{ localContext: '_transaction', target: tx1, targetContext: 'size' },
{ localContext: 'size', target: 2 }
]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .declareTransaction(tx2, 2)
host: 'host1', .buildV1()
pointers: [ );
{ localContext: '_transaction', target: tx2, targetContext: 'size' },
{ localContext: 'size', target: 2 }
]
}));
// Add deltas for both transactions // Add deltas for both transactions
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .inTransaction(tx1)
host: 'host1', .addPointer('status', 'order1', 'status')
pointers: [ .addPointer('value', 'pending')
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' }, .buildV1()
{ localContext: 'status', target: 'order1', targetContext: 'status' }, );
{ localContext: 'value', target: 'pending' }
]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user2', 'host2')
creator: 'user2', .inTransaction(tx2)
host: 'host2', .addPointer('status', 'order2', 'status')
pointers: [ .addPointer('value', 'shipped')
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' }, .buildV1()
{ localContext: 'status', target: 'order2', targetContext: 'status' }, );
{ localContext: 'value', target: 'shipped' }
]
}));
// Neither transaction is complete // Neither transaction is complete
let view = lossless.view(['order1', 'order2']); let view = lossless.view(['order1', 'order2']);
@ -128,15 +97,12 @@ describe('Transactions', () => {
expect(view.order2).toBeUndefined(); expect(view.order2).toBeUndefined();
// Complete tx1 // Complete tx1
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .inTransaction(tx1)
host: 'host1', .addPointer('total', 'order1', 'total')
pointers: [ .addPointer('value', 100)
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' }, .buildV1()
{ localContext: 'total', target: 'order1', targetContext: 'total' }, );
{ localContext: 'value', target: 100 }
]
}));
// tx1 is complete, tx2 is not // tx1 is complete, tx2 is not
view = lossless.view(['order1', 'order2']); view = lossless.view(['order1', 'order2']);
@ -146,15 +112,12 @@ describe('Transactions', () => {
expect(view.order2).toBeUndefined(); expect(view.order2).toBeUndefined();
// Complete tx2 // Complete tx2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user2', 'host2')
creator: 'user2', .inTransaction(tx2)
host: 'host2', .addPointer('tracking', 'order2', 'tracking')
pointers: [ .addPointer('value', 'TRACK123')
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' }, .buildV1()
{ localContext: 'tracking', target: 'order2', targetContext: 'tracking' }, );
{ localContext: 'value', target: 'TRACK123' }
]
}));
// Both transactions complete // Both transactions complete
view = lossless.view(['order1', 'order2']); view = lossless.view(['order1', 'order2']);
@ -168,35 +131,25 @@ describe('Transactions', () => {
const transactionId = 'tx-filter-test'; const transactionId = 'tx-filter-test';
// Create transaction with 2 deltas // Create transaction with 2 deltas
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .declareTransaction(transactionId, 2)
host: 'host1', .buildV1()
pointers: [ );
{ localContext: '_transaction', target: transactionId, targetContext: 'size' },
{ localContext: 'size', target: 2 }
]
}));
// Add both deltas // Add both deltas
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .inTransaction(transactionId)
host: 'host1', .addPointer('type', 'doc1', 'type')
pointers: [ .addPointer('value', 'report')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'type', target: 'doc1', targetContext: 'type' }, );
{ localContext: 'value', target: 'report' }
]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user2', 'host2')
creator: 'user2', .inTransaction(transactionId)
host: 'host2', .addPointer('author', 'doc1', 'author')
pointers: [ .addPointer('value', 'Bob')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'author', target: 'doc1', targetContext: 'author' }, );
{ localContext: 'value', target: 'Bob' }
]
}));
// Create a filter that only accepts deltas from user1 // Create a filter that only accepts deltas from user1
const userFilter: DeltaFilter = (delta) => delta.creator === 'user1'; const userFilter: DeltaFilter = (delta) => delta.creator === 'user1';
@ -215,37 +168,28 @@ describe('Transactions', () => {
const transactionId = 'tx-multi-entity'; const transactionId = 'tx-multi-entity';
// Transaction that updates multiple entities atomically // Transaction that updates multiple entities atomically
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .addPointer('_transaction', transactionId, 'size')
host: 'host1', .addPointer('size', 3)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 3 }
]
}));
// Transfer money from account1 to account2 // Transfer money from account1 to account2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('bank', 'host1')
creator: 'bank', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('balance', 'account1', 'balance')
pointers: [ .addPointer('value', 900)
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .addPointer('operation', 'debit')
{ localContext: 'balance', target: 'account1', targetContext: 'balance' }, .buildV1()
{ localContext: 'value', target: 900 }, );
{ localContext: 'operation', target: 'debit' }
]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('bank', 'host1')
creator: 'bank', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('balance', 'account2', 'balance')
pointers: [ .addPointer('value', 1100)
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .addPointer('operation', 'credit')
{ localContext: 'balance', target: 'account2', targetContext: 'balance' }, .buildV1()
{ localContext: 'value', target: 1100 }, );
{ localContext: 'operation', target: 'credit' }
]
}));
// Transaction incomplete - no entities should show updates // Transaction incomplete - no entities should show updates
let view = lossless.view(['account1', 'account2']); let view = lossless.view(['account1', 'account2']);
@ -253,17 +197,14 @@ describe('Transactions', () => {
expect(view.account2).toBeUndefined(); expect(view.account2).toBeUndefined();
// Complete transaction with audit log // Complete transaction with audit log
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('bank', 'host1')
creator: 'bank', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('transfer', 'transfer123', 'details')
pointers: [ .addPointer('from', 'account1')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .addPointer('to', 'account2')
{ localContext: 'transfer', target: 'transfer123', targetContext: 'details' }, .addPointer('amount', 100)
{ localContext: 'from', target: 'account1' }, .buildV1()
{ localContext: 'to', target: 'account2' }, );
{ localContext: 'amount', target: 100 }
]
}));
// All entities should now be visible // All entities should now be visible
view = lossless.view(['account1', 'account2', 'transfer123']); view = lossless.view(['account1', 'account2', 'transfer123']);
@ -285,40 +226,29 @@ describe('Transactions', () => {
}); });
// Create transaction // Create transaction
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .addPointer('_transaction', transactionId, 'size')
host: 'host1', .addPointer('size', 2)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 2 }
]
}));
// Add first delta // Add first delta
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('field1', 'entity1', 'field1')
pointers: [ .addPointer('value', 'value1')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1();
{ localContext: 'field1', target: 'entity1', targetContext: 'field1' },
{ localContext: 'value', target: 'value1' }
]
});
lossless.ingestDelta(delta1); lossless.ingestDelta(delta1);
// No events should be emitted yet // No events should be emitted yet
expect(updateEvents).toHaveLength(0); expect(updateEvents).toHaveLength(0);
// Add second delta to complete transaction // Add second delta to complete transaction
const delta2 = new Delta({ const delta2 = createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('field2', 'entity1', 'field2')
pointers: [ .addPointer('value', 'value2')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1();
{ localContext: 'field2', target: 'entity1', targetContext: 'field2' },
{ localContext: 'value', target: 'value2' }
]
});
lossless.ingestDelta(delta2); lossless.ingestDelta(delta2);
// Wait for async event processing // Wait for async event processing
@ -339,25 +269,19 @@ describe('Transactions', () => {
const transactionId = 'tx-wait'; const transactionId = 'tx-wait';
// Create transaction // Create transaction
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .addPointer('_transaction', transactionId, 'size')
host: 'host1', .addPointer('size', 2)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 2 }
]
}));
// Add first delta // Add first delta
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('status', 'job1', 'status')
pointers: [ .addPointer('value', 'processing')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'status', target: 'job1', targetContext: 'status' }, );
{ localContext: 'value', target: 'processing' }
]
}));
// Start waiting for transaction // Start waiting for transaction
const waitPromise = lossless.transactions.waitFor(transactionId); const waitPromise = lossless.transactions.waitFor(transactionId);
@ -369,15 +293,12 @@ describe('Transactions', () => {
expect(isResolved).toBe(false); expect(isResolved).toBe(false);
// Complete transaction // Complete transaction
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('status', 'job1', 'status')
pointers: [ .addPointer('value', 'completed')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'status', target: 'job1', targetContext: 'status' }, );
{ localContext: 'value', target: 'completed' }
]
}));
// Wait should now resolve // Wait should now resolve
await waitPromise; await waitPromise;
@ -391,14 +312,10 @@ describe('Transactions', () => {
it('should handle non-transactional deltas normally', () => { it('should handle non-transactional deltas normally', () => {
// Regular delta without transaction // Regular delta without transaction
const regularDelta = new Delta({ const regularDelta = createDelta('user1', 'host1')
creator: 'user1', .addPointer('name', 'user456', 'name')
host: 'host1', .addPointer('value', 'Charlie')
pointers: [ .buildV1();
{ localContext: 'name', target: 'user456', targetContext: 'name' },
{ localContext: 'value', target: 'Charlie' }
]
});
const updateEvents: string[] = []; const updateEvents: string[] = [];
lossless.eventStream.on('updated', (entityId) => { lossless.eventStream.on('updated', (entityId) => {
@ -422,33 +339,24 @@ describe('Transactions', () => {
const transactionId = 'tx-resize'; const transactionId = 'tx-resize';
// Initially declare transaction with size 2 // Initially declare transaction with size 2
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .addPointer('_transaction', transactionId, 'size')
host: 'host1', .addPointer('size', 2)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 2 }
]
}));
// Add 2 deltas // Add 2 deltas
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('item1', 'cart1', 'items')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ localContext: 'item1', target: 'cart1', targetContext: 'items' }
]
}));
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('item2', 'cart1', 'items')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ localContext: 'item2', target: 'cart1', targetContext: 'items' }
]
}));
// Transaction should be complete // Transaction should be complete
expect(lossless.transactions.isComplete(transactionId)).toBe(true); expect(lossless.transactions.isComplete(transactionId)).toBe(true);
@ -462,15 +370,12 @@ describe('Transactions', () => {
const transactionId = 'tx-no-size'; const transactionId = 'tx-no-size';
// Add delta with transaction reference but no size declaration // Add delta with transaction reference but no size declaration
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('user1', 'host1')
creator: 'user1', .addPointer('_transaction', transactionId, 'deltas')
host: 'host1', .addPointer('data', 'entity1', 'data')
pointers: [ .addPointer('value', 'test')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'data', target: 'entity1', targetContext: 'data' }, );
{ localContext: 'value', target: 'test' }
]
}));
// Transaction should not be complete (no size) // Transaction should not be complete (no size)
expect(lossless.transactions.isComplete(transactionId)).toBe(false); expect(lossless.transactions.isComplete(transactionId)).toBe(false);
@ -480,14 +385,11 @@ describe('Transactions', () => {
expect(view.entity1).toBeUndefined(); expect(view.entity1).toBeUndefined();
// Declare size after the fact // Declare size after the fact
lossless.ingestDelta(new Delta({ lossless.ingestDelta(createDelta('system', 'host1')
creator: 'system', .addPointer('_transaction', transactionId, 'size')
host: 'host1', .addPointer('size', 1)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 1 }
]
}));
// Now transaction should be complete // Now transaction should be complete
expect(lossless.transactions.isComplete(transactionId)).toBe(true); expect(lossless.transactions.isComplete(transactionId)).toBe(true);

View File

@ -9,7 +9,9 @@
"test": "jest", "test": "jest",
"coverage": "./scripts/coverage.sh", "coverage": "./scripts/coverage.sh",
"coverage-report": "npm run test -- --coverage --coverageDirectory=coverage", "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": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",
@ -35,11 +37,10 @@
"forceExit": true, "forceExit": true,
"verbose": true, "verbose": true,
"testEnvironmentOptions": { "testEnvironmentOptions": {
"NODE_ENV": "test", "NODE_ENV": "test"
"DEBUG": "rz:*"
} }
}, },
"author": "Taliesin (Ladd) <ladd@dgov.io>", "author": "Lentil <lentil@laddhoffman.com>",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@types/dockerode": "^3.3.40", "@types/dockerode": "^3.3.40",

View File

@ -134,6 +134,13 @@ export class DeltaBuilder {
buildV1(): DeltaV1 { buildV1(): DeltaV1 {
return this.buildV2().toV1(); return this.buildV2().toV1();
} }
/**
* Default to V1 for now
*/
build(): DeltaV1 {
return this.buildV1();
}
} }
/** /**

View File

@ -1,4 +1,5 @@
export * from './delta'; export * from './delta';
export * from './delta-builder';
export * from './types'; export * from './types';
export * from './context'; export * from './context';
export { Entity } from './entity'; export { Entity } from './entity';

View File

@ -63,16 +63,11 @@ export class ImageManager implements IImageManager {
debug('Created build context tar stream'); debug('Created build context tar stream');
testImageBuildPromise = new Promise<void>((resolve, reject) => { testImageBuildPromise = new Promise<void>((resolve, reject) => {
const logMessages: string[] = [];
const log = (...args: any[]) => { const log = (...args: any[]) => {
const timestamp = new Date().toISOString();
const message = args.map(arg => const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' '); ).join(' ');
const logMessage = `[${timestamp}] ${message}\n`; debug(message);
process.stdout.write(logMessage);
logMessages.push(logMessage);
}; };
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => { 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<boolean> {
try {
const image = this.docker.getImage(imageName);
await image.inspect();
return true;
} catch (error) {
return false;
}
}
} }

View File

@ -268,9 +268,13 @@ export class QueryEngine {
switch (propertySchema.type) { switch (propertySchema.type) {
case 'primitive': { case 'primitive': {
// Use last-write-wins for primitives // Use last-write-wins for primitives
const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0]; const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId); for (let delta of deltasSorted) {
obj[propertyId] = primitiveValue; const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
if (primitiveValue !== null) {
obj[propertyId] = primitiveValue;
}
}
break; break;
} }
@ -304,12 +308,12 @@ export class QueryEngine {
/** /**
* Extract primitive value from a delta for a given property * 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 // Look for the value in collapsed pointers
// CollapsedPointer is {[key: PropertyID]: PropertyTypes} // CollapsedPointer is {[key: PropertyID]: PropertyTypes}
for (const pointer of delta.pointers) { for (const pointer of delta.pointers) {
if (pointer.value !== undefined) { if (pointer[propertyId] !== undefined) {
return pointer.value; return pointer[propertyId];
} }
} }
return null; return null;

View File

@ -263,6 +263,8 @@ export class StorageQueryEngine {
default: default:
properties[propertyId] = propDeltas.length; properties[propertyId] = propDeltas.length;
} }
debug(`Resolved property ${propertyId}:`, properties[propertyId]);
} }
return properties; return properties;
@ -271,9 +273,9 @@ export class StorageQueryEngine {
/** /**
* Extract primitive value from a delta for a given property * 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) { for (const pointer of delta.pointers) {
if (pointer.localContext === 'value') { if (pointer.localContext === propertyId) {
return pointer.target; 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) { 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; return pointer.target;
} }
} }