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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
// Set up environment variables for tests
process.env.DEBUG = 'rz:*';
// DEBUG handling examples:
// npm test // will set DEBUG=rz:* by default
// NO_DEBUG=true npm test // will not set DEBUG
// DEBUG=other npm test // will set DEBUG=other
if (!process.env.DEBUG && !process.env.NO_DEBUG) {
process.env.DEBUG = 'rz:*';
}
// Extend the global Jest namespace
declare global {

View File

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

View File

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

View File

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

View File

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

View File

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

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 } from '../src/core';
import { Delta, createDelta } from '../src/core';
import { DefaultSchemaRegistry } from '../src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema';
import { TypedCollectionImpl } from '../src/collections';
@ -80,14 +80,9 @@ describe('Nested Object Resolution Performance', () => {
const friendIndex = Math.floor(Math.random() * userCount);
if (friendIndex !== i) {
const friendId = userIds[friendIndex];
const friendshipDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: userId, targetContext: 'friends' },
{ localContext: 'friends', target: friendId }
]
});
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'friends', friendId, 'users')
.buildV1();
node.lossless.ingestDelta(friendshipDelta);
}
}
@ -98,14 +93,9 @@ describe('Nested Object Resolution Performance', () => {
const followerIndex = Math.floor(Math.random() * userCount);
if (followerIndex !== i) {
const followerId = userIds[followerIndex];
const followDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: userId, targetContext: 'followers' },
{ localContext: 'followers', target: followerId }
]
});
const followDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'followers', followerId, 'users')
.buildV1();
node.lossless.ingestDelta(followDelta);
}
}
@ -114,14 +104,9 @@ describe('Nested Object Resolution Performance', () => {
if (i > 0) {
const mentorIndex = Math.floor(i / 2); // Create a tree-like mentor structure
const mentorId = userIds[mentorIndex];
const mentorshipDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: userId, targetContext: 'mentor' },
{ localContext: 'mentor', target: mentorId }
]
});
const mentorshipDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(userId, 'mentor', mentorId, 'users')
.buildV1();
node.lossless.ingestDelta(mentorshipDelta);
}
}
@ -209,14 +194,9 @@ describe('Nested Object Resolution Performance', () => {
const currentId = userIds[i];
const nextId = userIds[i + 1];
const linkDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: currentId, targetContext: 'next' },
{ localContext: 'next', target: nextId }
]
});
const linkDelta = createDelta(node.config.creator, node.config.peerId)
.setProperty(currentId, 'next', nextId, 'users')
.buildV1();
node.lossless.ingestDelta(linkDelta);
}
@ -308,14 +288,10 @@ describe('Nested Object Resolution Performance', () => {
const connectedIndex = (i + j) % userCount;
const connectedId = userIds[connectedIndex];
const connectionDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: userId, targetContext: 'connections' },
{ localContext: 'connections', target: connectedId }
]
});
const connectionDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', userId, 'connections')
.addPointer('connections', connectedId)
.buildV1();
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,14 +55,10 @@ describe('Nested Object Resolution', () => {
});
// Create friendship relationship
const friendshipDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'bob')
.buildV1();
node.lossless.ingestDelta(friendshipDelta);
// Get Alice's lossless view
@ -107,14 +103,10 @@ describe('Nested Object Resolution', () => {
// Create user with reference to non-existent friend
await userCollection.put('alice', { name: 'Alice' });
const friendshipDelta = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'nonexistent' }
]
});
const friendshipDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'nonexistent')
.buildV1();
node.lossless.ingestDelta(friendshipDelta);
const aliceViews = node.lossless.view(['alice']);
@ -162,25 +154,17 @@ describe('Nested Object Resolution', () => {
await userCollection.put('charlie', { name: 'Charlie' });
// Alice's mentor is Bob
const mentorshipDelta1 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'deep-users', target: 'alice', targetContext: 'mentor' },
{ localContext: 'mentor', target: 'bob' }
]
});
const mentorshipDelta1 = createDelta(node.config.creator, node.config.peerId)
.addPointer('deep-users', 'alice', 'mentor')
.addPointer('mentor', 'bob')
.buildV1();
node.lossless.ingestDelta(mentorshipDelta1);
// Bob's mentor is Charlie
const mentorshipDelta2 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'deep-users', target: 'bob', targetContext: 'mentor' },
{ localContext: 'mentor', target: 'charlie' }
]
});
const mentorshipDelta2 = createDelta(node.config.creator, node.config.peerId)
.addPointer('deep-users', 'bob', 'mentor')
.addPointer('mentor', 'charlie')
.buildV1();
node.lossless.ingestDelta(mentorshipDelta2);
const aliceViews = node.lossless.view(['alice']);
@ -246,24 +230,16 @@ describe('Nested Object Resolution', () => {
await userCollection.put('bob', { name: 'Bob' });
// Create circular friendship: Alice -> Bob -> Alice
const friendship1 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
const friendship1 = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'bob')
.buildV1();
node.lossless.ingestDelta(friendship1);
const friendship2 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'bob', targetContext: 'friends' },
{ localContext: 'friends', target: 'alice' }
]
});
const friendship2 = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'bob', 'friends')
.addPointer('friends', 'alice')
.buildV1();
node.lossless.ingestDelta(friendship2);
const aliceViews = node.lossless.view(['alice']);
@ -295,14 +271,10 @@ describe('Nested Object Resolution', () => {
await userCollection.put('alice', { name: 'Alice' });
// Alice is friends with herself
const selfFriendship = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'alice' }
]
});
const selfFriendship = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'alice')
.buildV1();
node.lossless.ingestDelta(selfFriendship);
const aliceViews = node.lossless.view(['alice']);
@ -335,24 +307,16 @@ describe('Nested Object Resolution', () => {
await userCollection.put('charlie', { name: 'Charlie' });
// Alice has multiple friends
const friendship1 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
const friendship1 = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'bob')
.buildV1();
node.lossless.ingestDelta(friendship1);
const friendship2 = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'charlie' }
]
});
const friendship2 = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'charlie')
.buildV1();
node.lossless.ingestDelta(friendship2);
const aliceViews = node.lossless.view(['alice']);
@ -405,14 +369,10 @@ describe('Nested Object Resolution', () => {
});
// Create friendship
const friendship = new Delta({
creator: node.config.creator,
host: node.config.peerId,
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friends', target: 'bob' }
]
});
const friendship = createDelta(node.config.creator, node.config.peerId)
.addPointer('users', 'alice', 'friends')
.addPointer('friends', 'bob')
.buildV1();
node.lossless.ingestDelta(friendship);
const aliceViews = node.lossless.view(['alice']);

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

