Delta Builder Fluent API #4

Merged
lentil merged 7 commits from chore/delta-format-standardization into main 2025-06-20 22:55:33 -05:00
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
Review

I wasn't specific enough here, It didn't get setProperty on the first pass, but in the IDE it was able to catch on to the pattern and tab through to convert pairs of addPointer into single setProperty calls

I wasn't specific enough here, It didn't get `setProperty` on the first pass, but in the IDE it was able to catch on to the pattern and tab through to convert pairs of `addPointer` into single `setProperty` calls

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;
@ -21,7 +21,9 @@ describe('Schema System', () => {
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

@ -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);