Merge pull request 'Delta Builder Fluent API' (#4) from chore/delta-format-standardization into main

Reviewed-on: #4
This commit is contained in:
lentil 2025-06-20 22:55:33 -05:00
commit 62d0f5355c
32 changed files with 1658 additions and 2589 deletions

View File

@ -0,0 +1,7 @@
---
description: Update the current file to use delta builder
---
Replace each delta instantiation with a fluent call to createDelta from delta builder
- pass creator and host as arguments to createDelta
- use setProperty where appropriate

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

@ -0,0 +1,187 @@
import { createDelta } from '../src/core/delta-builder';
import { DeltaV1, DeltaV2 } from '../src/core/delta';
import { Lossless } from '../src/views/lossless';
import { RhizomeNode } from '../src/node';
import { LastWriteWins } from '../src/views/resolvers/last-write-wins';
describe('DeltaBuilder', () => {
const creator = 'creator-123';
const host = 'host-456';
const node = new RhizomeNode();
describe('V1 Deltas', () => {
it('should create a basic V1 delta', () => {
const delta = createDelta(creator, host)
.addPointer('name', 'Test Delta', 'title')
.addPointer('description', 'A test delta', 'description')
.buildV1();
expect(delta).toBeInstanceOf(DeltaV1);
expect(delta.id).toBeDefined();
expect(delta.creator).toBe(creator);
expect(delta.host).toBe(host);
expect(delta.pointers).toHaveLength(2);
expect(delta.pointers[0]).toEqual({
localContext: 'name',
target: 'Test Delta',
targetContext: 'title'
});
});
it('should create a V1 delta with setProperty', () => {
const delta = createDelta(creator, host)
.setProperty('entity-1', 'name', 'Test Entity')
.buildV1();
expect(delta).toBeInstanceOf(DeltaV1);
expect(delta.pointers).toContainEqual({
localContext: 'entity',
target: 'entity-1',
targetContext: 'name'
});
expect(delta.pointers).toContainEqual({
localContext: 'name',
target: 'Test Entity',
});
// Verify that the entity property resolves correctly
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new LastWriteWins(lossless);
const result = lossy.resolve();
expect(result).toBeDefined();
expect(result!['entity-1'].properties.name).toBe('Test Entity');
});
it('should create a V1 delta with setProperty and entityLabel', () => {
const delta = createDelta(creator, host)
.setProperty('entity-1', 'name', 'Test Entity', 'user')
.buildV1();
expect(delta).toBeInstanceOf(DeltaV1);
expect(delta.pointers).toContainEqual({
localContext: 'user',
target: 'entity-1',
targetContext: 'name'
});
expect(delta.pointers).toContainEqual({
localContext: 'name',
target: 'Test Entity',
});
// Verify that the entity property resolves correctly
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new LastWriteWins(lossless);
const result = lossy.resolve();
expect(result).toBeDefined();
expect(result!['entity-1'].properties.name).toBe('Test Entity');
});
it('should create a V1 delta with relationships', () => {
const delta = createDelta(creator, host)
.relate('user-1', 'follows', 'user-2')
.buildV1();
expect(delta.pointers).toContainEqual({
localContext: 'follows',
target: 'user-2',
targetContext: 'follows'
});
expect(delta.pointers).toContainEqual({
localContext: 'source',
target: 'user-1',
targetContext: 'follows'
});
});
});
describe('V2 Deltas', () => {
it('should create a basic V2 delta', () => {
const delta = createDelta(creator, host)
.addPointer('name', 'Test Delta V2', 'title')
.buildV2();
expect(delta).toBeInstanceOf(DeltaV2);
expect(delta.id).toBeDefined();
expect(delta.creator).toBe(creator);
expect(delta.host).toBe(host);
expect(delta.pointers).toHaveProperty('name');
expect(delta.pointers.name).toEqual({ 'Test Delta V2': 'title' });
});
it('should create a V2 delta with setProperty', () => {
const delta = createDelta(creator, host)
.setProperty('entity-1', 'name', 'Test Entity')
.buildV2();
expect(delta.pointers).toHaveProperty('name', 'Test Entity');
expect(delta.pointers).toHaveProperty('entity', { 'entity-1': 'name' });
});
it('should create a V2 delta with relationships', () => {
const delta = createDelta(creator, host)
.relate('user-1', 'follows', 'user-2')
.buildV2();
expect(delta.pointers).toHaveProperty('follows', { 'user-2': 'follows' });
expect(delta.pointers).toHaveProperty('source', { 'user-1': 'follows' });
});
});
describe('Common functionality', () => {
it('should support custom IDs', () => {
const customId = 'custom-delta-id';
const delta = createDelta(creator, host)
.withId(customId)
.buildV1();
expect(delta.id).toBe(customId);
});
it('should support transactions', () => {
const txId = 'tx-123';
const delta = createDelta(creator, host)
.inTransaction(txId)
.buildV1();
// Check for transaction ID in pointers
expect(delta.pointers).toContainEqual({
localContext: '_transaction',
target: txId,
targetContext: 'deltas'
});
});
it('should support transactions in V2', () => {
const txId = 'tx-123';
const delta = createDelta(creator, host)
.inTransaction(txId)
.buildV2();
// Check for transaction ID in V2 pointers
expect(delta.pointers['_transaction']).toEqual({ [txId]: 'deltas' });
});
it('should support negation', () => {
const negatedId = 'delta-to-negate';
const delta = createDelta(creator, host)
.negate(negatedId)
.buildV1();
// Check for negation in pointers
const negationPointer = delta.pointers.find(p => p.localContext === '_negates');
expect(negationPointer).toBeDefined();
expect(negationPointer?.target).toBe(negatedId);
});
it('should support custom timestamps', () => {
const timestamp = Date.now();
const delta = createDelta(creator, host)
.withTimestamp(timestamp)
.buildV1();
expect(delta.timeCreated).toBe(timestamp);
});
});
});

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,22 +1,19 @@
import {Delta, DeltaFilter, DeltaV2} from '../src/core'; import {Delta, DeltaFilter, DeltaV2} from '../src/core';
import {Lossless} from '../src/views'; import {Lossless} from '../src/views';
import {RhizomeNode} from '../src/node'; import {RhizomeNode} from '../src/node';
import {createDelta} from '../src/core/delta-builder';
describe('Lossless', () => { describe('Lossless', () => {
const node = new RhizomeNode(); const node = new RhizomeNode();
it('creates a lossless view of keanu as neo in the matrix', () => { it('creates a lossless view of keanu as neo in the matrix', () => {
const delta = new DeltaV2({ const delta = createDelta('a', 'h')
creator: 'a', .addPointer('actor', 'keanu', 'roles')
host: 'h', .addPointer('role', 'neo', 'actor')
pointers: { .addPointer('film', 'the_matrix', 'cast')
actor: {"keanu": "roles"}, .addPointer('base_salary', 1000000)
role: {"neo": "actor"}, .addPointer('salary_currency', 'usd')
film: {"the_matrix": "cast"}, .buildV1();
base_salary: 1000000,
salary_currency: "usd"
}
}).toV1();
expect(delta.pointers).toMatchObject([{ expect(delta.pointers).toMatchObject([{
localContext: "actor", localContext: "actor",
@ -94,30 +91,89 @@ describe('Lossless', () => {
}); });
}); });
it('accepts DeltaV2 instances', () => {
const delta = createDelta('a', 'h')
.addPointer('actor', 'keanu', 'roles')
.addPointer('role', 'neo', 'actor')
.addPointer('film', 'the_matrix', 'cast')
.addPointer('base_salary', 1000000)
.addPointer('salary_currency', 'usd')
.buildV2();
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
expect(lossless.view()).toMatchObject({
keanu: {
referencedAs: ["actor"],
propertyDeltas: {
roles: [{
creator: "a",
host: "h",
pointers: [
{actor: "keanu"},
{role: "neo"},
{film: "the_matrix"},
{base_salary: 1000000},
{salary_currency: "usd"},
],
}],
},
},
neo: {
referencedAs: ["role"],
propertyDeltas: {
actor: [{
creator: "a",
host: "h",
pointers: [
{actor: "keanu"},
{role: "neo"},
{film: "the_matrix"},
{base_salary: 1000000},
{salary_currency: "usd"},
],
}],
},
},
the_matrix: {
referencedAs: ["film"],
propertyDeltas: {
cast: [{
creator: "a",
host: "h",
pointers: [
{actor: "keanu"},
{role: "neo"},
{film: "the_matrix"},
{base_salary: 1000000},
{salary_currency: "usd"},
],
}],
},
},
});
});
describe('can filter deltas', () => { describe('can filter deltas', () => {
const lossless = new Lossless(node); const lossless = new Lossless(node);
beforeAll(() => { beforeAll(() => {
lossless.ingestDelta(new Delta({ // First delta
creator: 'A', lossless.ingestDelta(
host: 'H', createDelta('A', 'H')
pointers: [{ .addPointer('1', 'ace', 'value')
localContext: "1", .buildV1()
target: "ace", );
targetContext: "value"
}]
}));
lossless.ingestDelta(new Delta({ // Second delta
creator: 'B', lossless.ingestDelta(
host: 'H', createDelta('B', 'H')
pointers: [{
// 10 11j 12q 13k 14a // 10 11j 12q 13k 14a
localContext: "14", .addPointer('14', 'ace', 'value')
target: "ace", .buildV1()
targetContext: "value" );
}]
}));
expect(lossless.view()).toMatchObject({ expect(lossless.view()).toMatchObject({
ace: { ace: {
@ -182,51 +238,40 @@ describe('Lossless', () => {
const transactionId = 'tx-filter-test'; const transactionId = 'tx-filter-test';
// Declare transaction with 3 deltas // Declare transaction with 3 deltas
losslessT.ingestDelta(new Delta({ losslessT.ingestDelta(
creator: 'system', createDelta('system', 'H')
host: 'H', .declareTransaction(transactionId, 3)
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'size' }, );
{ localContext: 'size', target: 3 }
]
}));
// A1: First delta from creator A // A1: First delta from creator A
losslessT.ingestDelta(new Delta({ losslessT.ingestDelta(
creator: 'A', createDelta('A', 'H')
host: 'H', .inTransaction(transactionId)
pointers: [ .setProperty('process1', 'status', 'started', 'step')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'step', target: 'process1', targetContext: 'status' }, );
{ localContext: 'value', target: 'started' }
]
}));
// B: Delta from creator B // B: Delta from creator B
losslessT.ingestDelta(new Delta({ losslessT.ingestDelta(
creator: 'B', createDelta('B', 'H')
host: 'H', .inTransaction(transactionId)
pointers: [ .setProperty('process1', 'status', 'processing', 'step')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .buildV1()
{ localContext: 'step', target: 'process1', targetContext: 'status' }, );
{ localContext: 'value', target: 'processing' }
]
}));
// Transaction incomplete - nothing should show // Transaction incomplete - nothing should show
const incompleteView = losslessT.view(['process1']); const incompleteView = losslessT.view(['process1']);
expect(incompleteView.process1).toBeUndefined(); expect(incompleteView.process1).toBeUndefined();
// A2: Second delta from creator A completes transaction // A2: Second delta from creator A completes transaction
losslessT.ingestDelta(new Delta({ losslessT.ingestDelta(
creator: 'A', createDelta('A', 'H')
host: 'H', .inTransaction(transactionId)
pointers: [ .addPointer('step', 'process1', 'status')
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, .addPointer('value', 'completed')
{ localContext: 'step', target: 'process1', targetContext: 'status' }, .buildV1()
{ localContext: 'value', target: 'completed' } );
]
}));
// All deltas visible now // All deltas visible now
const completeView = losslessT.view(['process1']); const completeView = losslessT.view(['process1']);
@ -242,4 +287,5 @@ describe('Lossless', () => {
expect(filteredView.process1.propertyDeltas.status.every(d => d.creator === 'A')).toBe(true); expect(filteredView.process1.propertyDeltas.status.every(d => d.creator === 'A')).toBe(true);
}); });
}); });
}); });

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,5 +1,5 @@
import Debug from 'debug'; import Debug from 'debug';
import { Delta } from '../src/core'; import { createDelta } from '../src/core/delta-builder';
import { NegationHelper } from '../src/features'; import { NegationHelper } from '../src/features';
import { RhizomeNode } from '../src/node'; import { RhizomeNode } from '../src/node';
import { Lossless } from '../src/views'; import { Lossless } from '../src/views';
@ -17,14 +17,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,
@ -32,23 +27,20 @@ describe('Negation System', () => {
'host1' 'host1'
); );
expect(negationDelta.isNegation).toBe(true);
expect(negationDelta.negatedDeltaId).toBe(originalDelta.id);
expect(negationDelta.creator).toBe('moderator'); expect(negationDelta.creator).toBe('moderator');
expect(negationDelta.pointers).toHaveLength(1); expect(negationDelta.pointers).toHaveLength(1);
expect(negationDelta.pointers[0]).toEqual({ expect(negationDelta.pointers[0]).toEqual({
localContext: 'negates', localContext: '_negates',
target: originalDelta.id, target: originalDelta.id,
targetContext: 'negated_by' targetContext: 'negated_by'
}); });
expect(NegationHelper.isNegationDelta(negationDelta)).toBe(true);
}); });
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 +63,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 +96,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 +112,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 +136,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 +161,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 +188,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 +201,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 +221,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 +239,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 +264,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 +287,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 +318,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 +366,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 +385,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 +416,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 +489,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);
} }
} }
@ -209,14 +194,9 @@ describe('Nested Object Resolution Performance', () => {
const currentId = userIds[i]; const currentId = userIds[i];
const nextId = userIds[i + 1]; const nextId = userIds[i + 1];
const linkDelta = new Delta({ const linkDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .setProperty(currentId, 'next', nextId, 'users')
host: node.config.peerId, .buildV1();
pointers: [
{ localContext: 'users', target: currentId, targetContext: 'next' },
{ localContext: 'next', target: nextId }
]
});
node.lossless.ingestDelta(linkDelta); node.lossless.ingestDelta(linkDelta);
} }
@ -308,14 +288,10 @@ describe('Nested Object Resolution Performance', () => {
const connectedIndex = (i + j) % userCount; const connectedIndex = (i + j) % userCount;
const connectedId = userIds[connectedIndex]; const connectedId = userIds[connectedIndex];
const connectionDelta = new Delta({ const connectionDelta = createDelta(node.config.creator, node.config.peerId)
creator: node.config.creator, .addPointer('users', userId, 'connections')
host: node.config.peerId, .addPointer('connections', connectedId)
pointers: [ .buildV1();
{ localContext: 'users', target: userId, targetContext: 'connections' },
{ localContext: 'connections', target: connectedId }
]
});
node.lossless.ingestDelta(connectionDelta); node.lossless.ingestDelta(connectionDelta);
} }
} }

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
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 await orchestrator.cleanup();
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;
@ -20,8 +20,10 @@ describe('Schema System', () => {
schemaRegistry = new DefaultSchemaRegistry(); schemaRegistry = new DefaultSchemaRegistry();
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,21 @@ 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', .setProperty('user123', 'name', 'Alice')
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ 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', .setProperty('user123', 'age', 25)
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ 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 +43,10 @@ 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', .setProperty('user123', 'email', 'alice@example.com')
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ localContext: 'email', target: 'user123', targetContext: 'email' },
{ localContext: 'value', target: 'alice@example.com' }
]
});
lossless.ingestDelta(delta3); lossless.ingestDelta(delta3);
@ -83,44 +63,28 @@ 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', .setProperty('order1', 'status', 'pending')
pointers: [ .buildV1()
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' }, );
{ 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', .setProperty('order2', 'status', 'shipped')
pointers: [ .buildV1()
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' }, );
{ 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 +92,11 @@ 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', .setProperty('order1', 'total', 100)
pointers: [ .buildV1()
{ localContext: '_transaction', target: tx1, targetContext: 'deltas' }, );
{ 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 +106,11 @@ 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', .setProperty('order2', 'tracking', 'TRACK123')
pointers: [ .buildV1()
{ localContext: '_transaction', target: tx2, targetContext: 'deltas' }, );
{ 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 +124,23 @@ 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', .setProperty('doc1', 'type', 'report')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ 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', .setProperty('doc1', 'author', 'Bob')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ 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 +159,27 @@ 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', .declareTransaction(transactionId, 3)
host: 'host1', .buildV1()
pointers: [ );
{ 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', .inTransaction(transactionId)
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', .inTransaction(transactionId)
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 +187,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', .inTransaction(transactionId)
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 +216,26 @@ describe('Transactions', () => {
}); });
// Create transaction // Create transaction
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 first delta // Add first delta
const delta1 = new Delta({ const delta1 = createDelta('user1', 'host1')
creator: 'user1', .inTransaction(transactionId)
host: 'host1', .setProperty('entity1', 'field1', 'value1')
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ 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', .inTransaction(transactionId)
host: 'host1', .setProperty('entity1', 'field2', 'value2')
pointers: [ .buildV1();
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' },
{ 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 +256,17 @@ 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', .declareTransaction(transactionId, 2)
host: 'host1', .buildV1()
pointers: [ );
{ 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', .inTransaction(transactionId)
host: 'host1', .setProperty('job1', 'status', 'processing')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ 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 +278,11 @@ 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', .inTransaction(transactionId)
host: 'host1', .setProperty('job1', 'status', 'completed')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ localContext: 'status', target: 'job1', targetContext: 'status' },
{ localContext: 'value', target: 'completed' }
]
}));
// Wait should now resolve // Wait should now resolve
await waitPromise; await waitPromise;
@ -391,14 +296,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 +323,23 @@ 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', .declareTransaction(transactionId, 2)
host: 'host1', .buildV1()
pointers: [ );
{ 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', .inTransaction(transactionId)
host: 'host1', .setProperty('cart1', 'items', 'item1')
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', .inTransaction(transactionId)
host: 'host1', .setProperty('cart1', 'items', 'item2')
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 +353,11 @@ 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', .inTransaction(transactionId)
host: 'host1', .setProperty('entity1', 'data', 'test')
pointers: [ .buildV1()
{ localContext: '_transaction', target: transactionId, targetContext: 'deltas' }, );
{ 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 +367,10 @@ 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', .declareTransaction(transactionId, 1)
host: 'host1', .buildV1()
pointers: [ );
{ 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

@ -2,6 +2,7 @@ import Debug from 'debug';
import {randomUUID} from "node:crypto"; import {randomUUID} from "node:crypto";
import EventEmitter from "node:events"; import EventEmitter from "node:events";
import {Delta} from "../core/delta"; import {Delta} from "../core/delta";
import {createDelta} from "../core/delta-builder";
import {Entity, EntityProperties} from "../core/entity"; import {Entity, EntityProperties} from "../core/entity";
import {ResolvedViewOne} from '../views/resolvers/last-write-wins'; import {ResolvedViewOne} from '../views/resolvers/last-write-wins';
import {RhizomeNode} from "../node"; import {RhizomeNode} from "../node";
@ -72,18 +73,11 @@ export abstract class Collection<View> {
if (key === 'id') return; if (key === 'id') return;
if (oldProperties[key] !== value && host && creator) { if (oldProperties[key] !== value && host && creator) {
deltas.push(new Delta({ deltas.push(
creator, createDelta(creator, host)
host, .setProperty(entityId, key, value, this.name)
pointers: [{ .buildV1()
localContext: this.name, );
target: entityId,
targetContext: key
}, {
localContext: key,
target: value
}]
}));
} }
}); });
@ -91,18 +85,10 @@ export abstract class Collection<View> {
if (deltas.length > 1) { if (deltas.length > 1) {
// We can generate a separate delta describing this transaction // We can generate a separate delta describing this transaction
transactionDelta = new Delta({ transactionDelta = createDelta(creator, host)
creator, .addPointer('_transaction', transactionId, 'size')
host, .addPointer('size', deltas.length)
pointers: [{ .buildV1();
localContext: "_transaction",
target: transactionId,
targetContext: "size"
}, {
localContext: "size",
target: deltas.length
}]
});
// Also need to annotate the deltas with the transactionId // Also need to annotate the deltas with the transactionId
for (const delta of deltas) { for (const delta of deltas) {

141
src/core/delta-builder.ts Normal file
View File

@ -0,0 +1,141 @@
import { DeltaV1, DeltaV2 } from './delta';
import { randomUUID } from 'crypto';
import Debug from 'debug';
const debug = Debug('rz:delta-builder');
/**
* A fluent builder for creating Delta objects with proper validation and type safety.
* Supports both V1 and V2 delta formats.
*/
export class DeltaBuilder {
private id: string;
private timeCreated?: number;
private host: string;
private creator: string;
private pointers: Record<string, any> = {};
/**
* Create a new DeltaBuilder instance
* @param creator - The ID of the entity creating this delta
* @param host - The host where this delta is being created
*/
constructor(creator: string, host: string) {
this.id = randomUUID();
this.creator = creator;
this.host = host;
}
/**
* Set a custom ID for the delta
*/
withId(id: string): this {
this.id = id;
return this;
}
/**
* Set a custom creation timestamp
*/
withTimestamp(timestamp: number): this {
this.timeCreated = timestamp;
return this;
}
/**
* Set the transaction ID for this delta
*/
inTransaction(transactionId: string): this {
this.addPointer('_transaction', transactionId, 'deltas');
return this;
}
/**
* Declare a transaction with a size
* @param transactionId The ID of the transaction
* @param size The size of the transaction
* @returns
*/
declareTransaction(transactionId: string, size: number): this {
this.setProperty(transactionId, 'size', size, '_transaction');
return this;
}
/**
* Mark this delta as a negation of another delta
*/
negate(deltaId: string): this {
this.addPointer('_negates', deltaId, 'negated_by');
return this;
}
/**
* Add a pointer to the delta
* @param localContext The local context for the pointer
* @param target The target value (string, number, boolean, or null)
* @param targetContext Optional target context for the pointer
*/
addPointer(localContext: string, target: string | number | boolean | null, targetContext?: string): this {
if (targetContext && typeof target === 'string') {
this.pointers[localContext] = { [target]: targetContext };
} else {
this.pointers[localContext] = target;
}
return this;
}
/**
* Set a property on an entity
*/
setProperty(entityId: string, property: string, value: string | number | boolean | null, entityLabel = "entity"): this {
this.addPointer(entityLabel, entityId, property)
this.addPointer(property, value);
return this;
}
/**
* Create a relationship between two entities
*/
relate(sourceId: string, relationship: string, targetId: string): this {
this.pointers[relationship] = { [targetId]: relationship };
this.pointers.source = { [sourceId]: relationship };
return this;
}
/**
* Build and return a DeltaV2 instance
*/
buildV2(): DeltaV2 {
// For V2, we'll store transaction and negation info in the pointers object
const pointers = { ...this.pointers };
// Create the delta with all pointers
return new DeltaV2({
id: this.id,
host: this.host,
creator: this.creator,
timeCreated: this.timeCreated,
pointers
});
}
/**
* Build and return a DeltaV1 instance
*/
buildV1(): DeltaV1 {
return this.buildV2().toV1();
}
/**
* Default to V1 for now
*/
build(): DeltaV1 {
return this.buildV1();
}
}
/**
* Create a new DeltaBuilder instance (convenience function)
*/
export function createDelta(creator: string, host: string): DeltaBuilder {
return new DeltaBuilder(creator, host);
}

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

@ -1,21 +1,17 @@
import Debug from 'debug'; import Debug from 'debug';
import { Delta, DeltaID } from '../core/delta'; import { Delta, DeltaID } from '../core/delta';
import { createDelta } from '../core/delta-builder';
import { CreatorID, HostID } from '../core/types'; import { CreatorID, HostID } from '../core/types';
const debug = Debug('rz:negation'); const debug = Debug('rz:negation');
// Negation-specific types // Negation-specific types
export interface NegationPointer { export interface NegationPointer {
localContext: 'negates'; localContext: '_negates';
target: DeltaID; target: DeltaID;
targetContext: 'negated_by'; targetContext: 'negated_by';
} }
export interface NegationDelta extends Delta {
isNegation: true;
negatedDeltaId: DeltaID;
}
// Helper functions for creating and identifying negation deltas // Helper functions for creating and identifying negation deltas
export class NegationHelper { export class NegationHelper {
@ -26,19 +22,10 @@ export class NegationHelper {
deltaToNegate: DeltaID, deltaToNegate: DeltaID,
creator: CreatorID, creator: CreatorID,
host: HostID host: HostID
): NegationDelta { ): Delta {
const negationDelta = new Delta({ const negationDelta = createDelta(creator, host)
creator, .negate(deltaToNegate)
host, .buildV1();
pointers: [{
localContext: 'negates',
target: deltaToNegate,
targetContext: 'negated_by'
}]
}) as NegationDelta;
negationDelta.isNegation = true;
negationDelta.negatedDeltaId = deltaToNegate;
debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`); debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`);
return negationDelta; return negationDelta;
@ -47,9 +34,9 @@ export class NegationHelper {
/** /**
* Check if a delta is a negation delta * Check if a delta is a negation delta
*/ */
static isNegationDelta(delta: Delta): delta is NegationDelta { static isNegationDelta(delta: Delta): boolean {
return delta.pointers.some(pointer => return delta.pointers.some(pointer =>
pointer.localContext === 'negates' && pointer.localContext === '_negates' &&
pointer.targetContext === 'negated_by' pointer.targetContext === 'negated_by'
); );
} }
@ -59,7 +46,7 @@ export class NegationHelper {
*/ */
static getNegatedDeltaId(negationDelta: Delta): DeltaID | null { static getNegatedDeltaId(negationDelta: Delta): DeltaID | null {
const negationPointer = negationDelta.pointers.find(pointer => const negationPointer = negationDelta.pointers.find(pointer =>
pointer.localContext === 'negates' && pointer.localContext === '_negates' &&
pointer.targetContext === 'negated_by' pointer.targetContext === 'negated_by'
); );
@ -73,10 +60,10 @@ export class NegationHelper {
/** /**
* Find all negation deltas that negate a specific delta * Find all negation deltas that negate a specific delta
*/ */
static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): NegationDelta[] { static findNegationsFor(targetDeltaId: DeltaID, deltas: Delta[]): Delta[] {
return deltas return deltas
.filter(delta => this.isNegationDelta(delta)) .filter(delta => this.isNegationDelta(delta))
.filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId) as NegationDelta[]; .filter(delta => this.getNegatedDeltaId(delta) === targetDeltaId);
} }
/** /**
@ -152,7 +139,7 @@ export class NegationHelper {
// Create a map of delta ID to its negation status // Create a map of delta ID to its negation status
const deltaStatus = new Map<DeltaID, boolean>(); const deltaStatus = new Map<DeltaID, boolean>();
// Create a map of delta ID to its negation deltas // Create a map of delta ID to its negation deltas
const deltaToNegations = new Map<DeltaID, NegationDelta[]>(); const deltaToNegations = new Map<DeltaID, Delta[]>();
// First pass: collect all deltas and their negations // First pass: collect all deltas and their negations
for (const delta of deltas) { for (const delta of deltas) {

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

@ -258,7 +258,7 @@ export class QueryEngine {
// Convert each schema property from lossless view deltas // Convert each schema property from lossless view deltas
for (const [propertyId, propertySchema] of Object.entries(schema.properties)) { for (const [propertyId, propertySchema] of Object.entries(schema.properties)) {
const deltas = view.propertyDeltas[propertyId] || []; const deltas = view.propertyDeltas[propertyId] || [];
if (deltas.length === 0) { if (deltas.length === 0) {
obj[propertyId] = null; obj[propertyId] = null;
continue; continue;
@ -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;
} }
} }

View File

@ -3,12 +3,13 @@
import Debug from 'debug'; import Debug from 'debug';
import EventEmitter from 'events'; import EventEmitter from 'events';
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from '../core/delta'; import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1, DeltaV2} from '../core/delta';
import {RhizomeNode} from '../node'; import {RhizomeNode} from '../node';
import {Transactions} from '../features/transactions'; import {Transactions} from '../features/transactions';
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "../core/types"; import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "../core/types";
import {Negation} from '../features/negation'; import {Negation} from '../features/negation';
import {NegationHelper} from '../features/negation'; import {NegationHelper} from '../features/negation';
import { createDelta } from '../core/delta-builder';
const debug = Debug('rz:lossless'); const debug = Debug('rz:lossless');
export type CollapsedPointer = {[key: PropertyID]: PropertyTypes}; export type CollapsedPointer = {[key: PropertyID]: PropertyTypes};
@ -34,7 +35,11 @@ class LosslessEntity {
constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {} constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {}
addDelta(delta: Delta) { addDelta(delta: Delta | DeltaV2) {
// Convert DeltaV2 to DeltaV1 if needed
if (delta instanceof DeltaV2) {
delta = delta.toV1();
}
const targetContexts = delta.pointers const targetContexts = delta.pointers
.filter(({target}) => target === this.id) .filter(({target}) => target === this.id)
.map(({targetContext}) => targetContext) .map(({targetContext}) => targetContext)
@ -87,7 +92,12 @@ export class Lossless {
}); });
} }
ingestDelta(delta: Delta): TransactionID | undefined { ingestDelta(delta: Delta | DeltaV2): TransactionID | undefined {
// Convert DeltaV2 to DeltaV1 if needed
if (delta instanceof DeltaV2) {
delta = delta.toV1();
}
// Store delta for negation processing // Store delta for negation processing
this.allDeltas.set(delta.id, delta); this.allDeltas.set(delta.id, delta);
@ -114,10 +124,10 @@ export class Lossless {
// Add negation delta to the entity // Add negation delta to the entity
// For negation deltas, we need to add them to a special property // For negation deltas, we need to add them to a special property
// since they don't directly target the entity // since they don't directly target the entity
let negationDeltas = ent.properties.get('_negations'); let negationDeltas = ent.properties.get('_negates');
if (!negationDeltas) { if (!negationDeltas) {
negationDeltas = new Set<Delta>(); negationDeltas = new Set<Delta>();
ent.properties.set('_negations', negationDeltas); ent.properties.set('_negates', negationDeltas);
} }
negationDeltas.add(delta); negationDeltas.add(delta);
} }
@ -190,31 +200,35 @@ export class Lossless {
for (const delta of deltas) { for (const delta of deltas) {
if (!seenDeltaIds.has(delta.id)) { if (!seenDeltaIds.has(delta.id)) {
seenDeltaIds.add(delta.id); seenDeltaIds.add(delta.id);
// Convert CollapsedDelta back to Delta
const fullDelta = new Delta({ // Create a new delta using DeltaBuilder
id: delta.id, const builder = createDelta(delta.creator, delta.host)
creator: delta.creator, .withId(delta.id)
host: delta.host, .withTimestamp(delta.timeCreated);
timeCreated: delta.timeCreated,
pointers: delta.pointers.map(pointer => { // Add all pointers from the collapsed delta
// Convert back to V1 pointer format for Delta constructor for (const pointer of delta.pointers) {
const pointerEntries = Object.entries(pointer); const pointerEntries = Object.entries(pointer);
if (pointerEntries.length === 1) { if (pointerEntries.length === 1) {
const [localContext, target] = pointerEntries[0]; const [localContext, target] = pointerEntries[0];
if (typeof target === 'string' && this.domainEntities.has(target)) { if (target === null || target === undefined) {
// This is a reference pointer to an entity continue; // Skip null/undefined targets
// The targetContext is the property ID this delta appears under
return { localContext, target, targetContext: propertyId };
} else {
// Scalar pointer
return { localContext, target: target as PropertyTypes };
}
} }
// Fallback for unexpected pointer structure if (typeof target === 'string' && this.domainEntities.has(target)) {
return { localContext: 'unknown', target: 'unknown' }; // This is a reference pointer to an entity
}) builder.addPointer(localContext, target, propertyId);
}); } else if (typeof target === 'string' || typeof target === 'number' || typeof target === 'boolean') {
allDeltas.push(fullDelta); // Scalar pointer with valid type
builder.addPointer(localContext, target);
} else {
// For other types (objects, arrays), convert to string
builder.addPointer(localContext, JSON.stringify(target));
}
}
}
// Build the delta and add to results
allDeltas.push(builder.buildV1());
} }
} }
} }
@ -349,8 +363,8 @@ export class Lossless {
} }
for (const [property, deltas] of ent.properties.entries()) { for (const [property, deltas] of ent.properties.entries()) {
// Skip the special _negations property in the per-property stats // Skip the special _negates property in the per-property stats
if (property === '_negations') { if (property === '_negates') {
totalDeltas += deltas.size; totalDeltas += deltas.size;
totalNegationDeltas += deltas.size; totalNegationDeltas += deltas.size;
continue; continue;
@ -384,7 +398,7 @@ export class Lossless {
const ent = this.domainEntities.get(entityId); const ent = this.domainEntities.get(entityId);
if (!ent) return []; if (!ent) return [];
const negationProperty = ent.properties.get('_negations'); const negationProperty = ent.properties.get('_negates');
if (!negationProperty) return []; if (!negationProperty) return [];
return Array.from(negationProperty); return Array.from(negationProperty);