View File

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

View File

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

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 V2', () => {
describe('Docker Orchestrator', () => {
let docker: Docker;
let orchestrator: DockerOrchestrator;
let node: NodeHandle | null = null;
@ -43,27 +43,31 @@ describe('Docker Orchestrator V2', () => {
beforeAll(async () => {
debug('Setting up Docker client and orchestrator...');
// Initialize Docker client
docker = new Docker();
// Initialize Docker client with increased timeout
docker = new Docker({
timeout: 60000, // 60 second timeout for Docker operations
});
// Verify Docker is running
try {
await docker.ping();
debug('Docker daemon is responding');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
debug('Docker daemon is not responding: %o', error);
throw error;
throw new Error(`Docker daemon is not running or not accessible: ${errorMessage}`);
}
// Initialize the orchestrator with the Docker client and test image
orchestrator = createOrchestrator('docker') as DockerOrchestrator;
debug('Docker orchestrator initialized');
// Create a basic node config for testing
// Create a basic node config for testing with unique network ID
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
nodePort = 3000 + Math.floor(Math.random() * 1000);
nodeConfig = {
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
networkId: 'test-network',
id: `node-${testRunId}`,
networkId: `test-network-${testRunId}`,
port: nodePort,
resources: {
memory: 256, // 256MB
@ -79,85 +83,8 @@ describe('Docker Orchestrator V2', () => {
afterAll(async () => {
debug('Starting test cleanup...');
const cleanupPromises: Promise<unknown>[] = [];
// Helper function to clean up a node with retries
const cleanupNode = async (nodeToClean: NodeHandle | null, nodeName: string) => {
if (!nodeToClean) return;
debug(`[${nodeName}] Starting cleanup for node ${nodeToClean.id}...`);
try {
// First try the normal stop
await orchestrator.stopNode(nodeToClean).catch(error => {
debug(`[${nodeName}] Warning stopping node normally: %s`, error.message);
throw error; // Will be caught by outer catch
});
debug(`[${nodeName}] Node ${nodeToClean.id} stopped gracefully`);
} catch (error) {
debug(`[${nodeName}] Error stopping node ${nodeToClean.id}: %o`, error);
// If normal stop fails, try force cleanup
try {
debug(`[${nodeName}] Attempting force cleanup for node ${nodeToClean.id}...`);
const container = orchestrator.docker.getContainer(`rhizome-${nodeToClean.id}`);
await container.stop({ t: 1 }).catch(() => {
debug(`[${nodeName}] Container stop timed out, forcing removal...`);
});
await container.remove({ force: true });
debug(`[${nodeName}] Node ${nodeToClean.id} force-removed`);
} catch (forceError) {
debug(`[${nodeName}] Force cleanup failed for node ${nodeToClean.id}: %o`, forceError);
}
}
};
// Clean up all created nodes
if (node) {
cleanupPromises.push(cleanupNode(node, 'node1'));
}
if (node2) {
cleanupPromises.push(cleanupNode(node2, 'node2'));
}
// Wait for all node cleanups to complete before cleaning up networks
if (cleanupPromises.length > 0) {
debug('Waiting for node cleanups to complete...');
await Promise.race([
Promise.all(cleanupPromises),
new Promise(resolve => setTimeout(() => {
debug('Node cleanup timed out, proceeding with network cleanup...');
resolve(null);
}, 30000)) // 30s timeout for node cleanup
]);
}
// Clean up any dangling networks using NetworkManager
try {
debug('Cleaning up networks...');
// Get the network manager from the orchestrator
const networkManager = (orchestrator as any).networkManager;
if (!networkManager) {
debug('Network manager not available for cleanup');
return;
}
// Get all networks managed by this test
const networks = Array.from((orchestrator as any).networks.entries() || []);
const cleanupResults = await networkManager.cleanupNetworks((orchestrator as any).networks);
// Log any cleanup errors
cleanupResults.forEach(({ resource, error }: { resource: string; error: Error }) => {
if (error) {
debug(`Failed to clean up network ${resource || 'unknown'}: %s`, error.message);
} else {
debug(`Successfully cleaned up network ${resource || 'unknown'}`);
}
});
} catch (error) {
debug('Error during network cleanup: %o', error);
}
await orchestrator.cleanup();
debug('All test cleanups completed');
}, 120000); // 2 minute timeout for afterAll
@ -166,12 +93,19 @@ describe('Docker Orchestrator V2', () => {
debug('Starting test: should start and stop a node');
// Create a new config with a unique ID for this test
const testRunId = `test-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const testNodeConfig = {
...nodeConfig,
id: `test-node-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
id: `node-${testRunId}`,
networkId: `test-network-${testRunId}`,
network: {
...nodeConfig.network,
enableHttpApi: true
},
// Add retry logic for Docker operations
docker: {
maxRetries: 3,
retryDelay: 1000
}
};
@ -209,7 +143,7 @@ describe('Docker Orchestrator V2', () => {
debug('Error during node cleanup: %o', e);
}
}
}, 30000); // 30 second timeout for this test
});
it('should enforce resource limits', async () => {
debug('Starting test: should enforce resource limits');

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

View File

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

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

View File

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

View File

@ -9,7 +9,9 @@
"test": "jest",
"coverage": "./scripts/coverage.sh",
"coverage-report": "npm run test -- --coverage --coverageDirectory=coverage",
"example-app": "node dist/examples/app.js"
"example-app": "node dist/examples/app.js",
"stop-all": "docker ps -a --filter \"name=^/rhizome-node-\" --format {{.Names}} | xargs -r docker stop",
"build-test-image": "docker build -t rhizome-node-test -f Dockerfile.test ."
},
"jest": {
"testEnvironment": "node",
@ -35,11 +37,10 @@
"forceExit": true,
"verbose": true,
"testEnvironmentOptions": {
"NODE_ENV": "test",
"DEBUG": "rz:*"
"NODE_ENV": "test"
}
},
"author": "Taliesin (Ladd) <ladd@dgov.io>",
"author": "Lentil <lentil@laddhoffman.com>",
"license": "Unlicense",
"dependencies": {
"@types/dockerode": "^3.3.40",

View File

@ -2,6 +2,7 @@ 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";
@ -72,18 +73,11 @@ export abstract class Collection<View> {
if (key === 'id') return;
if (oldProperties[key] !== value && host && creator) {
deltas.push(new Delta({
creator,
host,
pointers: [{
localContext: this.name,
target: entityId,
targetContext: key
}, {
localContext: key,
target: value
}]
}));
deltas.push(
createDelta(creator, host)
.setProperty(entityId, key, value, this.name)
.buildV1()
);
}
});
@ -91,18 +85,10 @@ export abstract class Collection<View> {
if (deltas.length > 1) {
// We can generate a separate delta describing this transaction
transactionDelta = new Delta({
creator,
host,
pointers: [{
localContext: "_transaction",
target: transactionId,
targetContext: "size"
}, {
localContext: "size",
target: deltas.length
}]
});
transactionDelta = createDelta(creator, host)
.addPointer('_transaction', transactionId, 'size')
.addPointer('size', deltas.length)
.buildV1();
// Also need to annotate the deltas with the transactionId
for (const delta of deltas) {

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

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

View File

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

View File

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

View File

@ -63,16 +63,11 @@ 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(' ');
const logMessage = `[${timestamp}] ${message}\n`;
process.stdout.write(logMessage);
logMessages.push(logMessage);
debug(message);
};
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => {
@ -143,17 +138,4 @@ export class ImageManager implements IImageManager {
});
});
}
/**
* Check if an image exists locally
*/
async imageExists(imageName: string): Promise<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,9 +268,13 @@ export class QueryEngine {
switch (propertySchema.type) {
case 'primitive': {
// Use last-write-wins for primitives
const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0];
const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId);
obj[propertyId] = primitiveValue;
const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
for (let delta of deltasSorted) {
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
if (primitiveValue !== null) {
obj[propertyId] = primitiveValue;
}
}
break;
}
@ -304,12 +308,12 @@ export class QueryEngine {
/**
* Extract primitive value from a delta for a given property
*/
private extractPrimitiveValue(delta: CollapsedDelta, _propertyId: string): unknown {
private extractPrimitiveValue(delta: CollapsedDelta, propertyId: string): unknown {
// Look for the value in collapsed pointers
// CollapsedPointer is {[key: PropertyID]: PropertyTypes}
for (const pointer of delta.pointers) {
if (pointer.value !== undefined) {
return pointer.value;
if (pointer[propertyId] !== undefined) {
return pointer[propertyId];
}
}
return null;

View File

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

View File

@ -3,12 +3,13 @@
import Debug from 'debug';
import EventEmitter from 'events';
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1} from '../core/delta';
import {Delta, DeltaFilter, DeltaID, DeltaNetworkImageV1, DeltaV2} from '../core/delta';
import {RhizomeNode} from '../node';
import {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};
@ -34,7 +35,11 @@ class LosslessEntity {
constructor(readonly lossless: Lossless, readonly id: DomainEntityID) {}
addDelta(delta: Delta) {
addDelta(delta: Delta | DeltaV2) {
// Convert DeltaV2 to DeltaV1 if needed
if (delta instanceof DeltaV2) {
delta = delta.toV1();
}
const targetContexts = delta.pointers
.filter(({target}) => target === this.id)
.map(({targetContext}) => targetContext)
@ -87,7 +92,12 @@ export class Lossless {
});
}
ingestDelta(delta: Delta): TransactionID | undefined {
ingestDelta(delta: Delta | DeltaV2): TransactionID | undefined {
// Convert DeltaV2 to DeltaV1 if needed
if (delta instanceof DeltaV2) {
delta = delta.toV1();
}
// Store delta for negation processing
this.allDeltas.set(delta.id, delta);
@ -114,10 +124,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('_negations');
let negationDeltas = ent.properties.get('_negates');
if (!negationDeltas) {
negationDeltas = new Set<Delta>();
ent.properties.set('_negations', negationDeltas);
ent.properties.set('_negates', negationDeltas);
}
negationDeltas.add(delta);
}
@ -190,31 +200,35 @@ export class Lossless {
for (const delta of deltas) {
if (!seenDeltaIds.has(delta.id)) {
seenDeltaIds.add(delta.id);
// 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 };
}
// 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
}
// Fallback for unexpected pointer structure
return { localContext: 'unknown', target: 'unknown' };
})
});
allDeltas.push(fullDelta);
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());
}
}
}
@ -349,8 +363,8 @@ export class Lossless {
}
for (const [property, deltas] of ent.properties.entries()) {
// Skip the special _negations property in the per-property stats
if (property === '_negations') {
// Skip the special _negates property in the per-property stats
if (property === '_negates') {
totalDeltas += deltas.size;
totalNegationDeltas += deltas.size;
continue;
@ -384,7 +398,7 @@ export class Lossless {
const ent = this.domainEntities.get(entityId);
if (!ent) return [];
const negationProperty = ent.properties.get('_negations');
const negationProperty = ent.properties.get('_negates');
if (!negationProperty) return [];
return Array.from(negationProperty);