Delta Builder Fluent API #4
@ -2,5 +2,6 @@
|
||||
description: Update the current file to use delta builder
|
||||
---
|
||||
|
||||
Replace each deltav2 instantiation with a fluent call to createDelta from delta builder, using the following process:
|
||||
- pass creator and host as arguments to createDelta
|
||||
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
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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', () => {
|
||||
|
@ -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', () => {
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Debug from 'debug';
|
||||
import { createDelta } from '../src/core/delta-builder';
|
||||
import { Delta } from '../src/core';
|
||||
import { NegationHelper } from '../src/features';
|
||||
import { RhizomeNode } from '../src/node';
|
||||
@ -17,14 +18,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,
|
||||
@ -44,11 +40,9 @@ describe('Negation System', () => {
|
||||
});
|
||||
|
||||
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 +65,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 +98,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 +114,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 +138,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 +163,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 +190,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 +203,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 +223,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 +241,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 +266,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 +289,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 +320,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 +368,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 +387,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 +418,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 +491,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');
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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']);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 () => {
|
||||
|
@ -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 () => {
|
||||
|
@ -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');
|
||||
|
@ -10,7 +10,7 @@ import { DefaultSchemaRegistry } from '../src/schema';
|
||||
import { CommonSchemas } from '../util/schemas';
|
||||
import { TypedCollectionImpl, SchemaValidationError } from '../src/collections';
|
||||
import { RhizomeNode } from '../src/node';
|
||||
import { Delta } from '../src/core';
|
||||
import { createDelta } from '../src/core/delta-builder';
|
||||
|
||||
describe('Schema System', () => {
|
||||
let schemaRegistry: DefaultSchemaRegistry;
|
||||
@ -20,8 +20,10 @@ describe('Schema System', () => {
|
||||
schemaRegistry = new DefaultSchemaRegistry();
|
||||
node = new RhizomeNode();
|
||||
});
|
||||
|
||||
|
||||
describe('Schema Builder', () => {
|
||||
|
||||
it('should create a basic schema', () => {
|
||||
const schema = SchemaBuilder
|
||||
.create('user')
|
||||
@ -308,14 +310,10 @@ describe('Schema System', () => {
|
||||
await collection.put('user2', { name: 'Bob' });
|
||||
|
||||
// Manually create an invalid entity by bypassing validation
|
||||
const invalidDelta = new Delta({
|
||||
creator: node.config.creator,
|
||||
host: node.config.peerId,
|
||||
pointers: [
|
||||
{ localContext: 'users', target: 'user3', targetContext: 'email' },
|
||||
{ localContext: 'email', target: 'invalid@test.com' }
|
||||
]
|
||||
});
|
||||
const invalidDelta = createDelta(node.config.creator, node.config.peerId)
|
||||
.addPointer('users', 'user3', 'email')
|
||||
.addPointer('email', 'invalid@test.com')
|
||||
.buildV1();
|
||||
node.lossless.ingestDelta(invalidDelta);
|
||||
|
||||
const stats = collection.getValidationStats();
|
||||
@ -337,14 +335,10 @@ describe('Schema System', () => {
|
||||
await collection.put('user2', { name: 'Bob' });
|
||||
|
||||
// Create invalid entity manually
|
||||
const invalidDelta = new Delta({
|
||||
creator: node.config.creator,
|
||||
host: node.config.peerId,
|
||||
pointers: [
|
||||
{ localContext: 'users', target: 'user3', targetContext: 'age' },
|
||||
{ localContext: 'age', target: 'not-a-number' }
|
||||
]
|
||||
});
|
||||
const invalidDelta = createDelta(node.config.creator, node.config.peerId)
|
||||
.addPointer('users', 'user3', 'age')
|
||||
.addPointer('age', 'not-a-number')
|
||||
.buildV1();
|
||||
node.lossless.ingestDelta(invalidDelta);
|
||||
|
||||
const validIds = collection.getValidEntities();
|
||||
|
@ -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', () => {
|
||||
|
@ -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();
|
||||
|
@ -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,23 @@ 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)
|
||||
.addPointer('name', 'user123', 'name')
|
||||
.addPointer('value', '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)
|
||||
.addPointer('age', 'user123', 'age')
|
||||
.addPointer('value', 25)
|
||||
.buildV1();
|
||||
|
||||
// Ingest transaction declaration and first two deltas
|
||||
lossless.ingestDelta(txDeclaration);
|
||||
@ -58,15 +45,11 @@ 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)
|
||||
.addPointer('email', 'user123', 'email')
|
||||
.addPointer('value', 'alice@example.com')
|
||||
.buildV1();
|
||||
|
||||
lossless.ingestDelta(delta3);
|
||||
|
||||
@ -83,44 +66,30 @@ 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)
|
||||
.addPointer('status', 'order1', 'status')
|
||||
.addPointer('value', '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)
|
||||
.addPointer('status', 'order2', 'status')
|
||||
.addPointer('value', 'shipped')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Neither transaction is complete
|
||||
let view = lossless.view(['order1', 'order2']);
|
||||
@ -128,15 +97,12 @@ 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)
|
||||
.addPointer('total', 'order1', 'total')
|
||||
.addPointer('value', 100)
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// tx1 is complete, tx2 is not
|
||||
view = lossless.view(['order1', 'order2']);
|
||||
@ -146,15 +112,12 @@ 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)
|
||||
.addPointer('tracking', 'order2', 'tracking')
|
||||
.addPointer('value', 'TRACK123')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Both transactions complete
|
||||
view = lossless.view(['order1', 'order2']);
|
||||
@ -168,35 +131,25 @@ 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)
|
||||
.addPointer('type', 'doc1', 'type')
|
||||
.addPointer('value', '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)
|
||||
.addPointer('author', 'doc1', 'author')
|
||||
.addPointer('value', 'Bob')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Create a filter that only accepts deltas from user1
|
||||
const userFilter: DeltaFilter = (delta) => delta.creator === 'user1';
|
||||
@ -215,37 +168,28 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'size')
|
||||
.addPointer('size', 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.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 +197,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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.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 +226,29 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'size')
|
||||
.addPointer('size', 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('field1', 'entity1', 'field1')
|
||||
.addPointer('value', '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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('field2', 'entity1', 'field2')
|
||||
.addPointer('value', 'value2')
|
||||
.buildV1();
|
||||
lossless.ingestDelta(delta2);
|
||||
|
||||
// Wait for async event processing
|
||||
@ -339,25 +269,19 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'size')
|
||||
.addPointer('size', 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('status', 'job1', 'status')
|
||||
.addPointer('value', 'processing')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Start waiting for transaction
|
||||
const waitPromise = lossless.transactions.waitFor(transactionId);
|
||||
@ -369,15 +293,12 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('status', 'job1', 'status')
|
||||
.addPointer('value', 'completed')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Wait should now resolve
|
||||
await waitPromise;
|
||||
@ -391,14 +312,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 +339,24 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'size')
|
||||
.addPointer('size', 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('item1', 'cart1', 'items')
|
||||
.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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('item2', 'cart1', 'items')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Transaction should be complete
|
||||
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
||||
@ -462,15 +370,12 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'deltas')
|
||||
.addPointer('data', 'entity1', 'data')
|
||||
.addPointer('value', 'test')
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Transaction should not be complete (no size)
|
||||
expect(lossless.transactions.isComplete(transactionId)).toBe(false);
|
||||
@ -480,14 +385,11 @@ 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')
|
||||
.addPointer('_transaction', transactionId, 'size')
|
||||
.addPointer('size', 1)
|
||||
.buildV1()
|
||||
);
|
||||
|
||||
// Now transaction should be complete
|
||||
expect(lossless.transactions.isComplete(transactionId)).toBe(true);
|
||||
|
@ -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",
|
||||
|
@ -134,6 +134,13 @@ export class DeltaBuilder {
|
||||
buildV1(): DeltaV1 {
|
||||
return this.buildV2().toV1();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default to V1 for now
|
||||
*/
|
||||
build(): DeltaV1 {
|
||||
return this.buildV1();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './delta';
|
||||
export * from './delta-builder';
|
||||
export * from './types';
|
||||
export * from './context';
|
||||
export { Entity } from './entity';
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -258,7 +258,7 @@ export class QueryEngine {
|
||||
// Convert each schema property from lossless view deltas
|
||||
for (const [propertyId, propertySchema] of Object.entries(schema.properties)) {
|
||||
const deltas = view.propertyDeltas[propertyId] || [];
|
||||
|
||||
|
||||
if (deltas.length === 0) {
|
||||
obj[propertyId] = null;
|
||||
continue;
|
||||
@ -268,9 +268,13 @@ export class QueryEngine {
|
||||
switch (propertySchema.type) {
|
||||
case 'primitive': {
|
||||
// Use last-write-wins for primitives
|
||||
const lastDelta = deltas.sort((a, b) => b.timeCreated - a.timeCreated)[0];
|
||||
const primitiveValue = this.extractPrimitiveValue(lastDelta, propertyId);
|
||||
obj[propertyId] = primitiveValue;
|
||||
const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
|
||||
for (let delta of deltasSorted) {
|
||||
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
|
||||
if (primitiveValue !== null) {
|
||||
obj[propertyId] = primitiveValue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -304,12 +308,12 @@ export class QueryEngine {
|
||||
/**
|
||||
* Extract primitive value from a delta for a given property
|
||||
*/
|
||||
private extractPrimitiveValue(delta: CollapsedDelta, _propertyId: string): unknown {
|
||||
private extractPrimitiveValue(delta: CollapsedDelta, propertyId: string): unknown {
|
||||
// Look for the value in collapsed pointers
|
||||
// CollapsedPointer is {[key: PropertyID]: PropertyTypes}
|
||||
for (const pointer of delta.pointers) {
|
||||
if (pointer.value !== undefined) {
|
||||
return pointer.value;
|
||||
if (pointer[propertyId] !== undefined) {
|
||||
return pointer[propertyId];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user
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 ofaddPointer
into singlesetProperty
calls