Compare commits

..

No commits in common. "main" and "feature/docker-orchestrator-tests" have entirely different histories.

32 changed files with 2589 additions and 1658 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,5 @@
// Set up environment variables for tests
// 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:*';
}
process.env.DEBUG = 'rz:*';
// Extend the global Jest namespace
declare global {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ interface ExtendedNodeStatus extends Omit<NodeStatus, 'network'> {
// Set default timeout for all tests to 5 minutes
jest.setTimeout(300000);
describe('Docker Orchestrator', () => {
describe('Docker Orchestrator V2', () => {
let docker: Docker;
let orchestrator: DockerOrchestrator;
let node: NodeHandle | null = null;
@ -43,31 +43,27 @@ describe('Docker Orchestrator', () => {
beforeAll(async () => {
debug('Setting up Docker client and orchestrator...');
// Initialize Docker client with increased timeout
docker = new Docker({
timeout: 60000, // 60 second timeout for Docker operations
});
// Initialize Docker client
docker = new Docker();
// Verify Docker is running
try {
await docker.ping();
debug('Docker daemon is responding');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
debug('Docker daemon is not responding: %o', error);
throw new Error(`Docker daemon is not running or not accessible: ${errorMessage}`);
throw error;
}
// Initialize the orchestrator with the Docker client and test image
orchestrator = createOrchestrator('docker') as DockerOrchestrator;
debug('Docker orchestrator initialized');
// Create a basic node config for testing with unique network ID
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
// Create a basic node config for testing
nodePort = 3000 + Math.floor(Math.random() * 1000);
nodeConfig = {
id: `node-${testRunId}`,
networkId: `test-network-${testRunId}`,
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
networkId: 'test-network',
port: nodePort,
resources: {
memory: 256, // 256MB
@ -83,8 +79,85 @@ describe('Docker Orchestrator', () => {
afterAll(async () => {
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'));
}
await orchestrator.cleanup();
// Wait for all node cleanups to complete before cleaning up networks
if (cleanupPromises.length > 0) {
debug('Waiting for node cleanups to complete...');
await Promise.race([
Promise.all(cleanupPromises),
new Promise(resolve => setTimeout(() => {
debug('Node cleanup timed out, proceeding with network cleanup...');
resolve(null);
}, 30000)) // 30s timeout for node cleanup
]);
}
// Clean up any dangling networks using NetworkManager
try {
debug('Cleaning up networks...');
// Get the network manager from the orchestrator
const networkManager = (orchestrator as any).networkManager;
if (!networkManager) {
debug('Network manager not available for cleanup');
return;
}
// Get all networks managed by this test
const networks = Array.from((orchestrator as any).networks.entries() || []);
const cleanupResults = await networkManager.cleanupNetworks((orchestrator as any).networks);
// Log any cleanup errors
cleanupResults.forEach(({ resource, error }: { resource: string; error: Error }) => {
if (error) {
debug(`Failed to clean up network ${resource || 'unknown'}: %s`, error.message);
} else {
debug(`Successfully cleaned up network ${resource || 'unknown'}`);
}
});
} catch (error) {
debug('Error during network cleanup: %o', error);
}
debug('All test cleanups completed');
}, 120000); // 2 minute timeout for afterAll
@ -93,19 +166,12 @@ describe('Docker Orchestrator', () => {
debug('Starting test: should start and stop a node');
// Create a new config with a unique ID for this test
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const testNodeConfig = {
...nodeConfig,
id: `node-${testRunId}`,
networkId: `test-network-${testRunId}`,
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
network: {
...nodeConfig.network,
enableHttpApi: true
},
// Add retry logic for Docker operations
docker: {
maxRetries: 3,
retryDelay: 1000
}
};
@ -143,7 +209,7 @@ describe('Docker Orchestrator', () => {
debug('Error during node cleanup: %o', e);
}
}
});
}, 30000); // 30 second timeout for this test
it('should enforce resource limits', async () => {
debug('Starting test: should enforce resource limits');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,141 +0,0 @@
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,5 +1,4 @@
export * from './delta';
export * from './delta-builder';
export * from './types';
export * from './context';
export { Entity } from './entity';

View File

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

View File

@ -63,11 +63,16 @@ export class ImageManager implements IImageManager {
debug('Created build context tar stream');
testImageBuildPromise = new Promise<void>((resolve, reject) => {
const logMessages: string[] = [];
const log = (...args: any[]) => {
const timestamp = new Date().toISOString();
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
debug(message);
const logMessage = `[${timestamp}] ${message}\n`;
process.stdout.write(logMessage);
logMessages.push(logMessage);
};
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => {
@ -138,4 +143,17 @@ 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
for (const [propertyId, propertySchema] of Object.entries(schema.properties)) {
const deltas = view.propertyDeltas[propertyId] || [];
if (deltas.length === 0) {
obj[propertyId] = null;
continue;
@ -268,13 +268,9 @@ export class QueryEngine {
switch (propertySchema.type) {
case 'primitive': {
// Use last-write-wins for primitives
const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
for (let delta of deltasSorted) {
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
if (primitiveValue !== null) {
obj[propertyId] = primitiveValue;
}
}
const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0];
const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId);
obj[propertyId] = primitiveValue;
break;
}
@ -308,12 +304,12 @@ export class QueryEngine {
/**
* Extract primitive value from a delta for a given property
*/
private extractPrimitiveValue(delta: CollapsedDelta, propertyId: string): unknown {
private extractPrimitiveValue(delta: CollapsedDelta, _propertyId: string): unknown {
// Look for the value in collapsed pointers
// CollapsedPointer is {[key: PropertyID]: PropertyTypes}
for (const pointer of delta.pointers) {
if (pointer[propertyId] !== undefined) {
return pointer[propertyId];
if (pointer.value !== undefined) {
return pointer.value;
}
}
return null;

View File

@ -263,8 +263,6 @@ export class StorageQueryEngine {
default:
properties[propertyId] = propDeltas.length;
}
debug(`Resolved property ${propertyId}:`, properties[propertyId]);
}
return properties;
@ -273,9 +271,9 @@ export class StorageQueryEngine {
/**
* Extract primitive value from a delta for a given property
*/
private extractPrimitiveValue(delta: Delta, propertyId: string): unknown {
private extractPrimitiveValue(delta: Delta, _propertyId: string): unknown {
for (const pointer of delta.pointers) {
if (pointer.localContext === propertyId) {
if (pointer.localContext === 'value') {
return pointer.target;
}
}
@ -283,11 +281,11 @@ export class StorageQueryEngine {
}
/**
* Extract reference value from a delta for a given property
* Extract reference value (target ID) from a delta for a given property
*/
private extractReferenceValue(delta: Delta, propertyId: string): string | null {
private extractReferenceValue(delta: Delta, _propertyId: string): string | null {
for (const pointer of delta.pointers) {
if (pointer.localContext === propertyId && typeof pointer.target === 'string') {
if (pointer.localContext === 'value' && typeof pointer.target === 'string') {
return pointer.target;
}
}

View File

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